[open-ils-commits] [GIT] Evergreen ILS branch master updated. 0469a1cb06a6adcaf80e04325618be9fca3895c7

Evergreen Git git at git.evergreen-ils.org
Thu Sep 6 17:31:40 EDT 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  0469a1cb06a6adcaf80e04325618be9fca3895c7 (commit)
       via  9d2ddf6d75b674cbf905817fa637b28397e1e354 (commit)
       via  e3032ad3cd1fe2e684c949613b82841efd401867 (commit)
       via  23fd5ef82afefa0732df9f4f386888ccffad8b1a (commit)
       via  81324a87efcaa46b2d4bf289d04280eacf7be625 (commit)
       via  03d94e21e5052156efecefb1ac87de11206c9954 (commit)
       via  52f83fe0fd7b5765dd541d597def27040eceee35 (commit)
       via  c37f7ba9e3cfbf7ff0feeb9c77710d706d63697d (commit)
       via  d28eeddfd5dda73db0b017a94c37573f68057f33 (commit)
       via  48055f0527c77b71fb402d2af15c47b4d8db40e0 (commit)
       via  060af9b3739033bc276b692b45c118b6eb4ba83b (commit)
       via  9a79de4aa33b52f55397bcbcd6cd9f84c3dff149 (commit)
       via  e8cf82d0ddb13dfbcbf96bf7528ce34e9574c0d1 (commit)
       via  a29408d79df4b5240bd206fcd2158cadc9547bae (commit)
       via  038cd40207c5f63001bae2b68defdf44163c352d (commit)
       via  6c706f454b2b9cdf2d46df3c60e53a0d3cb531d7 (commit)
       via  6bcefced08f07d783b1d46bb4ee441ecde70df02 (commit)
       via  b0f99db6c9cf141fe10addccce075b95a26e6595 (commit)
      from  46543fa2fb9dd628131887ea4bdd35c39366fd6a (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 0469a1cb06a6adcaf80e04325618be9fca3895c7
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Sep 5 16:44:08 2018 -0400

    LP#1790923: add release notes
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Client/Disabling_Legacy_Staff_client.adoc b/docs/RELEASE_NOTES_NEXT/Client/Disabling_Legacy_Staff_client.adoc
new file mode 100644
index 0000000..31bf946
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Client/Disabling_Legacy_Staff_client.adoc
@@ -0,0 +1,13 @@
+Disabling of legacy XUL staff client
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The legacy XUL staff client is no longer supported in Evergreen
+3.2.x and the server-side installation no longer supports a
+direct connection by a version XUL client by default.  All
+users of Evergreen 3.2.x are strongly urged to complete their
+switch to the web staff client as part of upgrading to 3.2.x.
+
+Evergreen administrators who for some reason continue to wish
+to deploy the XUL staff client can do so at their risk by
+supplying `STAFF_CLIENT_STAMP_ID` during the `make install` step
+and using `make_release` to create installers for the staff client.
+However, no community support will be provided for the XUL client.

commit 9d2ddf6d75b674cbf905817fa637b28397e1e354
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Sep 5 16:39:13 2018 -0400

    LP#1790923: adjust or remove references to old staff client in install doc
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/docs/installation/server_installation.adoc b/docs/installation/server_installation.adoc
index 9c7eedd..ffca119 100644
--- a/docs/installation/server_installation.adoc
+++ b/docs/installation/server_installation.adoc
@@ -111,7 +111,7 @@ additional packages may be required.  As the *root* Linux account:
 +
  * To install packages needed for retrieving and managing web dependencies,
    use the <osname>-developer Makefile.install target.  Currently, 
-   this is only needed for building and installing the (preview) browser 
+   this is only needed for building and installing the web
    staff client.
 +
 [source, bash]
@@ -245,9 +245,8 @@ Installation instructions
 -------------------------
 
 1. Once you have configured and compiled Evergreen, issue the following
-   command as the *root* Linux account to install Evergreen, build the server
-   portion of the staff client, and copy example configuration files to
-   `/openils/conf`.
+   command as the *root* Linux account to install Evergreen and copy
+   example configuration files to `/openils/conf`.
 +
 [source, bash]
 ------------------------------------------------------------------------------
@@ -303,7 +302,7 @@ Configure the Apache Web server
 . Use the example configuration files in `Open-ILS/examples/apache/` (for
 Apache versions below 2.4) or `Open-ILS/examples/apache_24/` (for Apache
 versions 2.4 or greater) to configure your Web server for the Evergreen
-catalog, staff client, Web services, and administration interfaces. Issue the
+catalog, web staff client, Web services, and administration interfaces. Issue the
 following commands as the *root* Linux account:
 +
 .Debian Wheezy
@@ -330,7 +329,7 @@ cd /etc/apache2/ssl
 +
 . The `openssl` command cuts a new SSL key for your Apache server. For a
 production server, you should purchase a signed SSL certificate, but you can
-just use a self-signed certificate and accept the warnings in the staff client
+just use a self-signed certificate and accept the warnings in the
 and browser during testing and development. Create an SSL key for the Apache
 server by issuing the following command as the *root* Linux account:
 +
@@ -621,7 +620,7 @@ osrf_control -l --start-all
 export PATH=$PATH:/openils/bin
 ------------------------------------------------------------------------------
 +
-3. As the *opensrf* Linux account, generate the Web files needed by the staff
+3. As the *opensrf* Linux account, generate the Web files needed by the web staff
    client and catalogue and update the organization unit proximity (you need to do
    this the first time you start Evergreen, and after that each time you change the library org unit configuration.
 ):
@@ -639,7 +638,7 @@ autogen.sh
 ------------------------------------------------------------------------------
 +
 If the Apache Web server was running when you started the OpenSRF services, you
-might not be able to successfully log in to the OPAC or staff client until the
+might not be able to successfully log in to the OPAC or web staff client until the
 Apache Web server is restarted.
 
 Testing connections to Evergreen

commit e3032ad3cd1fe2e684c949613b82841efd401867
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Sep 5 16:36:53 2018 -0400

    LP#1790923: make_release no longer munges STAMP_ID in the README
    
    (As that text no longer exists; the installation instructions simply
     will not mention the old staff client.)
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/build/tools/make_release b/build/tools/make_release
index 9ebc432..c3fd2ed 100755
--- a/build/tools/make_release
+++ b/build/tools/make_release
@@ -199,8 +199,6 @@ if [ "$PREV_BRANCH" != "PACKAGE" ]; then
     fi
 
     echo "Applying to README:"
-    echo "STAMP_ID with $UNDER_VERSION"
-    sed -i -e "s/STAMP_ID=rel_[^ ]*/STAMP_ID=rel_$UNDER_VERSION/" $GIT_ABS/README
 
     if [ "$PREV_BRANCH" != "TAG" ]; then
         if [ "$(grep "$RELEASE_PREAMBLE_HEAD" $GIT_ABS/README )" ]; then

commit 23fd5ef82afefa0732df9f4f386888ccffad8b1a
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Sep 5 16:29:22 2018 -0400

    LP#1790923: disable XUL staff client by default
    
    This patch discourages use of the legacy XUL staff client
    by adjusting the installation process so that a versioned XUL
    server directory is not created. Instead, by default XUL
    server files (which is still needed by a few web staff client
    interfaces) end up in /openils/var/web/xul/legacy. During installation
    and upgrade, the /openils/var/web/xul/server symbolic link is
    set to point to /openils/var/web/xul/legacy/server if possible.
    
    If for some reason a given installation of Evergreen 3.2.x does not
    wish to stop using the XUL staff client, STAFF_CLIENT_STAMP_ID
    can still be provided during the 'make install' step, and the
    make_release script can still create the XUL client installers
    if given the -x switch.
    
    Note, however, that use of the XUL staff client in 3.2.x is
    NOT RECOMMENDED and no longer under any guarantee of community
    support.
    
    To test
    -------
    [1] Perform a fresh installation and verify that /openils/var/web/xul/legacy
        is created and that /openils/var/web/xul/server is a symlink
        pointing to /openils/var/web/xul/legacy/server.
    [2] Verify that the web staff client works and that the
        user permissions editor in particular continues to work.
    [3] Perform an upgrade; verify that /openils/var/web/xul/legacy exists
        and that if /openils/var/web/xul/server started out as a symlink,
        it has been repointed.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/xul/staff_client/Makefile.am b/Open-ILS/xul/staff_client/Makefile.am
index 0da672f..7834a01 100644
--- a/Open-ILS/xul/staff_client/Makefile.am
+++ b/Open-ILS/xul/staff_client/Makefile.am
@@ -5,7 +5,7 @@
 export WEBDIR=@localstatedir@/web
 export STAFF_CLIENT_BUILD_ID = $$(/bin/cat build/BUILD_ID)
 export STAFF_CLIENT_VERSION = $$(/bin/cat build/VERSION)
-export STAFF_CLIENT_STAMP_ID = $$(/bin/cat build/STAMP_ID)
+export STAFF_CLIENT_STAMP_ID = legacy
 
 # from http://closure-compiler.googlecode.com/files/compiler-latest.zip  FIXME: Autotools this?
 export CLOSURE_COMPILER_JAR = ~/closure-compiler/compiler.jar
@@ -201,6 +201,26 @@ server-xul: needwebdir build
 	@echo "Copying xul into $(DESTDIR)$(WEBDIR)/xul/$(STAFF_CLIENT_STAMP_ID)"
 	mkdir -p "$(DESTDIR)$(WEBDIR)/xul/$(STAFF_CLIENT_STAMP_ID)"
 	cp -R @top_srcdir@/Open-ILS/xul/staff_client/build/server "$(DESTDIR)${WEBDIR}/xul/${STAFF_CLIENT_STAMP_ID}/"
+	@if [ "${STAFF_CLIENT_STAMP_ID}" == "legacy" ]; then \
+		echo "Installing the legacy parts of the XUL staff client"; \
+		if [ -L "$(DESTDIR)${WEBDIR}/xul/server" ]; then \
+			echo "Updating XUL server symlink to point to legacy code"; \
+			rm $(DESTDIR)${WEBDIR}/xul/server; \
+			( cd "$(DESTDIR)${WEBDIR}/xul"; ln -s legacy/server server; ); \
+		else \
+			if [ ! -f "$(DESTDIR)${WEBDIR}/xul/server" ]; then \
+				echo "Creating XUL server symlink to point to legacy code"; \
+				( cd "$(DESTDIR)${WEBDIR}/xul"; ln -s legacy/server server; ); \
+			else \
+				if [ -d "$(DESTDIR)${WEBDIR}/xul/server" ]; then \
+					echo "WARNING: $(DESTDIR)${WEBDIR}/xul/server is a directory; "; \
+					echo "Not overwriting it, but you should probably do the following: "; \
+					echo "1. Remove or move it, and "; \
+					echo "2. Create a symlink from $(DESTDIR)${WEBDIR}/xul/legacy/server to $(DESTDIR)${WEBDIR}/xul/server "; \
+				fi; \
+			fi; \
+		fi; \
+	fi; # manage the server symlink if installing the 'legacy' staff client
 
 compress-javascript: build
 	@echo "Size of build/ before compression = " `du -sh build/`
diff --git a/docs/installation/server_installation.adoc b/docs/installation/server_installation.adoc
index c057e59..9c7eedd 100644
--- a/docs/installation/server_installation.adoc
+++ b/docs/installation/server_installation.adoc
@@ -248,24 +248,10 @@ Installation instructions
    command as the *root* Linux account to install Evergreen, build the server
    portion of the staff client, and copy example configuration files to
    `/openils/conf`.
-   Change the value of the `STAFF_CLIENT_STAMP_ID` variable to match the version
-   of the staff client that you will use to connect to the Evergreen server.
 +
 [source, bash]
 ------------------------------------------------------------------------------
-make STAFF_CLIENT_STAMP_ID=rel_name install
-------------------------------------------------------------------------------
-+
-2. The server portion of the staff client expects `http://hostname/xul/server`
-   to resolve. Issue the following commands as the *root* Linux account to
-   create a symbolic link pointing to the `server` subdirectory of the server
-   portion of the staff client that we just built using the staff client ID
-   'rel_name':
-+
-[source, bash]
-------------------------------------------------------------------------------
-cd /openils/var/web/xul
-ln -sf rel_name/server server
+make install
 ------------------------------------------------------------------------------
 
 Change ownership of the Evergreen files
diff --git a/docs/installation/server_upgrade.adoc b/docs/installation/server_upgrade.adoc
index 3f1c7ea..c7c1bf0 100644
--- a/docs/installation/server_upgrade.adoc
+++ b/docs/installation/server_upgrade.adoc
@@ -94,6 +94,17 @@ These instructions assume that you have also installed OpenSRF under /openils/.
 [source, bash]
 ------------------------------------------------------------
 cd /home/opensrf/Evergreen-ILS-2.12.0
+make install
+------------------------------------------------------------
++
+
+**Note** that this version of Evergreen does not use the legacy XUL staff
+client by default, but if you wish to use a versioned XUL staff client, you
+can supply `STAFF_CLIENT_STAMP` during the `make install` step like this:
++
+[source, bash]
+------------------------------------------------------------
+cd /home/opensrf/Evergreen-ILS-2.12.0
 make STAFF_CLIENT_STAMP_ID=rel_2_12_rc install
 ------------------------------------------------------------
 +
@@ -104,7 +115,8 @@ make STAFF_CLIENT_STAMP_ID=rel_2_12_rc install
 chown -R opensrf:opensrf /openils
 ------------------------------------------------------------
 +
-. As the *opensrf* user, update the server symlink in /openils/var/web/xul/:
+. (Optional, only if you are using the legacy staff client)
+  As the *opensrf* user, update the server symlink in /openils/var/web/xul/:
 +
 [source, bash]
 -----------------------------------------------------------

commit 81324a87efcaa46b2d4bf289d04280eacf7be625
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Sep 6 17:07:35 2018 -0400

    LP#1775466 Stamping DB upgrade for Ang6 app
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 220c094..1fbf78c 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1128', :eg_version); -- berick/kmlussier
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1129', :eg_version); -- berick
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.acq-grid-settings.sql b/Open-ILS/src/sql/Pg/upgrade/1129.data.acq-grid-settings.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.acq-grid-settings.sql
rename to Open-ILS/src/sql/Pg/upgrade/1129.data.acq-grid-settings.sql
index 19a8322..d1ba1e7 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.acq-grid-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1129.data.acq-grid-settings.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1129', :eg_version);
 
 INSERT into config.workstation_setting_type (name, grp, datatype, label)
 VALUES (

commit 03d94e21e5052156efecefb1ac87de11206c9954
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Sep 6 17:03:58 2018 -0400

    LP#1775466: tweak how make_release prunes eg2/node_modules
    
    Need the -f since some stuff under eg2/node_modules/.cache
    ends up as 0444 for some reason, meaning that 'rm -r' doesn't
    remove those files and complains about it.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/build/tools/make_release b/build/tools/make_release
index 050f95b..9ebc432 100755
--- a/build/tools/make_release
+++ b/build/tools/make_release
@@ -348,7 +348,7 @@ if [ "$BUILD_BROWSER_CLIENT" == "YES" ]; then
     npm install   # fetch build dependencies
     ng build --prod
     # npm cache is big and unnecessary in the final build. remove it.
-    rm -r node_modules 
+    rm -rf node_modules 
     cd ../../../../ # release dir
 else
     echo "Skipping browser client build"

commit 52f83fe0fd7b5765dd541d597def27040eceee35
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Sep 6 16:27:27 2018 -0400

    LP#1775466: add a newly-added entry to the ang6 navbar
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html
index 419f5d5..feed30b 100644
--- a/Open-ILS/src/eg2/src/app/staff/nav.component.html
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html
@@ -202,6 +202,10 @@
             <span class="material-icons">lock</span>
             <span i18n>Manage Authorities</span>
           </a>
+          <a href="/eg/staff/cat/catalog/retrieve_by_authority_id" class="dropdown-item">
+            <span class="material-icons">collections</span>
+            <span i18n>Retrieve Authority Record by ID</span>
+          </a>
         </div>
       </div>
     </div>

commit c37f7ba9e3cfbf7ff0feeb9c77710d706d63697d
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Sep 6 16:26:45 2018 -0400

    LP#1775466: improve release notes for the Angular6 app
    
    More needs to be added to eg_vhost.conf when upgrading an
    existing installation.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc b/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc
index a15583b..80359f3 100644
--- a/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc
@@ -42,6 +42,10 @@ Add the following stanza to /etc/apache2/eg_vhost.conf.
 
 [source,conf]
 --------------------------------------------------------------------------
+RewriteCond %{REQUEST_URI}  ^/eg2/
+RewriteCond %{REQUEST_URI}  !^/eg2/([a-z]{2}-[A-Z]{2})/
+RewriteRule ^/eg2/(.*) https://%{HTTP_HOST}/eg2/en-US/$1 [R=307,L]
+
 <Directory "/openils/var/web/eg2/en-US">                                       
     FallbackResource /eg2/en-US/index.html                                     
 </Directory>  

commit d28eeddfd5dda73db0b017a94c37573f68057f33
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 15:53:28 2018 -0400

    LP#1775466 Angular6 base app release notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc b/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc
new file mode 100644
index 0000000..a15583b
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc
@@ -0,0 +1,66 @@
+Angular6 Base Application
+^^^^^^^^^^^^^^^^^^^^^^^^^
+With Evergreen 3.2, we introduce the initial infrastructure for
+migrating to a new version of Angular.  The structure of the new code
+is quite different from the AngularJS code and it runs as a separate
+application which communicates with the AngularJS app via shared storage
+and in-page URLs that link back and fort between the two.
+
+For this release, users will only be directed to the new Angular site
+when navigating to Administration => Acquisitions Administration.  Once
+on this page, some of the admin interfaces will presented as Angular6
+interfaces, while others will direct users back to the AngularJS
+application.  The Angular6 interfaces are the simpler, grid-based
+interfaces.
+
+Acquisitions Admin Angular6 Interfaces
+++++++++++++++++++++++++++++++++++++++
+
+ * Cancel Reasons
+ * Claim Event Types
+ * Claim Policies
+ * Claim Policy Actions
+ * Claim Types
+ * Currency Types
+ * EDI Accounts
+ * EDI Messages
+ * Exchange Rates
+ * Fund Tags
+ * Invoice Item Types
+ * Invoice Payment Method
+ * Line Item Alerts
+ * Line Item MARC Attribute Definitions
+
+System Admin Upgrade Notes
+++++++++++++++++++++++++++
+
+Like the AngularJS application, Evergreen releases will come with all
+web browser staff client code pre-compiled.  Admins only need to add an
+Apache configuration change.
+
+Add the following stanza to /etc/apache2/eg_vhost.conf.
+
+[source,conf]
+--------------------------------------------------------------------------
+<Directory "/openils/var/web/eg2/en-US">                                       
+    FallbackResource /eg2/en-US/index.html                                     
+</Directory>  
+--------------------------------------------------------------------------
+
+For multi-locale sites, see the bottom section of
+Open-ILS/examples/apache[_24]/eg_vhost.conf.in for a sample fr-CA
+configuration.  The section starts with "/eg2/ client setup and locale
+configuration"
+
+Developer Upgrade Notes
++++++++++++++++++++++++
+
+Developers building Angular code on existing installations need to update 
+their version of NodeJS by re-running the -developer prereqs installer.
+
+[source,sh]
+--------------------------------------------------------------------------
+sudo make -f Open-ILS/src/extras/Makefile.install <osname>-developer
+--------------------------------------------------------------------------
+
+

commit 48055f0527c77b71fb402d2af15c47b4d8db40e0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 15:55:11 2018 -0400

    LP#1775466 make_release builds Angular app
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/build/tools/make_release b/build/tools/make_release
index f82ff3d..050f95b 100755
--- a/build/tools/make_release
+++ b/build/tools/make_release
@@ -332,14 +332,24 @@ XULRUNNER_VERSION=${XULRUNNER_VERSION##XULRUNNER_VERSION=}
 echo "Prepping server download files"
 
 if [ "$BUILD_BROWSER_CLIENT" == "YES" ]; then
+    # AngularJS staff client
     cd ../../../
-    echo "Building browser staff client"
+    echo "Building AngularJS browser staff client"
     cd Open-ILS/web/js/ui/default/staff/
     npm install   # fetch build dependencies
     npm run build-prod # copy to build dir and minify JS files
     # npm cache is big and unnecessary in the final build. remove it.
     rm -r node_modules 
-    cd ../../../../../../../ # release dir
+    cd ../../../../../ # Open-ILS dir
+
+    # Angular staff client
+    echo "Building Angular browser staff client"
+    cd src/eg2
+    npm install   # fetch build dependencies
+    ng build --prod
+    # npm cache is big and unnecessary in the final build. remove it.
+    rm -r node_modules 
+    cd ../../../../ # release dir
 else
     echo "Skipping browser client build"
     cd ../../../../

commit 060af9b3739033bc276b692b45c118b6eb4ba83b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 15:57:51 2018 -0400

    LP#1775466 Add Angular building to install docs
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/docs/installation/server_installation.adoc b/docs/installation/server_installation.adoc
index df3b055..c057e59 100644
--- a/docs/installation/server_installation.adoc
+++ b/docs/installation/server_installation.adoc
@@ -142,8 +142,8 @@ Extra steps for web staff client
 Skip this entire section if you are using an official release tarball downloaded
 from http://evergreen-ils.org/downloads
 
-Install dependencies for web staff client
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Install dependencies for AngularJS web staff client
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 [NOTE]
 You may skip this section if you have installed the
@@ -183,6 +183,47 @@ npm run build-prod
 npm run test
 ------------------------------------------------------------------------------
 
+Install dependencies for Angular web staff client
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[NOTE]
+You may skip this section if you have installed the
+<<optional_developer_additions,optional developer additions>>.  You will still need to do the following
+steps in <<install_files_for_angular_web_staff_client,Install files for web staff client>>.
+
+1. Install the long-term stability (LTS) release of
+https://nodejs.org[Node.js]. Add the Node.js `/bin` directory to your
+environment variable `PATH`.
+
+[[install_files_for_angular_web_staff_client]]
+Install files for web staff client
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. Building, Testing, Minification: The remaining steps all take place within
+   the Angalar staff root:
++
+[source,sh]
+------------------------------------------------------------------------------
+cd $EVERGREEN_ROOT/Open-ILS/src/eg2/
+------------------------------------------------------------------------------
++
+2. Install Project-local Dependencies. npm inspects the 'package.json' file
+   for dependencies and fetches them from the Node package network.
++
+[source,sh]
+------------------------------------------------------------------------------
+npm install   # fetch JS dependencies
+------------------------------------------------------------------------------
++
+3. Run the build script.
++
+[source,sh]
+------------------------------------------------------------------------------
+# build and run tests
+ng build --prod
+npm run test
+------------------------------------------------------------------------------
+
 Configuration and compilation instructions
 ------------------------------------------
 

commit 9a79de4aa33b52f55397bcbcd6cd9f84c3dff149
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 15:56:41 2018 -0400

    LP#1775466 developer prereqs update Node / add angular/cli
    
    NodeJS version updated from v6 to v8.
    Install angular/cli globally, needed for building the Angular app.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/extras/Makefile.install b/Open-ILS/src/extras/Makefile.install
index abb1086..fde4423 100644
--- a/Open-ILS/src/extras/Makefile.install
+++ b/Open-ILS/src/extras/Makefile.install
@@ -40,7 +40,7 @@ export LIBDBI_HOST=http://open-ils.org/~denials/evergreen
 
 # some OSes require a newer version of NodeJS
 # NOTE: Using the LTS binaries for NodeJS
-export NODEJS_VERSION=v6.11.3
+export NODEJS_VERSION=v8.11.4
 export NODEJS_BINDIR=node-$(NODEJS_VERSION)-linux-x64
 export NODEJS_TARBIN=$(NODEJS_BINDIR).tar.xz
 export NODEJS_BINARY="https://nodejs.org/dist/$(NODEJS_VERSION)/$(NODEJS_TARBIN)"
diff --git a/Open-ILS/src/extras/install/Makefile.common b/Open-ILS/src/extras/install/Makefile.common
index 0cc35b1..c542287 100644
--- a/Open-ILS/src/extras/install/Makefile.common
+++ b/Open-ILS/src/extras/install/Makefile.common
@@ -33,7 +33,7 @@ install_nodejs_from_source:
 	wget -N $(NODEJS_BINARY)
 	tar -C /usr/local --strip-components 1 -xJf $(NODEJS_TARBIN)
 	npm update
-	npm install -g grunt-cli
+	npm install -g @angular/cli
 
 clean:
 	make -C $(LIBDBI) clean

commit e8cf82d0ddb13dfbcbf96bf7528ce34e9574c0d1
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 15:58:23 2018 -0400

    LP#1775466 Angular Apache configuration updates
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/apache/eg_vhost.conf.in b/Open-ILS/examples/apache/eg_vhost.conf.in
index 501b5e2..f32e6fc 100644
--- a/Open-ILS/examples/apache/eg_vhost.conf.in
+++ b/Open-ILS/examples/apache/eg_vhost.conf.in
@@ -826,3 +826,34 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT]
 # and you will suffer a performance hit.
 #RewriteCond %{HTTPS} off
 #RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [NE,R,L]
+
+
+# ------------------------------------------------------------------------
+# /eg2/ client setup and locale configuration.
+#
+# If a valid locale cookie is present that does not match the current
+# path, redirect to the requested locale path.
+# Otherwise, if no locale is active, redirect to the default locale.
+
+# fr-CA
+#RewriteCond %{REQUEST_URI} ^/eg2/
+#RewriteCond %{REQUEST_URI} !^/eg2/fr-CA/
+#RewriteCond %{HTTP_COOKIE} eg_locale=fr_ca
+#RewriteRule ^/eg2/(.*) https://%{HTTP_HOST}/eg2/fr-CA/$1 [R=307,L]
+
+# Default / en-US.
+# No alternate supported cookie provided.
+RewriteCond %{REQUEST_URI}  ^/eg2/
+RewriteCond %{REQUEST_URI}  !^/eg2/([a-z]{2}-[A-Z]{2})/
+RewriteRule ^/eg2/(.*) https://%{HTTP_HOST}/eg2/en-US/$1 [R=307,L]
+
+# en-US build
+# This is the only required configuration when only using the default locale.
+<Directory "/openils/var/web/eg2/en-US">
+    FallbackResource /eg2/en-US/index.html
+</Directory>
+
+# fr-CA build
+#<Directory "/openils/var/web/eg2/fr-CA">
+#    FallbackResource /eg2/fr-CA/index.html
+#</Directory>
diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in
index 73d6239..cfeb27e 100644
--- a/Open-ILS/examples/apache_24/eg_vhost.conf.in
+++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in
@@ -823,3 +823,33 @@ RewriteRule ^/openurl$ ${openurl:%1} [NE,PT]
 # and you will suffer a performance hit.
 #RewriteCond %{HTTPS} off
 #RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [NE,R,L]
+
+# ------------------------------------------------------------------------
+# /eg2/ client setup and locale configuration.
+#
+# If a valid locale cookie is present that does not match the current
+# path, redirect to the requested locale path.
+# Otherwise, if no locale is active, redirect to the default locale.
+
+# fr-CA
+#RewriteCond %{REQUEST_URI} ^/eg2/
+#RewriteCond %{REQUEST_URI} !^/eg2/fr-CA/
+#RewriteCond %{HTTP_COOKIE} eg_locale=fr_ca
+#RewriteRule ^/eg2/(.*) https://%{HTTP_HOST}/eg2/fr-CA/$1 [R=307,L]
+
+# Default / en-US.
+# No alternate supported cookie provided.
+RewriteCond %{REQUEST_URI}  ^/eg2/
+RewriteCond %{REQUEST_URI}  !^/eg2/([a-z]{2}-[A-Z]{2})/
+RewriteRule ^/eg2/(.*) https://%{HTTP_HOST}/eg2/en-US/$1 [R=307,L]
+
+# en-US build
+# This is the only required configuration when only using the default locale.
+<Directory "/openils/var/web/eg2/en-US">
+    FallbackResource /eg2/en-US/index.html
+</Directory>
+
+# fr-CA build
+#<Directory "/openils/var/web/eg2/fr-CA">
+#    FallbackResource /eg2/fr-CA/index.html
+#</Directory>

commit a29408d79df4b5240bd206fcd2158cadc9547bae
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 15:54:21 2018 -0400

    LP#1775466 Acq admin grid workstation settings
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index e0e02c6..6503dfe 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -19312,5 +19312,106 @@ VALUES (
     )
 );
 
+INSERT into config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.admin.acq.cancel_reason', 'gui', 'object',
+    oils_i18n_gettext (
+        'eg.grid.admin.acq.cancel_reason',
+        'Grid Config: admin.acq.cancel_reason',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_event_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_event_type',
+        'Grid Config: admin.acq.claim_event_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_policy', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_policy',
+        'Grid Config: admin.acq.claim_policy',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_policy_action', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_policy_action',
+        'Grid Config: admin.acq.claim_policy_action',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_type',
+        'Grid Config: admin.acq.claim_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.currency_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.currency_type',
+        'Grid Config: admin.acq.currency_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.edi_account', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.edi_account',
+        'Grid Config: admin.acq.edi_account',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.edi_message', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.edi_message',
+        'Grid Config: admin.acq.edi_message',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.exchange_rate', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.exchange_rate',
+        'Grid Config: admin.acq.exchange_rate',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.fund_tag', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.fund_tag',
+        'Grid Config: admin.acq.fund_tag',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.invoice_item_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.invoice_item_type',
+        'Grid Config: admin.acq.invoice_item_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.invoice_payment_method', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.invoice_payment_method',
+        'Grid Config: admin.acq.invoice_payment_method',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.lineitem_alert_text', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.lineitem_alert_text',
+        'Grid Config: admin.acq.lineitem_alert_text',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.lineitem_marc_attr_definition', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.lineitem_marc_attr_definition',
+        'Grid Config: admin.acq.lineitem_marc_attr_definition',
+        'cwst', 'label'
+    )
+);
+
 
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.acq-grid-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.acq-grid-settings.sql
new file mode 100644
index 0000000..19a8322
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.acq-grid-settings.sql
@@ -0,0 +1,106 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT into config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.admin.acq.cancel_reason', 'gui', 'object',
+    oils_i18n_gettext (
+        'eg.grid.admin.acq.cancel_reason',
+        'Grid Config: admin.acq.cancel_reason',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_event_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_event_type',
+        'Grid Config: admin.acq.claim_event_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_policy', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_policy',
+        'Grid Config: admin.acq.claim_policy',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_policy_action', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_policy_action',
+        'Grid Config: admin.acq.claim_policy_action',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.claim_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.claim_type',
+        'Grid Config: admin.acq.claim_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.currency_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.currency_type',
+        'Grid Config: admin.acq.currency_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.edi_account', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.edi_account',
+        'Grid Config: admin.acq.edi_account',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.edi_message', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.edi_message',
+        'Grid Config: admin.acq.edi_message',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.exchange_rate', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.exchange_rate',
+        'Grid Config: admin.acq.exchange_rate',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.fund_tag', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.fund_tag',
+        'Grid Config: admin.acq.fund_tag',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.invoice_item_type', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.invoice_item_type',
+        'Grid Config: admin.acq.invoice_item_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.invoice_payment_method', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.invoice_payment_method',
+        'Grid Config: admin.acq.invoice_payment_method',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.lineitem_alert_text', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.lineitem_alert_text',
+        'Grid Config: admin.acq.lineitem_alert_text',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.acq.lineitem_marc_attr_definition', 'gui', 'object',
+    oils_i18n_gettext (
+    'eg.grid.admin.acq.lineitem_marc_attr_definition',
+        'Grid Config: admin.acq.lineitem_marc_attr_definition',
+        'cwst', 'label'
+    )
+);
+
+COMMIT;

commit 038cd40207c5f63001bae2b68defdf44163c352d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 16:04:19 2018 -0400

    LP#1775466 AngularJS updates for Angular integration
    
    Changes include:
    
    * Navbar links to Angular acquisitions admin page.
    * Auth cookie migration tool for moving AngularJS cookies from /eg/staff
      to /.
    * Store last printed receipt (etc) in its final compiled form so it can
      be directly reprinted without having to recompile (via AngularJS).
      This allows the reprint-last action to work in the Angular app.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index a5d9888..7d9e1ce 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -251,6 +251,18 @@
               [% l('Search the Catalog') %]
             </a>
           </li>
+          <!--
+            Link to experimental Angular staff catalog.
+            Leaving disabled until more functionality can be fleshed out.
+          -->
+          <!--
+          <li>
+            <a href="/eg2/staff/catalog/search" target="_self">
+              <span class="glyphicon glyphicon-search"></span>
+              <span>[% l('Staff Catalog (Experimental)') %]</span>
+            </a>
+          </li>
+          -->
           <li>
             <a href="./cat/bucket/record/view" target="_self">
               <span class="glyphicon glyphicon-list-alt"></span>
@@ -484,7 +496,7 @@
             </a>
           </li>
           <li>
-            <a href="./admin/acq/index" target="_self">
+            <a href="/eg2/staff/admin/acq/splash">
               <span class="glyphicon glyphicon-usd"></span>
               [% l('Acquisitions Administration') %]
             </a>
diff --git a/Open-ILS/web/js/ui/default/staff/services/auth.js b/Open-ILS/web/js/ui/default/staff/services/auth.js
index c46e37e..03e5d43 100644
--- a/Open-ILS/web/js/ui/default/staff/services/auth.js
+++ b/Open-ILS/web/js/ui/default/staff/services/auth.js
@@ -63,6 +63,10 @@ function($q , $timeout , $rootScope , $window , $location , egNet , egHatch) {
      * authtoken is found, otherwise rejected */
     service.testAuthToken = function() {
         var deferred = $q.defer();
+
+        // Move legacy cookies from /eg/staff to / before fetching the token.
+        egHatch.migrateAuthCookies();
+
         var token = service.token();
 
         if (token) {
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index bb12479..467091a 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -352,6 +352,24 @@ angular.module('egCoreMod')
         }
     }
 
+    // Force auth cookies to live under path "/" instead of "/eg/staff"
+    // so they may be shared with the Angular app.
+    // There's no way to tell under what path a cookie is stored in
+    // the browser, all we can do is migrate it regardless.
+    service.migrateAuthCookies = function() {
+        [   'eg.auth.token', 
+            'eg.auth.time', 
+            'eg.auth.token.oc', 
+            'eg.auth.time.oc'
+        ].forEach(function(key) {
+            var val = service.getLoginSessionItem(key);
+            if (val) {
+                $cookies.remove(key, {path: '/eg/staff/'});
+                service.setLoginSessionItem(key, val);
+            }
+        });
+    }
+
     service.getLoginSessionItem = function(key) {
         var val = $cookies.get(key);
         if (val == null) return;
@@ -651,7 +669,7 @@ angular.module('egCoreMod')
         service.addLoginSessionKey(key);
         if (jsonified === undefined ) 
             jsonified = JSON.stringify(value);
-        $cookies.put(key, jsonified);
+        $cookies.put(key, jsonified, {path: '/'});
     }
 
     // Set the value for the given key.  
@@ -721,7 +739,7 @@ angular.module('egCoreMod')
 
     service.removeLoginSessionItem = function(key) {
         service.removeLoginSessionKey(key);
-        $cookies.remove(key);
+        $cookies.remove(key, {path: '/'});
     }
 
     service.removeSessionItem = function(key) {
diff --git a/Open-ILS/web/js/ui/default/staff/services/print.js b/Open-ILS/web/js/ui/default/staff/services/print.js
index fee4c8a..d12a6cd 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -156,20 +156,17 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
                   content;
 
         }).then(function(content) {
-            service.last_print.content = content;
-            service.last_print.content_type = type;
-            service.last_print.printScope = printScope
-
-            egHatch.setItem('eg.print.last_printed', service.last_print);
 
             // Ingest the content into the page DOM.
-            return service.ingest_print_content(
-                service.last_print.content_type,
-                service.last_print.content,
-                service.last_print.printScope
-            );
+            return service.ingest_print_content(type, content, printScope);
+
+        }).then(function(html) { 
+
+            // Note browser ignores print context
+            service.last_print.content = html;
+            service.last_print.content_type = type;
+            egHatch.setItem('eg.print.last_printed', service.last_print);
 
-        }).then(function() { 
             $window.print();
         });
     }
@@ -192,9 +189,7 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
                 } else {
                     promise.then(function () {
                         service.ingest_print_content(
-                            service.last_print.content_type,
-                            service.last_print.content,
-                            service.last_print.printScope
+                            null, null, null, service.last_print.content
                         ).then(function() { $window.print() });
                     });
                 }
@@ -300,7 +295,19 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
                 // For local printing, this lets us print directly from the
                 // DOM with print CSS.
                 // Returns a promise reolved with the compiled HTML as a string.
-                egPrint.ingest_print_content = function(type, content, printScope) {
+                //
+                // If a pre-compiled HTML string is provided, it's inserted
+                // as-is into the DOM for browser printing without any 
+                // additional interpolation.  This is useful for reprinting,
+                // previously compiled content.
+                egPrint.ingest_print_content = 
+                    function(type, content, printScope, compiledHtml) {
+
+                    if (compiledHtml) {
+                        $scope.elm.html(compiledHtml);
+                        return $q.when(compiledHtml);
+                    }
+                        
                     $scope.elm.html(content);
 
                     var sub_scope = $scope.$new(true);

commit 6c706f454b2b9cdf2d46df3c60e53a0d3cb531d7
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 16:11:41 2018 -0400

    LP#1775466 Angular(6) base application
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/.gitignore b/.gitignore
index 62dea57..bfcf501 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,3 +360,4 @@ Open-ILS/web/js/ui/default/staff/build/
 Open-ILS/web/js/ui/default/staff/node_modules/
 Open-ILS/web/js/ui/default/staff/bower_components/
 Open-ILS/web/js/ui/default/common/build/
+Open-ILS/web/eg2/
diff --git a/Open-ILS/src/eg2/.editorconfig b/Open-ILS/src/eg2/.editorconfig
new file mode 100644
index 0000000..6e87a00
--- /dev/null
+++ b/Open-ILS/src/eg2/.editorconfig
@@ -0,0 +1,13 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/Open-ILS/src/eg2/.gitignore b/Open-ILS/src/eg2/.gitignore
new file mode 100644
index 0000000..f59404c
--- /dev/null
+++ b/Open-ILS/src/eg2/.gitignore
@@ -0,0 +1,49 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# ------
+# Added locally...
+# ------
+
+src/test_data/IDL2js.js
+
+# ------
+# compiled output
+/dist
+/tmp
+/out-tsc
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+# System Files
+.DS_Store
+Thumbs.db
diff --git a/Open-ILS/src/eg2/CHEAT_SHEET.adoc b/Open-ILS/src/eg2/CHEAT_SHEET.adoc
new file mode 100644
index 0000000..84f4e5d
--- /dev/null
+++ b/Open-ILS/src/eg2/CHEAT_SHEET.adoc
@@ -0,0 +1,31 @@
+= Evergreen Angular App Cheatsheet
+
+== Basics
+
+[source,sh]
+---------------------------------------------------------------------
+npm update
+npm install
+ng lint             # check code formatting
+npm run test        # unit tests
+ng build --watch    # compile dev mode
+ng build --prod     # compile production mode
+---------------------------------------------------------------------
+
+== OPTIONAL: Adding a Locale 
+
+* Using fr-CA as an example.
+* An fr-CA configuration is supplied by default.  Additional configs
+  must be added where needed.
+* Currently translation builds are only available on --prod build mode.
+* Uncomment the locale lines in eg_vhost.conf and restart apache.
+* TODO: expand docs on package.json file changes required to add locales.
+
+[source,sh]
+---------------------------------------------------------------------
+npm run export-strings
+npm run merge-strings -- fr-CA
+# APPLY TRANSLATIONS TO src/locale/messages.fr-CA.xlf
+npm run build-fr-CA # modify package.json for other locales
+---------------------------------------------------------------------
+
diff --git a/Open-ILS/src/eg2/angular.json b/Open-ILS/src/eg2/angular.json
new file mode 100644
index 0000000..e50c8db
--- /dev/null
+++ b/Open-ILS/src/eg2/angular.json
@@ -0,0 +1,155 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "newProjectRoot": "projects",
+  "projects": {
+    "eg": {
+      "root": "",
+      "sourceRoot": "src",
+      "projectType": "application",
+      "architect": {
+        "build": {
+          "builder": "@angular-devkit/build-angular:browser",
+          "options": {
+            "baseHref": "/eg2/en-US",
+            "deployUrl": "/eg2/en-US/",
+            "outputPath": "../../web/eg2/en-US",
+            "index": "src/index.html",
+            "main": "src/main.ts",
+            "tsConfig": "src/tsconfig.app.json",
+            "polyfills": "src/polyfills.ts",
+            "assets": [
+              "src/assets",
+              "src/favicon.ico"
+            ],
+            "styles": [
+              "src/styles.css"
+            ],
+            "scripts": []
+          },
+          "configurations": {
+            "production": {
+              "optimization": true,
+              "outputHashing": "all",
+              "sourceMap": false,
+              "extractCss": true,
+              "namedChunks": false,
+              "aot": true,
+              "extractLicenses": true,
+              "vendorChunk": false,
+              "buildOptimizer": true,
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ]
+            },
+            "production-fr-CA": {
+              "optimization": true,
+              "outputHashing": "all",
+              "sourceMap": false,
+              "extractCss": true,
+              "namedChunks": false,
+              "aot": true,
+              "extractLicenses": true,
+              "vendorChunk": false,
+              "buildOptimizer": true,
+              "i18nFile": "src/locale/messages.fr-CA.xlf",
+              "i18nFormat": "xlf",
+              "i18nLocale": "fr-CA",
+              "i18nMissingTranslation": "ignore",
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ]
+            }
+          }
+        },
+        "serve": {
+          "builder": "@angular-devkit/build-angular:dev-server",
+          "options": {
+            "browserTarget": "eg:build"
+          },
+          "configurations": {
+            "production": {
+              "browserTarget": "eg:build:production"
+            }
+          }
+        },
+        "extract-i18n": {
+          "builder": "@angular-devkit/build-angular:extract-i18n",
+          "options": {
+            "browserTarget": "eg:build"
+          }
+        },
+        "test": {
+          "builder": "@angular-devkit/build-angular:karma",
+          "options": {
+            "main": "src/test.ts",
+            "karmaConfig": "./karma.conf.js",
+            "polyfills": "src/polyfills.ts",
+            "tsConfig": "src/tsconfig.spec.json",
+            "scripts": [],
+            "styles": [
+              "src/styles.css"
+            ],
+            "assets": [
+              "src/assets",
+              "src/favicon.ico"
+            ]
+          }
+        },
+        "lint": {
+          "builder": "@angular-devkit/build-angular:tslint",
+          "options": {
+            "tsConfig": [
+              "src/tsconfig.app.json",
+              "src/tsconfig.spec.json"
+            ],
+            "exclude": [
+              "**/node_modules/**"
+            ]
+          }
+        }
+      }
+    },
+    "eg-e2e": {
+      "root": "",
+      "sourceRoot": "",
+      "projectType": "application",
+      "architect": {
+        "e2e": {
+          "builder": "@angular-devkit/build-angular:protractor",
+          "options": {
+            "protractorConfig": "./protractor.conf.js",
+            "devServerTarget": "eg:serve"
+          }
+        },
+        "lint": {
+          "builder": "@angular-devkit/build-angular:tslint",
+          "options": {
+            "tsConfig": [
+              "e2e/tsconfig.e2e.json"
+            ],
+            "exclude": [
+              "**/node_modules/**"
+            ]
+          }
+        }
+      }
+    }
+  },
+  "defaultProject": "eg",
+  "schematics": {
+    "@schematics/angular:component": {
+      "prefix": "eg",
+      "styleext": "css"
+    },
+    "@schematics/angular:directive": {
+      "prefix": "eg"
+    }
+  }
+}
diff --git a/Open-ILS/src/eg2/e2e/app.e2e-spec.ts b/Open-ILS/src/eg2/e2e/app.e2e-spec.ts
new file mode 100644
index 0000000..c2a69a8
--- /dev/null
+++ b/Open-ILS/src/eg2/e2e/app.e2e-spec.ts
@@ -0,0 +1,14 @@
+import { AppPage } from './app.po';
+
+describe('eg App', () => {
+  let page: AppPage;
+
+  beforeEach(() => {
+    page = new AppPage();
+  });
+
+  it('should display welcome message', () => {
+    page.navigateTo();
+    expect(page.getParagraphText()).toEqual('Welcome to app!');
+  });
+});
diff --git a/Open-ILS/src/eg2/e2e/app.po.ts b/Open-ILS/src/eg2/e2e/app.po.ts
new file mode 100644
index 0000000..82ea75b
--- /dev/null
+++ b/Open-ILS/src/eg2/e2e/app.po.ts
@@ -0,0 +1,11 @@
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+  navigateTo() {
+    return browser.get('/');
+  }
+
+  getParagraphText() {
+    return element(by.css('app-root h1')).getText();
+  }
+}
diff --git a/Open-ILS/src/eg2/e2e/tsconfig.e2e.json b/Open-ILS/src/eg2/e2e/tsconfig.e2e.json
new file mode 100644
index 0000000..1d9e5ed
--- /dev/null
+++ b/Open-ILS/src/eg2/e2e/tsconfig.e2e.json
@@ -0,0 +1,14 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/e2e",
+    "baseUrl": "./",
+    "module": "commonjs",
+    "target": "es5",
+    "types": [
+      "jasmine",
+      "jasminewd2",
+      "node"
+    ]
+  }
+}
diff --git a/Open-ILS/src/eg2/karma.conf.js b/Open-ILS/src/eg2/karma.conf.js
new file mode 100644
index 0000000..63982de
--- /dev/null
+++ b/Open-ILS/src/eg2/karma.conf.js
@@ -0,0 +1,43 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+  config.set({
+    basePath: '',
+    frameworks: ['jasmine', '@angular-devkit/build-angular'],
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-chrome-launcher'),
+      require('karma-phantomjs-launcher'),
+      require('karma-jasmine-html-reporter'),
+      require('karma-coverage-istanbul-reporter'),
+      require('@angular-devkit/build-angular/plugins/karma')
+    ],
+    client:{
+      clearContext: false // leave Jasmine Spec Runner output visible in browser
+    },
+    coverageIstanbulReporter: {
+      dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
+      fixWebpackSourcePaths: true
+    },
+    angularCli: {
+      environment: 'dev'
+    },
+    reporters: ['progress', 'kjhtml'],
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['PhantomJS'],
+    singleRun: true,
+    files: [
+      '/openils/lib/javascript/md5.js',
+      '/openils/lib/javascript/JSON_v1.js',
+      '/openils/lib/javascript/opensrf.js',
+      '/openils/lib/javascript/opensrf_ws.js',
+      // mock data for testing only
+      'src/test_data/IDL2js.js',
+      'src/test_data/eg_mock.js',
+    ]
+  });
+};
diff --git a/Open-ILS/src/eg2/package-lock.json b/Open-ILS/src/eg2/package-lock.json
new file mode 100644
index 0000000..e7ce3aa
--- /dev/null
+++ b/Open-ILS/src/eg2/package-lock.json
@@ -0,0 +1,10689 @@
+{
+  "name": "eg",
+  "version": "0.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@angular-devkit/architect": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.7.5.tgz",
+      "integrity": "sha512-zwCpGdx3JDE+Y+LiWh9ErRX+fpFPTRHtEd2PDJmfQsdlIWfjxSR5U9vi3+bSRW2n6IFiH2GCYMS31R64rfMwbg==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "@angular-devkit/build-angular": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.7.5.tgz",
+      "integrity": "sha512-FYd2RigCbvm1i0aM1p+jO2145qm56iPgcW2TK3LBxllWFoz5v+wb086/aDzATG+2ETDZO1uENiVTWu5RSkYcSw==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/architect": "0.7.5",
+        "@angular-devkit/build-optimizer": "0.7.5",
+        "@angular-devkit/build-webpack": "0.7.5",
+        "@angular-devkit/core": "0.7.5",
+        "@ngtools/webpack": "6.1.5",
+        "ajv": "~6.4.0",
+        "autoprefixer": "^8.4.1",
+        "circular-dependency-plugin": "^5.0.2",
+        "clean-css": "^4.1.11",
+        "copy-webpack-plugin": "^4.5.2",
+        "file-loader": "^1.1.11",
+        "glob": "^7.0.3",
+        "html-webpack-plugin": "^3.0.6",
+        "istanbul": "^0.4.5",
+        "istanbul-instrumenter-loader": "^3.0.1",
+        "karma-source-map-support": "^1.2.0",
+        "less": "^3.7.1",
+        "less-loader": "^4.1.0",
+        "license-webpack-plugin": "^1.3.1",
+        "loader-utils": "^1.1.0",
+        "mini-css-extract-plugin": "~0.4.0",
+        "minimatch": "^3.0.4",
+        "node-sass": "^4.9.3",
+        "opn": "^5.1.0",
+        "parse5": "^4.0.0",
+        "portfinder": "^1.0.13",
+        "postcss": "^6.0.22",
+        "postcss-import": "^11.1.0",
+        "postcss-loader": "^2.1.5",
+        "postcss-url": "^7.3.2",
+        "raw-loader": "^0.5.1",
+        "rxjs": "^6.0.0",
+        "sass-loader": "~6.0.7",
+        "semver": "^5.5.0",
+        "source-map-loader": "^0.2.3",
+        "source-map-support": "^0.5.0",
+        "stats-webpack-plugin": "^0.6.2",
+        "style-loader": "^0.21.0",
+        "stylus": "^0.54.5",
+        "stylus-loader": "^3.0.2",
+        "tree-kill": "^1.2.0",
+        "uglifyjs-webpack-plugin": "^1.2.5",
+        "url-loader": "^1.0.1",
+        "webpack": "~4.9.2",
+        "webpack-dev-middleware": "^3.1.3",
+        "webpack-dev-server": "^3.1.4",
+        "webpack-merge": "^4.1.2",
+        "webpack-sources": "^1.1.0",
+        "webpack-subresource-integrity": "^1.1.0-rc.4"
+      }
+    },
+    "@angular-devkit/build-optimizer": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.7.5.tgz",
+      "integrity": "sha512-iZYUjNax6epTA4JjnDxhs6MQUtmwM04ZkJkTE3tVc01e80+wJ/f3+ja22BBVul2MsqchOsTUSQIJY3HxbV5aWw==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "source-map": "^0.5.6",
+        "typescript": "~2.9.1",
+        "webpack-sources": "^1.1.0"
+      },
+      "dependencies": {
+        "typescript": {
+          "version": "2.9.2",
+          "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
+          "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
+          "dev": true
+        }
+      }
+    },
+    "@angular-devkit/build-webpack": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.7.5.tgz",
+      "integrity": "sha512-PSkhBwJBLRMiBUGlK15CaVwbU4RzfCdF/GFS/CZSCsA3plLDJy+vXAPrUiuGvqYt/sVKBRavsNaEBCbK1t+1ig==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/architect": "0.7.5",
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "@angular-devkit/core": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.7.5.tgz",
+      "integrity": "sha512-r99BZvvuNAqSRm05jXfx0sb3Ip0zvHPtAM6NReXzWPoqaVFpjVUdj/CKA+9HWG/Zt9meG9pEQt/HKK8UXaZDVA==",
+      "dev": true,
+      "requires": {
+        "ajv": "~6.4.0",
+        "chokidar": "^2.0.3",
+        "rxjs": "^6.0.0",
+        "source-map": "^0.5.6"
+      }
+    },
+    "@angular-devkit/schematics": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.7.5.tgz",
+      "integrity": "sha512-E7HkQeJawUskf2gPnogMc+cTdjJ2Iv3QEZOgprh/ExEmBYByWkGDRX5fQOuy8wME8VZqUBvQACZaVkEredn5EA==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "@angular/animations": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.1.6.tgz",
+      "integrity": "sha512-fK7onQeVsPgUx/sFcBvcGisuIuxvodzATpoKV9SnsQc6xWE5qsvJRZijrzZIN+Hxy/DgsLaVWRCPn1hG75/D2Q==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/cli": {
+      "version": "6.1.5",
+      "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-6.1.5.tgz",
+      "integrity": "sha512-QNVUSC8mPdiaxubneqNZISy+wec3gwbKoXjcaQ9/45baOnp662j2iJXwiMh6Atn0YUM4u1iUsz1uHyARMtgZmw==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/architect": "0.7.5",
+        "@angular-devkit/core": "0.7.5",
+        "@angular-devkit/schematics": "0.7.5",
+        "@schematics/angular": "0.7.5",
+        "@schematics/update": "0.7.5",
+        "opn": "^5.3.0",
+        "rxjs": "^6.0.0",
+        "semver": "^5.1.0",
+        "symbol-observable": "^1.2.0",
+        "yargs-parser": "^10.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "yargs-parser": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
+      }
+    },
+    "@angular/common": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.1.6.tgz",
+      "integrity": "sha512-aFQcfCB2vFfNqR6/e6R34JjFpIFmF3zqr6Ubti1PJOsRuhITZHG/qRYIYA7mh1KVkkf0VXC56B+8QzYbdGcKOQ==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/compiler": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.1.6.tgz",
+      "integrity": "sha512-Z9Og0DVH5krG/xMhfcRJMr5GF2HzqnG3f6Hr+e6d6FB8oehnCX/w9b34zZfVGUWAydAYj32SpXJLE6fQm/ljzA==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/compiler-cli": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-6.1.6.tgz",
+      "integrity": "sha512-CvgQXuuUJDfmCwnuhZec41aMAiY7nJMSMJxvZWNbFLRiwq+05LiHc7EJYDc6uVQmddWmSqGwfyghjVaiaKJGMg==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^1.4.2",
+        "minimist": "^1.2.0",
+        "reflect-metadata": "^0.1.2",
+        "tsickle": "^0.32.1"
+      },
+      "dependencies": {
+        "anymatch": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+          "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+          "dev": true,
+          "requires": {
+            "micromatch": "^2.1.5",
+            "normalize-path": "^2.0.0"
+          }
+        },
+        "arr-diff": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+          "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+          "dev": true,
+          "requires": {
+            "arr-flatten": "^1.0.1"
+          }
+        },
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "1.8.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+          "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^1.8.1",
+            "preserve": "^0.2.0",
+            "repeat-element": "^1.1.2"
+          }
+        },
+        "chokidar": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+          "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+          "dev": true,
+          "requires": {
+            "anymatch": "^1.3.0",
+            "async-each": "^1.0.0",
+            "fsevents": "^1.0.0",
+            "glob-parent": "^2.0.0",
+            "inherits": "^2.0.1",
+            "is-binary-path": "^1.0.0",
+            "is-glob": "^2.0.0",
+            "path-is-absolute": "^1.0.0",
+            "readdirp": "^2.0.0"
+          }
+        },
+        "expand-brackets": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+          "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+          "dev": true,
+          "requires": {
+            "is-posix-bracket": "^0.1.0"
+          }
+        },
+        "extglob": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+          "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "^2.0.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        },
+        "micromatch": {
+          "version": "2.3.11",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+          "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+          "dev": true,
+          "requires": {
+            "arr-diff": "^2.0.0",
+            "array-unique": "^0.2.1",
+            "braces": "^1.8.2",
+            "expand-brackets": "^0.1.4",
+            "extglob": "^0.3.1",
+            "filename-regex": "^2.0.0",
+            "is-extglob": "^1.0.0",
+            "is-glob": "^2.0.1",
+            "kind-of": "^3.0.2",
+            "normalize-path": "^2.0.1",
+            "object.omit": "^2.0.0",
+            "parse-glob": "^3.0.4",
+            "regex-cache": "^0.4.2"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "@angular/core": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.1.6.tgz",
+      "integrity": "sha512-RFkxNDq8iIfO1SaOuUYqOGD/pujMqifJ9FeVg8M2v7ucW01coXAG0IwqUEMMShQj3GGJGHj+F9BNswN7aD2uvw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/forms": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.1.6.tgz",
+      "integrity": "sha512-6ddk8bhsEtSONctj9PUrEJnTTRL1xHCULaxo2N4GQh5XyV8ScRM0ewOTLcpoL0IU4lgtQmU0VsLWdQvKr3g3Ng==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/http": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.1.6.tgz",
+      "integrity": "sha512-V4qF68tUSsc3cKvQERJmpfXgZSKgxhb67I2jAfmwU9mEH66wh9FNfZ0b0GPV9hXoCulw3POz4ZUwZZ1E6mLy4A==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/language-service": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-6.1.6.tgz",
+      "integrity": "sha512-EEtM6mJtiEgmmm3VjzJxv5BavvonaBFtBrPUcevIW851DtIqn4CS8yDcLcGFiSvSLtAYxRX8dkacPv9vvM1Khg==",
+      "dev": true
+    },
+    "@angular/platform-browser": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.1.6.tgz",
+      "integrity": "sha512-fwI/w+MhdolVJEfdoCSZFarQo+SctG1pNa+V3PxMkXhxnAbv7oWPQdxzdCrhTWdxJTJ5enSfumMmlJEZtg1bag==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/platform-browser-dynamic": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.1.6.tgz",
+      "integrity": "sha512-Ep4vq2ssb1r8XOAw7dJW530vzFKKVY5fj0CYp7VMPfDkwYolEG4TBKQ/ouJkF8n/jdDVFP73+MzU1TLa9/lMQQ==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@angular/router": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.1.6.tgz",
+      "integrity": "sha512-fOFeOe3uBrSRUYhXdWxHjDPf80eq3ZNCeWfujzfBADtcmiezlO7cxc1v5Eu81t577frU/3z+w8JvmF257p4RZg==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@babel/code-frame": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.51.tgz",
+      "integrity": "sha1-vXHZsZKvl435FYKdOdQJRFZDmgw=",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "7.0.0-beta.51"
+      }
+    },
+    "@babel/generator": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.51.tgz",
+      "integrity": "sha1-bHV1/952HQdIXgS67cA5LG2eMPY=",
+      "dev": true,
+      "requires": {
+        "@babel/types": "7.0.0-beta.51",
+        "jsesc": "^2.5.1",
+        "lodash": "^4.17.5",
+        "source-map": "^0.5.0",
+        "trim-right": "^1.0.1"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "2.5.1",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz",
+          "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=",
+          "dev": true
+        }
+      }
+    },
+    "@babel/helper-function-name": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.51.tgz",
+      "integrity": "sha1-IbSHSiJ8+Z7K/MMKkDAtpaJkBWE=",
+      "dev": true,
+      "requires": {
+        "@babel/helper-get-function-arity": "7.0.0-beta.51",
+        "@babel/template": "7.0.0-beta.51",
+        "@babel/types": "7.0.0-beta.51"
+      }
+    },
+    "@babel/helper-get-function-arity": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.51.tgz",
+      "integrity": "sha1-MoGy0EWvlcFyzpGyCCXYXqRnZBE=",
+      "dev": true,
+      "requires": {
+        "@babel/types": "7.0.0-beta.51"
+      }
+    },
+    "@babel/helper-split-export-declaration": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.51.tgz",
+      "integrity": "sha1-imw/ZsTSZTUvwHdIT59ugKUauXg=",
+      "dev": true,
+      "requires": {
+        "@babel/types": "7.0.0-beta.51"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.51.tgz",
+      "integrity": "sha1-6IRK4loVlcz9QriWI7Q3bKBtIl0=",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.0"
+      }
+    },
+    "@babel/parser": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.51.tgz",
+      "integrity": "sha1-J87C30Cd9gr1gnDtj2qlVAnqhvY=",
+      "dev": true
+    },
+    "@babel/template": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.51.tgz",
+      "integrity": "sha1-lgKkCuvPNXrpZ34lMu9fyBD1+/8=",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "7.0.0-beta.51",
+        "@babel/parser": "7.0.0-beta.51",
+        "@babel/types": "7.0.0-beta.51",
+        "lodash": "^4.17.5"
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.51.tgz",
+      "integrity": "sha1-mB2vLOw0emIx06odnhgDsDqqpKg=",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "7.0.0-beta.51",
+        "@babel/generator": "7.0.0-beta.51",
+        "@babel/helper-function-name": "7.0.0-beta.51",
+        "@babel/helper-split-export-declaration": "7.0.0-beta.51",
+        "@babel/parser": "7.0.0-beta.51",
+        "@babel/types": "7.0.0-beta.51",
+        "debug": "^3.1.0",
+        "globals": "^11.1.0",
+        "invariant": "^2.2.0",
+        "lodash": "^4.17.5"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "globals": {
+          "version": "11.7.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
+          "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
+          "dev": true
+        }
+      }
+    },
+    "@babel/types": {
+      "version": "7.0.0-beta.51",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.51.tgz",
+      "integrity": "sha1-2AK3tUO1g2x3iqaReXq/APPZfqk=",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.5",
+        "to-fast-properties": "^2.0.0"
+      },
+      "dependencies": {
+        "to-fast-properties": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+          "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+          "dev": true
+        }
+      }
+    },
+    "@ng-bootstrap/ng-bootstrap": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-3.2.0.tgz",
+      "integrity": "sha512-P+baWRj0Fs2Hm6ZKN2Mtw/xdC6yeuQ0wv2pXGkI231vUb7Jaso28n+9Qc9HSSkfup2Xpm9WVQzhv8AJ4KUOpyA==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "@ngtools/webpack": {
+      "version": "6.1.5",
+      "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.1.5.tgz",
+      "integrity": "sha512-vrvFFvUqo4hlrLRBTG7a3gsAneitd0/tj2zHsiN97RmefxHSS+3m0pkVw8G3BMAagp2L42AiVfNV4wvYDe+TXA==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "rxjs": "^6.0.0",
+        "tree-kill": "^1.0.0",
+        "webpack-sources": "^1.1.0"
+      }
+    },
+    "@schematics/angular": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.7.5.tgz",
+      "integrity": "sha512-NrtvFwHCoWon8KInsvA1jdPu4pVJGa8GAWM/jqnE7HpwPwM7hMML08lV0P8r3NX5t2/i0CKvfp4AAEr5MXorEQ==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "@angular-devkit/schematics": "0.7.5",
+        "typescript": ">=2.6.2 <2.10"
+      }
+    },
+    "@schematics/update": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.7.5.tgz",
+      "integrity": "sha512-pwNkXGtlzyCV6tsTPe8AgUuMCkmubcz94zgL6pSMdEe122yXBcKnr/PKqG9QzD/gGwmOcHUE9EWcuRtU5kdFpA==",
+      "dev": true,
+      "requires": {
+        "@angular-devkit/core": "0.7.5",
+        "@angular-devkit/schematics": "0.7.5",
+        "npm-registry-client": "^8.5.1",
+        "rxjs": "^6.0.0",
+        "semver": "^5.3.0",
+        "semver-intersect": "^1.1.2"
+      }
+    },
+    "@types/jasmine": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz",
+      "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==",
+      "dev": true
+    },
+    "@types/jasminewd2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.3.tgz",
+      "integrity": "sha512-hYDVmQZT5VA2kigd4H4bv7vl/OhlympwREUemqBdOqtrYTo5Ytm12a5W5/nGgGYdanGVxj0x/VhZ7J3hOg/YKg==",
+      "dev": true,
+      "requires": {
+        "@types/jasmine": "*"
+      }
+    },
+    "@types/node": {
+      "version": "8.9.5",
+      "resolved": "http://registry.npmjs.org/@types/node/-/node-8.9.5.tgz",
+      "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==",
+      "dev": true
+    },
+    "@types/q": {
+      "version": "0.0.32",
+      "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
+      "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
+      "dev": true
+    },
+    "@types/selenium-webdriver": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.10.tgz",
+      "integrity": "sha512-ikB0JHv6vCR1KYUQAzTO4gi/lXLElT4Tx+6De2pc/OZwizE9LRNiTa+U8TBFKBD/nntPnr/MPSHSnOTybjhqNA==",
+      "dev": true
+    },
+    "@types/xmldom": {
+      "version": "0.1.29",
+      "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.29.tgz",
+      "integrity": "sha1-xEKLDKhtO4gUdXJv2UmAs4onw4E=",
+      "dev": true
+    },
+    "@webassemblyjs/ast": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.4.3.tgz",
+      "integrity": "sha512-S6npYhPcTHDYe9nlsKa9CyWByFi8Vj8HovcAgtmMAQZUOczOZbQ8CnwMYKYC5HEZzxEE+oY0jfQk4cVlI3J59Q==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/wast-parser": "1.4.3",
+        "debug": "^3.1.0",
+        "webassemblyjs": "1.4.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.4.3.tgz",
+      "integrity": "sha512-3zTkSFswwZOPNHnzkP9ONq4bjJSeKVMcuahGXubrlLmZP8fmTIJ58dW7h/zOVWiFSuG2em3/HH3BlCN7wyu9Rw==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-buffer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.4.3.tgz",
+      "integrity": "sha512-e8+KZHh+RV8MUvoSRtuT1sFXskFnWG9vbDy47Oa166xX+l0dD5sERJ21g5/tcH8Yo95e9IN3u7Jc3NbhnUcSkw==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/helper-code-frame": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.4.3.tgz",
+      "integrity": "sha512-9FgHEtNsZQYaKrGCtsjswBil48Qp1agrzRcPzCbQloCoaTbOXLJ9IRmqT+uEZbenpULLRNFugz3I4uw18hJM8w==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/wast-printer": "1.4.3"
+      }
+    },
+    "@webassemblyjs/helper-fsm": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.4.3.tgz",
+      "integrity": "sha512-JINY76U+702IRf7ePukOt037RwmtH59JHvcdWbTTyHi18ixmQ+uOuNhcdCcQHTquDAH35/QgFlp3Y9KqtyJsCQ==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.4.3.tgz",
+      "integrity": "sha512-I7bS+HaO0K07Io89qhJv+z1QipTpuramGwUSDkwEaficbSvCcL92CUZEtgykfNtk5wb0CoLQwWlmXTwGbNZUeQ==",
+      "dev": true
+    },
+    "@webassemblyjs/helper-wasm-section": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.4.3.tgz",
+      "integrity": "sha512-p0yeeO/h2r30PyjnJX9xXSR6EDcvJd/jC6xa/Pxg4lpfcNi7JUswOpqDToZQ55HMMVhXDih/yqkaywHWGLxqyQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-buffer": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/wasm-gen": "1.4.3",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/leb128": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.4.3.tgz",
+      "integrity": "sha512-4u0LJLSPzuRDWHwdqsrThYn+WqMFVqbI2ltNrHvZZkzFPO8XOZ0HFQ5eVc4jY/TNHgXcnwrHjONhPGYuuf//KQ==",
+      "dev": true,
+      "requires": {
+        "leb": "^0.3.0"
+      }
+    },
+    "@webassemblyjs/validation": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/validation/-/validation-1.4.3.tgz",
+      "integrity": "sha512-R+rRMKfhd9mq0rj2mhU9A9NKI2l/Rw65vIYzz4lui7eTKPcCu1l7iZNi4b9Gen8D42Sqh/KGiaQNk/x5Tn/iBQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wasm-edit": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.4.3.tgz",
+      "integrity": "sha512-qzuwUn771PV6/LilqkXcS0ozJYAeY/OKbXIWU3a8gexuqb6De2p4ya/baBeH5JQ2WJdfhWhSvSbu86Vienttpw==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-buffer": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/helper-wasm-section": "1.4.3",
+        "@webassemblyjs/wasm-gen": "1.4.3",
+        "@webassemblyjs/wasm-opt": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "@webassemblyjs/wast-printer": "1.4.3",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/wasm-gen": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.4.3.tgz",
+      "integrity": "sha512-eR394T8dHZfpLJ7U/Z5pFSvxl1L63JdREebpv9gYc55zLhzzdJPAuxjBYT4XqevUdW67qU2s0nNA3kBuNJHbaQ==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/leb128": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wasm-opt": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.4.3.tgz",
+      "integrity": "sha512-7Gp+nschuKiDuAL1xmp4Xz0rgEbxioFXw4nCFYEmy+ytynhBnTeGc9W9cB1XRu1w8pqRU2lbj2VBBA4cL5Z2Kw==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-buffer": "1.4.3",
+        "@webassemblyjs/wasm-gen": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "@webassemblyjs/wasm-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.4.3.tgz",
+      "integrity": "sha512-KXBjtlwA3BVukR/yWHC9GF+SCzBcgj0a7lm92kTOaa4cbjaTaa47bCjXw6cX4SGQpkncB9PU2hHGYVyyI7wFRg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/helper-wasm-bytecode": "1.4.3",
+        "@webassemblyjs/leb128": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "webassemblyjs": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wast-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.4.3.tgz",
+      "integrity": "sha512-QhCsQzqV0CpsEkRYyTzQDilCNUZ+5j92f+g35bHHNqS22FppNTywNFfHPq8ZWZfYCgbectc+PoghD+xfzVFh1Q==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/floating-point-hex-parser": "1.4.3",
+        "@webassemblyjs/helper-code-frame": "1.4.3",
+        "@webassemblyjs/helper-fsm": "1.4.3",
+        "long": "^3.2.0",
+        "webassemblyjs": "1.4.3"
+      }
+    },
+    "@webassemblyjs/wast-printer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.4.3.tgz",
+      "integrity": "sha512-EgXk4anf8jKmuZJsqD8qy5bz2frEQhBvZruv+bqwNoLWUItjNSFygk8ywL3JTEz9KtxTlAmqTXNrdD1d9gNDtg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/wast-parser": "1.4.3",
+        "long": "^3.2.0"
+      }
+    },
+    "abbrev": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
+      "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=",
+      "dev": true
+    },
+    "accepts": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+      "dev": true,
+      "requires": {
+        "mime-types": "~2.1.18",
+        "negotiator": "0.6.1"
+      }
+    },
+    "acorn": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz",
+      "integrity": "sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw==",
+      "dev": true
+    },
+    "acorn-dynamic-import": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz",
+      "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==",
+      "dev": true,
+      "requires": {
+        "acorn": "^5.0.0"
+      }
+    },
+    "adm-zip": {
+      "version": "0.4.11",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz",
+      "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==",
+      "dev": true
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+      "dev": true
+    },
+    "agent-base": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+      "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+      "dev": true,
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
+    "ajv": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz",
+      "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=",
+      "dev": true,
+      "requires": {
+        "fast-deep-equal": "^1.0.0",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.3.0",
+        "uri-js": "^3.0.2"
+      }
+    },
+    "ajv-errors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz",
+      "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=",
+      "dev": true
+    },
+    "ajv-keywords": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
+      "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=",
+      "dev": true
+    },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+      "dev": true
+    },
+    "ansi-colors": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.0.5.tgz",
+      "integrity": "sha512-VVjWpkfaphxUBFarydrQ3n26zX5nIK7hcbT3/ielrvwDDyBBjuh2vuSw1P9zkPq0cfqvdw7lkYHnu+OLSfIBsg==",
+      "dev": true
+    },
+    "ansi-html": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
+      "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "^1.9.0"
+      }
+    },
+    "anymatch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+      "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+      "dev": true,
+      "requires": {
+        "micromatch": "^3.1.4",
+        "normalize-path": "^2.1.1"
+      }
+    },
+    "app-root-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz",
+      "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=",
+      "dev": true
+    },
+    "append-transform": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz",
+      "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==",
+      "dev": true,
+      "requires": {
+        "default-require-extensions": "^2.0.0"
+      }
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+      "dev": true
+    },
+    "are-we-there-yet": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+      "dev": true,
+      "requires": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^2.0.6"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "dev": true
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-flatten": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz",
+      "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=",
+      "dev": true
+    },
+    "array-slice": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+      "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
+      "dev": true
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "^1.0.1"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+      "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+      "dev": true
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
+      "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+      "dev": true,
+      "optional": true
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "asn1.js": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+      "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "dev": true,
+      "requires": {
+        "util": "0.10.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        },
+        "util": {
+          "version": "0.10.3",
+          "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+          "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.1"
+          }
+        }
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+      "dev": true
+    },
+    "async": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+      "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+      "dev": true
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true
+    },
+    "async-foreach": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+      "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+      "dev": true,
+      "optional": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true
+    },
+    "autoprefixer": {
+      "version": "8.6.5",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.5.tgz",
+      "integrity": "sha512-PLWJN3Xo/rycNkx+mp8iBDMTm3FeWe4VmYaZDSqL5QQB9sLsQkG5k8n+LNDFnhh9kdq2K+egL/icpctOmDHwig==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^3.2.8",
+        "caniuse-lite": "^1.0.30000864",
+        "normalize-range": "^0.1.2",
+        "num2fraction": "^1.2.2",
+        "postcss": "^6.0.23",
+        "postcss-value-parser": "^3.2.3"
+      }
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+      "dev": true
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.1",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+      "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+      "dev": true,
+      "requires": {
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "detect-indent": "^4.0.0",
+        "jsesc": "^1.3.0",
+        "lodash": "^4.17.4",
+        "source-map": "^0.5.7",
+        "trim-right": "^1.0.1"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "dev": true,
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "debug": "^2.6.8",
+        "globals": "^9.18.0",
+        "invariant": "^2.2.2",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.4",
+        "to-fast-properties": "^1.0.3"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+      "dev": true
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "dev": true,
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
+      "dev": true
+    },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
+      "dev": true
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+      "dev": true
+    },
+    "batch": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+      "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
+      "dev": true
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "dev": true,
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "big.js": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
+      "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
+      "dev": true
+    },
+    "binary-extensions": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz",
+      "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
+      "dev": true
+    },
+    "blob": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+      "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=",
+      "dev": true
+    },
+    "block-stream": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+      "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "inherits": "~2.0.0"
+      }
+    },
+    "blocking-proxy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
+      "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "bluebird": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz",
+      "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==",
+      "dev": true
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "dev": true
+    },
+    "body-parser": {
+      "version": "1.18.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+      "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.1",
+        "http-errors": "~1.6.2",
+        "iconv-lite": "0.4.19",
+        "on-finished": "~2.3.0",
+        "qs": "6.5.1",
+        "raw-body": "2.3.2",
+        "type-is": "~1.6.15"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.1",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+          "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
+          "dev": true
+        }
+      }
+    },
+    "bonjour": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+      "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+      "dev": true,
+      "requires": {
+        "array-flatten": "^2.1.0",
+        "deep-equal": "^1.0.1",
+        "dns-equal": "^1.0.0",
+        "dns-txt": "^2.0.2",
+        "multicast-dns": "^6.0.1",
+        "multicast-dns-service-types": "^1.1.0"
+      }
+    },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
+      "dev": true
+    },
+    "bootstrap-css-only": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/bootstrap-css-only/-/bootstrap-css-only-4.1.1.tgz",
+      "integrity": "sha512-I/zU5T/KANTy+NNmsNC4lESiHWrdLtvgp9gutOVqbDKY0d4rycmX9fUp1jkFZ0vNd6dhY8oxY9j8RWZkjHVAuA=="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "^1.1.0",
+        "array-unique": "^0.3.2",
+        "extend-shallow": "^2.0.1",
+        "fill-range": "^4.0.0",
+        "isobject": "^3.0.1",
+        "repeat-element": "^1.1.2",
+        "snapdragon": "^0.8.1",
+        "snapdragon-node": "^2.0.1",
+        "split-string": "^3.0.2",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "dev": true
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "dev": true,
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+      "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "^1.0.4",
+        "browserify-des": "^1.0.0",
+        "evp_bytestokey": "^1.0.0"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+      "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "des.js": "^1.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "randombytes": "^2.0.1"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.1",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.2",
+        "elliptic": "^6.0.0",
+        "inherits": "^2.0.1",
+        "parse-asn1": "^5.0.0"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "~1.0.5"
+      }
+    },
+    "browserslist": {
+      "version": "3.2.8",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
+      "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30000844",
+        "electron-to-chromium": "^1.3.47"
+      }
+    },
+    "browserstack": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.1.tgz",
+      "integrity": "sha512-O8VMT64P9NOLhuIoD4YngyxBURefaSdR4QdhG8l6HZ9VxtU7jc3m6jLufFwKA5gaf7fetfB2TnRJnMxyob+heg==",
+      "dev": true,
+      "requires": {
+        "https-proxy-agent": "^2.2.1"
+      }
+    },
+    "buffer": {
+      "version": "4.9.1",
+      "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+      "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4",
+        "isarray": "^1.0.0"
+      }
+    },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
+      "dev": true
+    },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
+      "dev": true
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "buffer-indexof": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
+      "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
+      "dev": true
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "builtins": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+      "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
+    "cacache": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
+      "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.5.1",
+        "chownr": "^1.0.1",
+        "glob": "^7.1.2",
+        "graceful-fs": "^4.1.11",
+        "lru-cache": "^4.1.1",
+        "mississippi": "^2.0.0",
+        "mkdirp": "^0.5.1",
+        "move-concurrently": "^1.0.1",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^2.6.2",
+        "ssri": "^5.2.4",
+        "unique-filename": "^1.1.0",
+        "y18n": "^4.0.0"
+      }
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "dev": true,
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      }
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+      "dev": true
+    },
+    "camel-case": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+      "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0",
+        "upper-case": "^1.1.1"
+      }
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true,
+      "optional": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase": "^2.0.0",
+        "map-obj": "^1.0.0"
+      }
+    },
+    "caniuse-lite": {
+      "version": "1.0.30000884",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000884.tgz",
+      "integrity": "sha512-ibROerckpTH6U5zReSjbaitlH4gl5V4NWNCBzRNCa3GEDmzzkfStk+2k5mO4ZDM6pwtdjbZ3hjvsYhPGVLWgNw==",
+      "dev": true
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      }
+    },
+    "chokidar": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
+      "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
+      "dev": true,
+      "requires": {
+        "anymatch": "^2.0.0",
+        "async-each": "^1.0.0",
+        "braces": "^2.3.0",
+        "fsevents": "^1.2.2",
+        "glob-parent": "^3.1.0",
+        "inherits": "^2.0.1",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^4.0.0",
+        "lodash.debounce": "^4.0.8",
+        "normalize-path": "^2.1.1",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.0.0",
+        "upath": "^1.0.5"
+      }
+    },
+    "chownr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
+      "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
+      "dev": true
+    },
+    "chrome-trace-event": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-0.1.3.tgz",
+      "integrity": "sha512-sjndyZHrrWiu4RY7AkHgjn80GfAM2ZSzUkZLV/Js59Ldmh6JDThf0SUmOHU53rFu2rVxxfCzJ30Ukcfch3Gb/A==",
+      "dev": true
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "circular-dependency-plugin": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz",
+      "integrity": "sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==",
+      "dev": true
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "clean-css": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
+      "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
+      "dev": true,
+      "requires": {
+        "source-map": "~0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "cliui": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+      "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wrap-ansi": "^2.0.0"
+      }
+    },
+    "clone": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+      "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+      "dev": true
+    },
+    "clone-deep": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
+      "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
+      "dev": true,
+      "requires": {
+        "for-own": "^1.0.0",
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.0",
+        "shallow-clone": "^1.0.0"
+      }
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
+    "codelyzer": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.2.1.tgz",
+      "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==",
+      "dev": true,
+      "requires": {
+        "app-root-path": "^2.0.1",
+        "css-selector-tokenizer": "^0.7.0",
+        "cssauron": "^1.4.0",
+        "semver-dsl": "^1.0.1",
+        "source-map": "^0.5.6",
+        "sprintf-js": "^1.0.3"
+      }
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "dev": true,
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "dev": true
+    },
+    "colors": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+      "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+      "dev": true
+    },
+    "combine-lists": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
+      "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.5.0"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+      "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.17.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+      "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+      "dev": true
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+      "dev": true
+    },
+    "compare-versions": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.4.0.tgz",
+      "integrity": "sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg==",
+      "dev": true
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+      "dev": true
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+      "dev": true
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+      "dev": true
+    },
+    "compressible": {
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz",
+      "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=",
+      "dev": true,
+      "requires": {
+        "mime-db": ">= 1.34.0 < 2"
+      }
+    },
+    "compression": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz",
+      "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.5",
+        "bytes": "3.0.0",
+        "compressible": "~2.0.14",
+        "debug": "2.6.9",
+        "on-headers": "~1.0.1",
+        "safe-buffer": "5.1.2",
+        "vary": "~1.1.2"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "connect": {
+      "version": "3.6.6",
+      "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+      "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "finalhandler": "1.1.0",
+        "parseurl": "~1.3.2",
+        "utils-merge": "1.0.1"
+      },
+      "dependencies": {
+        "finalhandler": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+          "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+          "dev": true,
+          "requires": {
+            "debug": "2.6.9",
+            "encodeurl": "~1.0.1",
+            "escape-html": "~1.0.3",
+            "on-finished": "~2.3.0",
+            "parseurl": "~1.3.2",
+            "statuses": "~1.3.1",
+            "unpipe": "~1.0.0"
+          }
+        },
+        "statuses": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+          "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+          "dev": true
+        }
+      }
+    },
+    "connect-history-api-fallback": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz",
+      "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=",
+      "dev": true
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "dev": true,
+      "requires": {
+        "date-now": "^0.1.4"
+      }
+    },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "dev": true
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
+      "dev": true
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+      "dev": true
+    },
+    "copy-concurrently": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
+      "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1",
+        "fs-write-stream-atomic": "^1.0.8",
+        "iferr": "^0.1.5",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.0"
+      }
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+      "dev": true
+    },
+    "copy-webpack-plugin": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz",
+      "integrity": "sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "globby": "^7.1.1",
+        "is-glob": "^4.0.0",
+        "loader-utils": "^1.1.0",
+        "minimatch": "^3.0.4",
+        "p-limit": "^1.0.0",
+        "serialize-javascript": "^1.4.0"
+      }
+    },
+    "core-js": {
+      "version": "2.5.7",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
+      "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
+    "cosmiconfig": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz",
+      "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==",
+      "dev": true,
+      "requires": {
+        "is-directory": "^0.3.1",
+        "js-yaml": "^3.9.0",
+        "parse-json": "^4.0.0",
+        "require-from-string": "^2.0.1"
+      },
+      "dependencies": {
+        "parse-json": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+          "dev": true,
+          "requires": {
+            "error-ex": "^1.3.1",
+            "json-parse-better-errors": "^1.0.1"
+          }
+        }
+      }
+    },
+    "create-ecdh": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+      "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "elliptic": "^6.0.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "cross-spawn": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+      "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "lru-cache": "^4.0.1",
+        "which": "^1.2.9"
+      }
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "dev": true,
+      "requires": {
+        "browserify-cipher": "^1.0.0",
+        "browserify-sign": "^4.0.0",
+        "create-ecdh": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "create-hmac": "^1.1.0",
+        "diffie-hellman": "^5.0.0",
+        "inherits": "^2.0.1",
+        "pbkdf2": "^3.0.3",
+        "public-encrypt": "^4.0.0",
+        "randombytes": "^2.0.0",
+        "randomfill": "^1.0.3"
+      }
+    },
+    "css-parse": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz",
+      "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=",
+      "dev": true
+    },
+    "css-select": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
+      "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0",
+        "css-what": "2.1",
+        "domutils": "1.5.1",
+        "nth-check": "~1.0.1"
+      }
+    },
+    "css-selector-tokenizer": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz",
+      "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=",
+      "dev": true,
+      "requires": {
+        "cssesc": "^0.1.0",
+        "fastparse": "^1.1.1",
+        "regexpu-core": "^1.0.0"
+      }
+    },
+    "css-what": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
+      "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=",
+      "dev": true
+    },
+    "cssauron": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz",
+      "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=",
+      "dev": true,
+      "requires": {
+        "through": "X.X.X"
+      }
+    },
+    "cssesc": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz",
+      "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=",
+      "dev": true
+    },
+    "cuint": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
+      "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
+      "dev": true
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "^1.0.1"
+      }
+    },
+    "custom-event": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+      "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+      "dev": true
+    },
+    "cyclist": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
+      "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
+      "dev": true
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+      "dev": true
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+      "dev": true
+    },
+    "deep-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
+      "dev": true
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "default-gateway": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz",
+      "integrity": "sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ==",
+      "dev": true,
+      "requires": {
+        "execa": "^0.10.0",
+        "ip-regex": "^2.1.0"
+      }
+    },
+    "default-require-extensions": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz",
+      "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=",
+      "dev": true,
+      "requires": {
+        "strip-bom": "^3.0.0"
+      },
+      "dependencies": {
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        }
+      }
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "^1.0.12"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "dev": true,
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "del": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
+      "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=",
+      "dev": true,
+      "requires": {
+        "globby": "^6.1.0",
+        "is-path-cwd": "^1.0.0",
+        "is-path-in-cwd": "^1.0.0",
+        "p-map": "^1.1.1",
+        "pify": "^3.0.0",
+        "rimraf": "^2.2.8"
+      },
+      "dependencies": {
+        "globby": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+          "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "glob": "^7.0.3",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        }
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "dev": true
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+      "dev": true
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "dev": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "detect-node": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
+      "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
+      "dev": true
+    },
+    "di": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+      "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+      "dev": true
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "diffie-hellman": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "miller-rabin": "^4.0.0",
+        "randombytes": "^2.0.0"
+      }
+    },
+    "dir-glob": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz",
+      "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.1",
+        "path-type": "^3.0.0"
+      }
+    },
+    "dns-equal": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+      "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+      "dev": true
+    },
+    "dns-packet": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
+      "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
+      "dev": true,
+      "requires": {
+        "ip": "^1.1.0",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "dns-txt": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+      "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+      "dev": true,
+      "requires": {
+        "buffer-indexof": "^1.0.0"
+      }
+    },
+    "dom-converter": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz",
+      "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=",
+      "dev": true,
+      "requires": {
+        "utila": "~0.3"
+      },
+      "dependencies": {
+        "utila": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz",
+          "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=",
+          "dev": true
+        }
+      }
+    },
+    "dom-serialize": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+      "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+      "dev": true,
+      "requires": {
+        "custom-event": "~1.0.0",
+        "ent": "~2.2.0",
+        "extend": "^3.0.0",
+        "void-elements": "^2.0.0"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "~1.1.1",
+        "entities": "~1.1.1"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
+          "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=",
+          "dev": true
+        }
+      }
+    },
+    "domain-browser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+      "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+      "dev": true
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz",
+      "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=",
+      "dev": true
+    },
+    "domhandler": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz",
+      "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
+      "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "duplexify": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz",
+      "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "ejs": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
+      "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==",
+      "dev": true
+    },
+    "electron-to-chromium": {
+      "version": "1.3.62",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.62.tgz",
+      "integrity": "sha512-x09ndL/Gjnuk3unlAyoGyUg3wbs4w/bXurgL7wL913vXHAOWmMhrLf1VNGRaMLngmadd5Q8gsV9BFuIr6rP+Xg==",
+      "dev": true
+    },
+    "elliptic": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
+      "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      }
+    },
+    "emojis-list": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+      "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "dev": true
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+      "dev": true,
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz",
+      "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.3",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "2.3.3",
+        "engine.io-parser": "1.3.2",
+        "ws": "1.1.2"
+      },
+      "dependencies": {
+        "accepts": {
+          "version": "1.3.3",
+          "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
+          "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
+          "dev": true,
+          "requires": {
+            "mime-types": "~2.1.11",
+            "negotiator": "0.6.1"
+          }
+        },
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz",
+      "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "2.3.3",
+        "engine.io-parser": "1.3.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parsejson": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "1.1.2",
+        "xmlhttprequest-ssl": "1.5.3",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz",
+      "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=",
+      "dev": true,
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "0.0.6",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.4",
+        "has-binary": "0.1.7",
+        "wtf-8": "1.0.0"
+      }
+    },
+    "enhanced-resolve": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
+      "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "memory-fs": "^0.4.0",
+        "tapable": "^1.0.0"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+      "dev": true
+    },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
+      "dev": true
+    },
+    "errno": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+      "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+      "dev": true,
+      "requires": {
+        "prr": "~1.0.1"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz",
+      "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
+      "dev": true,
+      "requires": {
+        "es-to-primitive": "^1.1.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.1",
+        "is-callable": "^1.1.3",
+        "is-regex": "^1.0.4"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
+      "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=",
+      "dev": true,
+      "requires": {
+        "is-callable": "^1.1.1",
+        "is-date-object": "^1.0.1",
+        "is-symbol": "^1.0.1"
+      }
+    },
+    "es6-promise": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
+      "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
+      "dev": true
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "escodegen": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
+      "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=",
+      "dev": true,
+      "requires": {
+        "esprima": "^2.7.1",
+        "estraverse": "^1.9.1",
+        "esutils": "^2.0.2",
+        "optionator": "^0.8.1",
+        "source-map": "~0.2.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+          "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "eslint-scope": {
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+      "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+          "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+          "dev": true
+        }
+      }
+    },
+    "esprima": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+      "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
+      "dev": true
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+          "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+          "dev": true
+        }
+      }
+    },
+    "estraverse": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+      "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "dev": true
+    },
+    "eventemitter3": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
+      "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
+      "dev": true
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+      "dev": true
+    },
+    "eventsource": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz",
+      "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=",
+      "dev": true,
+      "requires": {
+        "original": ">=0.0.5"
+      }
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "dev": true,
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "execa": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
+      "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^6.0.0",
+        "get-stream": "^3.0.0",
+        "is-stream": "^1.1.0",
+        "npm-run-path": "^2.0.0",
+        "p-finally": "^1.0.0",
+        "signal-exit": "^3.0.0",
+        "strip-eof": "^1.0.0"
+      },
+      "dependencies": {
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "^1.0.4",
+            "path-key": "^2.0.1",
+            "semver": "^5.5.0",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        }
+      }
+    },
+    "exit": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+      "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+      "dev": true
+    },
+    "expand-braces": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz",
+      "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=",
+      "dev": true,
+      "requires": {
+        "array-slice": "^0.2.3",
+        "array-unique": "^0.2.1",
+        "braces": "^0.1.2"
+      },
+      "dependencies": {
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
+          "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^0.1.0"
+          }
+        },
+        "expand-range": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz",
+          "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=",
+          "dev": true,
+          "requires": {
+            "is-number": "^0.1.1",
+            "repeat-string": "^0.2.2"
+          }
+        },
+        "is-number": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
+          "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=",
+          "dev": true
+        },
+        "repeat-string": {
+          "version": "0.2.2",
+          "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz",
+          "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=",
+          "dev": true
+        }
+      }
+    },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.3.3",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "posix-character-classes": "^0.1.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "dev": true,
+      "requires": {
+        "fill-range": "^2.1.0"
+      },
+      "dependencies": {
+        "fill-range": {
+          "version": "2.2.4",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
+          "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+          "dev": true,
+          "requires": {
+            "is-number": "^2.1.0",
+            "isobject": "^2.0.0",
+            "randomatic": "^3.0.0",
+            "repeat-element": "^1.1.2",
+            "repeat-string": "^1.5.2"
+          }
+        },
+        "is-number": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+          "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+          "dev": true,
+          "requires": {
+            "kind-of": "^3.0.2"
+          }
+        },
+        "isobject": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+          "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+          "dev": true,
+          "requires": {
+            "isarray": "1.0.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "express": {
+      "version": "4.16.3",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
+      "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.5",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.2",
+        "content-disposition": "0.5.2",
+        "content-type": "~1.0.4",
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.1.1",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.3",
+        "qs": "6.5.1",
+        "range-parser": "~1.2.0",
+        "safe-buffer": "5.1.1",
+        "send": "0.16.2",
+        "serve-static": "1.13.2",
+        "setprototypeof": "1.1.0",
+        "statuses": "~1.4.0",
+        "type-is": "~1.6.16",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "dependencies": {
+        "array-flatten": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+          "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.5.1",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+          "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
+          "dev": true
+        },
+        "safe-buffer": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+          "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+          "dev": true
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "dev": true,
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+      "dev": true,
+      "requires": {
+        "array-unique": "^0.3.2",
+        "define-property": "^1.0.0",
+        "expand-brackets": "^2.1.4",
+        "extend-shallow": "^2.0.1",
+        "fragment-cache": "^0.2.1",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "extract-zip": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
+      "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
+      "dev": true,
+      "requires": {
+        "concat-stream": "1.6.2",
+        "debug": "2.6.9",
+        "mkdirp": "0.5.1",
+        "yauzl": "2.4.1"
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fastparse": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz",
+      "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=",
+      "dev": true
+    },
+    "faye-websocket": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+      "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+      "dev": true,
+      "requires": {
+        "websocket-driver": ">=0.5.1"
+      }
+    },
+    "fd-slicer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+      "dev": true,
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
+    "file-loader": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz",
+      "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.2",
+        "schema-utils": "^0.4.5"
+      }
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+      "dev": true
+    },
+    "fileset": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
+      "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.3",
+        "minimatch": "^3.0.3"
+      }
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1",
+        "to-regex-range": "^2.1.0"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+      "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.2",
+        "statuses": "~1.4.0",
+        "unpipe": "~1.0.0"
+      }
+    },
+    "find-cache-dir": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
+      "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
+      "dev": true,
+      "requires": {
+        "commondir": "^1.0.1",
+        "make-dir": "^1.0.0",
+        "pkg-dir": "^2.0.0"
+      }
+    },
+    "find-up": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+      "dev": true,
+      "requires": {
+        "locate-path": "^2.0.0"
+      }
+    },
+    "flush-write-stream": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
+      "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.4"
+      }
+    },
+    "follow-redirects": {
+      "version": "1.5.7",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.7.tgz",
+      "integrity": "sha512-NONJVIFiX7Z8k2WxfqBjtwqMifx7X42ORLFrOZ2LTKGj71G3C0kfdyTqGqr8fx5zSX6Foo/D95dgGWbPUiwnew==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "for-own": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+      "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.1"
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+      "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+      "dev": true,
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+      "dev": true
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "dev": true,
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "dev": true
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.0.0"
+      }
+    },
+    "fs-access": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
+      "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+      "dev": true,
+      "requires": {
+        "null-check": "^1.0.0"
+      }
+    },
+    "fs-extra": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz",
+      "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "jsonfile": "^2.1.0",
+        "klaw": "^1.0.0"
+      }
+    },
+    "fs-write-stream-atomic": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
+      "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "iferr": "^0.1.5",
+        "imurmurhash": "^0.1.4",
+        "readable-stream": "1 || 2"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
+      "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "^2.9.2",
+        "node-pre-gyp": "^0.10.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true,
+          "dev": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "2.6.9",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "deep-extend": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.21",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": "^2.1.0"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true,
+          "dev": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true,
+          "dev": true
+        },
+        "minipass": {
+          "version": "2.2.4",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "safe-buffer": "^5.1.1",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "^2.1.2",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.10.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.0",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.1.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.1.10",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.7",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.5.1",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "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"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.0.5"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.1",
+          "bundled": true,
+          "dev": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.5.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.0.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.2.4",
+            "minizlib": "^1.1.0",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.1",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        },
+        "yallist": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true
+        }
+      }
+    },
+    "fstream": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "inherits": "~2.0.0",
+        "mkdirp": ">=0.5 0",
+        "rimraf": "2"
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.0.3",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.0",
+        "object-assign": "^4.1.0",
+        "signal-exit": "^3.0.0",
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wide-align": "^1.1.0"
+      }
+    },
+    "gaze": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+      "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "globule": "^1.0.0"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+      "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+      "dev": true
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+      "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+      "dev": true
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "dev": true,
+      "requires": {
+        "glob-parent": "^2.0.0",
+        "is-glob": "^2.0.0"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "^2.0.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        }
+      }
+    },
+    "glob-parent": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+      "dev": true,
+      "requires": {
+        "is-glob": "^3.1.0",
+        "path-dirname": "^1.0.0"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^2.1.0"
+          }
+        }
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+      "dev": true
+    },
+    "globby": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
+      "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
+      "dev": true,
+      "requires": {
+        "array-union": "^1.0.1",
+        "dir-glob": "^2.0.0",
+        "glob": "^7.1.2",
+        "ignore": "^3.3.5",
+        "pify": "^3.0.0",
+        "slash": "^1.0.0"
+      }
+    },
+    "globule": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+      "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "glob": "~7.1.1",
+        "lodash": "~4.17.10",
+        "minimatch": "~3.0.2"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+      "dev": true
+    },
+    "handle-thing": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
+      "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
+      "dev": true
+    },
+    "handlebars": {
+      "version": "4.0.12",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz",
+      "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==",
+      "dev": true,
+      "requires": {
+        "async": "^2.5.0",
+        "optimist": "^0.6.1",
+        "source-map": "^0.6.1",
+        "uglify-js": "^3.1.4"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
+      "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
+      "dev": true,
+      "requires": {
+        "ajv": "^5.3.0",
+        "har-schema": "^2.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        }
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "has-binary": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz",
+      "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=",
+      "dev": true,
+      "requires": {
+        "isarray": "0.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+      "dev": true
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "dev": true,
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "hash-base": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+      "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
+      "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "hasha": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz",
+      "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=",
+      "dev": true,
+      "requires": {
+        "is-stream": "^1.0.1",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "dev": true,
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+      "dev": true
+    },
+    "hpack.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+      "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "obuf": "^1.0.0",
+        "readable-stream": "^2.0.1",
+        "wbuf": "^1.1.0"
+      }
+    },
+    "html-entities": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
+      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+      "dev": true
+    },
+    "html-minifier": {
+      "version": "3.5.20",
+      "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.20.tgz",
+      "integrity": "sha512-ZmgNLaTp54+HFKkONyLFEfs5dd/ZOtlquKaTnqIWFmx3Av5zG6ZPcV2d0o9XM2fXOTxxIf6eDcwzFFotke/5zA==",
+      "dev": true,
+      "requires": {
+        "camel-case": "3.0.x",
+        "clean-css": "4.2.x",
+        "commander": "2.17.x",
+        "he": "1.1.x",
+        "param-case": "2.1.x",
+        "relateurl": "0.2.x",
+        "uglify-js": "3.4.x"
+      }
+    },
+    "html-webpack-plugin": {
+      "version": "3.2.0",
+      "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
+      "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
+      "dev": true,
+      "requires": {
+        "html-minifier": "^3.2.3",
+        "loader-utils": "^0.2.16",
+        "lodash": "^4.17.3",
+        "pretty-error": "^2.0.2",
+        "tapable": "^1.0.0",
+        "toposort": "^1.0.0",
+        "util.promisify": "1.0.0"
+      },
+      "dependencies": {
+        "loader-utils": {
+          "version": "0.2.17",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
+          "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+          "dev": true,
+          "requires": {
+            "big.js": "^3.1.3",
+            "emojis-list": "^2.0.0",
+            "json5": "^0.5.0",
+            "object-assign": "^4.0.1"
+          }
+        }
+      }
+    },
+    "htmlparser2": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz",
+      "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1",
+        "domhandler": "2.1",
+        "domutils": "1.1",
+        "readable-stream": "1.0"
+      },
+      "dependencies": {
+        "domutils": {
+          "version": "1.1.6",
+          "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz",
+          "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=",
+          "dev": true,
+          "requires": {
+            "domelementtype": "1"
+          }
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "http-deceiver": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+      "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
+      "dev": true
+    },
+    "http-errors": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+      "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+      "dev": true,
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.0",
+        "statuses": ">= 1.4.0 < 2"
+      }
+    },
+    "http-parser-js": {
+      "version": "0.4.13",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz",
+      "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=",
+      "dev": true
+    },
+    "http-proxy": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
+      "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
+      "dev": true,
+      "requires": {
+        "eventemitter3": "^3.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "http-proxy-middleware": {
+      "version": "0.18.0",
+      "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
+      "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
+      "dev": true,
+      "requires": {
+        "http-proxy": "^1.16.2",
+        "is-glob": "^4.0.0",
+        "lodash": "^4.17.5",
+        "micromatch": "^3.1.9"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+      "dev": true
+    },
+    "https-proxy-agent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
+      "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.1.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+      "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
+      "dev": true
+    },
+    "ieee754": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+      "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+      "dev": true
+    },
+    "iferr": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
+      "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
+      "dev": true
+    },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+      "dev": true
+    },
+    "image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
+      "dev": true,
+      "optional": true
+    },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
+      "dev": true
+    },
+    "import-cwd": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
+      "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=",
+      "dev": true,
+      "requires": {
+        "import-from": "^2.1.0"
+      }
+    },
+    "import-from": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz",
+      "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=",
+      "dev": true,
+      "requires": {
+        "resolve-from": "^3.0.0"
+      }
+    },
+    "import-local": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
+      "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==",
+      "dev": true,
+      "requires": {
+        "pkg-dir": "^2.0.0",
+        "resolve-cwd": "^2.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "in-publish": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+      "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+      "dev": true,
+      "optional": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
+    "ini": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+      "dev": true
+    },
+    "internal-ip": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz",
+      "integrity": "sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q==",
+      "dev": true,
+      "requires": {
+        "default-gateway": "^2.6.0",
+        "ipaddr.js": "^1.5.2"
+      }
+    },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dev": true,
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
+    "ip": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+      "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
+      "dev": true
+    },
+    "ip-regex": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
+      "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=",
+      "dev": true
+    },
+    "ipaddr.js": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+      "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=",
+      "dev": true
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "^1.0.0"
+      }
+    },
+    "is-callable": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+      "dev": true
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+      "dev": true
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "dev": true,
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "is-directory": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
+      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+      "dev": true
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+      "dev": true
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "dev": true,
+      "requires": {
+        "is-primitive": "^2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-glob": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
+      "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+      "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+      "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+      "dev": true
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+      "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+      "dev": true,
+      "requires": {
+        "is-path-inside": "^1.0.0"
+      }
+    },
+    "is-path-inside": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+      "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+      "dev": true,
+      "requires": {
+        "path-is-inside": "^1.0.1"
+      }
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+      "dev": true
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+      "dev": true
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.1"
+      }
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+      "dev": true
+    },
+    "is-symbol": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz",
+      "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
+      "dev": true
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "dev": true
+    },
+    "is-wsl": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
+      "dev": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+      "dev": true
+    },
+    "isbinaryfile": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz",
+      "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==",
+      "dev": true,
+      "requires": {
+        "buffer-alloc": "^1.2.0"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "istanbul": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz",
+      "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1.0.x",
+        "async": "1.x",
+        "escodegen": "1.8.x",
+        "esprima": "2.7.x",
+        "glob": "^5.0.15",
+        "handlebars": "^4.0.1",
+        "js-yaml": "3.x",
+        "mkdirp": "0.5.x",
+        "nopt": "3.x",
+        "once": "1.x",
+        "resolve": "1.1.x",
+        "supports-color": "^3.1.0",
+        "which": "^1.1.1",
+        "wordwrap": "^1.0.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "5.0.15",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+          "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+          "dev": true,
+          "requires": {
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "2 || 3",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+          "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "dev": true,
+          "requires": {
+            "has-flag": "^1.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-api": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.0.5.tgz",
+      "integrity": "sha512-GE5gqFpZsHKgsAbVsgPXlcWKV7fAKP7Bbrma4BJbzBQ+O7KVd/o94WjXOTn4m6eThMhBjWOGOKmaWPwJ3tHVIA==",
+      "dev": true,
+      "requires": {
+        "async": "^2.6.1",
+        "compare-versions": "^3.2.1",
+        "fileset": "^2.0.3",
+        "istanbul-lib-coverage": "^2.0.1",
+        "istanbul-lib-hook": "^2.0.1",
+        "istanbul-lib-instrument": "^2.3.2",
+        "istanbul-lib-report": "^2.0.1",
+        "istanbul-lib-source-maps": "^2.0.1",
+        "istanbul-reports": "^2.0.0",
+        "js-yaml": "^3.12.0",
+        "make-dir": "^1.3.0",
+        "once": "^1.4.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        },
+        "istanbul-lib-coverage": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
+          "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
+          "dev": true
+        },
+        "istanbul-lib-instrument": {
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-2.3.2.tgz",
+          "integrity": "sha512-l7TD/VnBsIB2OJvSyxaLW/ab1+92dxZNH9wLH7uHPPioy3JZ8tnx2UXUdKmdkgmP2EFPzg64CToUP6dAS3U32Q==",
+          "dev": true,
+          "requires": {
+            "@babel/generator": "7.0.0-beta.51",
+            "@babel/parser": "7.0.0-beta.51",
+            "@babel/template": "7.0.0-beta.51",
+            "@babel/traverse": "7.0.0-beta.51",
+            "@babel/types": "7.0.0-beta.51",
+            "istanbul-lib-coverage": "^2.0.1",
+            "semver": "^5.5.0"
+          }
+        }
+      }
+    },
+    "istanbul-instrumenter-loader": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz",
+      "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==",
+      "dev": true,
+      "requires": {
+        "convert-source-map": "^1.5.0",
+        "istanbul-lib-instrument": "^1.7.3",
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^0.3.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "schema-utils": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
+          "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
+          "dev": true,
+          "requires": {
+            "ajv": "^5.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-lib-coverage": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz",
+      "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==",
+      "dev": true
+    },
+    "istanbul-lib-hook": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.1.tgz",
+      "integrity": "sha512-ufiZoiJ8CxY577JJWEeFuxXZoMqiKpq/RqZtOAYuQLvlkbJWscq9n3gc4xrCGH9n4pW0qnTxOz1oyMmVtk8E1w==",
+      "dev": true,
+      "requires": {
+        "append-transform": "^1.0.0"
+      }
+    },
+    "istanbul-lib-instrument": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz",
+      "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==",
+      "dev": true,
+      "requires": {
+        "babel-generator": "^6.18.0",
+        "babel-template": "^6.16.0",
+        "babel-traverse": "^6.18.0",
+        "babel-types": "^6.18.0",
+        "babylon": "^6.18.0",
+        "istanbul-lib-coverage": "^1.2.0",
+        "semver": "^5.3.0"
+      }
+    },
+    "istanbul-lib-report": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.1.tgz",
+      "integrity": "sha512-pXYOWwpDNc5AHIY93WjFTuxzkDOOZ7B8eSa0cBHTmTnKRst5ccc/xBfWu/5wcNJqB6/Qy0lDMhpn+Uy0qyyUjA==",
+      "dev": true,
+      "requires": {
+        "istanbul-lib-coverage": "^2.0.1",
+        "make-dir": "^1.3.0",
+        "supports-color": "^5.4.0"
+      },
+      "dependencies": {
+        "istanbul-lib-coverage": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
+          "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-lib-source-maps": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-2.0.1.tgz",
+      "integrity": "sha512-30l40ySg+gvBLcxTrLzR4Z2XTRj3HgRCA/p2rnbs/3OiTaoj054gAbuP5DcLOtwqmy4XW8qXBHzrmP2/bQ9i3A==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0",
+        "istanbul-lib-coverage": "^2.0.1",
+        "make-dir": "^1.3.0",
+        "rimraf": "^2.6.2",
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "istanbul-lib-coverage": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
+          "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-reports": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.0.0.tgz",
+      "integrity": "sha512-d2YRSnAOHHb+6vMc5qjJEyPN4VapkgUMhKlMmr3BzKdMDWdJbyYGEi/7m5AjDjkvRRTjs68ttPRZ7W2jBZ31SQ==",
+      "dev": true,
+      "requires": {
+        "handlebars": "^4.0.11"
+      }
+    },
+    "jasmine": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz",
+      "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=",
+      "dev": true,
+      "requires": {
+        "exit": "^0.1.2",
+        "glob": "^7.0.6",
+        "jasmine-core": "~2.8.0"
+      },
+      "dependencies": {
+        "jasmine-core": {
+          "version": "2.8.0",
+          "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
+          "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
+          "dev": true
+        }
+      }
+    },
+    "jasmine-core": {
+      "version": "2.99.1",
+      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz",
+      "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=",
+      "dev": true
+    },
+    "jasmine-diff": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/jasmine-diff/-/jasmine-diff-0.1.3.tgz",
+      "integrity": "sha1-k8zC3MQQKMXd1GBlWAdIOfLe6qg=",
+      "dev": true,
+      "requires": {
+        "diff": "^3.2.0"
+      }
+    },
+    "jasmine-spec-reporter": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
+      "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==",
+      "dev": true,
+      "requires": {
+        "colors": "1.1.2"
+      }
+    },
+    "jasminewd2": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz",
+      "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
+      "dev": true
+    },
+    "js-base64": {
+      "version": "2.4.9",
+      "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz",
+      "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==",
+      "dev": true,
+      "optional": true
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+      "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+          "dev": true
+        }
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true,
+      "optional": true
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+      "dev": true
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json3": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
+      "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
+      "dev": true
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+      "dev": true
+    },
+    "jsonfile": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+      "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "jszip": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
+      "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
+      "dev": true,
+      "requires": {
+        "core-js": "~2.3.0",
+        "es6-promise": "~3.0.2",
+        "lie": "~3.1.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.0.6"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+          "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU=",
+          "dev": true
+        },
+        "es6-promise": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+          "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
+          "dev": true
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+          "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+          "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~1.0.6",
+            "string_decoder": "~0.10.x",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "karma": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
+      "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==",
+      "dev": true,
+      "requires": {
+        "bluebird": "^3.3.0",
+        "body-parser": "^1.16.1",
+        "chokidar": "^1.4.1",
+        "colors": "^1.1.0",
+        "combine-lists": "^1.0.0",
+        "connect": "^3.6.0",
+        "core-js": "^2.2.0",
+        "di": "^0.0.1",
+        "dom-serialize": "^2.2.0",
+        "expand-braces": "^0.1.1",
+        "glob": "^7.1.1",
+        "graceful-fs": "^4.1.2",
+        "http-proxy": "^1.13.0",
+        "isbinaryfile": "^3.0.0",
+        "lodash": "^3.8.0",
+        "log4js": "^0.6.31",
+        "mime": "^1.3.4",
+        "minimatch": "^3.0.2",
+        "optimist": "^0.6.1",
+        "qjobs": "^1.1.4",
+        "range-parser": "^1.2.0",
+        "rimraf": "^2.6.0",
+        "safe-buffer": "^5.0.1",
+        "socket.io": "1.7.3",
+        "source-map": "^0.5.3",
+        "tmp": "0.0.31",
+        "useragent": "^2.1.12"
+      },
+      "dependencies": {
+        "anymatch": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+          "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+          "dev": true,
+          "requires": {
+            "micromatch": "^2.1.5",
+            "normalize-path": "^2.0.0"
+          }
+        },
+        "arr-diff": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+          "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+          "dev": true,
+          "requires": {
+            "arr-flatten": "^1.0.1"
+          }
+        },
+        "array-unique": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+          "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+          "dev": true
+        },
+        "braces": {
+          "version": "1.8.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+          "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+          "dev": true,
+          "requires": {
+            "expand-range": "^1.8.1",
+            "preserve": "^0.2.0",
+            "repeat-element": "^1.1.2"
+          }
+        },
+        "chokidar": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+          "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+          "dev": true,
+          "requires": {
+            "anymatch": "^1.3.0",
+            "async-each": "^1.0.0",
+            "fsevents": "^1.0.0",
+            "glob-parent": "^2.0.0",
+            "inherits": "^2.0.1",
+            "is-binary-path": "^1.0.0",
+            "is-glob": "^2.0.0",
+            "path-is-absolute": "^1.0.0",
+            "readdirp": "^2.0.0"
+          }
+        },
+        "expand-brackets": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+          "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+          "dev": true,
+          "requires": {
+            "is-posix-bracket": "^0.1.0"
+          }
+        },
+        "extglob": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+          "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "^2.0.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        },
+        "lodash": {
+          "version": "3.10.1",
+          "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+          "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
+          "dev": true
+        },
+        "micromatch": {
+          "version": "2.3.11",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+          "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+          "dev": true,
+          "requires": {
+            "arr-diff": "^2.0.0",
+            "array-unique": "^0.2.1",
+            "braces": "^1.8.2",
+            "expand-brackets": "^0.1.4",
+            "extglob": "^0.3.1",
+            "filename-regex": "^2.0.0",
+            "is-extglob": "^1.0.0",
+            "is-glob": "^2.0.1",
+            "kind-of": "^3.0.2",
+            "normalize-path": "^2.0.1",
+            "object.omit": "^2.0.0",
+            "parse-glob": "^3.0.4",
+            "regex-cache": "^0.4.2"
+          }
+        }
+      }
+    },
+    "karma-chrome-launcher": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz",
+      "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==",
+      "dev": true,
+      "requires": {
+        "fs-access": "^1.0.0",
+        "which": "^1.2.1"
+      }
+    },
+    "karma-coverage-istanbul-reporter": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.3.tgz",
+      "integrity": "sha512-UVs9IDulfwkBxjEnUzfR/nIc3oBneOPuorpLVBvEMtz2hy0wnVLhCMxpkqAtuQWqvOZRQlGqs+dDtMUeRydTQA==",
+      "dev": true,
+      "requires": {
+        "istanbul-api": "^2.0.5",
+        "minimatch": "^3.0.4"
+      }
+    },
+    "karma-jasmine": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz",
+      "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=",
+      "dev": true
+    },
+    "karma-jasmine-html-reporter": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz",
+      "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=",
+      "dev": true,
+      "requires": {
+        "karma-jasmine": "^1.0.2"
+      }
+    },
+    "karma-phantomjs-launcher": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz",
+      "integrity": "sha1-0jyjSAG9qYY60xjju0vUBisTrNI=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.0.1",
+        "phantomjs-prebuilt": "^2.1.7"
+      }
+    },
+    "karma-source-map-support": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz",
+      "integrity": "sha512-HcPqdAusNez/ywa+biN4EphGz62MmQyPggUsDfsHqa7tSe4jdsxgvTKuDfIazjL+IOxpVWyT7Pr4dhAV+sxX5Q==",
+      "dev": true,
+      "requires": {
+        "source-map-support": "^0.5.5"
+      }
+    },
+    "kew": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
+      "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=",
+      "dev": true
+    },
+    "killable": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz",
+      "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=",
+      "dev": true
+    },
+    "kind-of": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+      "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+      "dev": true
+    },
+    "klaw": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+      "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.9"
+      }
+    },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "dev": true,
+      "requires": {
+        "invert-kv": "^1.0.0"
+      }
+    },
+    "leb": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz",
+      "integrity": "sha1-Mr7p+tFoMo1q6oUi2DP0GA7tHaM=",
+      "dev": true
+    },
+    "less": {
+      "version": "3.8.1",
+      "resolved": "https://registry.npmjs.org/less/-/less-3.8.1.tgz",
+      "integrity": "sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q==",
+      "dev": true,
+      "requires": {
+        "clone": "^2.1.2",
+        "errno": "^0.1.1",
+        "graceful-fs": "^4.1.2",
+        "image-size": "~0.5.0",
+        "mime": "^1.4.1",
+        "mkdirp": "^0.5.0",
+        "promise": "^7.1.1",
+        "request": "^2.83.0",
+        "source-map": "~0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "less-loader": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz",
+      "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==",
+      "dev": true,
+      "requires": {
+        "clone": "^2.1.1",
+        "loader-utils": "^1.1.0",
+        "pify": "^3.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "license-webpack-plugin": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.4.0.tgz",
+      "integrity": "sha512-iwuNFMWbXS76WiQXJBTs8/7Tby4NQnY8AIkBMuJG5El79UT8zWrJQMfpW+KRXt4Y2Bs5uk+Myg/MO7ROSF8jzA==",
+      "dev": true,
+      "requires": {
+        "ejs": "^2.5.7"
+      }
+    },
+    "lie": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+      "dev": true,
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "strip-bom": "^2.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "loader-runner": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz",
+      "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=",
+      "dev": true
+    },
+    "loader-utils": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
+      "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
+      "dev": true,
+      "requires": {
+        "big.js": "^3.1.3",
+        "emojis-list": "^2.0.0",
+        "json5": "^0.5.0"
+      }
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "dev": true,
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.10",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+      "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
+      "dev": true
+    },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+      "dev": true,
+      "optional": true
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+      "dev": true
+    },
+    "lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+      "dev": true
+    },
+    "lodash.mergewith": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
+      "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+      "dev": true,
+      "optional": true
+    },
+    "lodash.tail": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
+      "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
+      "dev": true
+    },
+    "log4js": {
+      "version": "0.6.38",
+      "resolved": "http://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz",
+      "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "~1.0.2",
+        "semver": "~4.3.3"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "semver": {
+          "version": "4.3.6",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
+          "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
+          "dev": true
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "loglevel": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
+      "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=",
+      "dev": true
+    },
+    "long": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
+      "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=",
+      "dev": true
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "^0.4.1",
+        "signal-exit": "^3.0.0"
+      }
+    },
+    "lower-case": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+      "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
+      "dev": true
+    },
+    "lru-cache": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
+      "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
+      "dev": true,
+      "requires": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "make-dir": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+      "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+      "dev": true,
+      "requires": {
+        "pify": "^3.0.0"
+      }
+    },
+    "make-error": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+      "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
+      "dev": true
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+      "dev": true
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "dev": true,
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "math-random": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
+      "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
+      "dev": true
+    },
+    "md5.js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
+      "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "dev": true
+    },
+    "mem": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
+      "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^1.0.0"
+      }
+    },
+    "memory-fs": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+      "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+      "dev": true,
+      "requires": {
+        "errno": "^0.1.3",
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase-keys": "^2.0.0",
+        "decamelize": "^1.1.2",
+        "loud-rejection": "^1.0.0",
+        "map-obj": "^1.0.1",
+        "minimist": "^1.1.3",
+        "normalize-package-data": "^2.3.4",
+        "object-assign": "^4.0.1",
+        "read-pkg-up": "^1.0.1",
+        "redent": "^1.0.0",
+        "trim-newlines": "^1.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+      "dev": true
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+      "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "braces": "^2.3.1",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "extglob": "^2.0.4",
+        "fragment-cache": "^0.2.1",
+        "kind-of": "^6.0.2",
+        "nanomatch": "^1.2.9",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.2"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.0.0",
+        "brorand": "^1.0.1"
+      }
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true
+    },
+    "mime-db": {
+      "version": "1.36.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
+      "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.20",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz",
+      "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==",
+      "dev": true,
+      "requires": {
+        "mime-db": "~1.36.0"
+      }
+    },
+    "mimic-fn": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+      "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+      "dev": true
+    },
+    "mini-css-extract-plugin": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.2.tgz",
+      "integrity": "sha512-ots7URQH4wccfJq9Ssrzu2+qupbncAce4TmTzunI9CIwlQMp2XI+WNUw6xWF6MMAGAm1cbUVINrSjATaVMyKXg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^1.0.0",
+        "webpack-sources": "^1.1.0"
+      },
+      "dependencies": {
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+      "dev": true
+    },
+    "mississippi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz",
+      "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==",
+      "dev": true,
+      "requires": {
+        "concat-stream": "^1.5.0",
+        "duplexify": "^3.4.2",
+        "end-of-stream": "^1.1.0",
+        "flush-write-stream": "^1.0.0",
+        "from2": "^2.1.0",
+        "parallel-transform": "^1.1.0",
+        "pump": "^2.0.1",
+        "pumpify": "^1.3.3",
+        "stream-each": "^1.1.0",
+        "through2": "^2.0.0"
+      }
+    },
+    "mixin-deep": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
+      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mixin-object": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
+      "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=",
+      "dev": true,
+      "requires": {
+        "for-in": "^0.1.3",
+        "is-extendable": "^0.1.1"
+      },
+      "dependencies": {
+        "for-in": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
+          "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=",
+          "dev": true
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "move-concurrently": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
+      "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1",
+        "copy-concurrently": "^1.0.0",
+        "fs-write-stream-atomic": "^1.0.8",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.5.4",
+        "run-queue": "^1.0.3"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
+    },
+    "multicast-dns": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
+      "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+      "dev": true,
+      "requires": {
+        "dns-packet": "^1.3.1",
+        "thunky": "^1.0.2"
+      }
+    },
+    "multicast-dns-service-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
+      "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
+      "dev": true
+    },
+    "nan": {
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
+      "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==",
+      "dev": true,
+      "optional": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      }
+    },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+      "dev": true
+    },
+    "neo-async": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz",
+      "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==",
+      "dev": true
+    },
+    "ngx-cookie": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-4.0.2.tgz",
+      "integrity": "sha512-YCak+Itql8EDkMfr9lzCNd2wEeV+uflbv2V1mi9LCzUyFcO+W53S/BbuZS5r9M8MZzUiBl4AmpEDEKYiXrb3Sw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "ngx-i18nsupport": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/ngx-i18nsupport/-/ngx-i18nsupport-0.17.0.tgz",
+      "integrity": "sha512-iGH3CnEehukzuU9OFai3Kwi06CsNRMI3zquIjUTBUDlVgRwpfGse0BGrr/RRJ359i9P0aeNWtjnDKsW4apSAOw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "commander": "^2.15.1",
+        "he": "^1.1.1",
+        "ngx-i18nsupport-lib": "^1.10.0",
+        "request": "^2.85.0",
+        "rxjs": "^6.0.0"
+      }
+    },
+    "ngx-i18nsupport-lib": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/ngx-i18nsupport-lib/-/ngx-i18nsupport-lib-1.10.0.tgz",
+      "integrity": "sha512-J+0EvMrG31o5SJrb3sZS9WPdc34Qbmq9CglVlemV68kPcCvPbe8yj3qJthOmtoVz8t9ksGugYkB42KZPEMTSeA==",
+      "dev": true,
+      "requires": {
+        "@types/xmldom": "^0.1.29",
+        "tokenizr": "^1.3.4",
+        "xmldom": "^0.1.27"
+      }
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
+    "no-case": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+      "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+      "dev": true,
+      "requires": {
+        "lower-case": "^1.1.1"
+      }
+    },
+    "node-forge": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
+      "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==",
+      "dev": true
+    },
+    "node-gyp": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+      "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "fstream": "^1.0.0",
+        "glob": "^7.0.3",
+        "graceful-fs": "^4.1.2",
+        "mkdirp": "^0.5.0",
+        "nopt": "2 || 3",
+        "npmlog": "0 || 1 || 2 || 3 || 4",
+        "osenv": "0",
+        "request": "^2.87.0",
+        "rimraf": "2",
+        "semver": "~5.3.0",
+        "tar": "^2.0.0",
+        "which": "1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+          "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "node-libs-browser": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
+      "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==",
+      "dev": true,
+      "requires": {
+        "assert": "^1.1.1",
+        "browserify-zlib": "^0.2.0",
+        "buffer": "^4.3.0",
+        "console-browserify": "^1.1.0",
+        "constants-browserify": "^1.0.0",
+        "crypto-browserify": "^3.11.0",
+        "domain-browser": "^1.1.1",
+        "events": "^1.0.0",
+        "https-browserify": "^1.0.0",
+        "os-browserify": "^0.3.0",
+        "path-browserify": "0.0.0",
+        "process": "^0.11.10",
+        "punycode": "^1.2.4",
+        "querystring-es3": "^0.2.0",
+        "readable-stream": "^2.3.3",
+        "stream-browserify": "^2.0.1",
+        "stream-http": "^2.7.2",
+        "string_decoder": "^1.0.0",
+        "timers-browserify": "^2.0.4",
+        "tty-browserify": "0.0.0",
+        "url": "^0.11.0",
+        "util": "^0.10.3",
+        "vm-browserify": "0.0.4"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "node-sass": {
+      "version": "4.9.3",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz",
+      "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "async-foreach": "^0.1.3",
+        "chalk": "^1.1.1",
+        "cross-spawn": "^3.0.0",
+        "gaze": "^1.0.0",
+        "get-stdin": "^4.0.1",
+        "glob": "^7.0.3",
+        "in-publish": "^2.0.0",
+        "lodash.assign": "^4.2.0",
+        "lodash.clonedeep": "^4.3.2",
+        "lodash.mergewith": "^4.6.0",
+        "meow": "^3.7.0",
+        "mkdirp": "^0.5.1",
+        "nan": "^2.10.0",
+        "node-gyp": "^3.8.0",
+        "npmlog": "^4.0.0",
+        "request": "2.87.0",
+        "sass-graph": "^2.2.4",
+        "stdout-stream": "^1.4.0",
+        "true-case-path": "^1.0.2"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true,
+          "optional": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "har-validator": {
+          "version": "5.0.3",
+          "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
+          "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ajv": "^5.1.0",
+            "har-schema": "^2.0.0"
+          }
+        },
+        "oauth-sign": {
+          "version": "0.8.2",
+          "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+          "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+          "dev": true,
+          "optional": true
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true,
+          "optional": true
+        },
+        "request": {
+          "version": "2.87.0",
+          "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
+          "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aws-sign2": "~0.7.0",
+            "aws4": "^1.6.0",
+            "caseless": "~0.12.0",
+            "combined-stream": "~1.0.5",
+            "extend": "~3.0.1",
+            "forever-agent": "~0.6.1",
+            "form-data": "~2.3.1",
+            "har-validator": "~5.0.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.17",
+            "oauth-sign": "~0.8.2",
+            "performance-now": "^2.1.0",
+            "qs": "~6.5.1",
+            "safe-buffer": "^5.1.1",
+            "tough-cookie": "~2.3.3",
+            "tunnel-agent": "^0.6.0",
+            "uuid": "^3.1.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true,
+          "optional": true
+        },
+        "tough-cookie": {
+          "version": "2.3.4",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+          "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "punycode": "^1.4.1"
+          }
+        }
+      }
+    },
+    "nopt": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "is-builtin-module": "^1.0.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "^1.0.1"
+      }
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "npm-package-arg": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz",
+      "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.6.0",
+        "osenv": "^0.1.5",
+        "semver": "^5.5.0",
+        "validate-npm-package-name": "^3.0.0"
+      }
+    },
+    "npm-registry-client": {
+      "version": "8.6.0",
+      "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.6.0.tgz",
+      "integrity": "sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg==",
+      "dev": true,
+      "requires": {
+        "concat-stream": "^1.5.2",
+        "graceful-fs": "^4.1.6",
+        "normalize-package-data": "~1.0.1 || ^2.0.0",
+        "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
+        "npmlog": "2 || ^3.1.0 || ^4.0.0",
+        "once": "^1.3.3",
+        "request": "^2.74.0",
+        "retry": "^0.10.0",
+        "safe-buffer": "^5.1.1",
+        "semver": "2 >=2.2.1 || 3.x || 4 || 5",
+        "slide": "^1.1.3",
+        "ssri": "^5.2.4"
+      }
+    },
+    "npm-run-path": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+      "dev": true,
+      "requires": {
+        "path-key": "^2.0.0"
+      }
+    },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+      "dev": true,
+      "requires": {
+        "are-we-there-yet": "~1.1.2",
+        "console-control-strings": "~1.1.0",
+        "gauge": "~2.7.3",
+        "set-blocking": "~2.0.0"
+      }
+    },
+    "nth-check": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
+      "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=",
+      "dev": true,
+      "requires": {
+        "boolbase": "~1.0.0"
+      }
+    },
+    "null-check": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz",
+      "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=",
+      "dev": true
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
+      "dev": true
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "dev": true,
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "object-keys": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
+      "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
+      "dev": true
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.0"
+      }
+    },
+    "object.getownpropertydescriptors": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
+      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "es-abstract": "^1.5.1"
+      }
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "dev": true,
+      "requires": {
+        "for-own": "^0.1.4",
+        "is-extendable": "^0.1.1"
+      },
+      "dependencies": {
+        "for-own": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+          "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+          "dev": true,
+          "requires": {
+            "for-in": "^1.0.1"
+          }
+        }
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "dev": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      }
+    },
+    "obuf": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+      "dev": true
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "opn": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
+      "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==",
+      "dev": true,
+      "requires": {
+        "is-wsl": "^1.1.0"
+      }
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "options": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
+      "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=",
+      "dev": true
+    },
+    "original": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
+      "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
+      "dev": true,
+      "requires": {
+        "url-parse": "^1.4.3"
+      }
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+      "dev": true
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "dev": true
+    },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "lcid": "^1.0.0"
+      }
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "osenv": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+      "dev": true,
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.0"
+      }
+    },
+    "p-finally": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+      "dev": true
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "dev": true,
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-map": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
+      "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
+      "dev": true
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "dev": true
+    },
+    "pako": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+      "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
+      "dev": true
+    },
+    "parallel-transform": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
+      "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
+      "dev": true,
+      "requires": {
+        "cyclist": "~0.2.2",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.1.5"
+      }
+    },
+    "param-case": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+      "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+      "dev": true,
+      "requires": {
+        "no-case": "^2.2.0"
+      }
+    },
+    "parse-asn1": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+      "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
+      "dev": true,
+      "requires": {
+        "asn1.js": "^4.0.0",
+        "browserify-aes": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.0",
+        "pbkdf2": "^3.0.3"
+      }
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "dev": true,
+      "requires": {
+        "glob-base": "^0.3.0",
+        "is-dotfile": "^1.0.0",
+        "is-extglob": "^1.0.0",
+        "is-glob": "^2.0.0"
+      },
+      "dependencies": {
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        },
+        "is-glob": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+          "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "^1.0.0"
+          }
+        }
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parse5": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+      "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+      "dev": true
+    },
+    "parsejson": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
+      "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "dev": true,
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "dev": true
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+      "dev": true
+    },
+    "path-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+      "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
+      "dev": true
+    },
+    "path-dirname": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+      "dev": true
+    },
+    "path-type": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+      "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+      "dev": true,
+      "requires": {
+        "pify": "^3.0.0"
+      }
+    },
+    "pbkdf2": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",
+      "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==",
+      "dev": true,
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+      "dev": true
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
+    "phantomjs-prebuilt": {
+      "version": "2.1.16",
+      "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz",
+      "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3",
+        "extract-zip": "^1.6.5",
+        "fs-extra": "^1.0.0",
+        "hasha": "^2.2.0",
+        "kew": "^0.7.0",
+        "progress": "^1.1.8",
+        "request": "^2.81.0",
+        "request-progress": "^2.0.1",
+        "which": "^1.2.10"
+      }
+    },
+    "pify": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+      "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg-dir": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
+      "dev": true,
+      "requires": {
+        "find-up": "^2.1.0"
+      }
+    },
+    "portfinder": {
+      "version": "1.0.17",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.17.tgz",
+      "integrity": "sha512-syFcRIRzVI1BoEFOCaAiizwDolh1S1YXSodsVhncbhjzjZQulhczNRbqnUl9N31Q4dKGOXsNDqxC2BWBgSMqeQ==",
+      "dev": true,
+      "requires": {
+        "async": "^1.5.2",
+        "debug": "^2.2.0",
+        "mkdirp": "0.5.x"
+      }
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+      "dev": true
+    },
+    "postcss": {
+      "version": "6.0.23",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+      "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "source-map": "^0.6.1",
+        "supports-color": "^5.4.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "postcss-import": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz",
+      "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==",
+      "dev": true,
+      "requires": {
+        "postcss": "^6.0.1",
+        "postcss-value-parser": "^3.2.3",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      }
+    },
+    "postcss-load-config": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz",
+      "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==",
+      "dev": true,
+      "requires": {
+        "cosmiconfig": "^4.0.0",
+        "import-cwd": "^2.0.0"
+      }
+    },
+    "postcss-loader": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.6.tgz",
+      "integrity": "sha512-hgiWSc13xVQAq25cVw80CH0l49ZKlAnU1hKPOdRrNj89bokRr/bZF2nT+hebPPF9c9xs8c3gw3Fr2nxtmXYnNg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "postcss": "^6.0.0",
+        "postcss-load-config": "^2.0.0",
+        "schema-utils": "^0.4.0"
+      }
+    },
+    "postcss-url": {
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.2.tgz",
+      "integrity": "sha512-QMV5mA+pCYZQcUEPQkmor9vcPQ2MT+Ipuu8qdi1gVxbNiIiErEGft+eny1ak19qALoBkccS5AHaCaCDzh7b9MA==",
+      "dev": true,
+      "requires": {
+        "mime": "^1.4.1",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.0",
+        "postcss": "^6.0.1",
+        "xxhashjs": "^0.2.1"
+      }
+    },
+    "postcss-value-parser": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz",
+      "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+      "dev": true
+    },
+    "pretty-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz",
+      "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=",
+      "dev": true,
+      "requires": {
+        "renderkid": "^2.0.1",
+        "utila": "~0.4"
+      }
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+      "dev": true
+    },
+    "progress": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+      "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+      "dev": true
+    },
+    "promise": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+      "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "asap": "~2.0.3"
+      }
+    },
+    "promise-inflight": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+      "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
+      "dev": true
+    },
+    "protractor": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.0.tgz",
+      "integrity": "sha512-6TSYqMhUUzxr4/wN0ttSISqPMKvcVRXF4k8jOEpGWD8OioLak4KLgfzHK9FJ49IrjzRrZ+Mx1q2Op8Rk0zEcnQ==",
+      "dev": true,
+      "requires": {
+        "@types/node": "^6.0.46",
+        "@types/q": "^0.0.32",
+        "@types/selenium-webdriver": "^3.0.0",
+        "blocking-proxy": "^1.0.0",
+        "browserstack": "^1.5.1",
+        "chalk": "^1.1.3",
+        "glob": "^7.0.3",
+        "jasmine": "2.8.0",
+        "jasminewd2": "^2.1.0",
+        "optimist": "~0.6.0",
+        "q": "1.4.1",
+        "saucelabs": "^1.5.0",
+        "selenium-webdriver": "3.6.0",
+        "source-map-support": "~0.4.0",
+        "webdriver-js-extender": "2.0.0",
+        "webdriver-manager": "^12.0.6"
+      },
+      "dependencies": {
+        "@types/node": {
+          "version": "6.0.117",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.117.tgz",
+          "integrity": "sha512-sihk0SnN8PpiS5ihu5xJQ5ddnURNq4P+XPmW+nORlKkHy21CoZO/IVHK/Wq/l3G8fFW06Fkltgnqx229uPlnRg==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "del": {
+          "version": "2.2.2",
+          "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+          "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+          "dev": true,
+          "requires": {
+            "globby": "^5.0.0",
+            "is-path-cwd": "^1.0.0",
+            "is-path-in-cwd": "^1.0.0",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0",
+            "rimraf": "^2.2.8"
+          }
+        },
+        "globby": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+          "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+          "dev": true,
+          "requires": {
+            "array-union": "^1.0.1",
+            "arrify": "^1.0.0",
+            "glob": "^7.0.3",
+            "object-assign": "^4.0.1",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        },
+        "source-map-support": {
+          "version": "0.4.18",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+          "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+          "dev": true,
+          "requires": {
+            "source-map": "^0.5.6"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        },
+        "webdriver-manager": {
+          "version": "12.1.0",
+          "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.0.tgz",
+          "integrity": "sha512-oEc5fmkpz6Yh6udhwir5m0eN5mgRPq9P/NU5YWuT3Up5slt6Zz+znhLU7q4+8rwCZz/Qq3Fgpr/4oao7NPCm2A==",
+          "dev": true,
+          "requires": {
+            "adm-zip": "^0.4.9",
+            "chalk": "^1.1.1",
+            "del": "^2.2.0",
+            "glob": "^7.0.3",
+            "ini": "^1.3.4",
+            "minimist": "^1.2.0",
+            "q": "^1.4.1",
+            "request": "^2.87.0",
+            "rimraf": "^2.5.2",
+            "semver": "^5.3.0",
+            "xml2js": "^0.4.17"
+          }
+        }
+      }
+    },
+    "proxy-addr": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+      "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+      "dev": true,
+      "requires": {
+        "forwarded": "~0.1.2",
+        "ipaddr.js": "1.8.0"
+      }
+    },
+    "prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "dev": true
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
+    "psl": {
+      "version": "1.1.29",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
+      "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
+      "dev": true
+    },
+    "public-encrypt": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
+      "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==",
+      "dev": true,
+      "requires": {
+        "bn.js": "^4.1.0",
+        "browserify-rsa": "^4.0.0",
+        "create-hash": "^1.1.0",
+        "parse-asn1": "^5.0.0",
+        "randombytes": "^2.0.1"
+      }
+    },
+    "pump": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "pumpify": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+      "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+      "dev": true,
+      "requires": {
+        "duplexify": "^3.6.0",
+        "inherits": "^2.0.3",
+        "pump": "^2.0.0"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "q": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
+      "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+      "dev": true
+    },
+    "qjobs": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
+      "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
+      "dev": true
+    },
+    "qs": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "dev": true
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "querystringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz",
+      "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==",
+      "dev": true
+    },
+    "randomatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz",
+      "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^4.0.0",
+        "kind-of": "^6.0.0",
+        "math-random": "^1.0.1"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+          "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+          "dev": true
+        }
+      }
+    },
+    "randombytes": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
+      "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+      "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+      "dev": true,
+      "requires": {
+        "randombytes": "^2.0.5",
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "raw-body": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+      "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "http-errors": "1.6.2",
+        "iconv-lite": "0.4.19",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "depd": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+          "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
+          "dev": true
+        },
+        "http-errors": {
+          "version": "1.6.2",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+          "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+          "dev": true,
+          "requires": {
+            "depd": "1.1.1",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.0.3",
+            "statuses": ">= 1.3.1 < 2"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+          "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=",
+          "dev": true
+        }
+      }
+    },
+    "raw-loader": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
+      "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=",
+      "dev": true
+    },
+    "read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+      "dev": true,
+      "requires": {
+        "pify": "^2.3.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^1.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^1.0.0"
+      },
+      "dependencies": {
+        "path-type": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+          "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "pify": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.0.0",
+        "read-pkg": "^1.0.0"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+          "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+          "dev": true,
+          "requires": {
+            "path-exists": "^2.0.0",
+            "pinkie-promise": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "^2.0.0"
+          }
+        }
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+      "dev": true,
+      "requires": {
+        "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"
+      }
+    },
+    "readdirp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
+      "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "minimatch": "^3.0.2",
+        "readable-stream": "^2.0.2",
+        "set-immediate-shim": "^1.0.1"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "indent-string": "^2.1.0",
+        "strip-indent": "^1.0.1"
+      }
+    },
+    "reflect-metadata": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",
+      "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==",
+      "dev": true
+    },
+    "regenerate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+      "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
+      "dev": true
+    },
+    "regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+      "dev": true
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+      "dev": true,
+      "requires": {
+        "is-equal-shallow": "^0.1.3"
+      }
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regexpu-core": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
+      "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
+      "dev": true,
+      "requires": {
+        "regenerate": "^1.2.1",
+        "regjsgen": "^0.2.0",
+        "regjsparser": "^0.1.4"
+      }
+    },
+    "regjsgen": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+      "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+      "dev": true
+    },
+    "regjsparser": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+      "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+      "dev": true,
+      "requires": {
+        "jsesc": "~0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+          "dev": true
+        }
+      }
+    },
+    "relateurl": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+      "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
+      "dev": true
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "renderkid": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz",
+      "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=",
+      "dev": true,
+      "requires": {
+        "css-select": "^1.1.0",
+        "dom-converter": "~0.1",
+        "htmlparser2": "~3.3.0",
+        "strip-ansi": "^3.0.0",
+        "utila": "~0.3"
+      },
+      "dependencies": {
+        "utila": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz",
+          "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=",
+          "dev": true
+        }
+      }
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "dev": true,
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "dev": true,
+      "requires": {
+        "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-progress": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz",
+      "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=",
+      "dev": true,
+      "requires": {
+        "throttleit": "^1.0.0"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "dev": true
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+      "dev": true
+    },
+    "resolve": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+      "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+      "dev": true
+    },
+    "resolve-cwd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
+      "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
+      "dev": true,
+      "requires": {
+        "resolve-from": "^3.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+      "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
+      "dev": true
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+      "dev": true
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true
+    },
+    "retry": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
+      "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+      "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.5"
+      }
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "dev": true,
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "run-queue": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
+      "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
+      "dev": true,
+      "requires": {
+        "aproba": "^1.1.1"
+      }
+    },
+    "rxjs": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.2.tgz",
+      "integrity": "sha512-hV7criqbR0pe7EeL3O66UYVg92IR0XsA97+9y+BWTePK9SKmEI5Qd3Zj6uPnGkNzXsBywBQWTvujPl+1Kn9Zjw==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "rxjs-compat": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.3.2.tgz",
+      "integrity": "sha512-eH0ANsX4ufMSDmSDwWbsWYgZDDDxxLHxsSwApbQumHTFm83RP4AI594QtXv3Jup+hVjXfE2dRSAVKbMh2a2hcw=="
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "dev": true,
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
+    "sass-graph": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+      "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "glob": "^7.0.0",
+        "lodash": "^4.0.0",
+        "scss-tokenizer": "^0.2.3",
+        "yargs": "^7.0.0"
+      }
+    },
+    "sass-loader": {
+      "version": "6.0.7",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.7.tgz",
+      "integrity": "sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==",
+      "dev": true,
+      "requires": {
+        "clone-deep": "^2.0.1",
+        "loader-utils": "^1.0.1",
+        "lodash.tail": "^4.1.1",
+        "neo-async": "^2.5.0",
+        "pify": "^3.0.0"
+      }
+    },
+    "saucelabs": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz",
+      "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==",
+      "dev": true,
+      "requires": {
+        "https-proxy-agent": "^2.2.1"
+      }
+    },
+    "sax": {
+      "version": "0.5.8",
+      "resolved": "http://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
+      "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=",
+      "dev": true
+    },
+    "schema-utils": {
+      "version": "0.4.7",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
+      "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0"
+      }
+    },
+    "scss-tokenizer": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+      "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "js-base64": "^2.1.8",
+        "source-map": "^0.4.2"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "select-hose": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+      "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
+      "dev": true
+    },
+    "selenium-webdriver": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
+      "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==",
+      "dev": true,
+      "requires": {
+        "jszip": "^3.1.3",
+        "rimraf": "^2.5.4",
+        "tmp": "0.0.30",
+        "xml2js": "^0.4.17"
+      },
+      "dependencies": {
+        "tmp": {
+          "version": "0.0.30",
+          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
+          "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
+          "dev": true,
+          "requires": {
+            "os-tmpdir": "~1.0.1"
+          }
+        }
+      }
+    },
+    "selfsigned": {
+      "version": "1.10.3",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz",
+      "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==",
+      "dev": true,
+      "requires": {
+        "node-forge": "0.7.5"
+      }
+    },
+    "semver": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
+      "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
+      "dev": true
+    },
+    "semver-dsl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz",
+      "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=",
+      "dev": true,
+      "requires": {
+        "semver": "^5.3.0"
+      }
+    },
+    "semver-intersect": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.4.0.tgz",
+      "integrity": "sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ==",
+      "dev": true,
+      "requires": {
+        "semver": "^5.0.0"
+      }
+    },
+    "send": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "~1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.0",
+        "statuses": "~1.4.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+          "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
+          "dev": true
+        }
+      }
+    },
+    "serialize-javascript": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz",
+      "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==",
+      "dev": true
+    },
+    "serve-index": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+      "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.4",
+        "batch": "0.6.1",
+        "debug": "2.6.9",
+        "escape-html": "~1.0.3",
+        "http-errors": "~1.6.2",
+        "mime-types": "~2.1.17",
+        "parseurl": "~1.3.2"
+      }
+    },
+    "serve-static": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+      "dev": true,
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.2",
+        "send": "0.16.2"
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+      "dev": true
+    },
+    "set-value": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
+      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+      "dev": true
+    },
+    "setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "shallow-clone": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
+      "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
+      "dev": true,
+      "requires": {
+        "is-extendable": "^0.1.1",
+        "kind-of": "^5.0.0",
+        "mixin-object": "^2.0.1"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true
+        }
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+      "dev": true
+    },
+    "slide": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
+      "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=",
+      "dev": true
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "dev": true,
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.2.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "socket.io": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz",
+      "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=",
+      "dev": true,
+      "requires": {
+        "debug": "2.3.3",
+        "engine.io": "1.8.3",
+        "has-binary": "0.1.7",
+        "object-assign": "4.1.0",
+        "socket.io-adapter": "0.5.0",
+        "socket.io-client": "1.7.3",
+        "socket.io-parser": "2.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        },
+        "object-assign": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
+          "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz",
+      "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=",
+      "dev": true,
+      "requires": {
+        "debug": "2.3.3",
+        "socket.io-parser": "2.3.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-client": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz",
+      "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=",
+      "dev": true,
+      "requires": {
+        "backo2": "1.0.2",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "2.3.3",
+        "engine.io-client": "1.8.3",
+        "has-binary": "0.1.7",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "2.3.1",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
+          "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.2"
+          }
+        },
+        "ms": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
+          "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz",
+      "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.1.2",
+        "debug": "2.2.0",
+        "isarray": "0.0.1",
+        "json3": "3.3.2"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz",
+          "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=",
+          "dev": true
+        },
+        "debug": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
+          "dev": true,
+          "requires": {
+            "ms": "0.7.1"
+          }
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "ms": {
+          "version": "0.7.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
+          "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
+          "dev": true
+        }
+      }
+    },
+    "sockjs": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
+      "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
+      "dev": true,
+      "requires": {
+        "faye-websocket": "^0.10.0",
+        "uuid": "^3.0.1"
+      }
+    },
+    "sockjs-client": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz",
+      "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.6",
+        "eventsource": "0.1.6",
+        "faye-websocket": "~0.11.0",
+        "inherits": "^2.0.1",
+        "json3": "^3.3.2",
+        "url-parse": "^1.1.8"
+      },
+      "dependencies": {
+        "faye-websocket": {
+          "version": "0.11.1",
+          "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz",
+          "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=",
+          "dev": true,
+          "requires": {
+            "websocket-driver": ">=0.5.1"
+          }
+        }
+      }
+    },
+    "source-list-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
+      "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "source-map-loader": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz",
+      "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==",
+      "dev": true,
+      "requires": {
+        "async": "^2.5.0",
+        "loader-utils": "^1.1.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        }
+      }
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "dev": true,
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-support": {
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz",
+      "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+      "dev": true
+    },
+    "spdx-correct": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
+      "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
+      "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz",
+      "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==",
+      "dev": true
+    },
+    "spdy": {
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz",
+      "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.8",
+        "handle-thing": "^1.2.5",
+        "http-deceiver": "^1.2.7",
+        "safe-buffer": "^5.0.1",
+        "select-hose": "^2.0.0",
+        "spdy-transport": "^2.0.18"
+      }
+    },
+    "spdy-transport": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz",
+      "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.8",
+        "detect-node": "^2.0.3",
+        "hpack.js": "^2.1.6",
+        "obuf": "^1.1.1",
+        "readable-stream": "^2.2.9",
+        "safe-buffer": "^5.0.1",
+        "wbuf": "^1.7.2"
+      }
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.14.2",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
+      "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+      "dev": true,
+      "requires": {
+        "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": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz",
+      "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "dev": true,
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "stats-webpack-plugin": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/stats-webpack-plugin/-/stats-webpack-plugin-0.6.2.tgz",
+      "integrity": "sha1-LFlJtTHgf4eojm6k3PrFOqjHWis=",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.4"
+      }
+    },
+    "statuses": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+      "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+      "dev": true
+    },
+    "stdout-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
+      "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "readable-stream": "^2.0.1"
+      }
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "dev": true,
+      "requires": {
+        "inherits": "~2.0.1",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "stream-each": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
+      "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
+      "dev": true,
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "stream-shift": "^1.0.0"
+      }
+    },
+    "stream-http": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+      "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "^3.0.0",
+        "inherits": "^2.0.1",
+        "readable-stream": "^2.3.6",
+        "to-arraybuffer": "^1.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "stream-shift": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
+      "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
+      "dev": true
+    },
+    "string-width": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "dev": true,
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "strip-ansi": "^3.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
+    "strip-eof": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+      "dev": true
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "get-stdin": "^4.0.1"
+      }
+    },
+    "style-loader": {
+      "version": "0.21.0",
+      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz",
+      "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^0.4.5"
+      }
+    },
+    "stylus": {
+      "version": "0.54.5",
+      "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz",
+      "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=",
+      "dev": true,
+      "requires": {
+        "css-parse": "1.7.x",
+        "debug": "*",
+        "glob": "7.0.x",
+        "mkdirp": "0.5.x",
+        "sax": "0.5.x",
+        "source-map": "0.1.x"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.0.6",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
+          "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.2",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "source-map": {
+          "version": "0.1.43",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+          "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+          "dev": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "stylus-loader": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz",
+      "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.0.2",
+        "lodash.clonedeep": "^4.5.0",
+        "when": "~3.6.x"
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+      "dev": true
+    },
+    "tapable": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz",
+      "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==",
+      "dev": true
+    },
+    "tar": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+      "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "block-stream": "*",
+        "fstream": "^1.0.2",
+        "inherits": "2"
+      }
+    },
+    "throttleit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+      "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "through2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+      "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "^2.1.5",
+        "xtend": "~4.0.1"
+      }
+    },
+    "thunky": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz",
+      "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=",
+      "dev": true
+    },
+    "timers-browserify": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
+      "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==",
+      "dev": true,
+      "requires": {
+        "setimmediate": "^1.0.4"
+      }
+    },
+    "tmp": {
+      "version": "0.0.31",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz",
+      "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.1"
+      }
+    },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=",
+      "dev": true
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+      "dev": true
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "dev": true,
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "dev": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      }
+    },
+    "tokenizr": {
+      "version": "1.3.10",
+      "resolved": "https://registry.npmjs.org/tokenizr/-/tokenizr-1.3.10.tgz",
+      "integrity": "sha512-XlYlczHEQrbmj/JInA9vcsBJlukyTJWvjmQodjlbkul5fZ4o1JDNYAvLlrHZs03CSR8nFjNmTEqN3NrjTjmN+A==",
+      "dev": true
+    },
+    "toposort": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
+      "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=",
+      "dev": true
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "tree-kill": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz",
+      "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==",
+      "dev": true
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true,
+      "optional": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
+      "dev": true
+    },
+    "true-case-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+      "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "glob": "^7.1.2"
+      }
+    },
+    "ts-node": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz",
+      "integrity": "sha512-XK7QmDcNHVmZkVtkiwNDWiERRHPyU8nBqZB1+iv2UhOG0q3RQ9HsZ2CMqISlFbxjrYFGfG2mX7bW4dAyxBVzUw==",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.0",
+        "chalk": "^2.3.0",
+        "diff": "^3.1.0",
+        "make-error": "^1.1.1",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "source-map-support": "^0.5.3",
+        "yn": "^2.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "tsickle": {
+      "version": "0.32.1",
+      "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.32.1.tgz",
+      "integrity": "sha512-JW9j+W0SaMSZGejIFZBk0AiPfnhljK3oLx5SaqxrJhjlvzFyPml5zqG1/PuScUj6yTe1muEqwk5CnDK0cOZmKw==",
+      "dev": true,
+      "requires": {
+        "jasmine-diff": "^0.1.3",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "source-map": "^0.6.0",
+        "source-map-support": "^0.5.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "tslib": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
+    },
+    "tslint": {
+      "version": "5.9.1",
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz",
+      "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.22.0",
+        "builtin-modules": "^1.1.1",
+        "chalk": "^2.3.0",
+        "commander": "^2.12.1",
+        "diff": "^3.2.0",
+        "glob": "^7.1.1",
+        "js-yaml": "^3.7.0",
+        "minimatch": "^3.0.4",
+        "resolve": "^1.3.2",
+        "semver": "^5.3.0",
+        "tslib": "^1.8.0",
+        "tsutils": "^2.12.1"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.8.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+          "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.5"
+          }
+        }
+      }
+    },
+    "tsutils": {
+      "version": "2.29.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+      "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.8.1"
+      }
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true,
+      "optional": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-is": {
+      "version": "1.6.16",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+      "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+      "dev": true,
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.18"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "typescript": {
+      "version": "2.7.2",
+      "resolved": "http://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz",
+      "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==",
+      "dev": true
+    },
+    "uglify-js": {
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
+      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+      "dev": true,
+      "requires": {
+        "commander": "~2.17.1",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "uglifyjs-webpack-plugin": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
+      "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
+      "dev": true,
+      "requires": {
+        "cacache": "^10.0.4",
+        "find-cache-dir": "^1.0.0",
+        "schema-utils": "^0.4.5",
+        "serialize-javascript": "^1.4.0",
+        "source-map": "^0.6.1",
+        "uglify-es": "^3.3.4",
+        "webpack-sources": "^1.1.0",
+        "worker-farm": "^1.5.2"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.13.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
+          "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        },
+        "uglify-es": {
+          "version": "3.3.9",
+          "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
+          "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
+          "dev": true,
+          "requires": {
+            "commander": "~2.13.0",
+            "source-map": "~0.6.1"
+          }
+        }
+      }
+    },
+    "ultron": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
+      "dev": true
+    },
+    "union-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
+      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "dev": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^0.4.3"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        },
+        "set-value": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
+          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
+          "dev": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-extendable": "^0.1.1",
+            "is-plain-object": "^2.0.1",
+            "to-object-path": "^0.3.0"
+          }
+        }
+      }
+    },
+    "unique-filename": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz",
+      "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=",
+      "dev": true,
+      "requires": {
+        "unique-slug": "^2.0.0"
+      }
+    },
+    "unique-slug": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz",
+      "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=",
+      "dev": true,
+      "requires": {
+        "imurmurhash": "^0.1.4"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "dev": true
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "dev": true,
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "dev": true,
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "dev": true,
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+          "dev": true
+        }
+      }
+    },
+    "upath": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
+      "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==",
+      "dev": true
+    },
+    "upper-case": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+      "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
+      "dev": true
+    },
+    "uri-js": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz",
+      "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "url-join": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz",
+      "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=",
+      "dev": true
+    },
+    "url-loader": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.1.tgz",
+      "integrity": "sha512-vugEeXjyYFBCUOpX+ZuaunbK3QXMKaQ3zUnRfIpRBlGkY7QizCnzyyn2ASfcxsvyU3ef+CJppVywnl3Kgf13Gg==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "mime": "^2.0.3",
+        "schema-utils": "^1.0.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
+          "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        }
+      }
+    },
+    "url-parse": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
+      "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
+      "dev": true,
+      "requires": {
+        "querystringify": "^2.0.0",
+        "requires-port": "^1.0.0"
+      }
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+      "dev": true
+    },
+    "useragent": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
+      "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.x",
+        "tmp": "0.0.x"
+      }
+    },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "util.promisify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
+      "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "object.getownpropertydescriptors": "^2.0.3"
+      }
+    },
+    "utila": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+      "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=",
+      "dev": true
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "validate-npm-package-name": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
+      "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
+      "dev": true,
+      "requires": {
+        "builtins": "^1.0.3"
+      }
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "dev": true
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "dev": true,
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
+      "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
+      "dev": true
+    },
+    "watchpack": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
+      "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^2.0.2",
+        "graceful-fs": "^4.1.2",
+        "neo-async": "^2.5.0"
+      }
+    },
+    "wbuf": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+      "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+      "dev": true,
+      "requires": {
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "webassemblyjs": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/webassemblyjs/-/webassemblyjs-1.4.3.tgz",
+      "integrity": "sha512-4lOV1Lv6olz0PJkDGQEp82HempAn147e6BXijWDzz9g7/2nSebVP9GVg62Fz5ZAs55mxq13GA0XLyvY8XkyDjg==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/validation": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "@webassemblyjs/wast-parser": "1.4.3",
+        "long": "^3.2.0"
+      }
+    },
+    "webdriver-js-extender": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.0.0.tgz",
+      "integrity": "sha512-fbyKiVu3azzIc5d4+26YfuPQcFTlgFQV5yQ/0OQj4Ybkl4g1YQuIPskf5v5wqwRJhHJnPHthB6tqCjWHOKLWag==",
+      "dev": true,
+      "requires": {
+        "@types/selenium-webdriver": "^3.0.0",
+        "selenium-webdriver": "^3.0.1"
+      }
+    },
+    "webpack": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.9.2.tgz",
+      "integrity": "sha512-jlWrCrJDU3sdWFprel6jHH8esN2C++Q8ehedRo74u7MWLTUJn9SD7RSgsCTEZCSRpVpMascDylAqPoldauOMfA==",
+      "dev": true,
+      "requires": {
+        "@webassemblyjs/ast": "1.4.3",
+        "@webassemblyjs/wasm-edit": "1.4.3",
+        "@webassemblyjs/wasm-parser": "1.4.3",
+        "acorn": "^5.0.0",
+        "acorn-dynamic-import": "^3.0.0",
+        "ajv": "^6.1.0",
+        "ajv-keywords": "^3.1.0",
+        "chrome-trace-event": "^0.1.1",
+        "enhanced-resolve": "^4.0.0",
+        "eslint-scope": "^3.7.1",
+        "json-parse-better-errors": "^1.0.2",
+        "loader-runner": "^2.3.0",
+        "loader-utils": "^1.1.0",
+        "memory-fs": "~0.4.1",
+        "micromatch": "^3.1.8",
+        "mkdirp": "~0.5.0",
+        "neo-async": "^2.5.0",
+        "node-libs-browser": "^2.0.0",
+        "schema-utils": "^0.4.4",
+        "tapable": "^1.0.0",
+        "uglifyjs-webpack-plugin": "^1.2.4",
+        "watchpack": "^1.5.0",
+        "webpack-sources": "^1.0.1"
+      }
+    },
+    "webpack-core": {
+      "version": "0.6.9",
+      "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
+      "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
+      "dev": true,
+      "requires": {
+        "source-list-map": "~0.1.7",
+        "source-map": "~0.4.1"
+      },
+      "dependencies": {
+        "source-list-map": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz",
+          "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "webpack-dev-middleware": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.2.0.tgz",
+      "integrity": "sha512-YJLMF/96TpKXaEQwaLEo+Z4NDK8aV133ROF6xp9pe3gQoS7sxfpXh4Rv9eC+8vCvWfmDjRQaMSlRPbO+9G6jgA==",
+      "dev": true,
+      "requires": {
+        "loud-rejection": "^1.6.0",
+        "memory-fs": "~0.4.1",
+        "mime": "^2.3.1",
+        "path-is-absolute": "^1.0.0",
+        "range-parser": "^1.0.3",
+        "url-join": "^4.0.0",
+        "webpack-log": "^2.0.0"
+      },
+      "dependencies": {
+        "mime": {
+          "version": "2.3.1",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
+          "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-dev-server": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.7.tgz",
+      "integrity": "sha512-KagFrNHf3QKndS61cXqzkQ4gpdXo0d1LZTTplAJzNK1Ev2ZyJiu+BzerW/2dixYYfpnGzp0AcvCXpmYXIOkFOA==",
+      "dev": true,
+      "requires": {
+        "ansi-html": "0.0.7",
+        "bonjour": "^3.5.0",
+        "chokidar": "^2.0.0",
+        "compression": "^1.5.2",
+        "connect-history-api-fallback": "^1.3.0",
+        "debug": "^3.1.0",
+        "del": "^3.0.0",
+        "express": "^4.16.2",
+        "html-entities": "^1.2.0",
+        "http-proxy-middleware": "~0.18.0",
+        "import-local": "^1.0.0",
+        "internal-ip": "^3.0.1",
+        "ip": "^1.1.5",
+        "killable": "^1.0.0",
+        "loglevel": "^1.4.1",
+        "opn": "^5.1.0",
+        "portfinder": "^1.0.9",
+        "schema-utils": "^1.0.0",
+        "selfsigned": "^1.9.1",
+        "serve-index": "^1.7.2",
+        "sockjs": "0.3.19",
+        "sockjs-client": "1.1.5",
+        "spdy": "^3.4.1",
+        "strip-ansi": "^3.0.0",
+        "supports-color": "^5.1.0",
+        "webpack-dev-middleware": "3.2.0",
+        "webpack-log": "^2.0.0",
+        "yargs": "12.0.1"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0",
+            "wrap-ansi": "^2.0.0"
+          },
+          "dependencies": {
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        },
+        "cross-spawn": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+          "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^4.0.1",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "decamelize": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz",
+          "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==",
+          "dev": true,
+          "requires": {
+            "xregexp": "4.0.0"
+          }
+        },
+        "execa": {
+          "version": "0.7.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+          "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^5.0.1",
+            "get-stream": "^3.0.0",
+            "is-stream": "^1.1.0",
+            "npm-run-path": "^2.0.0",
+            "p-finally": "^1.0.0",
+            "signal-exit": "^3.0.0",
+            "strip-eof": "^1.0.0"
+          }
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "os-locale": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
+          "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
+          "dev": true,
+          "requires": {
+            "execa": "^0.7.0",
+            "lcid": "^1.0.0",
+            "mem": "^1.1.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
+          "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "p-try": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
+          "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
+          "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
+          "dev": true,
+          "requires": {
+            "ajv": "^6.1.0",
+            "ajv-errors": "^1.0.0",
+            "ajv-keywords": "^3.1.0"
+          }
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          },
+          "dependencies": {
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        },
+        "which-module": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+          "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+          "dev": true
+        },
+        "yargs": {
+          "version": "12.0.1",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.1.tgz",
+          "integrity": "sha512-B0vRAp1hRX4jgIOWFtjfNjd9OA9RWYZ6tqGA9/I/IrTMsxmKvtWy+ersM+jzpQqbC3YfLzeABPdeTgcJ9eu1qQ==",
+          "dev": true,
+          "requires": {
+            "cliui": "^4.0.0",
+            "decamelize": "^2.0.0",
+            "find-up": "^3.0.0",
+            "get-caller-file": "^1.0.1",
+            "os-locale": "^2.0.0",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^1.0.1",
+            "set-blocking": "^2.0.0",
+            "string-width": "^2.0.0",
+            "which-module": "^2.0.0",
+            "y18n": "^3.2.1 || ^4.0.0",
+            "yargs-parser": "^10.1.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
+      }
+    },
+    "webpack-log": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
+      "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "^3.0.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "webpack-merge": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz",
+      "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==",
+      "dev": true,
+      "requires": {
+        "lodash": "^4.17.5"
+      }
+    },
+    "webpack-sources": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.2.0.tgz",
+      "integrity": "sha512-9BZwxR85dNsjWz3blyxdOhTgtnQvv3OEs5xofI0wPYTwu5kaWxS08UuD1oI7WLBLpRO+ylf0ofnXLXWmGb2WMw==",
+      "dev": true,
+      "requires": {
+        "source-list-map": "^2.0.0",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "webpack-subresource-integrity": {
+      "version": "1.1.0-rc.4",
+      "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.4.tgz",
+      "integrity": "sha1-xcTj1pD50vZKlVDgeodn+Xlqpdg=",
+      "dev": true,
+      "requires": {
+        "webpack-core": "^0.6.8"
+      }
+    },
+    "websocket-driver": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
+      "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
+      "dev": true,
+      "requires": {
+        "http-parser-js": ">=0.4.0",
+        "websocket-extensions": ">=0.1.1"
+      }
+    },
+    "websocket-extensions": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
+      "dev": true
+    },
+    "when": {
+      "version": "3.6.4",
+      "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz",
+      "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=",
+      "dev": true
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+      "dev": true,
+      "optional": true
+    },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
+    "worker-farm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
+      "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
+      "dev": true,
+      "requires": {
+        "errno": "~0.1.7"
+      }
+    },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "ws": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz",
+      "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=",
+      "dev": true,
+      "requires": {
+        "options": ">=0.0.5",
+        "ultron": "1.0.x"
+      }
+    },
+    "wtf-8": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz",
+      "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=",
+      "dev": true
+    },
+    "xml2js": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+      "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+      "dev": true,
+      "requires": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~9.0.1"
+      },
+      "dependencies": {
+        "sax": {
+          "version": "1.2.4",
+          "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+          "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+          "dev": true
+        }
+      }
+    },
+    "xmlbuilder": {
+      "version": "9.0.7",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+      "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
+      "dev": true
+    },
+    "xmldom": {
+      "version": "0.1.27",
+      "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
+      "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=",
+      "dev": true
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
+      "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=",
+      "dev": true
+    },
+    "xregexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz",
+      "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==",
+      "dev": true
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    },
+    "xxhashjs": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
+      "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
+      "dev": true,
+      "requires": {
+        "cuint": "^0.2.2"
+      }
+    },
+    "y18n": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+      "dev": true
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
+    },
+    "yargs": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+      "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase": "^3.0.0",
+        "cliui": "^3.2.0",
+        "decamelize": "^1.1.1",
+        "get-caller-file": "^1.0.1",
+        "os-locale": "^1.4.0",
+        "read-pkg-up": "^1.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^1.0.1",
+        "set-blocking": "^2.0.0",
+        "string-width": "^1.0.2",
+        "which-module": "^1.0.0",
+        "y18n": "^3.2.1",
+        "yargs-parser": "^5.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true,
+          "optional": true
+        },
+        "y18n": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+          "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+      "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "camelcase": "^3.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "yauzl": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+      "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+      "dev": true,
+      "requires": {
+        "fd-slicer": "~1.0.1"
+      }
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
+      "dev": true
+    },
+    "yn": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+      "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
+      "dev": true
+    },
+    "zone.js": {
+      "version": "0.8.26",
+      "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz",
+      "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA=="
+    }
+  }
+}
diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json
new file mode 100644
index 0000000..9174d1b
--- /dev/null
+++ b/Open-ILS/src/eg2/package.json
@@ -0,0 +1,84 @@
+{
+  "name": "eg",
+  "version": "0.0.0",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve",
+    "build": "ng build",
+    "test": "npm run create-mock-idl; ng test",
+    "lint": "ng lint",
+    "e2e": "ng e2e",
+    "create-mock-idl": "cd src/test_data && perl idl2js.pl",
+    "export-strings": "ng xi18n --output-path locale",
+    "merge-strings": "xliffmerge",
+    "build-fr-CA": "ng build --configuration=production-fr-CA --output-path ../../web/eg2/fr-CA --deploy-url /eg2/fr-CA/ --base-href /eg2/fr-CA; sed -i s/IDL2js\\\"/IDL2js?locale=fr-CA\\\"/g ../../web/eg2/fr-CA/index.html; sed -i s/lang=\\\"en\\\"/lang=\\\"fr\\\"/g ../../web/eg2/fr-CA/index.html"
+  },
+  "private": true,
+  "dependencies": {
+    "@angular/animations": "^6.1.0",
+    "@angular/common": "^6.1.0",
+    "@angular/compiler": "^6.1.0",
+    "@angular/core": "^6.1.0",
+    "@angular/forms": "^6.1.0",
+    "@angular/http": "^6.1.0",
+    "@angular/platform-browser": "^6.1.0",
+    "@angular/platform-browser-dynamic": "^6.1.0",
+    "@angular/router": "^6.1.0",
+    "@ng-bootstrap/ng-bootstrap": "^3.2.0",
+    "bootstrap-css-only": "^4.1.1",
+    "core-js": "^2.5.4",
+    "ngx-cookie": "^4.0.2",
+    "rxjs": "^6.0.0",
+    "rxjs-compat": "^6.3.2",
+    "zone.js": "~0.8.26"
+  },
+  "devDependencies": {
+    "@angular-devkit/build-angular": "~0.7.0",
+    "@angular/cli": "~6.1.5",
+    "@angular/compiler-cli": "^6.1.0",
+    "@angular/language-service": "^6.1.0",
+    "@types/jasmine": "~2.8.6",
+    "@types/jasminewd2": "~2.0.3",
+    "@types/node": "~8.9.4",
+    "codelyzer": "~4.2.1",
+    "jasmine-core": "~2.99.1",
+    "jasmine-spec-reporter": "~4.2.1",
+    "karma": "~1.7.1",
+    "karma-chrome-launcher": "~2.2.0",
+    "karma-coverage-istanbul-reporter": "~2.0.0",
+    "karma-jasmine": "~1.1.1",
+    "karma-jasmine-html-reporter": "^0.2.2",
+    "karma-phantomjs-launcher": "^1.0.4",
+    "ngx-i18nsupport": "^0.17.0",
+    "protractor": "~5.4.0",
+    "ts-node": "~5.0.1",
+    "tslint": "~5.9.1",
+    "typescript": "~2.7.2"
+  },
+  "xliffmergeOptions": {
+    "srcDir": "src/locale",
+    "genDir": "src/locale",
+    "i18nFile": "messages.xlf",
+    "i18nBaseFile": "messages",
+    "i18nFormat": "xlf",
+    "encoding": "UTF-8",
+    "defaultLanguage": "en",
+    "languages": [
+      "en",
+      "fr-CA"
+    ],
+    "removeUnusedIds": true,
+    "supportNgxTranslate": false,
+    "ngxTranslateExtractionPattern": "@@|ngx-translate",
+    "useSourceAsTarget": true,
+    "targetPraefix": "",
+    "targetSuffix": "",
+    "beautifyOutput": false,
+    "allowIdChange": false,
+    "autotranslate": false,
+    "apikey": "",
+    "apikeyfile": "",
+    "verbose": false,
+    "quiet": false
+  }
+}
diff --git a/Open-ILS/src/eg2/protractor.conf.js b/Open-ILS/src/eg2/protractor.conf.js
new file mode 100644
index 0000000..7ee3b5e
--- /dev/null
+++ b/Open-ILS/src/eg2/protractor.conf.js
@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+  allScriptsTimeout: 11000,
+  specs: [
+    './e2e/**/*.e2e-spec.ts'
+  ],
+  capabilities: {
+    'browserName': 'chrome'
+  },
+  directConnect: true,
+  baseUrl: 'http://localhost:4200/',
+  framework: 'jasmine',
+  jasmineNodeOpts: {
+    showColors: true,
+    defaultTimeoutInterval: 30000,
+    print: function() {}
+  },
+  onPrepare() {
+    require('ts-node').register({
+      project: 'e2e/tsconfig.e2e.json'
+    });
+    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+  }
+};
diff --git a/Open-ILS/src/eg2/src/app/app.component.ts b/Open-ILS/src/eg2/src/app/app.component.ts
new file mode 100644
index 0000000..3f95092
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import {Component} from '@angular/core';
+
+ at Component({
+  selector: 'eg-root',
+  template: '<router-outlet></router-outlet><eg-print></eg-print>'
+})
+
+export class BaseComponent {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/app.module.ts b/Open-ILS/src/eg2/src/app/app.module.ts
new file mode 100644
index 0000000..20de8ab
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/app.module.ts
@@ -0,0 +1,33 @@
+/**
+ * BaseModule is the shared starting point for all apps.  It provides
+ * the root route and a simple welcome page for users that end up here
+ * accidentally.
+ */
+import {BrowserModule} from '@angular/platform-browser';
+import {NgModule} from '@angular/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; // ng-bootstrap
+import {CookieModule} from 'ngx-cookie'; // import CookieMonster
+
+import {EgCommonModule} from './common.module';
+import {BaseComponent} from './app.component';
+import {BaseRoutingModule} from './routing.module';
+import {WelcomeComponent} from './welcome.component';
+
+ at NgModule({
+  declarations: [
+    BaseComponent,
+    WelcomeComponent
+  ],
+  imports: [
+    EgCommonModule.forRoot(),
+    BaseRoutingModule,
+    BrowserModule,
+    NgbModule.forRoot(),
+    CookieModule.forRoot()
+  ],
+  exports: [],
+  bootstrap: [BaseComponent]
+})
+
+export class BaseModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/common.module.ts b/Open-ILS/src/eg2/src/app/common.module.ts
new file mode 100644
index 0000000..c83ad39
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/common.module.ts
@@ -0,0 +1,71 @@
+/**
+ * Modules, services, and components used by all apps.
+ */
+import {CommonModule, DatePipe, CurrencyPipe} from '@angular/common';
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {FormsModule} from '@angular/forms';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+
+/*
+Note core services are injected into 'root'.
+They do not have to be added to the providers list.
+*/
+
+// consider moving these to core...
+import {FormatService} from '@eg/core/format.service';
+import {PrintService} from '@eg/share/print/print.service';
+
+// Globally available components
+import {PrintComponent} from '@eg/share/print/print.component';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+
+ at NgModule({
+  declarations: [
+    PrintComponent,
+    DialogComponent,
+    ConfirmDialogComponent,
+    PromptDialogComponent,
+    ProgressInlineComponent,
+    ProgressDialogComponent
+  ],
+  imports: [
+    CommonModule,
+    FormsModule,
+    RouterModule,
+    NgbModule
+  ],
+  exports: [
+    CommonModule,
+    RouterModule,
+    NgbModule,
+    FormsModule,
+    PrintComponent,
+    DialogComponent,
+    ConfirmDialogComponent,
+    PromptDialogComponent,
+    ProgressInlineComponent,
+    ProgressDialogComponent
+  ]
+})
+
+export class EgCommonModule {
+    /** forRoot() lets us define services that should only be
+     * instantiated once for all loaded routes */
+    static forRoot(): ModuleWithProviders {
+        return {
+            ngModule: EgCommonModule,
+            providers: [
+                DatePipe,
+                CurrencyPipe,
+                PrintService,
+                FormatService
+            ]
+        };
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/README b/Open-ILS/src/eg2/src/app/core/README
new file mode 100644
index 0000000..3cf0ec4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/README
@@ -0,0 +1,9 @@
+Core Angular services and assocated types/classes.
+
+Core services are imported and exported by the base module and 
+automatically added as dependencies to ALL applications.
+
+1. Only add services here that are universally required.
+2. Avoid URL path navigation in the core services as paths will vary 
+   by application.
+
diff --git a/Open-ILS/src/eg2/src/app/core/auth.service.ts b/Open-ILS/src/eg2/src/app/core/auth.service.ts
new file mode 100644
index 0000000..dad2acd
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/auth.service.ts
@@ -0,0 +1,341 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {NetService} from './net.service';
+import {EventService, EgEvent} from './event.service';
+import {IdlService, IdlObject} from './idl.service';
+import {StoreService} from './store.service';
+
+// Not universally available.
+declare var BroadcastChannel;
+
+// Models a login instance.
+class AuthUser {
+    user:        IdlObject; // actor.usr (au) object
+    workstation: string; // workstation name
+    token:       string;
+    authtime:    number;
+
+    constructor(token: string, authtime: number, workstation?: string) {
+        this.token = token;
+        this.workstation = workstation;
+        this.authtime = authtime;
+    }
+}
+
+// Params required for calling the login() method.
+interface AuthLoginArgs {
+    username: string;
+    password: string;
+    type: string;
+    workstation?: string;
+}
+
+export enum AuthWsState {
+    PENDING,
+    NOT_USED,
+    NOT_FOUND_SERVER,
+    NOT_FOUND_LOCAL,
+    VALID
+}
+
+ at Injectable({providedIn: 'root'})
+export class AuthService {
+
+    private authChannel: any;
+
+    private activeUser: AuthUser = null;
+
+    workstationState: AuthWsState = AuthWsState.PENDING;
+
+    // Used by auth-checking resolvers
+    redirectUrl: string;
+
+    // reference to active auth validity setTimeout handler.
+    pollTimeout: any;
+
+    constructor(
+        private egEvt: EventService,
+        private net: NetService,
+        private store: StoreService
+    ) {
+
+        // BroadcastChannel is not yet defined in PhantomJS and elsewhere
+        this.authChannel = (typeof BroadcastChannel === 'undefined') ?
+            {} : new BroadcastChannel('eg.auth');
+    }
+
+    // Returns true if we are currently in op-change mode.
+    opChangeIsActive(): boolean {
+        return Boolean(this.store.getLoginSessionItem('eg.auth.time.oc'));
+    }
+
+    // - Accessor functions always refer to the active user.
+
+    user(): IdlObject {
+        return this.activeUser ? this.activeUser.user : null;
+    }
+
+    // Workstation name.
+    workstation(): string {
+        return this.activeUser ? this.activeUser.workstation : null;
+    }
+
+    token(): string {
+        return this.activeUser ? this.activeUser.token : null;
+    }
+
+    authtime(): number {
+        return this.activeUser ? this.activeUser.authtime : 0;
+    }
+
+    // NOTE: NetService emits an event if the auth session has expired.
+    // This only rejects when no authtoken is found.
+    testAuthToken(): Promise<any> {
+
+        if (!this.activeUser) {
+            // Only necessary on new page loads.  During op-change,
+            // for example, we already have an activeUser.
+            this.activeUser = new AuthUser(
+                this.store.getLoginSessionItem('eg.auth.token'),
+                this.store.getLoginSessionItem('eg.auth.time')
+            );
+        }
+
+        if (!this.token()) {
+            return Promise.reject('no authtoken');
+        }
+
+        return this.net.request(
+            'open-ils.auth',
+            'open-ils.auth.session.retrieve', this.token()).toPromise()
+        .then(user => {
+            // NetService interceps NO_SESSION events.
+            // We can only get here if the session is valid.
+            this.activeUser.user = user;
+            this.listenForLogout();
+            this.sessionPoll();
+        });
+    }
+
+    loginApi(args: AuthLoginArgs, service: string,
+        method: string, isOpChange?: boolean): Promise<void> {
+
+        return this.net.request(service, method, args)
+        .toPromise().then(res => {
+            return this.handleLoginResponse(
+                args, this.egEvt.parse(res), isOpChange);
+        });
+    }
+
+    login(args: AuthLoginArgs, isOpChange?: boolean): Promise<void> {
+        let service = 'open-ils.auth';
+        let method = 'open-ils.auth.login';
+
+        return this.net.request(
+            'open-ils.auth_proxy',
+            'open-ils.auth_proxy.enabled')
+        .toPromise().then(
+            enabled => {
+                if (Number(enabled) === 1) {
+                    service = 'open-ils.auth_proxy';
+                    method = 'open-ils.auth_proxy.login';
+                }
+                return this.loginApi(args, service, method, isOpChange);
+            },
+            error => {
+                // auth_proxy check resulted in a low-level error.
+                // Likely the service is not running.  Fall back to
+                // standard auth login.
+                return this.loginApi(args, service, method, isOpChange);
+            }
+        );
+    }
+
+    handleLoginResponse(
+        args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
+
+        switch (evt.textcode) {
+            case 'SUCCESS':
+                return this.handleLoginOk(args, evt, isOpChange);
+
+            case 'WORKSTATION_NOT_FOUND':
+                console.error(`No such workstation "${args.workstation}"`);
+                this.workstationState = AuthWsState.NOT_FOUND_SERVER;
+                delete args.workstation;
+                return this.login(args, isOpChange);
+
+            default:
+                console.error(`Login returned unexpected event: ${evt}`);
+                return Promise.reject('login failed');
+        }
+    }
+
+    // Stash the login data
+    handleLoginOk(args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
+
+        if (isOpChange) {
+            this.store.setLoginSessionItem('eg.auth.token.oc', this.token());
+            this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime());
+        }
+
+        this.activeUser = new AuthUser(
+            evt.payload.authtoken,
+            evt.payload.authtime,
+            args.workstation
+        );
+
+        this.store.setLoginSessionItem('eg.auth.token', this.token());
+        this.store.setLoginSessionItem('eg.auth.time', this.authtime());
+
+        return Promise.resolve();
+    }
+
+    undoOpChange(): Promise<any> {
+        if (this.opChangeIsActive()) {
+            this.deleteSession();
+            this.activeUser = new AuthUser(
+                this.store.getLoginSessionItem('eg.auth.token.oc'),
+                this.store.getLoginSessionItem('eg.auth.time.oc'),
+                this.activeUser.workstation
+            );
+            this.store.removeLoginSessionItem('eg.auth.token.oc');
+            this.store.removeLoginSessionItem('eg.auth.time.oc');
+            this.store.setLoginSessionItem('eg.auth.token', this.token());
+            this.store.setLoginSessionItem('eg.auth.time', this.authtime());
+        }
+        // Re-fetch the user.
+        return this.testAuthToken();
+    }
+
+    /**
+     * Listen for logout events initiated by other browser tabs.
+     */
+    listenForLogout(): void {
+        if (this.authChannel.onmessage) {
+            return;
+        }
+
+        this.authChannel.onmessage = (e) => {
+            console.debug(
+                `received eg.auth broadcast ${JSON.stringify(e.data)}`);
+
+            if (e.data.action === 'logout') {
+                // Logout will be handled by the originating tab.
+                // We just need to clear tab-local memory.
+                this.cleanup();
+                this.net.authExpired$.emit({viaExternal: true});
+            }
+        };
+    }
+
+    /**
+     * Force-check the validity of the authtoken on occasion.
+     * This allows us to redirect an idle staff client back to the login
+     * page after the session times out.  Otherwise, the UI would stay
+     * open with potentially sensitive data visible.
+     * TODO: What is the practical difference (for a browser) between
+     * checking auth validity and the ui.general.idle_timeout setting?
+     * Does that setting serve a purpose in a browser environment?
+     */
+    sessionPoll(): void {
+
+        // add a 5 second delay to give the token plenty of time
+        // to expire on the server.
+        let pollTime = this.authtime() * 1000 + 5000;
+
+        if (pollTime < 60000) {
+            // Never poll more often than once per minute.
+            pollTime = 60000;
+        } else if (pollTime > 2147483647) {
+            // Avoid integer overflow resulting in $timeout() effectively
+            // running with timeout=0 in a loop.
+            pollTime = 2147483647;
+        }
+
+        this.pollTimeout = setTimeout(() => {
+            this.net.request(
+                'open-ils.auth',
+                'open-ils.auth.session.retrieve',
+                this.token(),
+                0, // return extra auth details, unneeded here.
+                1  // avoid extending the auth timeout
+
+            // NetService intercepts NO_SESSION events.
+            // If the promise resolves, the session is valid.
+            ).subscribe(
+                user => this.sessionPoll(),
+                err  => console.warn('auth poll error: ' + err)
+            );
+
+        }, pollTime);
+    }
+
+
+    // Resolves if login workstation matches a workstation known to this
+    // browser instance.  No attempt is made to see if the workstation
+    // is present on the server.  That happens at login time.
+    verifyWorkstation(): Promise<void> {
+
+        if (!this.user()) {
+            this.workstationState = AuthWsState.PENDING;
+            return Promise.reject('Cannot verify workstation without user');
+        }
+
+        if (!this.user().wsid()) {
+            this.workstationState = AuthWsState.NOT_USED;
+            return Promise.reject('User has no workstation ID to verify');
+        }
+
+        return new Promise((resolve, reject) => {
+            const workstations =
+                this.store.getLocalItem('eg.workstation.all');
+
+            if (workstations) {
+                const ws = workstations.filter(
+                    w => Number(w.id) === Number(this.user().wsid()))[0];
+
+                if (ws) {
+                    this.activeUser.workstation = ws.name;
+                    this.workstationState = AuthWsState.VALID;
+                    return resolve();
+                }
+            }
+
+            this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
+            reject();
+        });
+    }
+
+    deleteSession(): void {
+        if (this.token()) {
+            // note we have to subscribe to the net.request or it will
+            // not fire -- observables only run when subscribed to.
+            this.net.request(
+                'open-ils.auth',
+                'open-ils.auth.session.delete', this.token())
+            .subscribe(x => {});
+        }
+    }
+
+    // Tell all listening browser tabs that it's time to logout.
+    // This should only be invoked by one tab.
+    broadcastLogout(): void {
+        console.debug('Notifying tabs of imminent auth token removal');
+        this.authChannel.postMessage({action : 'logout'});
+    }
+
+    // Remove/reset session data
+    cleanup(): void {
+        this.activeUser = null;
+        if (this.pollTimeout) {
+            clearTimeout(this.pollTimeout);
+            this.pollTimeout = null;
+        }
+    }
+
+    // Invalidate server auth token and clean up.
+    logout(): void {
+        this.deleteSession();
+        this.store.clearLoginSessionItems();
+        this.cleanup();
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/event.service.ts b/Open-ILS/src/eg2/src/app/core/event.service.ts
new file mode 100644
index 0000000..0bbf60b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/event.service.ts
@@ -0,0 +1,55 @@
+import {Injectable} from '@angular/core';
+
+export class EgEvent {
+    code: number;
+    textcode: string;
+    payload: any;
+    desc: string;
+    debug: string;
+    note: string;
+    servertime: string;
+    ilsperm: string;
+    ilspermloc: number;
+    success: Boolean = false;
+
+    toString(): string {
+        let s = `Event: ${this.code}:${this.textcode} -> ${this.desc}`;
+        if (this.ilsperm) {
+            s += `  ${this.ilsperm}@${this.ilspermloc}`;
+        }
+        if (this.note) {
+            s += `\n${this.note}`;
+        }
+        return s;
+    }
+}
+
+ at Injectable({providedIn: 'root'})
+export class EventService {
+
+    /**
+     * Returns an Event if 'thing' is an event, null otherwise.
+     */
+    parse(thing: any): EgEvent {
+
+        // All events have a textcode
+        if (thing && typeof thing === 'object' && 'textcode' in thing) {
+
+            const evt = new EgEvent();
+
+            ['textcode', 'payload', 'desc', 'note', 'servertime', 'ilsperm']
+                .forEach(field => { evt[field] = thing[field]; });
+
+            evt.debug = thing.stacktrace;
+            evt.code = +(thing.ilsevent || -1);
+            evt.ilspermloc = +(thing.ilspermloc || -1);
+            evt.success = thing.textcode === 'SUCCESS';
+
+            return evt;
+        }
+
+        return null;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/event.spec.ts b/Open-ILS/src/eg2/src/app/core/event.spec.ts
new file mode 100644
index 0000000..3dfdab2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/event.spec.ts
@@ -0,0 +1,47 @@
+import {EventService} from './event.service';
+
+describe('EventService', () => {
+    let service: EventService;
+    beforeEach(() => {
+        service = new EventService();
+    });
+
+    const evt = {
+        ilsevent: '12345',
+        pid: '12345',
+        desc: 'Test Event Description',
+        payload: {test : 'xyz'},
+        textcode: 'TEST_EVENT',
+        servertime: 'Wed Nov 6 16:05:50 2013'
+    };
+
+    it('should parse an event object', () => {
+        expect(service.parse(evt)).not.toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse({})).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse({abc : '123'})).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse([])).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse('STRING')).toBe(null);
+    });
+
+    it('should not parse a non-event', () => {
+        expect(service.parse(true)).toBe(null);
+    });
+
+    it('should stringify an event', () => {
+        expect(service.parse(evt).toString()).toBe(
+            'Event: 12345:TEST_EVENT -> Test Event Description');
+    });
+
+});
diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts
new file mode 100644
index 0000000..2c7e388
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/format.service.ts
@@ -0,0 +1,103 @@
+import {Injectable} from '@angular/core';
+import {DatePipe, CurrencyPipe} from '@angular/common';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+
+/**
+ * Format IDL vield values for display.
+ */
+
+declare var OpenSRF;
+
+export interface FormatParams {
+    value: any;
+    idlClass?: string;
+    idlField?: string;
+    datatype?: string;
+    orgField?: string; // 'shortname' || 'name'
+    datePlusTime?: boolean;
+}
+
+ at Injectable({providedIn: 'root'})
+export class FormatService {
+
+    dateFormat = 'shortDate';
+    dateTimeFormat = 'short';
+    wsOrgTimezone: string = OpenSRF.tz;
+
+    constructor(
+        private datePipe: DatePipe,
+        private currencyPipe: CurrencyPipe,
+        private idl: IdlService,
+        private org: OrgService
+    ) {
+
+        // Create an inilne polyfill for Number.isNaN, which is
+        // not available in PhantomJS for unit testing.
+        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
+        if (!Number.isNaN) {
+            // "The following works because NaN is the only value
+            // in javascript which is not equal to itself."
+            Number.isNaN = (value: any) => {
+                return value !== value;
+            };
+        }
+    }
+
+    /**
+     * Create a human-friendly display version of any field type.
+     */
+    transform(params: FormatParams): string {
+        const value = params.value;
+
+        if (   value === undefined
+            || value === null
+            || value === ''
+            || Number.isNaN(value)) {
+            return '';
+        }
+
+        let datatype = params.datatype;
+
+        if (!datatype) {
+            if (params.idlClass && params.idlField) {
+                datatype = this.idl.classes[params.idlClass]
+                    .field_map[params.idlField].datatype;
+            } else {
+                // Assume it's a primitive value
+                return value + '';
+            }
+        }
+
+        switch (datatype) {
+
+            case 'org_unit':
+                const orgField = params.orgField || 'shortname';
+                const org = this.org.get(value);
+                return org ? org[orgField]() : '';
+
+            case 'timestamp':
+                const date = new Date(value);
+                let fmt = this.dateFormat || 'shortDate';
+                if (params.datePlusTime) {
+                    fmt = this.dateTimeFormat || 'short';
+                }
+                return this.datePipe.transform(date, fmt);
+
+            case 'money':
+                return this.currencyPipe.transform(value);
+
+            case 'bool':
+                // Slightly better than a bare 't' or 'f'.
+                // Should probably add a global true/false string.
+                return Boolean(
+                    value === 't' || value === 1 ||
+                    value === '1' || value === true
+                ).toString();
+
+            default:
+                return value + '';
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/format.spec.ts b/Open-ILS/src/eg2/src/app/core/format.spec.ts
new file mode 100644
index 0000000..05991df
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/format.spec.ts
@@ -0,0 +1,90 @@
+import {DatePipe, CurrencyPipe} from '@angular/common';
+import {IdlService} from './idl.service';
+import {EventService} from './event.service';
+import {NetService} from './net.service';
+import {AuthService} from './auth.service';
+import {PcrudService} from './pcrud.service';
+import {StoreService} from './store.service';
+import {OrgService} from './org.service';
+import {FormatService} from './format.service';
+
+
+describe('FormatService', () => {
+
+    let currencyPipe: CurrencyPipe;
+    let datePipe: DatePipe;
+    let idlService: IdlService;
+    let netService: NetService;
+    let authService: AuthService;
+    let pcrudService: PcrudService;
+    let orgService: OrgService;
+    let evtService: EventService;
+    let storeService: StoreService;
+    let service: FormatService;
+
+    beforeEach(() => {
+        currencyPipe = new CurrencyPipe('en');
+        datePipe = new DatePipe('en');
+        idlService = new IdlService();
+        evtService = new EventService();
+        storeService = new StoreService(null /* CookieService */);
+        netService = new NetService(evtService);
+        authService = new AuthService(evtService, netService, storeService);
+        pcrudService = new PcrudService(idlService, netService, authService);
+        orgService = new OrgService(netService, authService, pcrudService);
+        service = new FormatService(
+            datePipe,
+            currencyPipe,
+            idlService,
+            orgService
+        );
+    });
+
+    const initTestData = () => {
+        idlService.parseIdl();
+        const win: any = window; // trick TS
+        win._eg_mock_data.generateOrgTree(idlService, orgService);
+    };
+
+    it('should format an org unit name', () => {
+        initTestData();
+        const str = service.transform({
+            value: orgService.root(),
+            datatype: 'org_unit',
+            orgField: 'shortname' // currently the default
+        });
+        expect(str).toBe('ROOT');  // from eg_mock.js
+    });
+
+    it('should format a date', () => {
+        initTestData();
+        const str = service.transform({
+            value: new Date(2018, 6, 5),
+            datatype: 'timestamp',
+        });
+        expect(str).toBe('7/5/18');
+    });
+
+    it('should format a date plus time', () => {
+        initTestData();
+        const str = service.transform({
+            value: new Date(2018, 6, 5, 12, 30, 1),
+            datatype: 'timestamp',
+            datePlusTime: true
+        });
+        expect(str).toBe('7/5/18, 12:30 PM');
+    });
+
+
+
+    it('should format money', () => {
+        initTestData();
+        const str = service.transform({
+            value: '12.1',
+            datatype: 'money'
+        });
+        expect(str).toBe('$12.10');
+    });
+
+});
+
diff --git a/Open-ILS/src/eg2/src/app/core/idl.service.ts b/Open-ILS/src/eg2/src/app/core/idl.service.ts
new file mode 100644
index 0000000..89f8411
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/idl.service.ts
@@ -0,0 +1,137 @@
+import {Injectable} from '@angular/core';
+
+// Added globally by /IDL2js
+declare var _preload_fieldmapper_IDL: Object;
+
+/**
+ * Every IDL object class implements this interface.
+ */
+export interface IdlObject {
+    a: any[];
+    classname: string;
+    _isfieldmapper: boolean;
+    // Dynamically appended functions from the IDL.
+    [fields: string]: any;
+}
+
+ at Injectable({providedIn: 'root'})
+export class IdlService {
+
+    classes: any = {}; // IDL class metadata
+    constructors = {}; // IDL instance generators
+
+    /**
+     * Create a new IDL object instance.
+     */
+    create(cls: string, seed?: any[]): IdlObject {
+        if (this.constructors[cls]) {
+            return new this.constructors[cls](seed);
+        }
+        throw new Error(`No such IDL class ${cls}`);
+    }
+
+    parseIdl(): void {
+
+        try {
+            this.classes = _preload_fieldmapper_IDL;
+        } catch (E) {
+            console.error('IDL (IDL2js) not found.  Is the system running?');
+            return;
+        }
+
+        /**
+         * Creates the class constructor and getter/setter
+         * methods for each IDL class.
+         */
+        const mkclass = (cls, fields) => {
+            this.classes[cls].classname = cls;
+
+            // This dance lets us encode each IDL object with the
+            // IdlObject interface.  Useful for adding type restrictions
+            // where desired for functions, etc.
+            const generator: any = ((): IdlObject => {
+
+                const x: any = function(seed) {
+                    this.a = seed || [];
+                    this.classname = cls;
+                    this._isfieldmapper = true;
+                };
+
+                fields.forEach(function(field, idx) {
+                    x.prototype[field.name] = function(n) {
+                        if (arguments.length === 1) {
+                            this.a[idx] = n;
+                        }
+                        return this.a[idx];
+                    };
+
+                    if (!field.label) {
+                        field.label = field.name;
+                    }
+
+                    // Coerce 'aou' links to datatype org_unit for consistency.
+                    if (field.datatype === 'link' && field.class === 'aou') {
+                        field.datatype = 'org_unit';
+                    }
+                });
+
+                return x;
+            });
+
+            this.constructors[cls] = generator();
+
+            // global class constructors required for JSON_v1.js
+            // TODO: polluting the window namespace w/ every IDL class
+            // is less than ideal.
+            window[cls] = this.constructors[cls];
+        };
+
+        Object.keys(this.classes).forEach(class_ => {
+            mkclass(class_, this.classes[class_].fields);
+        });
+    }
+
+    // Makes a deep copy of an IdlObject's / structures containing
+    // IdlObject's.  Note we don't use JSON cross-walk because our
+    // JSON lib does not handle circular references.
+    // @depth specifies the maximum number of steps through IdlObject'
+    // we will traverse.
+    clone(source: any, depth?: number): any {
+        if (depth === undefined) {
+            depth = 100;
+        }
+
+        let result;
+        if (typeof source === 'undefined' || source === null) {
+            return source;
+
+        } else if (source._isfieldmapper) {
+            // same depth because we're still cloning this same object
+            result = this.create(source.classname, this.clone(source.a, depth));
+
+        } else {
+            if (Array.isArray(source)) {
+                result = [];
+            } else if (typeof source === 'object') { // source is not null
+                result = {};
+            } else {
+                return source; // primitive
+            }
+
+            for (const j in source) {
+                if (source[j] === null || typeof source[j] === 'undefined') {
+                    result[j] = source[j];
+                } else if (source[j]._isfieldmapper) {
+                    if (depth) {
+                        result[j] = this.clone(source[j], depth - 1);
+                    }
+                } else {
+                    result[j] = this.clone(source[j], depth);
+                }
+            }
+        }
+
+        return result;
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/idl.spec.ts b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
new file mode 100644
index 0000000..8138bf4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
@@ -0,0 +1,28 @@
+import {IdlService} from './idl.service';
+
+describe('IdlService', () => {
+    let service: IdlService;
+    beforeEach(() => {
+        service = new IdlService();
+    });
+
+    it('should parse the IDL', () => {
+        service.parseIdl();
+        expect(service.classes['aou'].fields.length).toBeGreaterThan(0);
+    });
+
+    it('should create an aou object', () => {
+        service.parseIdl();
+        const org = service.create('aou');
+        expect(typeof org.id).toBe('function');
+    });
+
+    it('should create an aou object with accessor/mutators', () => {
+        service.parseIdl();
+        const org = service.create('aou');
+        org.name('AN ORG');
+        expect(org.name()).toBe('AN ORG');
+    });
+
+});
+
diff --git a/Open-ILS/src/eg2/src/app/core/locale.service.ts b/Open-ILS/src/eg2/src/app/core/locale.service.ts
new file mode 100644
index 0000000..0ffbfd4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/locale.service.ts
@@ -0,0 +1,69 @@
+import {Injectable} from '@angular/core';
+import {Location} from '@angular/common';
+import {environment} from '../../environments/environment';
+import {Observable} from 'rxjs/Observable';
+import {of} from 'rxjs';
+import {CookieService} from 'ngx-cookie';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+ at Injectable({providedIn: 'root'})
+export class LocaleService {
+
+    constructor(
+        private ngLocation: Location,
+        private cookieService: CookieService,
+        private pcrud: PcrudService) {
+    }
+
+    setLocale(code: string) {
+        let url = this.ngLocation.prepareExternalUrl('/');
+
+        // The last part of the base path will be the locale
+        // Replace it with the selected locale
+        url = url.replace(/\/[a-z]{2}-[A-Z]{2}\/$/, `/${code}`);
+
+        // Finally tack the path of the current page back onto the URL
+        // which is more friendly than forcing them back to the splash page.
+        url += this.ngLocation.path();
+
+        // Set a 10-year locale cookie to maintain compatibility
+        // with the AngularJS client.
+        // Cookie takes the form aa_bb instead of aa-BB
+        const cookie = code.replace(/-/, '_').toLowerCase();
+        this.cookieService.put('eg_locale',
+            cookie, {path : '/', secure: true, expires: '+10y'});
+
+        window.location.href = url;
+    }
+
+    // Returns codes supported for the current environment.
+    supportedLocaleCodes(): string[] {
+        return environment.locales || [];
+    }
+
+    // Returns i18n_l objects matching the locales supported
+    // in the current environment.
+    supportedLocales(): Observable<IdlObject> {
+        const locales = this.supportedLocaleCodes();
+
+        if (locales.length === 0) {
+            return of();
+        }
+
+        return this.pcrud.search('i18n_l', {code: locales});
+    }
+
+    // Extract the local from the URL.
+    // It's the last component of the base path.
+    // Note we don't extract it from the cookie since using cookies
+    // to store the locale will not be necessary when AngularJS
+    // is deprecated.
+    currentLocaleCode(): string {
+        const base = this.ngLocation.prepareExternalUrl('/');
+        const code = base.match(/\/([a-z]{2}-[A-Z]{2})\/$/);
+        return code ? code[1] : '';
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/net.service.ts b/Open-ILS/src/eg2/src/app/core/net.service.ts
new file mode 100644
index 0000000..3c3435b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/net.service.ts
@@ -0,0 +1,187 @@
+/**
+ *
+ * constructor(private net : NetService) {
+ *   ...
+ *   this.net.request(service, method, param1 [, param2, ...])
+ *     .subscribe(
+ *       (res) => console.log('received one resopnse: ' + res),
+ *       (err) => console.error('recived request error: ' + err),
+ *       ()    => console.log('request complete')
+ *     )
+ *   );
+ *   ...
+ *
+ *  // Example translating a net request into a promise.
+ *  this.net.request(service, method, param1)
+ *  .toPromise().then(result => console.log(result));
+ *
+ * }
+ *
+ * Each response is relayed via Observable.next().  The interface is
+ * the same for streaming and atomic requests.
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {EventService, EgEvent} from './event.service';
+
+// Global vars from opensrf.js
+// These are availavble at runtime, but are not exported.
+declare var OpenSRF, OSRF_TRANSPORT_TYPE_WS;
+
+export class NetRequest {
+    service: string;
+    method: string;
+    params: any[];
+    observer: Observer<any>;
+    superseded = false;
+    // If set, this will be used instead of a one-off OpenSRF.ClientSession.
+    session?: any;
+    // True if we're using a single-use local session
+    localSession = true;
+
+    // Last Event encountered by this request.
+    // Most callers will not need to import Event since the parsed
+    // event will be available here.
+    evt: EgEvent;
+
+    constructor(service: string, method: string, params: any[], session?: any) {
+        this.service = service;
+        this.method = method;
+        this.params = params;
+        if (session) {
+            this.session = session;
+            this.localSession = false;
+        } else {
+            this.session = new OpenSRF.ClientSession(service);
+        }
+    }
+}
+
+export interface AuthExpiredEvent {
+    // request is set when the auth expiration was determined as a
+    // by-product of making an API call.
+    request?: NetRequest;
+
+    // True if this environment (e.g. browser tab) was notified of the
+    // expired auth token from an external source (e.g. another browser tab).
+    viaExternal?: boolean;
+}
+
+ at Injectable({providedIn: 'root'})
+export class NetService {
+
+    permFailed$: EventEmitter<NetRequest>;
+    authExpired$: EventEmitter<AuthExpiredEvent>;
+
+    // If true, permission failures are emitted via permFailed$
+    // and the active request is marked as superseded.
+    permFailedHasHandler: Boolean = false;
+
+    constructor(
+        private egEvt: EventService
+    ) {
+        this.permFailed$ = new EventEmitter<NetRequest>();
+        this.authExpired$ = new EventEmitter<AuthExpiredEvent>();
+    }
+
+    // Standard request call -- Variadic params version
+    request(service: string, method: string, ...params: any[]): Observable<any> {
+        return this.requestWithParamList(service, method, params);
+    }
+
+    // Array params version
+    requestWithParamList(service: string,
+        method: string, params: any[]): Observable<any> {
+        return this.requestCompiled(
+            new NetRequest(service, method, params));
+    }
+
+    // Request with pre-compiled NetRequest
+    requestCompiled(request: NetRequest): Observable<any> {
+        return Observable.create(
+            observer => {
+                request.observer = observer;
+                this.sendCompiledRequest(request);
+            }
+        );
+    }
+
+    // Send the compiled request to the server via WebSockets
+    sendCompiledRequest(request: NetRequest): void {
+        OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
+        console.debug(`Net: request ${request.method}`);
+
+        request.session.request({
+            async  : true, // WS only operates in async mode
+            method : request.method,
+            params : request.params,
+            oncomplete : () => {
+
+                // TODO: teach opensrf.js to call cleanup() inside
+                // disconnect() and teach Pcrud to call cleanup()
+                // as needed to avoid long-lived session data bloat.
+                if (request.localSession) {
+                    request.session.cleanup();
+                }
+
+                // A superseded request will be complete()'ed by the
+                // superseder at a later time.
+                if (!request.superseded) {
+                    request.observer.complete();
+                }
+            },
+            onresponse : r => {
+                this.dispatchResponse(request, r.recv().content());
+            },
+            onerror : errmsg => {
+                const msg = `${request.method} failed! See server logs. ${errmsg}`;
+                console.error(msg);
+                request.observer.error(msg);
+            },
+            onmethoderror : (req, statCode, statMsg) => {
+                const msg =
+                    `${request.method} failed! stat=${statCode} msg=${statMsg}`;
+                console.error(msg);
+
+                if (request.service === 'open-ils.pcrud'
+                    && Number(statCode) === 401) {
+                    // 401 is the PCRUD equivalent of a NO_SESSION event
+                    this.authExpired$.emit({request: request});
+                }
+
+                request.observer.error(msg);
+            }
+
+        }).send();
+    }
+
+    // Relay response object to the caller for typical/successful
+    // responses.  Applies special handling to response events that
+    // require global attention.
+    private dispatchResponse(request, response): void {
+        request.evt = this.egEvt.parse(response);
+
+        if (request.evt) {
+            switch (request.evt.textcode) {
+
+                case 'NO_SESSION':
+                    console.debug(`Net emitting event: ${request.evt}`);
+                    request.observer.error(request.evt.toString());
+                    this.authExpired$.emit({request: request});
+                    return;
+
+                case 'PERM_FAILURE':
+                    if (this.permFailedHasHandler) {
+                        console.debug(`Net emitting event: ${request.evt}`);
+                        request.superseded = true;
+                        this.permFailed$.emit(request);
+                        return;
+                    }
+            }
+        }
+
+        // Pass the response to the caller.
+        request.observer.next(response);
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/org.service.ts b/Open-ILS/src/eg2/src/app/core/org.service.ts
new file mode 100644
index 0000000..38faaff
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/org.service.ts
@@ -0,0 +1,278 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {IdlObject, IdlService} from './idl.service';
+import {NetService} from './net.service';
+import {AuthService} from './auth.service';
+import {PcrudService} from './pcrud.service';
+
+type OrgNodeOrId = number | IdlObject;
+
+interface OrgFilter {
+    canHaveUsers?: boolean;
+    canHaveVolumes?: boolean;
+    opacVisible?: boolean;
+    inList?: number[];
+    notInList?: number[];
+}
+
+interface OrgSettingsBatch {
+    [key: string]: any;
+}
+
+ at Injectable({providedIn: 'root'})
+export class OrgService {
+
+    private orgList: IdlObject[] = [];
+    private orgTree: IdlObject; // root node + children
+    private orgMap: {[id: number]: IdlObject} = {};
+    private settingsCache: OrgSettingsBatch = {};
+
+    constructor(
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService
+    ) {}
+
+    get(nodeOrId: OrgNodeOrId): IdlObject {
+        if (typeof nodeOrId === 'object') {
+            return nodeOrId;
+        }
+        return this.orgMap[nodeOrId];
+    }
+
+    list(): IdlObject[] {
+        return this.orgList;
+    }
+
+    /**
+     * Returns a list of org units that match the selected criteria.
+     * All filters must match for an org to be included in the result set.
+     * Unset filter options are ignored.
+     */
+    filterList(filter: OrgFilter, asId?: boolean): any[] {
+        const list = [];
+        this.list().forEach(org => {
+
+            const chu = filter.canHaveUsers;
+            if (chu && !this.canHaveUsers(org)) { return; }
+            if (chu === false && this.canHaveUsers(org)) { return; }
+
+            const chv = filter.canHaveVolumes;
+            if (chv && !this.canHaveVolumes(org)) { return; }
+            if (chv === false && this.canHaveVolumes(org)) { return; }
+
+            const ov = filter.opacVisible;
+            if (ov && !this.opacVisible(org)) { return; }
+            if (ov === false && this.opacVisible(org)) { return; }
+
+            if (filter.inList && !filter.inList.includes(org.id())) {
+                return;
+            }
+
+            if (filter.notInList && filter.notInList.includes(org.id())) {
+                return;
+            }
+
+            // All filter tests passed.  Add it to the list
+            list.push(asId ? org.id() : org);
+        });
+
+        return list;
+    }
+
+    tree(): IdlObject {
+        return this.orgTree;
+    }
+
+    // get the root OU
+    root(): IdlObject {
+        return this.orgList[0];
+    }
+
+    // list of org_unit objects or IDs for ancestors + me
+    ancestors(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
+        let node = this.get(nodeOrId);
+        if (!node) { return []; }
+        const nodes = [node];
+        while ( (node = this.get(node.parent_ou())) ) {
+            nodes.push(node);
+        }
+        if (asId) {
+            return nodes.map(n => n.id());
+        }
+        return nodes;
+    }
+
+    // tests that a node can have users
+    canHaveUsers(nodeOrId): boolean {
+        return this.get(nodeOrId).ou_type().can_have_users() === 't';
+    }
+
+    // tests that a node can have volumes
+    canHaveVolumes(nodeOrId): boolean {
+        return this
+            .get(nodeOrId)
+            .ou_type()
+            .can_have_vols() === 't';
+    }
+
+    opacVisible(nodeOrId): boolean {
+        return this.get(nodeOrId).opac_visible() === 't';
+    }
+
+    // list of org_unit objects  or IDs for me + descendants
+    descendants(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
+        const node = this.get(nodeOrId);
+        if (!node) { return []; }
+        const nodes = [];
+        const descend = (n) => {
+            nodes.push(n);
+            n.children().forEach(descend);
+        };
+        descend(node);
+        if (asId) {
+            return nodes.map(n => n.id());
+        }
+        return nodes;
+    }
+
+    // list of org_unit objects or IDs for ancestors + me + descendants
+    fullPath(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
+        const list = this.ancestors(nodeOrId, false).concat(
+          this.descendants(nodeOrId, false).slice(1));
+        if (asId) {
+            return list.map(n => n.id());
+        }
+        return list;
+    }
+
+    sortTree(sortField?: string, node?: IdlObject): void {
+        if (!sortField) { sortField = 'shortname'; }
+        if (!node) { node = this.orgTree; }
+        node.children(
+            node.children.sort((a, b) => {
+                return a[sortField]() < b[sortField]() ? -1 : 1;
+            })
+        );
+        node.children.forEach(n => this.sortTree(n));
+    }
+
+    absorbTree(node?: IdlObject): void {
+        if (!node) {
+            node = this.orgTree;
+            this.orgMap = {};
+            this.orgList = [];
+        }
+        this.orgMap[node.id()] = node;
+        this.orgList.push(node);
+        node.children().forEach(c => this.absorbTree(c));
+    }
+
+    /**
+     * Grabs all of the org units from the server, chops them up into
+     * various shapes, then returns an "all done" promise.
+     */
+    fetchOrgs(): Promise<void> {
+        return this.pcrud.search('aou', {parent_ou : null},
+            {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}},
+            {anonymous : true}
+        ).toPromise().then(tree => {
+            // ingest tree, etc.
+            this.orgTree = tree;
+            this.absorbTree();
+        });
+    }
+
+    /**
+     * Populate 'target' with settings from cache where available.
+     * Return the list of settings /not/ pulled from cache.
+     */
+    private settingsFromCache(names: string[], target: any) {
+        const cacheKeys = Object.keys(this.settingsCache);
+
+        cacheKeys.forEach(key => {
+            const matchIdx = names.indexOf(key);
+            if (matchIdx > -1) {
+                target[key] = this.settingsCache[key];
+                names.splice(matchIdx, 1);
+            }
+        });
+
+        return names;
+    }
+
+    /**
+     * Fetch org settings from the network.
+     * 'auth' is null for anonymous lookup.
+     */
+    private settingsFromNet(orgId: number,
+        names: string[], auth?: string): Promise<any> {
+
+        const settings = {};
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.ou_setting.ancestor_default.batch',
+                orgId, names, auth
+            ).subscribe(
+                blob => {
+                    Object.keys(blob).forEach(key => {
+                        const val = blob[key]; // null or hash
+                        settings[key] = val ? val.value : null;
+                    });
+                    resolve(settings);
+                },
+                err => reject(err)
+            );
+        });
+    }
+
+
+    /**
+     *
+     */
+    settings(names: string[],
+        orgId?: number, anonymous?: boolean): Promise<OrgSettingsBatch> {
+
+        const settings = {};
+        let auth: string = null;
+        let useCache = false;
+
+        if (this.auth.user()) {
+            if (orgId) {
+                useCache = Number(orgId) === Number(this.auth.user().ws_ou());
+            } else {
+                orgId = this.auth.user().ws_ou();
+                useCache = true;
+            }
+
+            // avoid passing auth token when anonymous is requested.
+            if (!anonymous) {
+                auth = this.auth.token();
+            }
+
+        } else if (!anonymous) {
+            return Promise.reject(
+                'Use "anonymous" To retrieve org settings without an authtoken');
+        }
+
+        if (useCache) {
+            names = this.settingsFromCache(names, settings);
+        }
+
+        // All requested settings found in cache (or name list is empty)
+        if (names.length === 0) {
+            return Promise.resolve(settings);
+        }
+
+        return this.settingsFromNet(orgId, names, auth)
+        .then(sets => {
+            if (useCache) {
+                Object.keys(sets).forEach(key => {
+                    this.settingsCache[key] = sets[key];
+                });
+            }
+            return sets;
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/org.spec.ts b/Open-ILS/src/eg2/src/app/core/org.spec.ts
new file mode 100644
index 0000000..78c2f26
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/org.spec.ts
@@ -0,0 +1,66 @@
+import {IdlService} from './idl.service';
+import {EventService} from './event.service';
+import {NetService} from './net.service';
+import {AuthService} from './auth.service';
+import {PcrudService} from './pcrud.service';
+import {StoreService} from './store.service';
+import {OrgService} from './org.service';
+
+describe('OrgService', () => {
+    let idlService: IdlService;
+    let netService: NetService;
+    let authService: AuthService;
+    let pcrudService: PcrudService;
+    let orgService: OrgService;
+    let evtService: EventService;
+    let storeService: StoreService;
+
+    beforeEach(() => {
+        idlService = new IdlService();
+        evtService = new EventService();
+        storeService = new StoreService(null /* CookieService */);
+        netService = new NetService(evtService);
+        authService = new AuthService(evtService, netService, storeService);
+        pcrudService = new PcrudService(idlService, netService, authService);
+        orgService = new OrgService(netService, authService, pcrudService);
+    });
+
+    const initTestData = () => {
+        idlService.parseIdl();
+        const win: any = window; // trick TS
+        win._eg_mock_data.generateOrgTree(idlService, orgService);
+    };
+
+    it('should provide get by ID', () => {
+        initTestData();
+        expect(orgService.get(orgService.tree().id())).toBe(orgService.root());
+    });
+
+    it('should provide get by node', () => {
+        initTestData();
+        expect(orgService.get(orgService.tree())).toBe(orgService.root());
+    });
+
+    it('should provide ancestors', () => {
+        initTestData();
+        expect(orgService.ancestors(2, true)).toEqual([2, 1]);
+    });
+
+    it('should provide descendants', () => {
+        initTestData();
+        expect(orgService.descendants(2, true)).toEqual([2, 4]);
+    });
+
+    it('should provide full path', () => {
+        initTestData();
+        expect(orgService.fullPath(4, true)).toEqual([4, 2, 1]);
+    });
+
+    it('should provide root', () => {
+        initTestData();
+        expect(orgService.root().id()).toEqual(1);
+    });
+
+});
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/pcrud.service.ts b/Open-ILS/src/eg2/src/app/core/pcrud.service.ts
new file mode 100644
index 0000000..76ee341
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/pcrud.service.ts
@@ -0,0 +1,305 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {IdlService, IdlObject} from './idl.service';
+import {NetService, NetRequest} from './net.service';
+import {AuthService} from './auth.service';
+
+// Externally defined.  Used here for debugging.
+declare var js2JSON: (jsThing: any) => string;
+declare var OpenSRF: any; // creating sessions
+
+interface PcrudReqOps {
+    authoritative?: boolean;
+    anonymous?: boolean;
+    idlist?: boolean;
+    atomic?: boolean;
+}
+
+// For for documentation purposes.
+type PcrudResponse = any;
+
+export class PcrudContext {
+
+    static verboseLogging = true; //
+    static identGenerator = 0; // for debug logging
+
+    private ident: number;
+    private authoritative: boolean;
+    private xactCloseMode: string;
+    private cudIdx: number;
+    private cudAction: string;
+    private cudLast: PcrudResponse;
+    private cudList: IdlObject[];
+
+    private idl: IdlService;
+    private net: NetService;
+    private auth: AuthService;
+
+    // Tracks nested CUD actions
+    cudObserver: Observer<PcrudResponse>;
+
+    session: any; // OpenSRF.ClientSession
+
+    constructor( // passed in by parent service -- not injected
+        egIdl: IdlService,
+        egNet: NetService,
+        egAuth: AuthService
+    ) {
+        this.idl = egIdl;
+        this.net = egNet;
+        this.auth = egAuth;
+        this.xactCloseMode = 'rollback';
+        this.ident = PcrudContext.identGenerator++;
+        this.session = new OpenSRF.ClientSession('open-ils.pcrud');
+    }
+
+    toString(): string {
+        return '[PCRUDContext ' + this.ident + ']';
+    }
+
+    log(msg: string): void {
+        if (PcrudContext.verboseLogging) {
+            console.debug(this + ': ' + msg);
+        }
+    }
+
+    err(msg: string): void {
+        console.error(this + ': ' + msg);
+    }
+
+    token(reqOps?: PcrudReqOps): string {
+        return (reqOps && reqOps.anonymous) ?
+            'ANONYMOUS' : this.auth.token();
+    }
+
+    connect(): Promise<PcrudContext> {
+        this.log('connect');
+        return new Promise( (resolve, reject) => {
+            this.session.connect({
+                onconnect : () => { resolve(this); }
+            });
+        });
+    }
+
+    disconnect(): void {
+        this.log('disconnect');
+        this.session.disconnect();
+    }
+
+    retrieve(fmClass: string, pkey: Number | string,
+            pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        reqOps = reqOps || {};
+        this.authoritative = reqOps.authoritative || false;
+        return this.dispatch(
+            `open-ils.pcrud.retrieve.${fmClass}`,
+             [this.token(reqOps), pkey, pcrudOps]);
+    }
+
+    retrieveAll(fmClass: string, pcrudOps?: any,
+            reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        const search = {};
+        search[this.idl.classes[fmClass].pkey] = {'!=' : null};
+        return this.search(fmClass, search, pcrudOps, reqOps);
+    }
+
+    search(fmClass: string, search: any,
+            pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        reqOps = reqOps || {};
+        this.authoritative = reqOps.authoritative || false;
+
+        const returnType = reqOps.idlist ? 'id_list' : 'search';
+        let method = `open-ils.pcrud.${returnType}.${fmClass}`;
+
+        if (reqOps.atomic) { method += '.atomic'; }
+
+        return this.dispatch(method, [this.token(reqOps), search, pcrudOps]);
+    }
+
+    create(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.cud('create', list);
+    }
+    update(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.cud('update', list);
+    }
+    remove(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.cud('delete', list);
+    }
+    autoApply(list: IdlObject | IdlObject[]): Observable<PcrudResponse> { // RENAMED
+        return this.cud('auto',   list);
+    }
+
+    xactClose(): Observable<PcrudResponse> {
+        return this.sendRequest(
+            'open-ils.pcrud.transaction.' + this.xactCloseMode,
+            [this.token()]
+        );
+    }
+
+    xactBegin(): Observable<PcrudResponse> {
+        return this.sendRequest(
+            'open-ils.pcrud.transaction.begin', [this.token()]
+        );
+    }
+
+    private dispatch(method: string, params: any[]): Observable<PcrudResponse> {
+        if (this.authoritative) {
+            return this.wrapXact(() => {
+                return this.sendRequest(method, params);
+            });
+        } else {
+            return this.sendRequest(method, params);
+        }
+    }
+
+
+    // => connect
+    // => xact_begin
+    // => action
+    // => xact_close(commit/rollback)
+    // => disconnect
+    wrapXact(mainFunc: () => Observable<PcrudResponse>): Observable<PcrudResponse> {
+        return Observable.create(observer => {
+
+            // 1. connect
+            this.connect()
+
+            // 2. start the transaction
+            .then(() => this.xactBegin().toPromise())
+
+            // 3. execute the main body
+            .then(() => {
+
+                mainFunc().subscribe(
+                    res => observer.next(res),
+                    err => observer.error(err),
+                    ()  => {
+                        this.xactClose().toPromise().then(() => {
+                            // 5. disconnect
+                            this.disconnect();
+                            // 6. all done
+                            observer.complete();
+                        });
+                    }
+                );
+            });
+        });
+    }
+
+    private sendRequest(method: string,
+            params: any[]): Observable<PcrudResponse> {
+
+        // this.log(`sendRequest(${method})`);
+
+        return this.net.requestCompiled(
+            new NetRequest(
+                'open-ils.pcrud', method, params, this.session)
+        );
+    }
+
+    private cud(action: string,
+        list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        this.cudList = [].concat(list); // value or array
+
+        this.log(`CUD(): ${action}`);
+
+        this.cudIdx = 0;
+        this.cudAction = action;
+        this.xactCloseMode = 'commit';
+
+        return this.wrapXact(() => {
+            return Observable.create(observer => {
+                this.cudObserver = observer;
+                this.nextCudRequest();
+            });
+        });
+    }
+
+    /**
+     * Loops through the list of objects to update and sends
+     * them one at a time to the server for processing.  Once
+     * all are done, the cudObserver is resolved.
+     */
+    nextCudRequest(): void {
+        if (this.cudIdx >= this.cudList.length) {
+            this.cudObserver.complete();
+            return;
+        }
+
+        let action = this.cudAction;
+        const fmObj = this.cudList[this.cudIdx++];
+
+        if (action === 'auto') {
+            if (fmObj.ischanged()) { action = 'update'; }
+            if (fmObj.isnew())     { action = 'create'; }
+            if (fmObj.isdeleted()) { action = 'delete'; }
+
+            if (action === 'auto') {
+                // object does not need updating; move along
+                this.nextCudRequest();
+            }
+        }
+
+        this.sendRequest(
+            `open-ils.pcrud.${action}.${fmObj.classname}`,
+            [this.token(), fmObj]
+        ).subscribe(
+            res => this.cudObserver.next(res),
+            err => this.cudObserver.error(err),
+            ()  => this.nextCudRequest()
+        );
+    }
+}
+
+ at Injectable({providedIn: 'root'})
+export class PcrudService {
+
+    constructor(
+        private idl: IdlService,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    // Pass-thru functions for one-off PCRUD calls
+
+    connect(): Promise<PcrudContext> {
+        return this.newContext().connect();
+    }
+
+    newContext(): PcrudContext {
+        return new PcrudContext(this.idl, this.net, this.auth);
+    }
+
+    retrieve(fmClass: string, pkey: Number | string,
+        pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        return this.newContext().retrieve(fmClass, pkey, pcrudOps, reqOps);
+    }
+
+    retrieveAll(fmClass: string, pcrudOps?: any,
+        reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        return this.newContext().retrieveAll(fmClass, pcrudOps, reqOps);
+    }
+
+    search(fmClass: string, search: any,
+        pcrudOps?: any, reqOps?: PcrudReqOps): Observable<PcrudResponse> {
+        return this.newContext().search(fmClass, search, pcrudOps, reqOps);
+    }
+
+    create(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().create(list);
+    }
+
+    update(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().update(list);
+    }
+
+    remove(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().remove(list);
+    }
+
+    autoApply(list: IdlObject | IdlObject[]): Observable<PcrudResponse> {
+        return this.newContext().autoApply(list);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/core/perm.service.ts b/Open-ILS/src/eg2/src/app/core/perm.service.ts
new file mode 100644
index 0000000..44d3c63
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/perm.service.ts
@@ -0,0 +1,59 @@
+import {Injectable} from '@angular/core';
+import {NetService} from './net.service';
+import {OrgService} from './org.service';
+import {AuthService} from './auth.service';
+
+interface HasPermAtResult {
+    [permName: string]: any[]; // org IDs or org unit objects
+}
+
+interface HasPermHereResult {
+    [permName: string]: boolean;
+}
+
+ at Injectable({providedIn: 'root'})
+export class PermService {
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private auth: AuthService,
+    ) {}
+
+    // workstation not required.
+    hasWorkPermAt(permNames: string[], asId?: boolean): Promise<HasPermAtResult> {
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.has_work_perm_at.batch',
+            this.auth.token(), permNames
+        ).toPromise().then(resp => {
+            const answer: HasPermAtResult = {};
+            permNames.forEach(perm => {
+                let orgs = [];
+                resp[perm].forEach(oneOrg => {
+                    orgs = orgs.concat(this.org.descendants(oneOrg, asId));
+                });
+                answer[perm] = orgs;
+            });
+
+            return answer;
+        });
+    }
+
+    // workstation required
+    hasWorkPermHere(permNames: string[]): Promise<HasPermHereResult> {
+        const wsId: number = +this.auth.user().wsid();
+
+        if (!wsId) {
+            return Promise.reject('hasWorkPermHere requires a workstation');
+        }
+
+        return this.hasWorkPermAt(permNames, true).then(resp => {
+            const answer: HasPermHereResult = {};
+            Object.keys(resp).forEach(perm => {
+                answer[perm] = resp[perm].indexOf(wsId) > -1;
+            });
+            return answer;
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/core/server-store.service.ts b/Open-ILS/src/eg2/src/app/core/server-store.service.ts
new file mode 100644
index 0000000..43415c1
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/server-store.service.ts
@@ -0,0 +1,114 @@
+/**
+ * Set and get server-stored settings.
+ */
+import {Injectable} from '@angular/core';
+import {AuthService} from './auth.service';
+import {NetService} from './net.service';
+
+// Settings summary objects returned by the API
+interface ServerSettingSummary {
+    name: string;
+    value: string;
+    has_org_setting: boolean;
+    has_user_setting: boolean;
+    has_workstation_setting: boolean;
+}
+
+ at Injectable({providedIn: 'root'})
+export class ServerStoreService {
+
+    cache: {[key: string]: ServerSettingSummary};
+
+    constructor(
+        private net: NetService,
+        private auth: AuthService) {
+        this.cache = {};
+    }
+
+    setItem(key: string, value: any): Promise<any> {
+
+        if (!this.auth.token()) {
+            return Promise.reject('Auth required to apply settings');
+        }
+
+        const setting: any = {};
+        setting[key] = value;
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.settings.apply.user_or_ws',
+            this.auth.token(), setting)
+
+        .toPromise().then(appliedCount => {
+
+            if (Number(appliedCount) > 0) { // value applied
+                return this.cache[key] = value;
+            }
+
+            return Promise.reject(
+                `No user or workstation setting type exists for: "${key}".\n` +
+                'Create a ws/user setting type or use setLocalItem() to ' +
+                'store the value locally.'
+            );
+        });
+    }
+
+    // Returns a single setting value
+    getItem(key: string): Promise<any> {
+        return this.getItemBatch([key]).then(
+            settings => settings[key]
+        );
+    }
+
+    // Returns a set of key/value pairs for the requested settings
+    getItemBatch(keys: string[]): Promise<any> {
+
+        const values: any = {};
+        keys.forEach(key => {
+            if (this.cache[key]) {
+                values[key] = this.cache[key];
+            }
+        });
+
+        if (keys.length === Object.keys(values).length) {
+            // All values are cached already
+            return Promise.resolve(values);
+        }
+
+        if (!this.auth.token()) {
+            // Authtokens require for fetching server settings, but
+            // calls to retrieve settings could potentially occur
+            // before auth completes -- Ideally not, but just to be safe.
+            return Promise.resolve({});
+        }
+
+        // Server call required.  Limit the settings to lookup to those
+        // we don't already have cached.
+        const serverKeys = [];
+        keys.forEach(key => {
+            if (!Object.keys(values).includes(key)) {
+                serverKeys.push(key);
+            }
+        });
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.settings.retrieve',
+                serverKeys, this.auth.token()
+            ).subscribe(
+                summary => {
+                    this.cache[summary.name] =
+                        values[summary.name] = summary.value;
+                },
+                err => reject,
+                () => resolve(values)
+            );
+        });
+    }
+
+    removeItem(key: string): Promise<any> {
+        return this.setItem(key, null);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/store.service.ts b/Open-ILS/src/eg2/src/app/core/store.service.ts
new file mode 100644
index 0000000..46dd621
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/store.service.ts
@@ -0,0 +1,107 @@
+/**
+ * Store and retrieve data from various sources.
+ *
+ * Data Types:
+ * 1. LocalItem: Stored in window.localStorage and persist indefinitely.
+ * 2. SessionItem: Stored in window.sessionStorage and persist until
+ *    the end of the current browser tab/window.  Data is only available
+ *    to the tab/window where the data was set.
+ * 3. LoginItem: Stored as session cookies and persist until the browser
+ *    is closed.  These values are avalable to all browser windows/tabs.
+ */
+import {Injectable} from '@angular/core';
+import {CookieService} from 'ngx-cookie';
+
+ at Injectable({providedIn: 'root'})
+export class StoreService {
+
+    // Base path for cookie-based storage.
+    // Useful for limiting cookies to subsections of the application.
+    // Store cookies globally by default.
+    // Note cookies shared with /eg/staff must be stored at "/"
+    loginSessionBasePath = '/';
+
+    // Set of keys whose values should disappear at logout.
+    loginSessionKeys: string[] = [
+        'eg.auth.token',
+        'eg.auth.time',
+        'eg.auth.token.oc',
+        'eg.auth.time.oc'
+    ];
+
+    constructor(
+        private cookieService: CookieService) {
+    }
+
+    private parseJson(valJson: string): any {
+        if (valJson === undefined || valJson === null || valJson === '') {
+            return null;
+        }
+        try {
+            return JSON.parse(valJson);
+        } catch (E) {
+            console.error(`Failure to parse JSON: ${E} => ${valJson}`);
+            return null;
+        }
+    }
+
+    /**
+     * Add a an app-local login session key
+     */
+    addLoginSessionKey(key: string): void {
+        this.loginSessionKeys.push(key);
+    }
+
+    setLocalItem(key: string, val: any, isJson?: boolean): void {
+        if (!isJson) {
+            val = JSON.stringify(val);
+        }
+        window.localStorage.setItem(key, val);
+    }
+
+    setSessionItem(key: string, val: any, isJson?: boolean): void {
+        if (!isJson) {
+            val = JSON.stringify(val);
+        }
+        window.sessionStorage.setItem(key, val);
+    }
+
+    setLoginSessionItem(key: string, val: any, isJson?: boolean): void {
+        if (!isJson) {
+            val = JSON.stringify(val);
+        }
+        this.cookieService.put(key, val,
+            {path : this.loginSessionBasePath, secure: true});
+    }
+
+    getLocalItem(key: string): any {
+        return this.parseJson(window.localStorage.getItem(key));
+    }
+
+    getSessionItem(key: string): any {
+        return this.parseJson(window.sessionStorage.getItem(key));
+    }
+
+    getLoginSessionItem(key: string): any {
+        return this.parseJson(this.cookieService.get(key));
+    }
+
+    removeLocalItem(key: string): void {
+        window.localStorage.removeItem(key);
+    }
+
+    removeSessionItem(key: string): void {
+        window.sessionStorage.removeItem(key);
+    }
+
+    removeLoginSessionItem(key: string): void {
+        this.cookieService.remove(key, {path : this.loginSessionBasePath});
+    }
+
+    clearLoginSessionItems(): void {
+        this.loginSessionKeys.forEach(
+            key => this.removeLoginSessionItem(key)
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/core/store.spec.ts b/Open-ILS/src/eg2/src/app/core/store.spec.ts
new file mode 100644
index 0000000..ae6c27f
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/core/store.spec.ts
@@ -0,0 +1,22 @@
+import {StoreService} from './store.service';
+
+describe('StoreService', () => {
+    let service: StoreService;
+    beforeEach(() => {
+        service = new StoreService(null /* CookieService */);
+    });
+
+    it('should set/get a localStorage value', () => {
+        const str = 'hello, world';
+        service.setLocalItem('testKey', str);
+        expect(service.getLocalItem('testKey')).toBe(str);
+    });
+
+    it('should set/get a sessionStorage value', () => {
+        const str = 'hello, world again';
+        service.setLocalItem('testKey', str);
+        expect(service.getLocalItem('testKey')).toBe(str);
+    });
+
+});
+
diff --git a/Open-ILS/src/eg2/src/app/resolver.service.ts b/Open-ILS/src/eg2/src/app/resolver.service.ts
new file mode 100644
index 0000000..faa6038
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/resolver.service.ts
@@ -0,0 +1,36 @@
+import {Injectable} from '@angular/core';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRouteSnapshot} from '@angular/router';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {LocaleService} from '@eg/core/locale.service';
+
+// For locale application
+declare var OpenSRF;
+
+ at Injectable()
+export class BaseResolver implements Resolve<Promise<void>> {
+
+    constructor(
+        private router: Router,
+        private idl: IdlService,
+        private org: OrgService,
+        private locale: LocaleService
+    ) {}
+
+    /**
+     * Loads pre-auth data common to all applications.
+     * No auth token is available at this level.  When needed, auth is
+     * enforced by application/group-specific resolvers at lower levels.
+     */
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Promise<void> {
+
+        OpenSRF.locale = this.locale.currentLocaleCode();
+
+        this.idl.parseIdl();
+
+        return this.org.fetchOrgs(); // anonymous PCRUD.
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/routing.module.ts b/Open-ILS/src/eg2/src/app/routing.module.ts
new file mode 100644
index 0000000..db3ee19
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/routing.module.ts
@@ -0,0 +1,29 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {BaseResolver} from './resolver.service';
+import {WelcomeComponent} from './welcome.component';
+
+/**
+ * Avoid loading all application JS up front by lazy-loading sub-modules.
+ * When lazy loading, no module references should be directly imported.
+ * The refs are encoded in the loadChildren attribute of each route.
+ * These modules are encoded as separate JS chunks that are fetched
+ * from the server only when needed.
+ */
+const routes: Routes = [
+  { path: '',
+    component: WelcomeComponent
+  }, {
+    path: 'staff',
+    resolve : {startup : BaseResolver},
+    loadChildren: './staff/staff.module#StaffModule'
+  }
+];
+
+ at NgModule({
+  imports: [RouterModule.forRoot(routes)],
+  exports: [RouterModule],
+  providers: [BaseResolver]
+})
+
+export class BaseRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/share/README b/Open-ILS/src/eg2/src/app/share/README
new file mode 100644
index 0000000..f428f79
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/README
@@ -0,0 +1,6 @@
+Shared Angular services, components, directives, and associated classes.  
+
+These items are NOT automatically imported to the base module, though some
+may already be imported by intermediate modules (e.g. StaffCommonModule).   
+Import as needed.
+
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html
new file mode 100644
index 0000000..82ed72a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html
@@ -0,0 +1,26 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Access Key Assignments</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row border-bottom">
+      <div class="col-lg-3 p-1 border-right text-center" i18n>Command</div>
+      <div class="col-lg-6 p-1 border-right" i18n>Action</div>
+      <div class="col-lg-3 p-1" i18n>Context</div>
+    </div>
+    <div class="row border-bottom" *ngFor="let a of assignments()">
+      <div class="col-lg-3 p-1 border-right text-center">{{a.key}}</div>
+      <div class="col-lg-6 p-1 border-right">{{a.desc}}</div>
+      <div class="col-lg-3 p-1">{{a.ctx}}</div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close()" i18n>Close</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts
new file mode 100644
index 0000000..d713ee6
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts
@@ -0,0 +1,25 @@
+/**
+ */
+import {Component, Input, OnInit} from '@angular/core';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+ at Component({
+  selector: 'eg-accesskey-info',
+  templateUrl: './accesskey-info.component.html'
+})
+export class AccessKeyInfoComponent extends DialogComponent {
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private keyService: AccessKeyService) {
+        super(modal);
+    }
+
+    assignments(): any[] {
+        return this.keyService.infoIze();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts
new file mode 100644
index 0000000..dfc835d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts
@@ -0,0 +1,56 @@
+/**
+ * Assign access keys to <a> tags.
+ *
+ * Access key action is peformed via .click(). hrefs, routerLinks,
+ * and (click) actions are all supported.
+ *
+ *   <a
+ *     routerLink="/staff/splash"
+ *     egAccessKey
+ *     keySpec="alt+h" i18n-keySpec
+ *     keyDesc="My Description" 18n-keyDesc
+ *   >
+ */
+import {Directive, ElementRef, Input, OnInit} from '@angular/core';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+
+ at Directive({
+  selector: '[egAccessKey]'
+})
+export class AccessKeyDirective implements OnInit {
+
+    // Space-separated list of key combinations
+    // E.g. "ctrl+h", "alt+h ctrl+y"
+    @Input() keySpec: string;
+
+    // Description to display in the accesskey info dialog
+    @Input() keyDesc: string;
+
+    // Context info to display in the accesskey info dialog
+    // E.g. "navbar"
+    @Input() keyCtx: string;
+
+    constructor(
+        private elm: ElementRef,
+        private keyService: AccessKeyService
+    ) { }
+
+    ngOnInit() {
+
+        if (!this.keySpec) {
+            console.warn('AccessKey no keySpec provided');
+            return;
+        }
+
+        this.keySpec.split(/ /).forEach(keySpec => {
+            this.keyService.assign({
+                key: keySpec,
+                desc: this.keyDesc,
+                ctx: this.keyCtx,
+                action: () => this.elm.nativeElement.click()
+            });
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
new file mode 100644
index 0000000..51dda57
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
@@ -0,0 +1,67 @@
+import {Injectable, EventEmitter, HostListener} from '@angular/core';
+
+export interface AccessKeyAssignment {
+    key: string;      // keyboard command
+    desc: string;     // human-friendly description
+    ctx: string;      // template context
+    action: Function; // handler function
+}
+
+ at Injectable()
+export class AccessKeyService {
+
+    // Assignments stored as an array with most recently assigned
+    // items toward the front.  Most recent items have precedence.
+    assignments: AccessKeyAssignment[] = [];
+
+    constructor() {}
+
+    assign(assn: AccessKeyAssignment): void {
+        this.assignments.unshift(assn);
+    }
+
+    /**
+     * Compress a set of single-fire keyboard events into single
+     * string.  For example:  Control and 't' becomes 'ctrl+t'.
+     */
+    compressKeys(evt: KeyboardEvent): string {
+
+        let s = '';
+        if (evt.ctrlKey || evt.metaKey) { s += 'ctrl+'; }
+        if (evt.altKey) { s += 'alt+'; }
+        s += evt.key.toLowerCase();
+
+        return s;
+    }
+
+    /**
+     * Checks for a key assignment and fires the assigned action.
+     */
+    fire(evt: KeyboardEvent): void {
+        const keySpec = this.compressKeys(evt);
+        for (const i in this.assignments) { // for-loop to exit early
+            if (keySpec === this.assignments[i].key) {
+                const assign = this.assignments[i];
+                console.debug(`AccessKey assignment found for ${assign.key}`);
+                // Allow the current digest cycle to complete before
+                // firing the access key action.
+                setTimeout(assign.action, 0);
+                evt.preventDefault();
+                return;
+            }
+        }
+    }
+
+    /**
+     * Returns a simplified key assignment list containing just
+     * the key spec and the description.  Useful for inspecting
+     * without exposing the actions.
+     */
+    infoIze(): any[] {
+        return this.assignments.map(a => {
+            return {key: a.key, desc: a.desc, ctx: a.ctx};
+        });
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
new file mode 100644
index 0000000..19924d9
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
@@ -0,0 +1,249 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {from} from 'rxjs/observable/from';
+import {map} from 'rxjs/operators/map';
+import {OrgService} from '@eg/core/org.service';
+import {UnapiService} from '@eg/share/catalog/unapi.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+export const NAMESPACE_MAPS = {
+    'mods':     'http://www.loc.gov/mods/v3',
+    'biblio':   'http://open-ils.org/spec/biblio/v1',
+    'holdings': 'http://open-ils.org/spec/holdings/v1',
+    'indexing': 'http://open-ils.org/spec/indexing/v1'
+};
+
+export const HOLDINGS_XPATH =
+    '/holdings:holdings/holdings:counts/holdings:count';
+
+
+export class BibRecordSummary {
+    id: number; // == record.id() for convenience
+    orgId: number;
+    orgDepth: number;
+    record: IdlObject;
+    display: any;
+    attributes: any;
+    holdingsSummary: any;
+    holdCount: number;
+    bibCallNumber: string;
+    net: NetService;
+
+    constructor(record: IdlObject, orgId: number, orgDepth: number) {
+        this.id = record.id();
+        this.record = record;
+        this.orgId = orgId;
+        this.orgDepth = orgDepth;
+        this.display = {};
+        this.attributes = {};
+        this.bibCallNumber = null;
+    }
+
+    ingest() {
+        this.compileDisplayFields();
+        this.compileRecordAttrs();
+
+        // Normalize some data for JS consistency
+        this.record.creator(Number(this.record.creator()));
+        this.record.editor(Number(this.record.editor()));
+    }
+
+    compileDisplayFields() {
+        this.record.flat_display_entries().forEach(entry => {
+            if (entry.multi() === 't') {
+                if (this.display[entry.name()]) {
+                    this.display[entry.name()].push(entry.value());
+                } else {
+                    this.display[entry.name()] = [entry.value()];
+                }
+            } else {
+                this.display[entry.name()] = entry.value();
+            }
+        });
+    }
+
+    compileRecordAttrs() {
+        // Any attr can be multi-valued.
+        this.record.mattrs().forEach(attr => {
+            if (this.attributes[attr.attr()]) {
+                this.attributes[attr.attr()].push(attr.value());
+            } else {
+                this.attributes[attr.attr()] = [attr.value()];
+            }
+        });
+    }
+
+    // Get -> Set -> Return bib hold count
+    getHoldCount(): Promise<number> {
+
+        if (Number.isInteger(this.holdCount)) {
+            return Promise.resolve(this.holdCount);
+        }
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.bre.holds.count', this.id
+        ).toPromise().then(count => this.holdCount = count);
+    }
+
+    // Get -> Set -> Return bib-level call number
+    getBibCallNumber(): Promise<string> {
+
+        if (this.bibCallNumber !== null) {
+            return Promise.resolve(this.bibCallNumber);
+        }
+
+        // TODO labelClass = cat.default_classification_scheme YAOUS
+        const labelClass = 1;
+
+        return this.net.request(
+            'open-ils.cat',
+            'open-ils.cat.biblio.record.marc_cn.retrieve',
+            this.id, labelClass
+        ).toPromise().then(cnArray => {
+            if (cnArray && cnArray.length > 0) {
+                const key1 = Object.keys(cnArray[0])[0];
+                this.bibCallNumber = cnArray[0][key1];
+            } else {
+                this.bibCallNumber = '';
+            }
+            return this.bibCallNumber;
+        });
+    }
+}
+
+ at Injectable()
+export class BibRecordService {
+
+    // Cache of bib editor / creator objects
+    // Assumption is this list will be limited in size.
+    userCache: {[id: number]: IdlObject};
+
+    constructor(
+        private idl: IdlService,
+        private net: NetService,
+        private org: OrgService,
+        private unapi: UnapiService,
+        private pcrud: PcrudService
+    ) {
+        this.userCache = {};
+    }
+
+    // Avoid fetching the MARC blob by specifying which fields on the
+    // bre to select.  Note that fleshed fields are explicitly selected.
+    fetchableBreFields(): string[] {
+        return this.idl.classes.bre.fields
+            .filter(f => !f.virtual && f.name !== 'marc')
+            .map(f => f.name);
+    }
+
+    // Note when multiple IDs are provided, responses are emitted in order
+    // of receipt, not necessarily in the requested ID order.
+    getBibSummary(bibIds: number | number[],
+        orgId?: number, orgDepth?: number): Observable<BibRecordSummary> {
+
+        const ids = [].concat(bibIds);
+
+        if (ids.length === 0) {
+            return from([]);
+        }
+
+        return this.pcrud.search('bre', {id: ids},
+            {   flesh: 1,
+                flesh_fields: {bre: ['flat_display_entries', 'mattrs']},
+                select: {bre : this.fetchableBreFields()}
+            },
+            {anonymous: true} // skip unneccesary auth
+        ).pipe(mergeMap(bib => {
+            const summary = new BibRecordSummary(bib, orgId, orgDepth);
+            summary.net = this.net; // inject
+            summary.ingest();
+            return this.getHoldingsSummary(bib.id(), orgId, orgDepth)
+            .then(holdingsSummary => {
+                summary.holdingsSummary = holdingsSummary;
+                return summary;
+            });
+        }));
+    }
+
+    // Flesh the creator and editor fields.
+    // Handling this separately lets us pull from the cache and
+    // avoids the requirement that the main bib query use a staff
+    // (VIEW_USER) auth token.
+    fleshBibUsers(records: IdlObject[]): Promise<void> {
+
+        const search = [];
+
+        records.forEach(rec => {
+            ['creator', 'editor'].forEach(field => {
+                const id = rec[field]();
+                if (Number.isInteger(id)) {
+                    if (this.userCache[id]) {
+                        rec[field](this.userCache[id]);
+                    } else if (!search.includes(id)) {
+                        search.push(id);
+                    }
+                }
+            });
+        });
+
+        if (search.length === 0) {
+            return Promise.resolve();
+        }
+
+        return this.pcrud.search('au', {id: search})
+        .pipe(map(user => {
+            this.userCache[user.id()] = user;
+            records.forEach(rec => {
+                if (user.id() === rec.creator()) {
+                    rec.creator(user);
+                }
+                if (user.id() === rec.editor()) {
+                    rec.editor(user);
+                }
+            });
+        })).toPromise();
+    }
+
+    getHoldingsSummary(recordId: number,
+        orgId: number, orgDepth: number): Promise<any> {
+
+        const holdingsSummary = [];
+
+        return this.unapi.getAsXmlDocument({
+            target: 'bre',
+            id: recordId,
+            extras: '{holdings_xml}',
+            format: 'holdings_xml',
+            orgId: orgId,
+            depth: orgDepth
+        }).then(xmlDoc => {
+
+            // namespace resolver
+            const resolver: any = (prefix: string): string => {
+                return NAMESPACE_MAPS[prefix] || null;
+            };
+
+            // Extract the holdings data from the unapi xml doc
+            const result = xmlDoc.evaluate(HOLDINGS_XPATH,
+                xmlDoc, resolver, XPathResult.ANY_TYPE, null);
+
+            let node;
+            while (node = result.iterateNext()) {
+                const counts = {type : node.getAttribute('type')};
+                ['depth', 'org_unit', 'transcendant',
+                    'available', 'count', 'unshadow'].forEach(field => {
+                    counts[field] = Number(node.getAttribute(field));
+                });
+                holdingsSummary.push(counts);
+            }
+
+            return holdingsSummary;
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts
new file mode 100644
index 0000000..c370b30
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts
@@ -0,0 +1,28 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {CatalogService} from './catalog.service';
+import {CatalogUrlService} from './catalog-url.service';
+import {BibRecordService} from './bib-record.service';
+import {UnapiService} from './unapi.service';
+import {MarcHtmlComponent} from './marc-html.component';
+
+
+ at NgModule({
+    declarations: [
+        MarcHtmlComponent
+    ],
+    imports: [
+        EgCommonModule
+    ],
+    exports: [
+        MarcHtmlComponent
+    ],
+    providers: [
+        CatalogService,
+        CatalogUrlService,
+        UnapiService,
+        BibRecordService
+    ]
+})
+
+export class CatalogCommonModule {}
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts
new file mode 100644
index 0000000..253e3aa
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts
@@ -0,0 +1,143 @@
+import {Injectable} from '@angular/core';
+import {ParamMap} from '@angular/router';
+import {OrgService} from '@eg/core/org.service';
+import {CatalogSearchContext, FacetFilter} from './search-context';
+import {CATALOG_CCVM_FILTERS} from './catalog.service';
+
+ at Injectable()
+export class CatalogUrlService {
+
+    // consider supporting a param name prefix/namespace
+
+    constructor(private org: OrgService) { }
+
+    /**
+     * Returns a URL query structure suitable for using with
+     * router.navigate(..., {queryParams:...}).
+     * No navigation is performed within.
+     */
+    toUrlParams(context: CatalogSearchContext):
+            {[key: string]: string | string[]} {
+
+        const params = {
+            query: [],
+            fieldClass: [],
+            joinOp: [],
+            matchOp: [],
+            facets: [],
+            identQuery: null,
+            identQueryType: null,
+            org: null,
+            limit: null,
+            offset: null
+        };
+
+        params.org = context.searchOrg.id();
+
+        params.limit = context.pager.limit;
+        if (context.pager.offset) {
+            params.offset = context.pager.offset;
+        }
+
+        // These fields can be copied directly into place
+        ['format', 'sort', 'available', 'global', 'identQuery', 'identQueryType']
+        .forEach(field => {
+            if (context[field]) {
+                // Only propagate applied values to the URL.
+                params[field] = context[field];
+            }
+        });
+
+        if (params.identQuery) {
+            // Ident queries (e.g. tcn search) discards all remaining filters
+            return params;
+        }
+
+        context.query.forEach((q, idx) => {
+            ['query', 'fieldClass', 'joinOp', 'matchOp'].forEach(field => {
+                // Propagate all array-based fields regardless of
+                // whether a value is applied to ensure correct
+                // correlation between values.
+                params[field][idx] = context[field][idx];
+            });
+        });
+
+        // CCVM filters are encoded as comma-separated lists
+        Object.keys(context.ccvmFilters).forEach(code => {
+            if (context.ccvmFilters[code] &&
+                context.ccvmFilters[code][0] !== '') {
+                params[code] = context.ccvmFilters[code].join(',');
+            }
+        });
+
+        // Each facet is a JSON encoded blob of class, name, and value
+        context.facetFilters.forEach(facet => {
+            params.facets.push(JSON.stringify({
+                c : facet.facetClass,
+                n : facet.facetName,
+                v : facet.facetValue
+            }));
+        });
+
+        return params;
+    }
+
+    /**
+     * Creates a new search context from the active route params.
+     */
+    fromUrlParams(params: ParamMap): CatalogSearchContext {
+        const context = new CatalogSearchContext();
+
+        this.applyUrlParams(context, params);
+
+        return context;
+    }
+
+    applyUrlParams(context: CatalogSearchContext, params: ParamMap): void {
+
+        // Reset query/filter args.  The will be reconstructed below.
+        context.reset();
+
+        // These fields can be copied directly into place
+        ['format', 'sort', 'available', 'global', 'identQuery', 'identQueryType']
+        .forEach(field => {
+            const val = params.get(field);
+            if (val !== null) {
+                context[field] = val;
+            }
+        });
+
+        if (params.get('limit')) {
+            context.pager.limit = +params.get('limit');
+        }
+
+        if (params.get('offset')) {
+            context.pager.offset = +params.get('offset');
+        }
+
+        ['query', 'fieldClass', 'joinOp', 'matchOp'].forEach(field => {
+            const arr = params.getAll(field);
+            if (arr && arr.length) {
+                context[field] = arr;
+            }
+        });
+
+        CATALOG_CCVM_FILTERS.forEach(code => {
+            const val = params.get(code);
+            if (val) {
+                context.ccvmFilters[code] = val.split(/,/);
+            } else {
+                context.ccvmFilters[code] = [''];
+            }
+        });
+
+        params.getAll('facets').forEach(blob => {
+            const facet = JSON.parse(blob);
+            context.addFacet(new FacetFilter(facet.c, facet.n, facet.v));
+        });
+
+        if (params.get('org')) {
+            context.searchOrg = this.org.get(+params.get('org'));
+        }
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
new file mode 100644
index 0000000..95967cb
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
@@ -0,0 +1,210 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {map} from 'rxjs/operators/map';
+import {OrgService} from '@eg/core/org.service';
+import {UnapiService} from '@eg/share/catalog/unapi.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CatalogSearchContext, CatalogSearchState} from './search-context';
+import {BibRecordService, BibRecordSummary} from './bib-record.service';
+
+// CCVM's we care about in a catalog context
+// Don't fetch them all because there are a lot.
+export const CATALOG_CCVM_FILTERS = [
+    'item_type',
+    'item_form',
+    'item_lang',
+    'audience',
+    'audience_group',
+    'vr_format',
+    'bib_level',
+    'lit_form',
+    'search_format',
+    'icon_format'
+];
+
+ at Injectable()
+export class CatalogService {
+
+    ccvmMap: {[ccvm: string]: IdlObject[]} = {};
+    cmfMap: {[cmf: string]: IdlObject} = {};
+
+    // Keep a reference to the most recently retrieved facet data,
+    // since facet data is consistent across a given search.
+    // No need to re-fetch with every page of search data.
+    lastFacetData: any;
+    lastFacetKey: string;
+
+    constructor(
+        private idl: IdlService,
+        private net: NetService,
+        private org: OrgService,
+        private unapi: UnapiService,
+        private pcrud: PcrudService,
+        private bibService: BibRecordService
+    ) {}
+
+    search(ctx: CatalogSearchContext): Promise<void> {
+        ctx.searchState = CatalogSearchState.SEARCHING;
+
+        const fullQuery = ctx.compileSearch();
+
+        console.debug(`search query: ${fullQuery}`);
+
+        let method = 'open-ils.search.biblio.multiclass.query';
+        if (ctx.isStaff) {
+            method += '.staff';
+        }
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.search', method, {
+                    limit : ctx.pager.limit + 1,
+                    offset : ctx.pager.offset
+                }, fullQuery, true
+            ).subscribe(result => {
+                this.applyResultData(ctx, result);
+                ctx.searchState = CatalogSearchState.COMPLETE;
+                resolve();
+            });
+        });
+    }
+
+    applyResultData(ctx: CatalogSearchContext, result: any): void {
+        ctx.result = result;
+        ctx.pager.resultCount = result.count;
+
+        // records[] tracks the current page of bib summaries.
+        result.records = [];
+
+        // If this is a new search, reset the result IDs collection.
+        if (this.lastFacetKey !== result.facet_key) {
+            ctx.resultIds = [];
+        }
+
+        result.ids.forEach((blob, idx) => ctx.addResultId(blob[0], idx));
+    }
+
+    // Appends records to the search result set as they arrive.
+    // Returns a void promise once all records have been retrieved
+    fetchBibSummaries(ctx: CatalogSearchContext): Promise<void> {
+
+        const depth = ctx.global ?
+            ctx.org.root().ou_type().depth() :
+            ctx.searchOrg.ou_type().depth();
+
+        return this.bibService.getBibSummary(
+            ctx.currentResultIds(), ctx.searchOrg.id(), depth)
+        .pipe(map(summary => {
+            // Responses are not necessarily returned in request-ID order.
+            const idx = ctx.currentResultIds().indexOf(summary.record.id());
+            if (ctx.result.records) {
+                // May be reset when quickly navigating results.
+                ctx.result.records[idx] = summary;
+            }
+        })).toPromise();
+    }
+
+    fetchFacets(ctx: CatalogSearchContext): Promise<void> {
+
+        if (!ctx.result) {
+            return Promise.reject('Cannot fetch facets without results');
+        }
+
+        if (this.lastFacetKey === ctx.result.facet_key) {
+            ctx.result.facetData = this.lastFacetData;
+            return Promise.resolve();
+        }
+
+        return new Promise((resolve, reject) => {
+            this.net.request('open-ils.search',
+                'open-ils.search.facet_cache.retrieve',
+                ctx.result.facet_key
+            ).subscribe(facets => {
+                const facetData = {};
+                Object.keys(facets).forEach(cmfId => {
+                    const facetHash = facets[cmfId];
+                    const cmf = this.cmfMap[cmfId];
+
+                    const cmfData = [];
+                    Object.keys(facetHash).forEach(value => {
+                        const count = facetHash[value];
+                        cmfData.push({value : value, count : count});
+                    });
+
+                    if (!facetData[cmf.field_class()]) {
+                        facetData[cmf.field_class()] = {};
+                    }
+
+                    facetData[cmf.field_class()][cmf.name()] = {
+                        cmfLabel : cmf.label(),
+                        valueList : cmfData.sort((a, b) => {
+                            if (a.count > b.count) { return -1; }
+                            if (a.count < b.count) { return 1; }
+                            // secondary alpha sort on display value
+                            return a.value < b.value ? -1 : 1;
+                        })
+                    };
+                });
+
+                this.lastFacetKey = ctx.result.facet_key;
+                this.lastFacetData = ctx.result.facetData = facetData;
+                resolve();
+            });
+        });
+    }
+
+    fetchCcvms(): Promise<void> {
+
+        if (Object.keys(this.ccvmMap).length) {
+            return Promise.resolve();
+        }
+
+        return new Promise((resolve, reject) => {
+            this.pcrud.search('ccvm',
+                {ctype : CATALOG_CCVM_FILTERS}, {},
+                {atomic: true, anonymous: true}
+            ).subscribe(list => {
+                this.compileCcvms(list);
+                resolve();
+            });
+        });
+    }
+
+    compileCcvms(ccvms: IdlObject[]): void {
+        ccvms.forEach(ccvm => {
+            if (!this.ccvmMap[ccvm.ctype()]) {
+                this.ccvmMap[ccvm.ctype()] = [];
+            }
+            this.ccvmMap[ccvm.ctype()].push(ccvm);
+        });
+
+        Object.keys(this.ccvmMap).forEach(cType => {
+            this.ccvmMap[cType] =
+                this.ccvmMap[cType].sort((a, b) => {
+                    return a.value() < b.value() ? -1 : 1;
+                });
+        });
+    }
+
+
+    fetchCmfs(): Promise<void> {
+        // At the moment, we only need facet CMFs.
+        if (Object.keys(this.cmfMap).length) {
+            return Promise.resolve();
+        }
+
+        return new Promise((resolve, reject) => {
+            this.pcrud.search('cmf',
+                {facet_field : 't'}, {}, {atomic: true, anonymous: true}
+            ).subscribe(
+                cmfs => {
+                    cmfs.forEach(c => this.cmfMap[c.id()] = c);
+                    resolve();
+                }
+            );
+        });
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts b/Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts
new file mode 100644
index 0000000..38b1da7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts
@@ -0,0 +1,90 @@
+import {Component, OnInit, Input, ElementRef} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+
+ at Component({
+  selector: 'eg-marc-html',
+  // view is generated from MARC HTML
+  template: '<ng-template></ng-template>'
+})
+export class MarcHtmlComponent implements OnInit {
+
+    recId: number;
+    initDone = false;
+
+    @Input() set recordId(id: number) {
+        this.recId = id;
+        // Only force new data collection when recordId()
+        // is invoked after ngInit() has already run.
+        if (this.initDone) {
+            this.collectData();
+        }
+    }
+
+    recType: string;
+    @Input() set recordType(rtype: string) {
+        this.recType = rtype;
+    }
+
+    constructor(
+        private elm: ElementRef,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+        this.collectData();
+    }
+
+    collectData() {
+        if (!this.recId) { return; }
+
+        let service = 'open-ils.search';
+        let method = 'open-ils.search.biblio.record.html';
+        const params: any[] = [this.recId];
+
+        switch (this.recType) {
+
+            case 'authority':
+                method = 'open-ils.search.authority.to_html';
+                break;
+
+            case 'vandelay-authority':
+                params.unshift(this.auth.token());
+                service = 'open-ils.vandelay';
+                method = 'open-ils.vandelay.queued_authority_record.html';
+                break;
+
+            case 'vandelay-bib':
+                params.unshift(this.auth.token());
+                service = 'open-ils.vandelay';
+                method = 'open-ils.vandelay.queued_bib_record.html';
+                break;
+        }
+
+        this.net.requestWithParamList(service, method, params)
+        .toPromise().then(html => this.injectHtml(html));
+    }
+
+    injectHtml(html: string) {
+
+        // Remove embedded labels and actions.
+        html = html.replace(
+            /<button onclick="window.print(.*?)<\/button>/, '');
+
+        html = html.replace(/<title>(.*?)<\/title>/, '');
+
+        // remove reference to nonexistant CSS file
+        html = html.replace(/<link(.*?)\/>/, '');
+
+        // there shouldn't be any, but while we're at it,
+        // kill any embedded script tags
+        html = html.replace(/<script(.*?)<\/script>/, '');
+
+        this.elm.nativeElement.innerHTML = html;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
new file mode 100644
index 0000000..e4e64b2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
@@ -0,0 +1,266 @@
+import {OrgService} from '@eg/core/org.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {Pager} from '@eg/share/util/pager';
+import {Params} from '@angular/router';
+
+export enum CatalogSearchState {
+    PENDING,
+    SEARCHING,
+    COMPLETE
+}
+
+export class FacetFilter {
+    facetClass: string;
+    facetName: string;
+    facetValue: string;
+
+    constructor(cls: string, name: string, value: string) {
+        this.facetClass = cls;
+        this.facetName  = name;
+        this.facetValue = value;
+    }
+
+    equals(filter: FacetFilter): boolean {
+        return (
+            this.facetClass === filter.facetClass &&
+            this.facetName  === filter.facetName &&
+            this.facetValue === filter.facetValue
+        );
+    }
+}
+
+// Not an angular service.
+// It's conceviable there could be multiple contexts.
+export class CatalogSearchContext {
+
+    // Search options and filters
+    available = false;
+    global = false;
+    sort: string;
+    fieldClass: string[];
+    query: string[];
+    identQuery: string;
+    identQueryType: string; // isbn, issn, etc.
+    joinOp: string[];
+    matchOp: string[];
+    format: string;
+    searchOrg: IdlObject;
+    ccvmFilters: {[ccvmCode: string]: string[]};
+    facetFilters: FacetFilter[];
+    isStaff: boolean;
+
+    // Result from most recent search.
+    result: any = {};
+    searchState: CatalogSearchState = CatalogSearchState.PENDING;
+
+    // List of IDs in page/offset context.
+    resultIds: number[] = [];
+
+    // Utility stuff
+    pager: Pager;
+    org: OrgService;
+
+    constructor() {
+        this.pager = new Pager();
+        this.reset();
+    }
+
+    // List of result IDs for the current page of data.
+    currentResultIds(): number[] {
+        const ids = [];
+        const max = Math.min(
+            this.pager.offset + this.pager.limit,
+            this.pager.resultCount
+        );
+        for (let idx = this.pager.offset; idx < max; idx++) {
+            ids.push(this.resultIds[idx]);
+        }
+        return ids;
+    }
+
+    addResultId(id: number, resultIdx: number ): void {
+        this.resultIds[resultIdx + this.pager.offset] = id;
+    }
+
+    // Return the record at the requested index.
+    resultIdAt(index: number): number {
+        return this.resultIds[index] || null;
+    }
+
+    // Return the index of the requested record
+    indexForResult(id: number): number {
+        for (let i = 0; i < this.resultIds.length; i++) {
+            if (this.resultIds[i] === id) {
+                return i;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return search context to its default state, resetting search
+     * parameters and clearing any cached result data.
+     * This does not reset global filters like limit-to-available
+     * search-global, or search-org.
+     */
+    reset(): void {
+        this.pager.offset = 0;
+        this.format = '';
+        this.sort = '';
+        this.query = [''];
+        this.identQuery = null;
+        this.identQueryType = 'identifier|isbn';
+        this.fieldClass  = ['keyword'];
+        this.matchOp = ['contains'];
+        this.joinOp = [''];
+        this.ccvmFilters = {};
+        this.facetFilters = [];
+        this.result = {};
+        this.resultIds = [];
+        this.searchState = CatalogSearchState.PENDING;
+    }
+
+    isSearchable(): boolean {
+
+        if (this.identQuery && this.identQueryType) {
+            return true;
+        }
+
+        return this.query.length
+            && this.query[0] !== ''
+            && this.searchOrg !== null;
+    }
+
+    compileSearch(): string {
+        let str = '';
+
+        if (this.available) {
+            str += '#available';
+        }
+
+        if (this.sort) {
+            // e.g. title, title.descending
+            const parts = this.sort.split(/\./);
+            if (parts[1]) { str += ' #descending'; }
+            str += ' sort(' + parts[0] + ')';
+        }
+
+        if (this.identQuery && this.identQueryType) {
+            if (str) { str += ' '; }
+            str += this.identQueryType + ':' + this.identQuery;
+
+        } else {
+
+            // -------
+            // Compile boolean sub-query components
+            if (str.length) { str += ' '; }
+            const qcount = this.query.length;
+
+            // if we multiple boolean query components, wrap them in parens.
+            if (qcount > 1) { str += '('; }
+            this.query.forEach((q, idx) => {
+                str += this.compileBoolQuerySet(idx);
+            });
+            if (qcount > 1) { str += ')'; }
+            // -------
+        }
+
+        if (this.format) {
+            str += ' format(' + this.format + ')';
+        }
+
+        if (this.global) {
+            str += ' depth(' +
+                this.org.root().ou_type().depth() + ')';
+        }
+
+        str += ' site(' + this.searchOrg.shortname() + ')';
+
+        Object.keys(this.ccvmFilters).forEach(field => {
+            if (this.ccvmFilters[field][0] !== '') {
+                str += ' ' + field + '(' + this.ccvmFilters[field] + ')';
+            }
+        });
+
+        this.facetFilters.forEach(f => {
+            str += ' ' + f.facetClass + '|'
+                + f.facetName + '[' + f.facetValue + ']';
+        });
+
+        return str;
+    }
+
+    stripQuotes(query: string): string {
+        return query.replace(/"/g, '');
+    }
+
+    stripAnchors(query: string): string {
+        return query.replace(/[\^\$]/g, '');
+    }
+
+    addQuotes(query: string): string {
+        if (query.match(/ /)) {
+            return '"' + query + '"';
+        }
+        return query;
+    }
+
+    compileBoolQuerySet(idx: number): string {
+        let query = this.query[idx];
+        const joinOp = this.joinOp[idx];
+        const matchOp = this.matchOp[idx];
+        const fieldClass = this.fieldClass[idx];
+
+        let str = '';
+        if (!query) { return str; }
+
+        if (idx > 0) { str += ' ' + joinOp + ' '; }
+
+        str += '(';
+        if (fieldClass) { str += fieldClass + ':'; }
+
+        switch (matchOp) {
+            case 'phrase':
+                query = this.addQuotes(this.stripQuotes(query));
+                break;
+            case 'nocontains':
+                query = '-' + this.addQuotes(this.stripQuotes(query));
+                break;
+            case 'exact':
+                query = '^' + this.stripAnchors(query) + '$';
+                break;
+            case 'starts':
+                query = this.addQuotes('^' +
+                    this.stripAnchors(this.stripQuotes(query)));
+                break;
+        }
+
+        return str + query + ')';
+    }
+
+    hasFacet(facet: FacetFilter): boolean {
+        return Boolean(
+            this.facetFilters.filter(f => f.equals(facet))[0]
+        );
+    }
+
+    removeFacet(facet: FacetFilter): void {
+        this.facetFilters = this.facetFilters.filter(f => !f.equals(facet));
+    }
+
+    addFacet(facet: FacetFilter): void {
+        if (!this.hasFacet(facet)) {
+            this.facetFilters.push(facet);
+        }
+    }
+
+    toggleFacet(facet: FacetFilter): void {
+        if (this.hasFacet(facet)) {
+            this.removeFacet(facet);
+        } else {
+            this.facetFilters.push(facet);
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts
new file mode 100644
index 0000000..e285a89
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts
@@ -0,0 +1,54 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {OrgService} from '@eg/core/org.service';
+
+/*
+TODO: Add Display Fields to UNAPI
+https://library.biz/opac/extras/unapi?id=tag::U2@bre/1{bre.extern,holdings_xml,mra}/BR1/0&format=mods32
+*/
+
+const UNAPI_PATH = '/opac/extras/unapi?id=tag::U2@';
+
+interface UnapiParams {
+    target: string; // bre, ...
+    id: number | string; // 1 | 1,2,3,4,5
+    extras: string; // {holdings_xml,mra,...}
+    format: string; // mods32, marxml, ...
+    orgId?: number; // org unit ID
+    depth?: number; // org unit depth
+}
+
+ at Injectable()
+export class UnapiService {
+
+    constructor(private org: OrgService) {}
+
+    createUrl(params: UnapiParams): string {
+        const depth = params.depth || 0;
+        const org = params.orgId ? this.org.get(params.orgId) : this.org.root();
+
+        return `${UNAPI_PATH}${params.target}/${params.id}${params.extras}/` +
+            `${org.shortname()}/${depth}&format=${params.format}`;
+    }
+
+    getAsXmlDocument(params: UnapiParams): Promise<XMLDocument> {
+        // XReq creates an XML document for us.  Seems like the right
+        // tool for the job.
+        const url = this.createUrl(params);
+        return new Promise((resolve, reject) => {
+            const xhttp = new XMLHttpRequest();
+            xhttp.onreadystatechange = function() { // no () => {} !
+                if (this.readyState === 4) {
+                    if (this.status === 200) {
+                        resolve(xhttp.responseXML);
+                    } else {
+                        reject(`UNAPI request failed for ${url}`);
+                    }
+                }
+            };
+            xhttp.open('GET', url, true);
+            xhttp.send();
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts
new file mode 100644
index 0000000..26015b7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts
@@ -0,0 +1,25 @@
+import {Component, Input, Host, OnInit} from '@angular/core';
+import {ComboboxComponent} from './combobox.component';
+
+ at Component({
+  selector: 'eg-combobox-entry',
+  template: '<ng-template></ng-template>'
+})
+export class ComboboxEntryComponent implements OnInit {
+
+    @Input() entryId: any;
+    @Input() entryLabel: string;
+    @Input() selected: boolean;
+
+    constructor(@Host() private combobox: ComboboxComponent) {}
+
+    ngOnInit() {
+        if (this.selected) {
+            this.combobox.startId = this.entryId;
+        }
+        this.combobox.addEntry(
+            {id: this.entryId, label: this.entryLabel});
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
new file mode 100644
index 0000000..47237e9
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
@@ -0,0 +1,27 @@
+
+<!-- todo disabled -->
+<ng-template #displayTemplate let-r="result">
+{{r.label}}
+</ng-template>
+
+<div class="d-flex">
+  <input type="text" 
+    class="form-control"
+    [ngClass]="{'text-success font-italic font-weight-bold': selected && selected.freetext}"
+    [placeholder]="placeholder"
+    [name]="name"
+    [disabled]="isDisabled"
+    [required]="isRequired"
+    [(ngModel)]="selected" 
+    [ngbTypeahead]="filter"
+    [resultTemplate]="displayTemplate"
+    [inputFormatter]="formatDisplayString"
+    (click)="click$.next($event.target.value)"
+    (blur)="onBlur()"
+    (selectItem)="selectorChanged($event)"
+    #instance="ngbTypeahead"/>
+  <div class="d-flex flex-column icons" (click)="openMe($event)">
+    <span class="material-icons">keyboard_arrow_up</span>
+    <span class="material-icons">keyboard_arrow_down</span>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
new file mode 100644
index 0000000..40fc1c0
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
@@ -0,0 +1,241 @@
+/**
+ * <eg-combobox [allowFreeText]="true" [entries]="comboboxEntryList"/>
+ *  <!-- see also <eg-combobox-entry> -->
+ * </eg-combobox>
+ */
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter, ElementRef} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {tap} from 'rxjs/operators/tap';
+import {reduce} from 'rxjs/operators/reduce';
+import {of} from 'rxjs';
+import {mergeMap} from 'rxjs/operators/mergeMap';
+import {mapTo} from 'rxjs/operators/mapTo';
+import {debounceTime} from 'rxjs/operators/debounceTime';
+import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
+import {merge} from 'rxjs/operators/merge';
+import {filter} from 'rxjs/operators/filter';
+import {Subject} from 'rxjs/Subject';
+import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
+import {StoreService} from '@eg/core/store.service';
+
+export interface ComboboxEntry {
+  id: any;
+  label: string;
+  freetext?: boolean;
+}
+
+ at Component({
+  selector: 'eg-combobox',
+  templateUrl: './combobox.component.html',
+  styles: [`
+    .icons {margin-left:-18px}
+    .material-icons {font-size: 16px;font-weight:bold}
+  `]
+})
+export class ComboboxComponent implements OnInit {
+
+    selected: ComboboxEntry;
+    click$: Subject<string>;
+    entrylist: ComboboxEntry[];
+
+    @ViewChild('instance') instance: NgbTypeahead;
+
+    // Applies a name attribute to the input.
+    // Useful in forms.
+    @Input() name: string;
+
+    // Placeholder text for selector input
+    @Input() placeholder = '';
+
+    @Input() persistKey: string; // TODO
+
+    @Input() allowFreeText = false;
+
+    // Add a 'required' attribute to the input
+    isRequired: boolean;
+    @Input() set required(r: boolean) {
+        this.isRequired = r;
+    }
+
+    // Disable the input
+    isDisabled: boolean;
+    @Input() set disabled(d: boolean) {
+        this.isDisabled = d;
+    }
+
+    // Entry ID of the default entry to select (optional)
+    // onChange() is NOT fired when applying the default value,
+    // unless startIdFiresOnChange is set to true.
+    @Input() startId: any;
+    @Input() startIdFiresOnChange: boolean;
+
+    @Input() asyncDataSource: (term: string) => Observable<ComboboxEntry>;
+
+    // Useful for efficiently preventing duplicate async entries
+    asyncIds: {[idx: string]: boolean};
+
+    // True if a default selection has been made.
+    defaultSelectionApplied: boolean;
+
+    @Input() set entries(el: ComboboxEntry[]) {
+        this.entrylist = el;
+        this.applySelection();
+    }
+
+    // Emitted when the value is changed via UI.
+    // When the UI value is cleared, null is emitted.
+    @Output() onChange: EventEmitter<ComboboxEntry>;
+
+    // Useful for massaging the match string prior to comparison
+    // and display.  Default version trims leading/trailing spaces.
+    formatDisplayString: (ComboboxEntry) => string;
+
+    constructor(
+      private elm: ElementRef,
+      private store: StoreService,
+    ) {
+        this.entrylist = [];
+        this.asyncIds = {};
+        this.click$ = new Subject<string>();
+        this.onChange = new EventEmitter<ComboboxEntry>();
+        this.defaultSelectionApplied = false;
+
+        this.formatDisplayString = (result: ComboboxEntry) => {
+            return result.label.trim();
+        };
+    }
+
+    ngOnInit() {
+    }
+
+    openMe($event) {
+        // Give the input a chance to focus then fire the click
+        // handler to force open the typeahead
+        this.elm.nativeElement.getElementsByTagName('input')[0].focus();
+        setTimeout(() => this.click$.next(''));
+    }
+
+    // Apply a default selection where needed
+    applySelection() {
+
+        if (this.startId &&
+            this.entrylist && !this.defaultSelectionApplied) {
+
+            const entry =
+                this.entrylist.filter(e => e.id === this.startId)[0];
+
+            if (entry) {
+                this.selected = entry;
+                this.defaultSelectionApplied = true;
+                if (this.startIdFiresOnChange) {
+                    this.selectorChanged(
+                        {item: this.selected, preventDefault: () => true});
+                }
+            }
+        }
+    }
+
+    // Called by combobox-entry.component
+    addEntry(entry: ComboboxEntry) {
+        this.entrylist.push(entry);
+        this.applySelection();
+    }
+
+    onBlur() {
+        // When the selected value is a string it means we have either
+        // no value (user cleared the input) or a free-text value.
+
+        if (typeof this.selected === 'string') {
+
+            if (this.allowFreeText && this.selected !== '') {
+                // Free text entered which does not match a known entry
+                // translate it into a dummy ComboboxEntry
+                this.selected = {
+                    id: null,
+                    label: this.selected,
+                    freetext: true
+                };
+
+            } else {
+
+                this.selected = null;
+            }
+
+            // Manually fire the onchange since NgbTypeahead fails
+            // to fire the onchange when the value is cleared.
+            this.selectorChanged(
+                {item: this.selected, preventDefault: () => true});
+        }
+    }
+
+    // Fired by the typeahead to inform us of a change.
+    selectorChanged(selEvent: NgbTypeaheadSelectItemEvent) {
+        this.onChange.emit(selEvent.item);
+    }
+
+    // Adds matching async entries to the entry list
+    // and propagates the search term for pipelining.
+    addAsyncEntries(term: string): Observable<string> {
+
+        if (!term || !this.asyncDataSource) {
+            return of(term);
+        }
+
+        return new Observable(observer => {
+            this.asyncDataSource(term).subscribe(
+                (entry: ComboboxEntry) => {
+                    if (!this.asyncIds['' + entry.id]) {
+                        this.asyncIds['' + entry.id] = true;
+                        this.addEntry(entry);
+                    }
+                },
+                err => {},
+                ()  => {
+                    observer.next(term);
+                    observer.complete();
+                }
+            );
+        });
+    }
+
+    filter = (text$: Observable<string>): Observable<ComboboxEntry[]> => {
+        return text$.pipe(
+            debounceTime(200),
+            distinctUntilChanged(),
+
+            // Merge click actions in with the stream of text entry
+            merge(
+                // Inject a specifier indicating the source of the
+                // action is a user click instead of a text entry.
+                // This tells the filter to show all values in sync mode.
+                this.click$.pipe(filter(() =>
+                    !this.instance.isPopupOpen() && !this.asyncDataSource
+                )).pipe(mapTo('_CLICK_'))
+            ),
+
+            // mergeMap coalesces an observable into our stream.
+            mergeMap(term => this.addAsyncEntries(term)),
+            map((term: string) => {
+
+                if (term === '' || term === '_CLICK_') {
+                    if (this.asyncDataSource) {
+                        return [];
+                    } else {
+                        // In sync mode, a post-focus empty search or
+                        // click event displays the whole list.
+                        return this.entrylist;
+                    }
+                }
+
+                // Filter entrylist whose labels substring-match the
+                // text entered.
+                return this.entrylist.filter(entry =>
+                    entry.label.toLowerCase().indexOf(term.toLowerCase()) > -1
+                );
+            })
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
new file mode 100644
index 0000000..c686be4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
@@ -0,0 +1,21 @@
+
+<div class="input-group">
+  <input 
+    class="form-control" 
+    ngbDatepicker
+    #datePicker="ngbDatepicker"
+    placeholder="yyyy-mm-dd"
+    class="form-control"
+    name="{{fieldName}}"
+    [required]="required"
+    [(ngModel)]="current"
+    (dateSelect)="onDateSelect($event)">
+  <div class="input-group-append">
+    <button class="btn btn-outline-secondary" 
+      (click)="datePicker.toggle()" type="button">
+      <span title="Select Date" i18n-title                       
+        class="material-icons mat-icon-in-button">calendar_today</span>
+    </button>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts
new file mode 100644
index 0000000..ae3a729
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts
@@ -0,0 +1,70 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * RE: displaying locale dates in the input field:
+ * https://github.com/ng-bootstrap/ng-bootstrap/issues/754
+ * https://stackoverflow.com/questions/40664523/angular2-ngbdatepicker-how-to-format-date-in-inputfield
+ */
+
+ at Component({
+  selector: 'eg-date-select',
+  templateUrl: './date-select.component.html'
+})
+export class DateSelectComponent implements OnInit {
+
+    @Input() initialIso: string; // ISO string
+    @Input() initialYmd: string; // YYYY-MM-DD (uses local time zone)
+    @Input() initialDate: Date;  // Date object
+    @Input() required: boolean;
+    @Input() fieldName: string;
+
+    current: NgbDateStruct;
+
+    @Output() onChangeAsDate: EventEmitter<Date>;
+    @Output() onChangeAsIso: EventEmitter<string>;
+    @Output() onChangeAsYmd: EventEmitter<string>;
+
+    constructor() {
+        this.onChangeAsDate = new EventEmitter<Date>();
+        this.onChangeAsIso = new EventEmitter<string>();
+        this.onChangeAsYmd = new EventEmitter<string>();
+    }
+
+    ngOnInit() {
+
+        if (this.initialYmd) {
+            this.initialDate = this.localDateFromYmd(this.initialYmd);
+
+        } else if (this.initialIso) {
+            this.initialDate = new Date(this.initialIso);
+        }
+
+        if (this.initialDate) {
+            this.current = {
+                year: this.initialDate.getFullYear(),
+                month: this.initialDate.getMonth() + 1,
+                day: this.initialDate.getDate()
+            };
+        }
+    }
+
+    onDateSelect(evt) {
+        const ymd = `${evt.year}-${evt.month}-${evt.day}`;
+        const date = this.localDateFromYmd(ymd);
+        const iso = date.toISOString();
+        this.onChangeAsDate.emit(date);
+        this.onChangeAsYmd.emit(ymd);
+        this.onChangeAsIso.emit(iso);
+    }
+
+    // Create a date in the local time zone with selected YMD values.
+    // TODO: Consider moving this to a date service...
+    localDateFromYmd(ymd: string): Date {
+        const parts = ymd.split('-');
+        return new Date(
+            Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html
new file mode 100644
index 0000000..21766ca
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html
@@ -0,0 +1,17 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">{{dialogTitle}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body"><p>{{dialogBody}}</p></div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close('confirmed')" i18n>Confirm</button>
+    <button type="button" class="btn btn-warning" 
+      (click)="dismiss('canceled')" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts
new file mode 100644
index 0000000..efcbdeb
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts
@@ -0,0 +1,17 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+ at Component({
+  selector: 'eg-confirm-dialog',
+  templateUrl: './confirm.component.html'
+})
+
+/**
+ * Confirmation dialog that asks a yes/no question.
+ */
+export class ConfirmDialogComponent extends DialogComponent {
+    // What question are we asking?
+    @Input() public dialogBody: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
new file mode 100644
index 0000000..3ffd5db
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
@@ -0,0 +1,80 @@
+import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter} from '@angular/core';
+import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog base class.  Handles the ngbModal logic.
+ * Sub-classed component templates must have a #dialogContent selector
+ * at the root of the template (see ConfirmDialogComponent).
+ */
+
+ at Component({
+    selector: 'eg-dialog',
+    template: '<ng-template></ng-template>'
+})
+export class DialogComponent implements OnInit {
+
+    // Assume all dialogs support a title attribute.
+    @Input() public dialogTitle: string;
+
+    // Pointer to the dialog content template.
+    @ViewChild('dialogContent')
+    private dialogContent: TemplateRef<any>;
+
+    // Emitted after open() is called on the ngbModal.
+    // Note when overriding open(), this will not fire unless also
+    // called in the overridding method.
+    onOpen$ = new EventEmitter<any>();
+
+    // The modalRef allows direct control of the modal instance.
+    private modalRef: NgbModalRef = null;
+
+    constructor(private modalService: NgbModal) {}
+
+    ngOnInit() {
+        this.onOpen$ = new EventEmitter<any>();
+    }
+
+    open(options?: NgbModalOptions): Promise<any> {
+
+        if (this.modalRef !== null) {
+            console.warn('Dismissing existing dialog');
+            this.dismiss();
+        }
+
+        this.modalRef = this.modalService.open(this.dialogContent, options);
+
+        if (this.onOpen$) {
+            // Let the digest cycle complete
+            setTimeout(() => this.onOpen$.emit(true));
+        }
+
+        return new Promise( (resolve, reject) => {
+
+            this.modalRef.result.then(
+                (result) => {
+                    resolve(result);
+                    this.modalRef = null;
+                },
+                (result) => {
+                    console.debug('dialog closed with ' + result);
+                    reject(result);
+                    this.modalRef = null;
+                }
+            );
+        });
+    }
+
+    close(reason?: any): void {
+        if (this.modalRef) {
+            this.modalRef.close(reason);
+        }
+    }
+
+    dismiss(reason?: any): void {
+        if (this.modalRef) {
+            this.modalRef.dismiss(reason);
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css
new file mode 100644
index 0000000..fa08a1f
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css
@@ -0,0 +1,5 @@
+
+.eg-progress-inline progress {
+  width: 100%;
+  height: 25px;
+}
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html
new file mode 100644
index 0000000..615e867
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html
@@ -0,0 +1,28 @@
+<div class="eg-progress-inline">
+
+  <div *ngIf="hasValue() && hasMax()">
+    <!-- determinate progress bar.  shows max/value progress -->
+    <div class="row">
+      <div class="col-lg-10">
+        <progress max="{{max}}" value="{{value}}"></progress>
+      </div>
+      <div class="col-lg-2">{{percent()}}%</div>
+    </div>
+  </div>
+
+  <div *ngIf="hasValue() && !hasMax()">
+    <div class="row">
+      <!-- semi-determinate progress bar.  shows value -->
+      <div class="col-lg-10"><progress max="1"></progress></div>
+      <div class="col-lg-2">{{value}}...</div>
+    </div>
+  </div>
+
+  <div *ngIf="!hasValue()">
+    <div class="row">
+      <!-- indeterminate -->
+      <div class="col-lg-12"><progress max="1"></progress></div>
+    </div>
+  </div>
+
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts
new file mode 100644
index 0000000..9b131d1
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts
@@ -0,0 +1,92 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+
+/**
+ * Inline Progress Bar
+ *
+ * // assuming a template reference...
+ * @ViewChild('progress')
+ * private progress: progressInlineComponent;
+ *
+ * progress.update({value : 0, max : 123});
+ * progress.increment();
+ * progress.increment();
+ *
+ * Each progress has 2 numbers, 'max' and 'value'.
+ * The content of these values determines how the progress displays.
+ *
+ * There are 3 flavors:
+ *
+ * -- value is set, max is set
+ * determinate: shows a progression with a percent complete.
+ *
+ * -- value is set, max is unset
+ * semi-determinate, with a value report.  Shows a value-less
+ * <progress/>, but shows the value as a number in the progress.
+ *
+ * This is useful in cases where the total number of items to retrieve
+ * from the server is unknown, but we know how many items we've
+ * retrieved thus far.  It helps to reinforce that something specific
+ * is happening, but we don't know when it will end.
+ *
+ * -- value is unset
+ * indeterminate: shows a generic value-less <progress/> with no
+ * clear indication of progress.
+ */
+ at Component({
+  selector: 'eg-progress-inline',
+  templateUrl: './progress-inline.component.html',
+  styleUrls: ['progress-inline.component.css']
+})
+export class ProgressInlineComponent {
+
+    @Input() max: number;
+    @Input() value: number;
+
+    reset() {
+        delete this.max;
+        delete this.value;
+    }
+
+    hasValue(): boolean {
+        return Number.isInteger(this.value);
+    }
+
+    hasMax(): boolean {
+        return Number.isInteger(this.max);
+    }
+
+    percent(): number {
+        if (this.hasValue()  &&
+            this.hasMax()    &&
+            this.max > 0     &&
+            this.value <= this.max) {
+            return Math.floor((this.value / this.max) * 100);
+        }
+        return 100;
+    }
+
+    // Set the current state of the progress bar.
+    update(args: {[key: string]: number}) {
+        if (args.max !== undefined) {
+            this.max = args.max;
+        }
+        if (args.value !== undefined) {
+            this.value = args.value;
+        }
+    }
+
+    // Increment the current value.  If no amount is specified,
+    // it increments by 1.  Calling increment() on an indetermite
+    // progress bar will force it to be a (semi-)determinate bar.
+    increment(amt?: number) {
+        if (!Number.isInteger(amt)) { amt = 1; }
+
+        if (!this.hasValue()) {
+            this.value = 0;
+        }
+
+        this.value += amt;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.css b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.css
new file mode 100644
index 0000000..a79609e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.css
@@ -0,0 +1,5 @@
+
+.eg-progress-dialog progress {
+  width: 100%;
+  height: 25px;
+}
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html
new file mode 100644
index 0000000..78ca3d0
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.html
@@ -0,0 +1,33 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 *ngIf="dialogTitle" class="modal-title">{{dialogTitle}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+
+  <div class="modal-body eg-progress-dialog">
+
+    <div *ngIf="hasValue() && hasMax()">
+      <!-- determinate progress bar.  shows max/value progress -->
+      <div class="col-lg-10">
+        <progress max="{{max}}" value="{{value}}"></progress>
+      </div>
+      <div class="col-lg-2">{{percent()}}%</div>
+    </div>
+
+    <div *ngIf="hasValue() && !hasMax()">
+      <!-- semi-determinate progress bar.  shows value -->
+      <div class="col-lg-10"><progress max="1"></progress></div>
+      <div class="col-lg-2">{{value}}...</div>
+    </div>
+
+    <div *ngIf="!hasValue()">
+      <!-- indeterminate -->
+      <div class="col-lg-12"><progress max="1"></progress></div>
+    </div>
+
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts
new file mode 100644
index 0000000..6bf4edb
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts
@@ -0,0 +1,108 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+ at Component({
+  selector: 'eg-progress-dialog',
+  templateUrl: './progress.component.html',
+  styleUrls: ['progress.component.css']
+})
+
+/**
+ * TODO: This duplicates the code from ProgressInlineComponent.
+ * This component should insert to <eg-progress-inline/> into
+ * its template instead of duplicating the code.  However, until
+ * Angular bug https://github.com/angular/angular/issues/14842
+ * is fixed, it's not possible to get a reference to the embedded
+ * inline progress, which is needed for access the update/increment
+ * API.
+ * Also consider moving the progress traking logic to a service
+ * to reduce code duplication.
+ */
+
+/**
+ * Progress Dialog.
+ *
+ * // assuming a template reference...
+ * @ViewChild('progressDialog')
+ * private dialog: ProgressDialogComponent;
+ *
+ * dialog.open();
+ * dialog.update({value : 0, max : 123});
+ * dialog.increment();
+ * dialog.increment();
+ * dialog.close();
+ *
+ * Each dialog has 2 numbers, 'max' and 'value'.
+ * The content of these values determines how the dialog displays.
+ *
+ * There are 3 flavors:
+ *
+ * -- value is set, max is set
+ * determinate: shows a progression with a percent complete.
+ *
+ * -- value is set, max is unset
+ * semi-determinate, with a value report.  Shows a value-less
+ * <progress/>, but shows the value as a number in the dialog.
+ *
+ * This is useful in cases where the total number of items to retrieve
+ * from the server is unknown, but we know how many items we've
+ * retrieved thus far.  It helps to reinforce that something specific
+ * is happening, but we don't know when it will end.
+ *
+ * -- value is unset
+ * indeterminate: shows a generic value-less <progress/> with no
+ * clear indication of progress.
+ */
+export class ProgressDialogComponent extends DialogComponent {
+
+    max: number;
+    value: number;
+
+    reset() {
+        delete this.max;
+        delete this.value;
+    }
+
+    hasValue(): boolean {
+        return Number.isInteger(this.value);
+    }
+
+    hasMax(): boolean {
+        return Number.isInteger(this.max);
+    }
+
+    percent(): number {
+        if (this.hasValue()  &&
+            this.hasMax()    &&
+            this.max > 0     &&
+            this.value <= this.max) {
+            return Math.floor((this.value / this.max) * 100);
+        }
+        return 100;
+    }
+
+    // Set the current state of the progress bar.
+    update(args: {[key: string]: number}) {
+        if (args.max !== undefined) {
+            this.max = args.max;
+        }
+        if (args.value !== undefined) {
+            this.value = args.value;
+        }
+    }
+
+    // Increment the current value.  If no amount is specified,
+    // it increments by 1.  Calling increment() on an indetermite
+    // progress bar will force it to be a (semi-)determinate bar.
+    increment(amt?: number) {
+        if (!Number.isInteger(amt)) { amt = 1; }
+
+        if (!this.hasValue()) {
+            this.value = 0;
+        }
+
+        this.value += amt;
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html
new file mode 100644
index 0000000..1d7936b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html
@@ -0,0 +1,22 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">{{dialogTitle}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <p>{{dialogBody}}</p>
+    <div class="text-center">
+        <input class="form-control" [(ngModel)]="promptValue"/>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close(promptValue)" i18n>Confirm</button>
+    <button type="button" class="btn btn-warning" 
+      (click)="dismiss('canceled')" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts
new file mode 100644
index 0000000..ab7f77e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts
@@ -0,0 +1,19 @@
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+ at Component({
+  selector: 'eg-prompt-dialog',
+  templateUrl: './prompt.component.html'
+})
+
+/**
+ * Promptation dialog that requests user input.
+ */
+export class PromptDialogComponent extends DialogComponent {
+    // What question are we asking?
+    @Input() public dialogBody: string;
+    // Value to return to the caller
+    @Input() public promptValue: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
new file mode 100644
index 0000000..721423c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
@@ -0,0 +1,146 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Record Editor: {{recordLabel}}</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <form #fmEditForm="ngForm" role="form" class="form-validated common-form striped-odd">
+      <div class="form-group row" *ngFor="let field of fields">
+        <div class="col-lg-3 offset-lg-1">
+          <label for="rec-{{field.name}}">{{field.label}}</label>
+        </div>
+        <div class="col-lg-7">
+
+          <span *ngIf="field.template">
+            <ng-container
+              *ngTemplateOutlet="field.template; context:customTemplateFieldContext(field)">
+            </ng-container> 
+          </span>
+
+          <span *ngIf="!field.template">
+
+            <span *ngIf="field.datatype == 'id' && !pkeyIsEditable">
+              {{record[field.name]()}}
+            </span>
+  
+            <input *ngIf="field.datatype == 'id' && pkeyIsEditable"
+              class="form-control"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <input *ngIf="field.datatype == 'text' || field.datatype == 'interval'"
+              class="form-control"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+
+            <span *ngIf="field.datatype == 'timestamp'">
+              <eg-date-select
+                (onChangeAsIso)="record[field.name]($event)"
+                initialIso="{{record[field.name]()}}">
+              </eg-date-select>
+            </span>
+
+            <input *ngIf="field.datatype == 'int'"
+              class="form-control"
+              type="number"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <input *ngIf="field.datatype == 'float'"
+              class="form-control"
+              type="number" step="0.1"
+              name="{{field.name}}"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [readonly]="field.readOnly"
+              [required]="field.isRequired()"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <span *ngIf="field.datatype == 'money'">
+              <!-- in read-only mode display the local-aware currency -->
+              <input *ngIf="field.readOnly"
+                class="form-control"
+                type="number" step="0.1"
+                name="{{field.name}}"
+                [readonly]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]() | currency"/>
+  
+              <input *ngIf="!field.readOnly"
+                class="form-control"
+                type="number" step="0.1"
+                name="{{field.name}}"
+                placeholder="{{field.label}}..."
+                i18n-placeholder
+                [readonly]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)"/>
+            </span>
+  
+            <input *ngIf="field.datatype == 'bool'"
+              class="form-check-input"
+              type="checkbox"
+              name="{{field.name}}"
+              [readonly]="field.readOnly"
+              [ngModel]="record[field.name]()"
+              (ngModelChange)="record[field.name]($event)"/>
+  
+            <span *ngIf="field.datatype == 'link'"
+              [ngClass]="{nullable : !field.isRequired()}">
+              <select
+                class="form-control"
+                name="{{field.name}}"
+                [disabled]="field.readOnly"
+                [required]="field.isRequired()"
+                [ngModel]="record[field.name]()"
+                (ngModelChange)="record[field.name]($event)">
+                <option *ngFor="let item of field.linkedValues" 
+                  [value]="item.id">{{item.name}}</option>
+              </select>
+            </span>
+  
+            <eg-org-select *ngIf="field.datatype == 'org_unit'"
+              placeholder="{{field.label}}..."
+              i18n-placeholder
+              [limitPerms]="modePerms[mode]"
+              [applyDefault]="field.orgDefaultAllowed"
+              [initialOrgId]="record[field.name]()"
+              (onChange)="record[field.name]($event)">
+            </eg-org-select>
+
+          </span>
+        </div>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" *ngIf="mode == 'view'"
+      (click)="close()" i18n>Close</button>
+    <button type="button" class="btn btn-info" 
+      [disabled]="fmEditForm.invalid" *ngIf="mode != 'view'"
+      (click)="save()" i18n>Save</button>
+    <button type="button" class="btn btn-warning ml-2" *ngIf="mode != 'view'"
+      (click)="cancel()" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
new file mode 100644
index 0000000..308218a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
@@ -0,0 +1,302 @@
+import {Component, OnInit, Input,
+    Output, EventEmitter, TemplateRef} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+
+interface CustomFieldTemplate {
+    template: TemplateRef<any>;
+
+    // Allow the caller to pass in a free-form context blob to
+    // be addedto the caller's custom template context, along
+    // with our stock context.
+    context?: {[fields: string]: any};
+}
+
+interface CustomFieldContext {
+    // Current create/edit/view record
+    record: IdlObject;
+
+    // IDL field definition blob
+    field: any;
+
+    // additional context values passed via CustomFieldTemplate
+    [fields: string]: any;
+}
+
+ at Component({
+  selector: 'eg-fm-record-editor',
+  templateUrl: './fm-editor.component.html'
+})
+export class FmRecordEditorComponent
+    extends DialogComponent implements OnInit {
+
+    // IDL class hint (e.g. "aou")
+    @Input() idlClass: string;
+
+    // mode: 'create' for creating a new record,
+    //       'update' for editing an existing record
+    //       'view' for viewing an existing record without editing
+    mode: 'create' | 'update' | 'view' = 'create';
+    recId: any;
+    // IDL record we are editing
+    // TODO: allow this to be update in real time by the caller?
+    record: IdlObject;
+
+    // Permissions extracted from the permacrud defs in the IDL
+    // for the current IDL class
+    modePerms: {[mode: string]: string};
+
+    @Input() customFieldTemplates:
+        {[fieldName: string]: CustomFieldTemplate} = {};
+
+    // list of fields that should not be displayed
+    @Input() hiddenFieldsList: string[] = [];
+    @Input() hiddenFields: string; // comma-separated string version
+
+    // list of fields that should always be read-only
+    @Input() readonlyFieldsList: string[] = [];
+    @Input() readonlyFields: string; // comma-separated string version
+
+    // list of required fields; this supplements what the IDL considers
+    // required
+    @Input() requiredFieldsList: string[] = [];
+    @Input() requiredFields: string; // comma-separated string version
+
+    // list of org_unit fields where a default value may be applied by
+    // the org-select if no value is present.
+    @Input() orgDefaultAllowedList: string[] = [];
+    @Input() orgDefaultAllowed: string; // comma-separated string version
+
+    // hash, keyed by field name, of functions to invoke to check
+    // whether a field is required.  Each callback is passed the field
+    // name and the record and should return a boolean value. This
+    // supports cases where whether a field is required or not depends
+    // on the current value of another field.
+    @Input() isRequiredOverride:
+        {[field: string]: (field: string, record: IdlObject) => boolean};
+
+    // IDL record display label.  Defaults to the IDL label.
+    @Input() recordLabel: string;
+
+    // Emit the modified object when the save action completes.
+    @Output() onSave$ = new EventEmitter<IdlObject>();
+
+    // Emit the original object when the save action is canceled.
+    @Output() onCancel$ = new EventEmitter<IdlObject>();
+
+    // Emit an error message when the save action fails.
+    @Output() onError$ = new EventEmitter<string>();
+
+    // IDL info for the the selected IDL class
+    idlDef: any;
+
+    // Can we edit the primary key?
+    pkeyIsEditable = false;
+
+    // List of IDL field definitions.  This is a subset of the full
+    // list of fields on the IDL, since some are hidden, virtual, etc.
+    fields: any[];
+
+    @Input() editMode(mode: 'create' | 'update' | 'view') {
+        this.mode = mode;
+    }
+
+    // Record ID to view/update.  Value is dynamic.  Records are not
+    // fetched until .open() is called.
+    @Input() set recordId(id: any) {
+        if (id) { this.recId = id; }
+    }
+
+    constructor(
+      private modal: NgbModal, // required for passing to parent
+      private idl: IdlService,
+      private auth: AuthService,
+      private pcrud: PcrudService) {
+      super(modal);
+    }
+
+    // Avoid fetching data on init since that may lead to unnecessary
+    // data retrieval.
+    ngOnInit() {
+        this.listifyInputs();
+        this.idlDef = this.idl.classes[this.idlClass];
+        this.recordLabel = this.idlDef.label;
+    }
+
+    // Opening dialog, fetch data.
+    open(options?: NgbModalOptions): Promise<any> {
+        return this.initRecord().then(
+            ok => super.open(options),
+            err => console.warn(`Error fetching FM data: ${err}`)
+        );
+    }
+
+    // Translate comma-separated string versions of various inputs
+    // to arrays.
+    private listifyInputs() {
+        if (this.hiddenFields) {
+            this.hiddenFieldsList = this.hiddenFields.split(/,/);
+        }
+        if (this.readonlyFields) {
+            this.readonlyFieldsList = this.readonlyFields.split(/,/);
+        }
+        if (this.requiredFields) {
+            this.requiredFieldsList = this.requiredFields.split(/,/);
+        }
+        if (this.orgDefaultAllowed) {
+            this.orgDefaultAllowedList = this.orgDefaultAllowed.split(/,/);
+        }
+    }
+
+    private initRecord(): Promise<any> {
+
+        const pc = this.idlDef.permacrud || {};
+        this.modePerms = {
+            view:   pc.retrieve ? pc.retrieve.perms : [],
+            create: pc.create ? pc.create.perms : [],
+            update: pc.update ? pc.update.perms : [],
+        };
+
+        if (this.mode === 'update' || this.mode === 'view') {
+            return this.pcrud.retrieve(this.idlClass, this.recId)
+            .toPromise().then(rec => {
+
+                if (!rec) {
+                    return Promise.reject(`No '${this.idlClass}'
+                        record found with id ${this.recId}`);
+                }
+
+                this.record = rec;
+                this.convertDatatypesToJs();
+                return this.getFieldList();
+            });
+        }
+
+        // create a new record from scratch
+        this.pkeyIsEditable = !('pkey_sequence' in this.idlDef);
+        this.record = this.idl.create(this.idlClass);
+        return this.getFieldList();
+    }
+
+    // Modifies the FM record in place, replacing IDL-compatible values
+    // with native JS values.
+    private convertDatatypesToJs() {
+        this.idlDef.fields.forEach(field => {
+            if (field.datatype === 'bool') {
+                if (this.record[field.name]() === 't') {
+                    this.record[field.name](true);
+                } else if (this.record[field.name]() === 'f') {
+                    this.record[field.name](false);
+                }
+            }
+        });
+    }
+
+    // Modifies the provided FM record in place, replacing JS values
+    // with IDL-compatible values.
+    convertDatatypesToIdl(rec: IdlObject) {
+        const fields = this.idlDef.fields;
+        fields.forEach(field => {
+            if (field.datatype === 'bool') {
+                if (rec[field.name]() === true) {
+                    rec[field.name]('t');
+                // } else if (rec[field.name]() === false) {
+                } else { // TODO: some bools can be NULL
+                    rec[field.name]('f');
+                }
+            } else if (field.datatype === 'org_unit') {
+                const org = rec[field.name]();
+                if (org && typeof org === 'object') {
+                    rec[field.name](org.id());
+                }
+            }
+        });
+    }
+
+
+    private flattenLinkedValues(cls: string, list: IdlObject[]): any[] {
+        const idField = this.idl.classes[cls].pkey;
+        const selector =
+            this.idl.classes[cls].field_map[idField].selector || idField;
+
+        return list.map(item => {
+            return {id: item[idField](), name: item[selector]()};
+        });
+    }
+
+    private getFieldList(): Promise<any> {
+
+        this.fields = this.idlDef.fields.filter(f =>
+            !f.virtual && !this.hiddenFieldsList.includes(f.name)
+        );
+
+        const promises = [];
+
+        this.fields.forEach(field => {
+            field.readOnly = this.mode === 'view'
+                || this.readonlyFieldsList.includes(field.name);
+
+            if (this.isRequiredOverride &&
+                field.name in this.isRequiredOverride) {
+                field.isRequired = () => {
+                    return this.isRequiredOverride[field.name](field.name, this.record);
+                };
+            } else {
+                field.isRequired = () => {
+                    return field.required ||
+                        this.requiredFieldsList.includes(field.name);
+                };
+            }
+
+            if (field.datatype === 'link') {
+                promises.push(
+                    this.pcrud.retrieveAll(field.class, {}, {atomic : true})
+                    .toPromise().then(list => {
+                        field.linkedValues =
+                            this.flattenLinkedValues(field.class, list);
+                    })
+                );
+            } else if (field.datatype === 'org_unit') {
+                field.orgDefaultAllowed =
+                    this.orgDefaultAllowedList.includes(field.name);
+            }
+
+            if (this.customFieldTemplates[field.name]) {
+                field.template = this.customFieldTemplates[field.name].template;
+                field.context = this.customFieldTemplates[field.name].context;
+            }
+
+        });
+
+        // Wait for all network calls to complete
+        return Promise.all(promises);
+    }
+
+    // Returns a context object to be inserted into a custom
+    // field template.
+    customTemplateFieldContext(fieldDef: any): CustomFieldContext {
+        return Object.assign(
+            {   record : this.record,
+                field: fieldDef // from this.fields
+            },  fieldDef.context || {}
+        );
+    }
+
+    save() {
+        const recToSave = this.idl.clone(this.record);
+        this.convertDatatypesToIdl(recToSave);
+        this.pcrud[this.mode]([recToSave]).toPromise().then(
+            result => this.close(result),
+            error  => this.dismiss(error)
+        );
+    }
+
+    cancel() {
+        this.dismiss('canceled');
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html
new file mode 100644
index 0000000..3de90e4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html
@@ -0,0 +1,20 @@
+
+<span *ngIf="!column.cellTemplate"
+  [ngbTooltip]="tooltipContent"
+  placement="top-left"
+  class="{{context.cellClassCallback(row, column)}}"
+  triggers="mouseenter:mouseleave">
+  {{context.getRowColumnValue(row, column)}}
+</span>
+<span *ngIf="column.cellTemplate" 
+  class="{{context.cellClassCallback(row, column)}}"
+  [ngbTooltip]="tooltipContent"
+  placement="top-left"
+  #tooltip="ngbTooltip" 
+  (mouseenter)="tooltip.open(column.getCellContext(row))"
+  (mouseleave)="tooltip.close()" triggers="manual">
+  <ng-container #templateContainer
+    *ngTemplateOutlet="column.cellTemplate; context: column.getCellContext(row)">
+  </ng-container> 
+</span>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts
new file mode 100644
index 0000000..3d844f3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts
@@ -0,0 +1,57 @@
+import {Component, Input, OnInit, AfterViewInit,
+    TemplateRef, ElementRef, AfterContentChecked} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+
+ at Component({
+  selector: 'eg-grid-body-cell',
+  templateUrl: './grid-body-cell.component.html'
+})
+
+export class GridBodyCellComponent implements OnInit, AfterContentChecked {
+
+    @Input() context: GridContext;
+    @Input() row: any;
+    @Input() column: GridColumn;
+
+    initDone: boolean;
+    tooltipContent: string | TemplateRef<any>;
+
+    constructor(
+        private elm: ElementRef
+    ) {}
+
+    ngOnInit() {}
+
+    ngAfterContentChecked() {
+        this.setTooltip();
+    }
+
+    // Returns true if the contents of this cell exceed the
+    // boundaries of its container.
+    cellOverflows(): boolean {
+        let node = this.elm.nativeElement;
+        if (node) {
+            node = node.parentNode;
+            return node && (
+                node.scrollHeight > node.clientHeight ||
+                node.scrollWidth > node.clientWidth
+            );
+        }
+        return false;
+    }
+
+    // Tooltips are only applied to cells whose contents exceed
+    // their container.
+    // Applying an empty string value prevents a tooltip from rendering.
+    setTooltip() {
+        if (this.cellOverflows()) {
+            this.tooltipContent = this.column.cellTemplate ||
+                this.context.getRowColumnValue(this.row, this.column);
+        } else {
+            // No tooltip
+            this.tooltipContent = '';
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html
new file mode 100644
index 0000000..b7284fe
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html
@@ -0,0 +1,39 @@
+<!--
+  tabindex=1 so the grid body can capture keyboard events.
+-->
+<div class="eg-grid-body" tabindex="1" (keydown)="onGridKeyDown($event)">
+  <div class="eg-grid-row eg-grid-body-row {{context.rowClassCallback(row)}}"
+    [ngClass]="{'selected': context.rowSelector.contains(context.getRowIndex(row))}"
+    *ngFor="let row of context.dataSource.getPageOfRows(context.pager); let idx = index">
+
+    <div class="eg-grid-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
+      <input type='checkbox' [(ngModel)]="context.rowSelector.indexes[context.getRowIndex(row)]">
+    </div>
+    <div class="eg-grid-cell eg-grid-number-cell eg-grid-cell-skinny">
+      {{context.pager.rowNumber(idx)}}
+    </div>
+    <div *ngIf="context.rowFlairIsEnabled" class="eg-grid-cell eg-grid-flair-cell">
+      <!-- using *ngIf allows us to assign the flair callback to a value,
+            obviating the need for multiple calls of the same function -->
+      <ng-container *ngIf="context.rowFlairCallback(row); let flair">
+        <ng-container *ngIf="flair.icon">
+          <!-- tooltip is disabled when no title is set -->
+          <span class="material-icons"
+            ngbTooltip="{{flair.title || ''}}" triggers="mouseenter:mouseleave">
+            {{flair.icon}}
+          </span>
+        </ng-container>
+      </ng-container>
+    </div>
+    <div class="eg-grid-cell eg-grid-body-cell" [ngStyle]="{flex:col.flex}"
+      [ngClass]="{'eg-grid-cell-overflow': context.overflowCells}"
+      (dblclick)="onRowDblClick(row)"
+      (click)="onRowClick($event, row, idx)"
+      *ngFor="let col of context.columnSet.displayColumns()">
+
+      <eg-grid-body-cell [context]="context" [row]="row" [column]="col">
+      </eg-grid-body-cell>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts
new file mode 100644
index 0000000..e4829ce
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts
@@ -0,0 +1,77 @@
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+import {GridComponent} from './grid.component';
+
+ at Component({
+  selector: 'eg-grid-body',
+  templateUrl: './grid-body.component.html'
+})
+
+export class GridBodyComponent implements OnInit {
+
+    @Input() context: GridContext;
+
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {}
+
+    // Not using @HostListener because it only works globally.
+    onGridKeyDown(evt: KeyboardEvent) {
+        switch (evt.key) {
+            case 'ArrowUp':
+                this.context.selectPreviousRow();
+                evt.stopPropagation();
+                break;
+            case 'ArrowDown':
+                this.context.selectNextRow();
+                evt.stopPropagation();
+                break;
+            case 'ArrowLeft':
+                this.context.toPrevPage()
+                .then(ok => this.context.selectFirstRow(), err => {});
+                evt.stopPropagation();
+                break;
+            case 'ArrowRight':
+                this.context.toNextPage()
+                .then(ok => this.context.selectFirstRow(), err => {});
+                evt.stopPropagation();
+                break;
+            case 'Enter':
+                if (this.context.lastSelectedIndex) {
+                    this.grid.onRowActivate.emit(
+                        this.context.getRowByIndex(
+                            this.context.lastSelectedIndex)
+                    );
+                }
+                evt.stopPropagation();
+                break;
+        }
+    }
+
+    onRowClick($event: any, row: any, idx: number) {
+        const index = this.context.getRowIndex(row);
+
+        if (this.context.disableMultiSelect) {
+            this.context.selectOneRow(index);
+        } else if ($event.ctrlKey || $event.metaKey /* mac command */) {
+            if (this.context.toggleSelectOneRow(index)) {
+                this.context.lastSelectedIndex = index;
+            }
+
+        } else if ($event.shiftKey) {
+            // TODO shift range click
+
+        } else {
+            this.context.selectOneRow(index);
+        }
+
+        this.grid.onRowClick.emit(row);
+    }
+
+    onRowDblClick(row: any) {
+        this.grid.onRowActivate.emit(row);
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html
new file mode 100644
index 0000000..3af756c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html
@@ -0,0 +1,69 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Grid Columns Configuration</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body eg-grid-column-config-dialog">
+
+    <div class="row">
+      <div class="col-lg-1 eg-grid-header-cell" i18n>Visible</div>
+      <div class="col-lg-3 eg-grid-header-cell" i18n>Column Name</div>
+      <div class="col-lg-1 eg-grid-header-cell" i18n>Move Up</div>
+      <div class="col-lg-1 eg-grid-header-cell" i18n>Move Down</div>
+      <div class="col-lg-2 eg-grid-header-cell" i18n>First Visible</div>
+      <div class="col-lg-2 eg-grid-header-cell" i18n>Last Visible</div>
+      <div class="col-lg-2 eg-grid-header-cell" 
+        *ngIf="columnSet.isMultiSortable" i18n>Sort Priority</div>
+    </div>
+    <div class="row pt-1" *ngFor="let col of columnSet.columns"
+      [ngClass]="{visible : col.visible}">
+      <div class="col-lg-1" (click)="col.visible=!col.visible">
+        <span *ngIf="col.visible" class="badge badge-success">&#x2713;</span>
+        <span *ngIf="!col.visible" class="badge badge-warning">&#x2717;</span>
+      </div>
+      <div class="col-lg-3" (click)="col.visible=!col.visible">{{col.label}}</div>
+      <div class="col-lg-1">
+        <a class="no-href" title="Move column up" i18n-title
+          (click)="columnSet.moveColumn(col, -1)">
+          <span class="material-icons">arrow_upward</span>
+        </a>
+      </div>
+      <div class="col-lg-1">
+        <a class="no-href" title="Move column down" i18n-title
+          (click)="columnSet.moveColumn(col, 1)">
+          <span class="material-icons">arrow_downward</span>
+        </a>
+      </div>
+      <div class="col-lg-2">
+        <a class="no-href" title="Make first visible" i18n-title
+          (click)="columnSet.moveColumn(col, -10000)">
+          <span class="material-icons">vertical_align_top</span>
+        </a>
+      </div>
+      <div class="col-lg-2">
+        <a class="no-href" title="Make last visible" i18n-title
+          (click)="columnSet.moveColumn(col, 10000)">
+          <span class="material-icons">vertical_align_bottom</span>
+        </a>
+      </div>
+      <div class="col-lg-2" *ngIf="columnSet.isMultiSortable">
+        <div *ngIf="col.isMultiSortable">
+          <input type='number' [(ngModel)]="col.sort"
+            title="Sort Priority / Direction" i18n-title style='width:2.8em'/>
+        </div>
+      </div>
+
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-info" (click)="columnSet.moveVisibleToFront()">
+      Move Visible Columns To Top
+    </button>
+    <button type="button" class="btn btn-success ml-2" 
+      (click)="close('confirmed')" i18n>Close</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts
new file mode 100644
index 0000000..10ad606
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts
@@ -0,0 +1,16 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {GridColumnSet} from './grid';
+
+ at Component({
+  selector: 'eg-grid-column-config',
+  templateUrl: './grid-column-config.component.html'
+})
+
+/**
+ */
+export class GridColumnConfigComponent extends DialogComponent implements OnInit {
+    @Input() columnSet: GridColumnSet;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html
new file mode 100644
index 0000000..ca24c00
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html
@@ -0,0 +1,20 @@
+<div *ngIf="isVisible" class="eg-grid-column-width-config">
+  <div class="eg-grid-row">
+    <div class="eg-grid-column-width-header" i18n>Expand</div>
+    <div *ngFor="let col of columnSet.displayColumns()" 
+      class="eg-grid-cell text-center" [ngStyle]="{flex:col.flex}">
+      <a (click)="expandColumn(col)" title="Expand Column" i18n-title>
+        <span class="material-icons eg-grid-column-width-icon">call_made</span>
+      </a>
+    </div>
+  </div>
+  <div class="eg-grid-row">
+    <div class="eg-grid-column-width-header" i18n>Shrink</div>
+    <div *ngFor="let col of columnSet.displayColumns()" 
+      class="eg-grid-cell text-center" [ngStyle]="{flex:col.flex}">
+      <a (click)="shrinkColumn(col)" title="Shrink Column" i18n-title>
+        <span class="material-icons eg-grid-column-width-icon">call_received</span>
+      </a>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
new file mode 100644
index 0000000..f9bacf4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
@@ -0,0 +1,32 @@
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {GridContext, GridColumn, GridColumnSet,
+    GridDataSource} from './grid';
+
+ at Component({
+  selector: 'eg-grid-column-width',
+  templateUrl: './grid-column-width.component.html'
+})
+
+export class GridColumnWidthComponent implements OnInit {
+
+    @Input() gridContext: GridContext;
+    columnSet: GridColumnSet;
+    isVisible: boolean;
+
+    constructor() {}
+
+    ngOnInit() {
+        this.isVisible = false;
+        this.columnSet = this.gridContext.columnSet;
+    }
+
+    expandColumn(col: GridColumn) {
+        col.flex++;
+    }
+
+    shrinkColumn(col: GridColumn) {
+        if (col.flex > 1) { col.flex--; }
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
new file mode 100644
index 0000000..dffede1
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
@@ -0,0 +1,57 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridColumn, GridColumnSet} from './grid';
+import {GridComponent} from './grid.component';
+
+ at Component({
+  selector: 'eg-grid-column',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridColumnComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() name: string;
+    @Input() path: string;
+    @Input() label: string;
+    @Input() flex: number;
+    // is this the index field?
+    @Input() index: boolean;
+
+    // Columns are assumed to be visible unless hidden=true.
+    @Input() hidden: boolean;
+
+    @Input() sortable: boolean;
+    @Input() datatype: string;
+    @Input() multiSortable: boolean;
+
+    // Used in conjunction with cellTemplate
+    @Input() cellContext: any;
+    @Input() cellTemplate: TemplateRef<any>;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridColumnComponent needs an <eg-grid>');
+            return;
+        }
+
+        const col = new GridColumn();
+        col.name = this.name;
+        col.path = this.path;
+        col.label = this.label;
+        col.flex = this.flex;
+        col.hidden = this.hidden === true;
+        col.isIndex = this.index === true;
+        col.cellTemplate = this.cellTemplate;
+        col.cellContext = this.cellContext;
+        col.isSortable = this.sortable;
+        col.isMultiSortable = this.multiSortable;
+        col.datatype = this.datatype;
+        col.isAuto = false;
+        this.grid.context.columnSet.add(col);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
new file mode 100644
index 0000000..58e0c66
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
@@ -0,0 +1,32 @@
+
+<div class="eg-grid-row eg-grid-header-row">
+  <div class="eg-grid-cell eg-grid-header-cell eg-grid-checkbox-cell eg-grid-cell-skinny">
+    <input type='checkbox' (click)="handleBatchSelect($event)">
+  </div>
+  <div class="eg-grid-cell eg-grid-header-cell eg-grid-number-cell eg-grid-cell-skinny">
+    <span i18n="number|Row Number Header">#</span>
+  </div>
+  <div *ngIf="context.rowFlairIsEnabled" 
+    class="eg-grid-cell eg-grid-header-cell eg-grid-flair-cell">
+    <span class="material-icons">notifications</span>
+  </div>
+  <div *ngFor="let col of context.columnSet.displayColumns()" 
+    draggable="true" 
+    (dragstart)="dragColumn = col"
+    (drop)="onColumnDrop(col)"
+    (dragover)="onColumnDragEnter($event, col)"
+    (dragleave)="onColumnDragLeave($event, col)"
+    [ngClass]="{'dragover' : col.isDragTarget}"
+    class="eg-grid-cell eg-grid-header-cell" [ngStyle]="{flex:col.flex}">
+    <a class="sortable label-with-material-icon" *ngIf="col.isSortable" 
+      (click)="sortOneColumn(col)">
+      <span class="eg-grid-header-cell-sort-label">{{col.label}}</span>
+      <span class="material-icons eg-grid-header-cell-sort-arrow"
+        *ngIf="isColumnSorting(col, 'ASC')">arrow_downwards</span>
+      <span class="material-icons eg-grid-header-cell-sort-arrow"
+        *ngIf="isColumnSorting(col, 'DESC')">arrow_upwards</span>
+    </a>
+    <span *ngIf="!col.isSortable">{{col.label}}</span>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
new file mode 100644
index 0000000..0010a45
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
@@ -0,0 +1,85 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {GridContext, GridColumn, GridRowSelector,
+    GridColumnSet, GridDataSource} from './grid';
+
+ at Component({
+  selector: 'eg-grid-header',
+  templateUrl: './grid-header.component.html'
+})
+
+export class GridHeaderComponent implements OnInit {
+
+    @Input() context: GridContext;
+
+    dragColumn: GridColumn;
+
+    constructor() {}
+
+    ngOnInit() {}
+
+    onColumnDragEnter($event: any, col: any) {
+        if (this.dragColumn && this.dragColumn.name !== col.name) {
+            col.isDragTarget = true;
+        }
+        $event.preventDefault();
+    }
+
+    onColumnDragLeave($event: any, col: any) {
+        col.isDragTarget = false;
+        $event.preventDefault();
+    }
+
+    onColumnDrop(col: GridColumn) {
+        this.context.columnSet.insertBefore(this.dragColumn, col);
+        this.context.columnSet.columns.forEach(c => c.isDragTarget = false);
+    }
+
+    sortOneColumn(col: GridColumn) {
+        let dir = 'ASC';
+        const sort = this.context.dataSource.sort;
+
+        if (sort.length && sort[0].name === col.name && sort[0].dir === 'ASC') {
+            dir = 'DESC';
+        }
+
+        this.context.dataSource.sort = [{name: col.name, dir: dir}];
+
+        if (this.context.useLocalSort) {
+            this.context.sortLocal();
+        } else {
+            this.context.reload();
+        }
+    }
+
+    // Returns true if the provided column is sorting in the
+    // specified direction.
+    isColumnSorting(col: GridColumn, dir: string): boolean {
+        const sort = this.context.dataSource.sort.filter(c => c.name === col.name)[0];
+        return sort && sort.dir === dir;
+    }
+
+    handleBatchSelect($event) {
+        if ($event.target.checked) {
+            if (this.context.rowSelector.isEmpty() || !this.allRowsAreSelected()) {
+                // clear selections from other pages to avoid confusion.
+                this.context.rowSelector.clear();
+                this.selectAll();
+            }
+        } else {
+            this.context.rowSelector.clear();
+        }
+    }
+
+    selectAll() {
+        const rows = this.context.dataSource.getPageOfRows(this.context.pager);
+        const indexes = rows.map(r => this.context.getRowIndex(r));
+        this.context.rowSelector.select(indexes);
+    }
+
+    allRowsAreSelected(): boolean {
+        const rows = this.context.dataSource.getPageOfRows(this.context.pager);
+        const indexes = rows.map(r => this.context.getRowIndex(r));
+        return this.context.rowSelector.contains(indexes);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html
new file mode 100644
index 0000000..a098792
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html
@@ -0,0 +1,30 @@
+
+
+<ng-container>
+  <eg-progress-dialog #progressDialog></eg-progress-dialog>
+  <ng-template #printTemplate let-context>
+    <div>
+      <style>
+        .grid-print-table {
+          border-collapse: collapse;
+          margin: 1px;
+        }
+        .grid-print-table td {
+          padding: 2px;
+          border: 1px solid #aaa;
+        }
+      </style>
+      <table class="grid-print-table">
+        <thead>
+          <tr><th *ngFor="let col of context.columns">{{col.label}}</th></tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let row of context.rows; trackBy: index">
+            <!-- item values have already been filtered, etc. -->
+            <td *ngFor="let col of context.columns"><span>{{row[col.name]}}</span></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </ng-template>
+</ng-container>
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts
new file mode 100644
index 0000000..f73e26b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts
@@ -0,0 +1,45 @@
+import {Component, Input, TemplateRef, ViewChild} from '@angular/core';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {GridContext} from '@eg/share/grid/grid';
+
+ at Component({
+  selector: 'eg-grid-print',
+  templateUrl: './grid-print.component.html'
+})
+
+/**
+ */
+export class GridPrintComponent {
+
+    @Input() gridContext: GridContext;
+    @ViewChild('printTemplate') private printTemplate: TemplateRef<any>;
+    @ViewChild('progressDialog')
+        private progressDialog: ProgressDialogComponent;
+
+    constructor(private printer: PrintService) {}
+
+    printGrid() {
+        this.progressDialog.open();
+        const columns = this.gridContext.columnSet.displayColumns();
+        const textItems = {columns: columns, rows: []};
+
+        this.gridContext.getAllRowsAsText().subscribe(
+            row => {
+              this.progressDialog.increment();
+              textItems.rows.push(row);
+            },
+            err => this.progressDialog.close(),
+            ()  => {
+                this.progressDialog.close();
+                this.printer.print({
+                    template: this.printTemplate,
+                    contextData: textItems,
+                    printContext: 'default'
+                });
+            }
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
new file mode 100644
index 0000000..593530a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
@@ -0,0 +1,33 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridToolbarAction} from './grid';
+import {GridComponent} from './grid.component';
+
+ at Component({
+  selector: 'eg-grid-toolbar-action',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridToolbarActionComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() label: string;
+    @Input() action: (rows: any[]) => any;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridToolbarActionComponent needs a [grid]');
+            return;
+        }
+
+        const action = new GridToolbarAction();
+        action.label = this.label;
+        action.action = this.action;
+
+        this.grid.context.toolbarActions.push(action);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
new file mode 100644
index 0000000..8287483
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
@@ -0,0 +1,43 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridToolbarButton} from './grid';
+import {GridComponent} from './grid.component';
+
+ at Component({
+  selector: 'eg-grid-toolbar-button',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridToolbarButtonComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() label: string;
+    @Input() action: () => any;
+
+    @Input() set disabled(d: boolean) {
+        // Support asynchronous disabled values by appling directly
+        // to our button object as values arrive.
+        if (this.button) {
+            this.button.disabled = d;
+        }
+    }
+
+    button: GridToolbarButton;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {
+        this.button = new GridToolbarButton();
+    }
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridToolbarButtonComponent needs a [grid]');
+            return;
+        }
+
+        this.button.label = this.label;
+        this.button.action = this.action;
+        this.grid.context.toolbarButtons.push(this.button);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
new file mode 100644
index 0000000..f078797
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
@@ -0,0 +1,37 @@
+import {Component, Input, OnInit, Host, TemplateRef} from '@angular/core';
+import {GridToolbarCheckbox} from './grid';
+import {GridComponent} from './grid.component';
+
+ at Component({
+  selector: 'eg-grid-toolbar-checkbox',
+  template: '<ng-template></ng-template>'
+})
+
+export class GridToolbarCheckboxComponent implements OnInit {
+
+    // Note most input fields should match class fields for GridColumn
+    @Input() label: string;
+
+    // This is an input instead of an Output because the handler is
+    // passed off to the grid context for maintenance -- events
+    // are not fired directly from this component.
+    @Input() onChange: (checked: boolean) => void;
+
+    // get a reference to our container grid.
+    constructor(@Host() private grid: GridComponent) {}
+
+    ngOnInit() {
+
+        if (!this.grid) {
+            console.warn('GridToolbarCheckboxComponent needs a [grid]');
+            return;
+        }
+
+        const cb = new GridToolbarCheckbox();
+        cb.label = this.label;
+        cb.onChange = this.onChange;
+
+        this.grid.context.toolbarCheckboxes.push(cb);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
new file mode 100644
index 0000000..ae24021
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
@@ -0,0 +1,152 @@
+
+<div class="eg-grid-toolbar mb-2">
+
+  <div class="btn-toolbar">
+
+    <!-- buttons -->
+    <div class="btn-grp" *ngIf="gridContext.toolbarButtons.length">
+      <button *ngFor="let btn of gridContext.toolbarButtons" 
+        [disabled]="btn.disabled"
+        class="btn btn-outline-dark mr-1" (click)="btn.action()">
+        {{btn.label}}
+      </button>
+    </div>
+
+    <!-- checkboxes -->
+    <div class="form-check form-check-inline" 
+      *ngIf="gridContext.toolbarCheckboxes.length">
+      <ng-container *ngFor="let cb of gridContext.toolbarCheckboxes">
+        <label class="form-check-label">
+          <input class="form-check-input" type="checkbox" 
+            (click)="cb.onChange($event.target.checked)"/>
+            {{cb.label}}
+        </label>
+      </ng-container>
+    </div>
+  </div>
+
+  <!-- push everything else to the right -->
+  <div class="flex-1"></div>
+
+  <div ngbDropdown class="mr-1" placement="bottom-right">
+    <button ngbDropdownToggle [disabled]="!gridContext.toolbarActions.length"
+        class="btn btn-outline-dark no-dropdown-caret">
+      <span title="Actions For Selected Rows" i18n-title 
+        class="material-icons mat-icon-in-button">playlist_add_check</span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+      <a class="dropdown-item" (click)="performAction(action)"
+        *ngFor="let action of gridContext.toolbarActions">
+        <span class="ml-2">{{action.label}}</span>
+      </a>
+    </div>
+  </div>
+
+  <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+    class="btn btn-outline-dark mr-1" (click)="gridContext.pager.toFirst()">
+    <span title="First Page" i18n-title 
+        class="material-icons mat-icon-in-button">first_page</span>
+  </button>
+  <button [disabled]="gridContext.pager.isFirstPage()" type="button" 
+    class="btn btn-outline-dark mr-1" (click)="gridContext.pager.decrement()">
+    <span title="Previous Page" i18n-title 
+        class="material-icons mat-icon-in-button">keyboard_arrow_left</span>
+  </button>
+  <button [disabled]="gridContext.pager.isLastPage()" type="button" 
+    class="btn btn-outline-dark mr-1" (click)="gridContext.pager.increment()">
+    <span title="Next Page" i18n-title 
+        class="material-icons mat-icon-in-button">keyboard_arrow_right</span>
+  </button>
+
+  <!--
+  Hiding jump-to-last since there's no analog in the angularjs grid and
+  it has limited value since the size of the data set is often unknown.
+  <button [disabled]="!gridContext.pager.resultCount || gridContext.pager.isLastPage()" 
+    type="button" class="btn btn-outline-dark mr-1" (click)="gridContext.pager.toLast()">
+    <span title="First Page" i18n-title 
+        class="material-icons mat-icon-in-button">last_page</span>
+  </button>
+  -->
+
+  <div ngbDropdown class="mr-1" placement="bottom-right">
+    <button ngbDropdownToggle class="btn btn-outline-dark text-button">
+      <span title="Select Row Count" i18n-title i18n>
+        Rows {{gridContext.pager.limit}}
+      </span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+      <a class="dropdown-item" 
+        *ngFor="let count of [5, 10, 25, 50, 100]"
+        (click)="gridContext.pager.setLimit(count)">
+        <span class="ml-2">{{count}}</span>
+      </a>
+    </div>
+  </div>
+
+  <button type="button" 
+    class="btn btn-outline-dark mr-1" 
+    (click)="gridContext.overflowCells=!gridContext.overflowCells">
+    <span *ngIf="!gridContext.overflowCells"
+      title="Expand Cells Vertically" i18n-title 
+      class="material-icons mat-icon-in-button">expand_more</span>
+    <span *ngIf="gridContext.overflowCells"
+      title="Collaps Cells Vertically" i18n-title 
+      class="material-icons mat-icon-in-button">expand_less</span>
+  </button>
+
+  <eg-grid-column-config #columnConfDialog [columnSet]="gridContext.columnSet">
+  </eg-grid-column-config>
+  <div ngbDropdown placement="bottom-right">
+    <button ngbDropdownToggle class="btn btn-outline-dark no-dropdown-caret">
+      <span title="Show Grid Options" i18n-title 
+        class="material-icons mat-icon-in-button">settings</span>
+    </button>
+    <div class="dropdown-menu" ngbDropdownMenu>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="columnConfDialog.open({size:'lg'})">
+        <span class="material-icons">build</span>
+        <span class="ml-2" i18n>Manage Columns</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="colWidthConfig.isVisible = !colWidthConfig.isVisible">
+        <span class="material-icons">compare_arrows</span>
+        <span class="ml-2" i18n>Manage Column Widths</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="saveGridConfig()">
+        <span class="material-icons">save</span>
+        <span class="ml-2" i18n>Save Grid Settings</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="gridContext.columnSet.reset()">
+        <span class="material-icons">restore</span>
+        <span class="ml-2" i18n>Reset Columns</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="generateCsvExportUrl($event)"
+        [download]="csvExportFileName"
+        [href]="csvExportUrl">
+        <span class="material-icons">cloud_download</span>
+        <span class="ml-2" i18n>Download Full CSV</span>
+      </a>
+      <a class="dropdown-item label-with-material-icon" (click)="printHtml()">
+        <span class="material-icons">print</span>
+        <span class="ml-2" i18n>Print Full Grid</span>
+      </a>
+
+      <div class="dropdown-divider"></div>
+
+      <a class="dropdown-item label-with-material-icon" 
+        (click)="col.visible=!col.visible" *ngFor="let col of gridContext.columnSet.columns">
+        <span *ngIf="col.visible" class="badge badge-success">&#x2713;</span>
+        <span *ngIf="!col.visible" class="badge badge-warning">&#x2717;</span>
+        <span class="ml-2">{{col.label}}</span>
+      </a>
+
+    </div>
+  </div>
+
+<div>
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
new file mode 100644
index 0000000..5c8b523
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
@@ -0,0 +1,86 @@
+import {Component, Input, OnInit, Host} from '@angular/core';
+import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
+import {Pager} from '@eg/share/util/pager';
+import {GridColumn, GridColumnSet, GridToolbarButton,
+    GridToolbarAction, GridContext, GridDataSource} from '@eg/share/grid/grid';
+import {GridColumnWidthComponent} from './grid-column-width.component';
+import {GridPrintComponent} from './grid-print.component';
+
+ at Component({
+  selector: 'eg-grid-toolbar',
+  templateUrl: 'grid-toolbar.component.html'
+})
+
+export class GridToolbarComponent implements OnInit {
+
+    @Input() gridContext: GridContext;
+    @Input() colWidthConfig: GridColumnWidthComponent;
+    @Input() gridPrinter: GridPrintComponent;
+
+    csvExportInProgress: boolean;
+    csvExportUrl: SafeUrl;
+    csvExportFileName: string;
+
+    constructor(private sanitizer: DomSanitizer) {}
+
+    ngOnInit() {}
+
+    saveGridConfig() {
+        // TODO: when server-side settings are supported, this operation
+        // may offer to save to user/workstation OR org unit settings
+        // depending on perms.
+
+        this.gridContext.saveGridConfig().then(
+            // hide the with config after saving
+            ok => this.colWidthConfig.isVisible = false,
+            err => console.error(`Error saving columns: ${err}`)
+        );
+    }
+
+    performAction(action: GridToolbarAction) {
+        action.action(this.gridContext.getSelectedRows());
+    }
+
+    printHtml() {
+        this.gridPrinter.printGrid();
+    }
+
+    generateCsvExportUrl($event) {
+
+        if (this.csvExportInProgress) {
+            // This is secondary href click handler.  Give the
+            // browser a moment to start the download, then reset
+            // the CSV download attributes / state.
+            setTimeout(() => {
+                this.csvExportUrl = null;
+                this.csvExportFileName = '';
+                this.csvExportInProgress = false;
+               }, 500
+            );
+            return;
+        }
+
+        this.csvExportInProgress = true;
+
+        // let the file name describe the grid
+        this.csvExportFileName = (
+            this.gridContext.persistKey || 'eg_grid_data'
+        ).replace(/\s+/g, '_') + '.csv';
+
+        this.gridContext.gridToCsv().then(csv => {
+            const blob = new Blob([csv], {type : 'text/plain'});
+            const win: any = window; // avoid TS errors
+            this.csvExportUrl = this.sanitizer.bypassSecurityTrustUrl(
+                (win.URL || win.webkitURL).createObjectURL(blob)
+            );
+
+            // Fire the 2nd click event now that the browser has
+            // information on how to download the CSV file.
+            setTimeout(() => $event.target.click());
+        });
+
+        $event.preventDefault();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.css b/Open-ILS/src/eg2/src/app/share/grid/grid.component.css
new file mode 100644
index 0000000..9748c0c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.css
@@ -0,0 +1,142 @@
+
+.eg-grid {
+    width: 100%;
+    color: rgba(0,0,0,.87); 
+}
+    
+.eg-grid-row {
+    display: flex;
+    border-bottom: 1px solid rgba(0,0,0,.12);
+    padding-left: 10px;
+    padding-right: 10px;
+}
+
+.eg-grid-header-row {
+  /* matches bootstrap card-header css */
+  background-color: rgba(0,0,0,.03);
+  border-bottom: 1px solid rgba(0,0,0,.125);
+}
+
+.eg-grid-body {
+    outline: none; /* for keyboard events */
+}
+
+.eg-grid-body-row {
+}
+
+.eg-grid-body-row.selected, 
+.eg-grid-column-config-dialog .visible {
+  color: #004085;
+  background-color: #cce5ff;
+  border-color: #b8daff;
+}
+
+.eg-grid-header-cell {
+    font-weight: bold;
+}
+
+.eg-grid-header-cell.dragover {
+    background-color: #cce5ff;
+    border-color: #b8daff;
+}
+
+.eg-grid-header-cell-sort-label {
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.eg-grid-header-cell-sort-arrow {
+  font-size: 14px;
+}
+
+.eg-grid-cell {
+    flex: 2; /* applied per column */
+    padding: 6px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+/* allow tooltips to be wider than the default 200px */
+.eg-grid-cell .tooltip-inner {
+  max-width: 400px; 
+}
+
+/* in overflow mode, allow white space to wrap so the 
+ * full contents of the cell can be seen inline.  leaving 
+ * text-overflow and overlow as-is means long strings with
+ * no space will still be truncated with ellipses to avoid
+ * inconsistent grid column widths
+ */
+.eg-grid-cell-overflow {
+    white-space: normal;
+}
+
+.eg-grid-body-cell {
+}
+
+.eg-grid-toolbar {
+  display: flex;
+}
+
+.eg-grid-toolbar .material-icons {
+  font-size: 20px;
+}
+
+.eg-grid-toolbar .form-check-label:nth-child(even) {
+  padding-left: 5px;
+  padding-right: 5px;
+  margin-left: 3px;
+  margin-right: 3px;
+  border-radius: 5px;
+  background-color: rgba(0,0,0,.03);
+  border: 1px solid rgba(0,0,0,.125);
+}
+
+/* Kind of hacky -- only way to get a toolbar button with no 
+ * mat icon to line up horizontally with mat icon buttons */
+.eg-grid-toolbar .text-button {
+  padding-top: 11px;
+  padding-bottom: 11px;
+}
+
+.eg-grid-cell-skinny {
+  width: 2.2em;
+  text-align: center;
+  flex: none;
+}
+
+.eg-grid-flair-cell {
+  /* mat icons currently 22px, unclear why it needs this much space */
+  width: 34px; 
+  text-align: center;
+  flex: none;
+}
+
+/* depends on width of .eg-grid-cell-skinny */
+.eg-grid-column-width-header {
+  width: 4.4em;
+  text-align: center;
+  flex: none;
+  display: inline-flex;
+  vertical-align: middle;
+  align-items: center;
+}
+
+.eg-grid-column-width-config .eg-grid-cell {
+    border-left: 2px dashed grey;
+}
+
+.eg-grid-column-width-icon {
+  cursor: pointer;
+  font-size: 18px;
+  color: #007bff;
+}
+
+.eg-grid-column-config-dialog {
+  height: auto;
+  max-height: 400px;
+  overflow: auto;
+  box-shadow: none;
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
new file mode 100644
index 0000000..a98e17a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.html
@@ -0,0 +1,27 @@
+
+<div class="eg-grid">
+
+  <eg-grid-toolbar
+    [gridContext]="context" 
+    [gridPrinter]="gridPrinter"
+    [colWidthConfig]="colWidthConfig">
+  </eg-grid-toolbar>
+
+  <eg-grid-header [context]="context"></eg-grid-header>
+
+  <eg-grid-column-width #colWidthConfig [gridContext]="context">
+  </eg-grid-column-width>
+  
+  <eg-grid-print #gridPrinter [gridContext]="context">
+  </eg-grid-print>
+
+  <!-- move me too -->
+  <div class="row" *ngIf="dataSource.data.length == 0">
+    <div class="col-lg-12 text-center alert alert-light font-italic" i18n>
+      Nothing to Display
+    </div>
+  </div>
+
+  <eg-grid-body [context]="context"></eg-grid-body>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
new file mode 100644
index 0000000..1fa4c2c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
@@ -0,0 +1,149 @@
+import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
+    OnDestroy, HostListener, ViewEncapsulation} from '@angular/core';
+import {Subscription} from 'rxjs/Subscription';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {FormatService} from '@eg/core/format.service';
+import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
+
+/**
+ * Main grid entry point.
+ */
+
+ at Component({
+  selector: 'eg-grid',
+  templateUrl: './grid.component.html',
+  styleUrls: ['grid.component.css'],
+  // share grid css globally once imported so all grid component CSS
+  // can live in grid.component.css and to avoid multiple copies of
+  // the CSS when multiple grids are displayed.
+  encapsulation: ViewEncapsulation.None
+})
+
+export class GridComponent implements OnInit, AfterViewInit, OnDestroy {
+
+    // Source of row data.
+    @Input() dataSource: GridDataSource;
+
+    // IDL class for auto-generation of columns
+    @Input() idlClass: string;
+
+    // True if any columns are sortable
+    @Input() sortable: boolean;
+
+    // True if the grid supports sorting of multiple columns at once
+    @Input() multiSortable: boolean;
+
+    // If true, grid sort requests only operate on data that
+    // already exists in the grid data source -- no row fetching.
+    // The assumption is all data is already available.
+    @Input() useLocalSort: boolean;
+
+    // Storage persist key / per-grid-type unique identifier
+    // The value is prefixed with 'eg.grid.'
+    @Input() persistKey: string;
+
+    // Prevent selection of multiple rows
+    @Input() disableMultiSelect: boolean;
+
+    // Show an extra column in the grid where the caller can apply
+    // row-specific flair (material icons).
+    @Input() rowFlairIsEnabled: boolean;
+
+    // Returns a material icon name to display in the flar column
+    // (if enabled) for the given row.
+    @Input() rowFlairCallback: (row: any) => GridRowFlairEntry;
+
+    // Returns a space-separated list of CSS class names to apply to
+    // a given row
+    @Input() rowClassCallback: (row: any) => string;
+
+    // Returns a space-separated list of CSS class names to apply to
+    // a given cell or all cells in a column.
+    @Input() cellClassCallback: (row: any, col: GridColumn) => string;
+
+    // comma-separated list of fields to show by default.
+    // This field takes precedence over hideFields.
+    // When a value is applied, any field not in this list will
+    // be hidden.
+    @Input() showFields: string;
+
+    // comma-separated list of fields to hide.
+    // This does not imply all other fields should be visible, only that
+    // the selected fields will be hidden.
+    @Input() hideFields: string;
+
+    // Allow the caller to jump directly to a specific page of
+    // grid data.
+    @Input() pageOffset: number;
+
+    context: GridContext;
+
+    // These events are emitted from our grid-body component.
+    // They are defined here for ease of access to the caller.
+    @Output() onRowActivate: EventEmitter<any>;
+    @Output() onRowClick: EventEmitter<any>;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private store: ServerStoreService,
+        private format: FormatService
+    ) {
+        this.context =
+            new GridContext(this.idl, this.org, this.store, this.format);
+        this.onRowActivate = new EventEmitter<any>();
+        this.onRowClick = new EventEmitter<any>();
+    }
+
+    ngOnInit() {
+
+        if (!this.dataSource) {
+            throw new Error('<eg-grid/> requires a [dataSource]');
+        }
+
+        this.context.idlClass = this.idlClass;
+        this.context.dataSource = this.dataSource;
+        this.context.persistKey = this.persistKey;
+        this.context.isSortable = this.sortable === true;
+        this.context.isMultiSortable = this.multiSortable === true;
+        this.context.useLocalSort = this.useLocalSort === true;
+        this.context.disableMultiSelect = this.disableMultiSelect === true;
+        this.context.rowFlairIsEnabled = this.rowFlairIsEnabled  === true;
+        this.context.rowFlairCallback = this.rowFlairCallback;
+        if (this.showFields) {
+            this.context.defaultVisibleFields = this.showFields.split(',');
+        }
+        if (this.hideFields) {
+            this.context.defaultHiddenFields = this.hideFields.split(',');
+        }
+
+        if (this.pageOffset) {
+            this.context.pager.offset = this.pageOffset;
+        }
+
+        // TS doesn't seem to like: let foo = bar || () => '';
+        this.context.rowClassCallback =
+            this.rowClassCallback || function () { return ''; };
+        this.context.cellClassCallback =
+            this.cellClassCallback || function() { return ''; };
+
+        this.context.init();
+    }
+
+    ngAfterViewInit() {
+        this.context.initData();
+    }
+
+    ngOnDestroy() {
+        this.context.destroy();
+    }
+
+    reload() {
+        this.context.reload();
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
new file mode 100644
index 0000000..0773a7e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
@@ -0,0 +1,50 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {GridComponent} from './grid.component';
+import {GridColumnComponent} from './grid-column.component';
+import {GridHeaderComponent} from './grid-header.component';
+import {GridBodyComponent} from './grid-body.component';
+import {GridBodyCellComponent} from './grid-body-cell.component';
+import {GridToolbarComponent} from './grid-toolbar.component';
+import {GridToolbarButtonComponent} from './grid-toolbar-button.component';
+import {GridToolbarCheckboxComponent} from './grid-toolbar-checkbox.component';
+import {GridToolbarActionComponent} from './grid-toolbar-action.component';
+import {GridColumnConfigComponent} from './grid-column-config.component';
+import {GridColumnWidthComponent} from './grid-column-width.component';
+import {GridPrintComponent} from './grid-print.component';
+
+
+ at NgModule({
+    declarations: [
+        // public + internal components
+        GridComponent,
+        GridColumnComponent,
+        GridHeaderComponent,
+        GridBodyComponent,
+        GridBodyCellComponent,
+        GridToolbarComponent,
+        GridToolbarButtonComponent,
+        GridToolbarCheckboxComponent,
+        GridToolbarActionComponent,
+        GridColumnConfigComponent,
+        GridColumnWidthComponent,
+        GridPrintComponent
+    ],
+    imports: [
+        EgCommonModule
+    ],
+    exports: [
+        // public components
+        GridComponent,
+        GridColumnComponent,
+        GridToolbarButtonComponent,
+        GridToolbarCheckboxComponent,
+        GridToolbarActionComponent
+    ],
+    providers: [
+    ]
+})
+
+export class GridModule {
+
+}
diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
new file mode 100644
index 0000000..e04940d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts
@@ -0,0 +1,972 @@
+/**
+ * Collection of grid related classses and interfaces.
+ */
+import {TemplateRef} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {FormatService} from '@eg/core/format.service';
+import {Pager} from '@eg/share/util/pager';
+
+const MAX_ALL_ROW_COUNT = 10000;
+
+export class GridColumn {
+    name: string;
+    path: string;
+    label: string;
+    flex: number;
+    align: string;
+    hidden: boolean;
+    visible: boolean;
+    sort: number;
+    idlClass: string;
+    idlFieldDef: any;
+    datatype: string;
+    cellTemplate: TemplateRef<any>;
+    cellContext: any;
+    isIndex: boolean;
+    isDragTarget: boolean;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    comparator: (valueA: any, valueB: any) => number;
+
+    // True if the column was automatically generated.
+    isAuto: boolean;
+
+    flesher: (obj: any, col: GridColumn, item: any) => any;
+
+    getCellContext(row: any) {
+        return {
+          col: this,
+          row: row,
+          userContext: this.cellContext
+        };
+    }
+}
+
+export class GridColumnSet {
+    columns: GridColumn[];
+    idlClass: string;
+    indexColumn: GridColumn;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    stockVisible: string[];
+    idl: IdlService;
+    defaultHiddenFields: string[];
+    defaultVisibleFields: string[];
+
+    constructor(idl: IdlService, idlClass?: string) {
+        this.idl = idl;
+        this.columns = [];
+        this.stockVisible = [];
+        this.idlClass = idlClass;
+    }
+
+    add(col: GridColumn) {
+
+        this.applyColumnDefaults(col);
+
+        if (!this.insertColumn(col)) {
+            // Column was rejected as a duplicate.
+            return;
+        }
+
+        if (col.isIndex) { this.indexColumn = col; }
+
+        // track which fields are visible on page load.
+        if (col.visible) {
+            this.stockVisible.push(col.name);
+        }
+
+        this.applyColumnSortability(col);
+    }
+
+    // Returns true if the new column was inserted, false otherwise.
+    // Declared columns take precedence over auto-generated columns
+    // when collisions occur.
+    // Declared columns are inserted in front of auto columns.
+    insertColumn(col: GridColumn): boolean {
+
+        if (col.isAuto) {
+            if (this.getColByName(col.name)) {
+                // New auto-generated column conflicts with existing
+                // column.  Skip it.
+                return false;
+            } else {
+                // No collisions.  Add to the end of the list
+                this.columns.push(col);
+                return true;
+            }
+        }
+
+        // Adding a declared column.
+
+        // Check for dupes.
+        for (let idx = 0; idx < this.columns.length; idx++) {
+            const testCol = this.columns[idx];
+            if (testCol.name === col.name) { // match found
+                if (testCol.isAuto) {
+                    // new column takes precedence, remove the existing column.
+                    this.columns.splice(idx, 1);
+                    break;
+                } else {
+                    // New column does not take precedence.  Avoid
+                    // inserting it.
+                    return false;
+                }
+            }
+        }
+
+        // Delcared columns are inserted just before the first auto-column
+        for (let idx = 0; idx < this.columns.length; idx++) {
+            const testCol = this.columns[idx];
+            if (testCol.isAuto) {
+                if (idx === 0) {
+                    this.columns.unshift(col);
+                } else {
+                    this.columns.splice(idx - 1, 0, col);
+                }
+                return true;
+            }
+        }
+
+        // No insertion point found.  Toss the new column on the end.
+        this.columns.push(col);
+        return true;
+    }
+
+    getColByName(name: string): GridColumn {
+        return this.columns.filter(c => c.name === name)[0];
+    }
+
+    idlInfoFromDotpath(dotpath: string): any {
+        if (!dotpath || !this.idlClass) { return null; }
+
+        let idlParent;
+        let idlField;
+        let idlClass = this.idl.classes[this.idlClass];
+
+        const pathParts = dotpath.split(/\./);
+
+        for (let i = 0; i < pathParts.length; i++) {
+            const part = pathParts[i];
+            idlParent = idlField;
+            idlField = idlClass.field_map[part];
+
+            if (idlField) {
+                if (idlField['class'] && (
+                    idlField.datatype === 'link' ||
+                    idlField.datatype === 'org_unit')) {
+                    idlClass = this.idl.classes[idlField['class']];
+                }
+            } else {
+                return null;
+            }
+        }
+
+        return {
+            idlParent: idlParent,
+            idlField : idlField,
+            idlClass : idlClass
+        };
+    }
+
+
+    reset() {
+        this.columns.forEach(col => {
+            col.flex = 2;
+            col.sort = 0;
+            col.align = 'left';
+            col.visible = this.stockVisible.includes(col.name);
+        });
+    }
+
+    applyColumnDefaults(col: GridColumn) {
+
+        if (!col.idlFieldDef && col.path) {
+            const idlInfo = this.idlInfoFromDotpath(col.path);
+            if (idlInfo) {
+                col.idlFieldDef = idlInfo.idlField;
+                if (!col.label) {
+                    col.label = col.idlFieldDef.label || col.idlFieldDef.name;
+                    col.datatype = col.idlFieldDef.datatype;
+                }
+            }
+        }
+
+        if (!col.name) { col.name = col.path; }
+        if (!col.flex) { col.flex = 2; }
+        if (!col.align) { col.align = 'left'; }
+        if (!col.label) { col.label = col.name; }
+        if (!col.datatype) { col.datatype = 'text'; }
+
+        col.visible = !col.hidden;
+    }
+
+    applyColumnSortability(col: GridColumn) {
+        // column sortability defaults to the sortability of the column set.
+        if (col.isSortable === undefined && this.isSortable) {
+            col.isSortable = true;
+        }
+
+        if (col.isMultiSortable === undefined && this.isMultiSortable) {
+            col.isMultiSortable = true;
+        }
+
+        if (col.isMultiSortable) {
+            col.isSortable = true;
+        }
+    }
+
+    displayColumns(): GridColumn[] {
+        return this.columns.filter(c => c.visible);
+    }
+
+    insertBefore(source: GridColumn, target: GridColumn) {
+        let targetIdx = -1;
+        let sourceIdx = -1;
+        this.columns.forEach((col, idx) => {
+            if (col.name === target.name) { targetIdx = idx; }});
+
+        this.columns.forEach((col, idx) => {
+            if (col.name === source.name) { sourceIdx = idx; }});
+
+        if (sourceIdx >= 0) {
+            this.columns.splice(sourceIdx, 1);
+        }
+
+        this.columns.splice(targetIdx, 0, source);
+    }
+
+    // Move visible columns to the front of the list.
+    moveVisibleToFront() {
+        const newCols = this.displayColumns();
+        this.columns.forEach(col => {
+            if (!col.visible) { newCols.push(col); }});
+        this.columns = newCols;
+    }
+
+    moveColumn(col: GridColumn, diff: number) {
+        let srcIdx, targetIdx;
+
+        this.columns.forEach((c, i) => {
+          if (c.name === col.name) { srcIdx = i; }
+        });
+
+        targetIdx = srcIdx + diff;
+        if (targetIdx < 0) {
+            targetIdx = 0;
+        } else if (targetIdx >= this.columns.length) {
+            // Target index follows the last visible column.
+            let lastVisible = 0;
+            this.columns.forEach((c, idx) => {
+                if (c.visible) { lastVisible = idx; }
+            });
+
+            // When moving a column (down) causes one or more
+            // visible columns to shuffle forward, our column
+            // moves into the slot of the last visible column.
+            // Otherwise, put it into the slot directly following
+            // the last visible column.
+            targetIdx = srcIdx <= lastVisible ? lastVisible : lastVisible + 1;
+        }
+
+        // Splice column out of old position, insert at new position.
+        this.columns.splice(srcIdx, 1);
+        this.columns.splice(targetIdx, 0, col);
+    }
+
+    compileSaveObject(): GridColumnPersistConf[] {
+        // only store information about visible columns.
+        // scrunch the data down to just the needed info.
+        return this.displayColumns().map(col => {
+            const c: GridColumnPersistConf = {name : col.name};
+            if (col.align !== 'left') { c.align = col.align; }
+            if (col.flex !== 2) { c.flex = Number(col.flex); }
+            if (Number(col.sort)) { c.sort = Number(c.sort); }
+            return c;
+        });
+    }
+
+    applyColumnSettings(conf: GridColumnPersistConf[]) {
+
+        if (!conf || conf.length === 0) {
+            // No configuration is available, but we have a list of
+            // fields to show or hide by default
+
+            if (this.defaultVisibleFields) {
+                this.columns.forEach(col => {
+                    if (this.defaultVisibleFields.includes(col.name)) {
+                        col.visible = true;
+                    } else {
+                        col.visible = false;
+                    }
+                });
+
+            } else if (this.defaultHiddenFields) {
+                this.defaultHiddenFields.forEach(name => {
+                    const col = this.getColByName(name);
+                    if (col) {
+                        col.visible = false;
+                    }
+                });
+            }
+
+            return;
+        }
+
+        const newCols = [];
+
+        conf.forEach(colConf => {
+            const col = this.getColByName(colConf.name);
+            if (!col) { return; } // no such column in this grid.
+
+            col.visible = true;
+            if (colConf.align) { col.align = colConf.align; }
+            if (colConf.flex)  { col.flex = Number(colConf.flex); }
+            if (colConf.sort)  { col.sort = Number(colConf.sort); }
+
+            // Add to new columns array, avoid dupes.
+            if (newCols.filter(c => c.name === col.name).length === 0) {
+                newCols.push(col);
+            }
+        });
+
+        // columns which are not expressed within the saved
+        // configuration are marked as non-visible and
+        // appended to the end of the new list of columns.
+        this.columns.forEach(c => {
+            if (conf.filter(cf => cf.name === c.name).length === 0) {
+                c.visible = false;
+                newCols.push(c);
+            }
+        });
+
+        this.columns = newCols;
+    }
+}
+
+
+export class GridRowSelector {
+    indexes: {[string: string]: boolean};
+
+    constructor() {
+        this.clear();
+    }
+
+    // Returns true if all of the requested indexes exist in the selector.
+    contains(index: string | string[]): boolean {
+        const indexes = [].concat(index);
+        for (let i = 0; i < indexes.length; i++) { // early exit
+            if (!this.indexes[indexes[i]]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    select(index: string | string[]) {
+        const indexes = [].concat(index);
+        indexes.forEach(i => this.indexes[i] = true);
+    }
+
+    deselect(index: string | string[]) {
+        const indexes = [].concat(index);
+        indexes.forEach(i => delete this.indexes[i]);
+    }
+
+    // Returns the list of selected index values.
+    // in some contexts (template checkboxes) the value for an index is
+    // set to false to deselect instead of having it removed (via deselect()).
+    selected() {
+        return Object.keys(this.indexes).filter(
+            ind => Boolean(this.indexes[ind]));
+    }
+
+    isEmpty(): boolean {
+        return this.selected().length === 0;
+    }
+
+    clear() {
+        this.indexes = {};
+    }
+}
+
+export interface GridRowFlairEntry {
+    icon: string;   // name of material icon
+    title: string;  // tooltip string
+}
+
+export class GridColumnPersistConf {
+    name: string;
+    flex?: number;
+    sort?: number;
+    align?: string;
+}
+
+export class GridPersistConf {
+    version: number;
+    limit: number;
+    columns: GridColumnPersistConf[];
+}
+
+export class GridContext {
+
+    pager: Pager;
+    idlClass: string;
+    isSortable: boolean;
+    isMultiSortable: boolean;
+    useLocalSort: boolean;
+    persistKey: string;
+    disableMultiSelect: boolean;
+    dataSource: GridDataSource;
+    columnSet: GridColumnSet;
+    rowSelector: GridRowSelector;
+    toolbarButtons: GridToolbarButton[];
+    toolbarCheckboxes: GridToolbarCheckbox[];
+    toolbarActions: GridToolbarAction[];
+    lastSelectedIndex: any;
+    pageChanges: Subscription;
+    rowFlairIsEnabled: boolean;
+    rowFlairCallback: (row: any) => GridRowFlairEntry;
+    rowClassCallback: (row: any) => string;
+    cellClassCallback: (row: any, col: GridColumn) => string;
+    defaultVisibleFields: string[];
+    defaultHiddenFields: string[];
+    overflowCells: boolean;
+
+    // Services injected by our grid component
+    idl: IdlService;
+    org: OrgService;
+    store: ServerStoreService;
+    format: FormatService;
+
+    constructor(
+        idl: IdlService,
+        org: OrgService,
+        store: ServerStoreService,
+        format: FormatService) {
+
+        this.idl = idl;
+        this.org = org;
+        this.store = store;
+        this.format = format;
+        this.pager = new Pager();
+        this.pager.limit = 10;
+        this.rowSelector = new GridRowSelector();
+        this.toolbarButtons = [];
+        this.toolbarCheckboxes = [];
+        this.toolbarActions = [];
+    }
+
+    init() {
+        this.columnSet = new GridColumnSet(this.idl, this.idlClass);
+        this.columnSet.isSortable = this.isSortable === true;
+        this.columnSet.isMultiSortable = this.isMultiSortable === true;
+        this.columnSet.defaultHiddenFields = this.defaultHiddenFields;
+        this.columnSet.defaultVisibleFields = this.defaultVisibleFields;
+        this.generateColumns();
+    }
+
+    // Load initial settings and data.
+    initData() {
+        this.applyGridConfig()
+        .then(ok => this.dataSource.requestPage(this.pager))
+        .then(ok => this.listenToPager());
+    }
+
+    destroy() {
+        this.ignorePager();
+    }
+
+    applyGridConfig(): Promise<void> {
+        return this.getGridConfig(this.persistKey)
+        .then(conf => {
+            let columns = [];
+            if (conf) {
+                columns = conf.columns;
+                if (conf.limit) {
+                    this.pager.limit = conf.limit;
+                }
+            }
+
+            // This is called regardless of the presence of saved
+            // settings so defaults can be applied.
+            this.columnSet.applyColumnSettings(columns);
+        });
+    }
+
+    reload() {
+        // Give the UI time to settle before reloading grid data.
+        // This can help when data retrieval depends on a value
+        // getting modified by an angular digest cycle.
+        setTimeout(() => {
+            this.pager.reset();
+            this.dataSource.reset();
+            this.dataSource.requestPage(this.pager);
+        });
+    }
+
+    // Sort the existing data source instead of requesting sorted
+    // data from the client.  Reset pager to page 1.  As with reload(),
+    // give the client a chance to setting before redisplaying.
+    sortLocal() {
+        setTimeout(() => {
+            this.pager.reset();
+            this.sortLocalData();
+            this.dataSource.requestPage(this.pager);
+        });
+    }
+
+    // Subscribe or unsubscribe to page-change events from the pager.
+    listenToPager() {
+        if (this.pageChanges) { return; }
+        this.pageChanges = this.pager.onChange$.subscribe(
+            val => this.dataSource.requestPage(this.pager));
+    }
+
+    ignorePager() {
+        if (!this.pageChanges) { return; }
+        this.pageChanges.unsubscribe();
+        this.pageChanges = null;
+    }
+
+    // Sort data in the data source array
+    sortLocalData() {
+
+        const sortDefs = this.dataSource.sort.map(sort => {
+            const def = {
+                name: sort.name,
+                dir: sort.dir,
+                col: this.columnSet.getColByName(sort.name)
+            };
+
+            if (!def.col.comparator) {
+                def.col.comparator = (a, b) => {
+                    if (a < b) { return -1; }
+                    if (a > b) { return 1; }
+                    return 0;
+                };
+            }
+
+            return def;
+        });
+
+        this.dataSource.data.sort((rowA, rowB) => {
+
+            for (let idx = 0; idx < sortDefs.length; idx++) {
+                const sortDef = sortDefs[idx];
+
+                const valueA = this.getRowColumnValue(rowA, sortDef.col);
+                const valueB = this.getRowColumnValue(rowB, sortDef.col);
+
+                if (valueA === '' && valueB === '') { continue; }
+                if (valueA === '' && valueB !== '') { return 1; }
+                if (valueA !== '' && valueB === '') { return -1; }
+
+                const diff = sortDef.col.comparator(valueA, valueB);
+                if (diff === 0) { continue; }
+
+                console.log(valueA, valueB, diff);
+
+                return sortDef.dir === 'DESC' ? -diff : diff;
+            }
+
+            return 0; // No differences found.
+        });
+    }
+
+    getRowIndex(row: any): any {
+        const col = this.columnSet.indexColumn;
+        if (!col) {
+            throw new Error('grid index column required');
+        }
+        return this.getRowColumnValue(row, col);
+    }
+
+    // Returns position in the data source array of the row with
+    // the provided index.
+    getRowPosition(index: any): number {
+        // for-loop for early exit
+        for (let idx = 0; idx < this.dataSource.data.length; idx++) {
+            const row = this.dataSource.data[idx];
+            if (row !== undefined && index === this.getRowIndex(row)) {
+                return idx;
+            }
+        }
+    }
+
+    // Return the row with the provided index.
+    getRowByIndex(index: any): any {
+        for (let idx = 0; idx < this.dataSource.data.length; idx++) {
+            const row = this.dataSource.data[idx];
+            if (row !== undefined && index === this.getRowIndex(row)) {
+                return row;
+            }
+        }
+    }
+
+    // Returns all selected rows, regardless of whether they are
+    // currently visible in the grid display.
+    getSelectedRows(): any[] {
+        const selected = [];
+        this.rowSelector.selected().forEach(index => {
+            const row = this.getRowByIndex(index);
+            if (row) {
+                selected.push(row);
+            }
+        });
+        return selected;
+    }
+
+    getRowColumnValue(row: any, col: GridColumn): string {
+        let val;
+        if (col.name in row) {
+            val = this.getObjectFieldValue(row, col.name);
+        } else {
+            if (col.path) {
+                val = this.nestedItemFieldValue(row, col);
+            }
+        }
+        return this.format.transform({value: val, datatype: col.datatype});
+    }
+
+    getObjectFieldValue(obj: any, name: string): any {
+        if (typeof obj[name] === 'function') {
+            return obj[name]();
+        } else {
+            return obj[name];
+        }
+    }
+
+    nestedItemFieldValue(obj: any, col: GridColumn): string {
+
+        let idlField;
+        let idlClassDef;
+        const original = obj;
+        const steps = col.path.split('.');
+
+        for (let i = 0; i < steps.length; i++) {
+            const step = steps[i];
+
+            if (typeof obj !== 'object') {
+                // We have run out of data to step through before
+                // reaching the end of the path.  Conclude fleshing via
+                // callback if provided then exit.
+                if (col.flesher && obj !== undefined) {
+                    return col.flesher(obj, col, original);
+                }
+                return obj;
+            }
+
+            const class_ = obj.classname;
+            if (class_ && (idlClassDef = this.idl.classes[class_])) {
+                idlField = idlClassDef.field_map[step];
+            }
+
+            obj = this.getObjectFieldValue(obj, step);
+        }
+
+        // We found a nested IDL object which may or may not have
+        // been configured as a top-level column.  Flesh the column
+        // metadata with our newly found IDL info.
+        if (idlField) {
+            if (!col.datatype) {
+                col.datatype = idlField.datatype;
+            }
+            if (!col.label) {
+                col.label = idlField.label || idlField.name;
+            }
+        }
+
+        return obj;
+    }
+
+
+    getColumnTextContent(row: any, col: GridColumn): string {
+        if (col.cellTemplate) {
+            // TODO
+            // Extract the text content from the rendered template.
+        } else {
+            return this.getRowColumnValue(row, col);
+        }
+    }
+
+    selectOneRow(index: any) {
+        this.rowSelector.clear();
+        this.rowSelector.select(index);
+        this.lastSelectedIndex = index;
+    }
+
+    // selects or deselects an item, without affecting the others.
+    // returns true if the item is selected; false if de-selected.
+    toggleSelectOneRow(index: any) {
+        if (this.rowSelector.contains(index)) {
+            this.rowSelector.deselect(index);
+            return false;
+        }
+
+        this.rowSelector.select(index);
+        return true;
+    }
+
+    selectRowByPos(pos: number) {
+        const row = this.dataSource.data[pos];
+        if (row) {
+            this.selectOneRow(this.getRowIndex(row));
+        }
+    }
+
+    selectPreviousRow() {
+        if (!this.lastSelectedIndex) { return; }
+        const pos = this.getRowPosition(this.lastSelectedIndex);
+        if (pos === this.pager.offset) {
+            this.toPrevPage().then(ok => this.selectLastRow(), err => {});
+        } else {
+            this.selectRowByPos(pos - 1);
+        }
+    }
+
+    selectNextRow() {
+        if (!this.lastSelectedIndex) { return; }
+        const pos = this.getRowPosition(this.lastSelectedIndex);
+        if (pos === (this.pager.offset + this.pager.limit - 1)) {
+            this.toNextPage().then(ok => this.selectFirstRow(), err => {});
+        } else {
+            this.selectRowByPos(pos + 1);
+        }
+    }
+
+    selectFirstRow() {
+        this.selectRowByPos(this.pager.offset);
+    }
+
+    selectLastRow() {
+        this.selectRowByPos(this.pager.offset + this.pager.limit - 1);
+    }
+
+    toPrevPage(): Promise<any> {
+        if (this.pager.isFirstPage()) {
+            return Promise.reject('on first');
+        }
+        // temp ignore pager events since we're calling requestPage manually.
+        this.ignorePager();
+        this.pager.decrement();
+        this.listenToPager();
+        return this.dataSource.requestPage(this.pager);
+    }
+
+    toNextPage(): Promise<any> {
+        if (this.pager.isLastPage()) {
+            return Promise.reject('on last');
+        }
+        // temp ignore pager events since we're calling requestPage manually.
+        this.ignorePager();
+        this.pager.increment();
+        this.listenToPager();
+        return this.dataSource.requestPage(this.pager);
+    }
+
+    getAllRows(): Promise<any> {
+        const pager = new Pager();
+        pager.offset = 0;
+        pager.limit = MAX_ALL_ROW_COUNT;
+        return this.dataSource.requestPage(pager);
+    }
+
+    // Returns a key/value pair object of visible column data as text.
+    getRowAsFlatText(row: any): any {
+        const flatRow = {};
+        this.columnSet.displayColumns().forEach(col => {
+            flatRow[col.name] =
+                this.getColumnTextContent(row, col);
+        });
+        return flatRow;
+    }
+
+    getAllRowsAsText(): Observable<any> {
+        return Observable.create(observer => {
+            this.getAllRows().then(ok => {
+                this.dataSource.data.forEach(row => {
+                    observer.next(this.getRowAsFlatText(row));
+                });
+                observer.complete();
+            });
+        });
+    }
+
+    gridToCsv(): Promise<string> {
+
+        let csvStr = '';
+        const columns = this.columnSet.displayColumns();
+
+        // CSV header
+        columns.forEach(col => {
+            csvStr += this.valueToCsv(col.label),
+            csvStr += ',';
+        });
+
+        csvStr = csvStr.replace(/,$/, '\n');
+
+        return new Promise(resolve => {
+            this.getAllRowsAsText().subscribe(
+                row => {
+                    columns.forEach(col => {
+                        csvStr += this.valueToCsv(row[col.name]);
+                        csvStr += ',';
+                    });
+                    csvStr = csvStr.replace(/,$/, '\n');
+                },
+                err => {},
+                ()  => resolve(csvStr)
+            );
+        });
+    }
+
+
+    // prepares a string for inclusion within a CSV document
+    // by escaping commas and quotes and removing newlines.
+    valueToCsv(str: string): string {
+        str = '' + str;
+        if (!str) { return ''; }
+        str = str.replace(/\n/g, '');
+        if (str.match(/\,/) || str.match(/"/)) {
+            str = str.replace(/"/g, '""');
+            str = '"' + str + '"';
+        }
+        return str;
+    }
+
+    generateColumns() {
+        if (!this.columnSet.idlClass) { return; }
+
+        const pkeyField = this.idl.classes[this.columnSet.idlClass].pkey;
+
+        // generate columns for all non-virtual fields on the IDL class
+        this.idl.classes[this.columnSet.idlClass].fields
+        .filter(field => !field.virtual)
+        .forEach(field => {
+            const col = new GridColumn();
+            col.name = field.name;
+            col.label = field.label || field.name;
+            col.idlFieldDef = field;
+            col.datatype = field.datatype;
+            col.isIndex = (field.name === pkeyField);
+            col.isAuto = true;
+            this.columnSet.add(col);
+        });
+    }
+
+    saveGridConfig(): Promise<any> {
+        if (!this.persistKey) {
+            throw new Error('Grid persistKey required to save columns');
+        }
+        const conf = new GridPersistConf();
+        conf.version = 2;
+        conf.limit = this.pager.limit;
+        conf.columns = this.columnSet.compileSaveObject();
+
+        return this.store.setItem('eg.grid.' + this.persistKey, conf);
+    }
+
+    // TODO: saveGridConfigAsOrgSetting(...)
+
+    getGridConfig(persistKey: string): Promise<GridPersistConf> {
+        if (!persistKey) { return Promise.resolve(null); }
+        return this.store.getItem('eg.grid.' + persistKey);
+    }
+}
+
+
+// Actions apply to specific rows
+export class GridToolbarAction {
+    label: string;
+    action: (rows: any[]) => any;
+}
+
+// Buttons are global actions
+export class GridToolbarButton {
+    label: string;
+    action: () => any;
+    disabled: boolean;
+}
+
+export class GridToolbarCheckbox {
+    label: string;
+    onChange: (checked: boolean) => void;
+}
+
+export class GridDataSource {
+
+    data: any[];
+    sort: any[];
+    allRowsRetrieved: boolean;
+    getRows: (pager: Pager, sort: any[]) => Observable<any>;
+
+    constructor() {
+        this.sort = [];
+        this.reset();
+    }
+
+    reset() {
+        this.data = [];
+        this.allRowsRetrieved = false;
+    }
+
+    // called from the template -- no data fetching
+    getPageOfRows(pager: Pager): any[] {
+        if (this.data) {
+            return this.data.slice(
+                pager.offset, pager.limit + pager.offset
+            ).filter(row => row !== undefined);
+        }
+        return [];
+    }
+
+    // called on initial component load and user action (e.g. paging, sorting).
+    requestPage(pager: Pager): Promise<any> {
+
+        if (
+            this.getPageOfRows(pager).length === pager.limit
+            // already have all data
+            || this.allRowsRetrieved
+            // have no way to get more data.
+            || !this.getRows
+        ) {
+            return Promise.resolve();
+        }
+
+        return new Promise((resolve, reject) => {
+            let idx = pager.offset;
+            return this.getRows(pager, this.sort).subscribe(
+                row => this.data[idx++] = row,
+                err => {
+                    console.error(`grid getRows() error ${err}`);
+                    reject(err);
+                },
+                ()  => {
+                    this.checkAllRetrieved(pager, idx);
+                    resolve();
+                }
+            );
+        });
+    }
+
+    // See if the last getRows() call resulted in the final set of data.
+    checkAllRetrieved(pager: Pager, idx: number) {
+        if (this.allRowsRetrieved) { return; }
+
+        if (idx === 0 || idx < (pager.limit + pager.offset)) {
+            // last query returned nothing or less than one page.
+            // confirm we have all of the preceding pages.
+            if (!this.data.includes(undefined)) {
+                this.allRowsRetrieved = true;
+                pager.resultCount = this.data.length;
+            }
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
new file mode 100644
index 0000000..2a4bd3a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
@@ -0,0 +1,17 @@
+
+<!-- todo disabled -->
+<ng-template #displayTemplate let-r="result">
+{{r.label}}
+</ng-template>
+
+<input type="text" 
+  class="form-control"
+  [placeholder]="placeholder"
+  [(ngModel)]="selected" 
+  [ngbTypeahead]="filter"
+  [resultTemplate]="displayTemplate"
+  [inputFormatter]="formatter"
+  (click)="click$.next($event.target.value)"
+  (selectItem)="orgChanged($event)"
+  #instance="ngbTypeahead"
+/>
diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
new file mode 100644
index 0000000..39e0cff
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
@@ -0,0 +1,212 @@
+/** TODO PORT ME TO <eg-combobox> */
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {mapTo} from 'rxjs/operators/mapTo';
+import {debounceTime} from 'rxjs/operators/debounceTime';
+import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
+import {merge} from 'rxjs/operators/merge';
+import {filter} from 'rxjs/operators/filter';
+import {Subject} from 'rxjs/Subject';
+import {AuthService} from '@eg/core/auth.service';
+import {StoreService} from '@eg/core/store.service';
+import {OrgService} from '@eg/core/org.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {PermService} from '@eg/core/perm.service';
+import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
+
+// Use a unicode char for spacing instead of ASCII=32 so the browser
+// won't collapse the nested display entries down to a single space.
+const PAD_SPACE = ' '; // U+2007
+
+interface OrgDisplay {
+  id: number;
+  label: string;
+  disabled: boolean;
+}
+
+ at Component({
+  selector: 'eg-org-select',
+  templateUrl: './org-select.component.html'
+})
+export class OrgSelectComponent implements OnInit {
+
+    selected: OrgDisplay;
+    hidden: number[] = [];
+    disabled: number[] = [];
+    click$ = new Subject<string>();
+    startOrg: IdlObject;
+
+    @ViewChild('instance') instance: NgbTypeahead;
+
+    // Placeholder text for selector input
+    @Input() placeholder = '';
+    @Input() stickySetting: string;
+
+    // Org unit field displayed in the selector
+    @Input() displayField = 'shortname';
+
+    // Apply a default org unit value when none is set.
+    // First tries workstation org unit, then user home org unit.
+    // An onChange event WILL be generated when a default is applied.
+    @Input() applyDefault = false;
+
+    // List of org unit IDs to exclude from the selector
+    @Input() set hideOrgs(ids: number[]) {
+        if (ids) { this.hidden = ids; }
+    }
+
+    // List of org unit IDs to disable in the selector
+    @Input() set disableOrgs(ids: number[]) {
+        if (ids) { this.disabled = ids; }
+    }
+
+    // Apply an org unit value at load time.
+    // This will NOT result in an onChange event.
+    @Input() set initialOrg(org: IdlObject) {
+        if (org) { this.startOrg = org; }
+    }
+
+    // Apply an org unit value by ID at load time.
+    // This will NOT result in an onChange event.
+    @Input() set initialOrgId(id: number) {
+        if (id) { this.startOrg = this.org.get(id); }
+    }
+
+    // Modify the selected org unit via data binding.
+    // This WILL result in an onChange event firing.
+    @Input() set applyOrg(org: IdlObject) {
+        if (org) {
+            this.selected = this.formatForDisplay(org);
+        }
+    }
+
+    permLimitOrgs: number[];
+    @Input() set limitPerms(perms: string[]) {
+        this.applyPermLimitOrgs(perms);
+    }
+
+    // Modify the selected org unit by ID via data binding.
+    // This WILL result in an onChange event firing.
+    @Input() set applyOrgId(id: number) {
+        if (id) {
+            this.selected = this.formatForDisplay(this.org.get(id));
+        }
+    }
+
+    // Emitted when the org unit value is changed via the selector.
+    // Does not fire on initialOrg
+    @Output() onChange = new EventEmitter<IdlObject>();
+
+    constructor(
+      private auth: AuthService,
+      private store: StoreService,
+      private org: OrgService,
+      private perm: PermService
+    ) { }
+
+    ngOnInit() {
+
+        // Apply a default org unit if desired and possible.
+        if (!this.startOrg && this.applyDefault && this.auth.user()) {
+            // note: ws_ou defaults to home_ou on the server
+            // when when no workstation is used
+            this.startOrg = this.org.get(this.auth.user().ws_ou());
+            this.selected = this.formatForDisplay(
+                this.org.get(this.auth.user().ws_ou())
+            );
+
+            // avoid notifying mid-digest
+            setTimeout(() => this.onChange.emit(this.startOrg), 0);
+        }
+
+        if (this.startOrg) {
+            this.selected = this.formatForDisplay(this.startOrg);
+        }
+    }
+
+    //
+    applyPermLimitOrgs(perms: string[]) {
+
+        if (!perms) {
+            return;
+        }
+
+        // handle lazy clients that pass null perm names
+        perms = perms.filter(p => p !== null && p !== undefined);
+
+        if (perms.length === 0) {
+            return;
+        }
+
+        // NOTE: If permLimitOrgs is useful in a non-staff context
+        // we need to change this to support non-staff perm checks.
+        this.perm.hasWorkPermAt(perms, true).then(permMap => {
+            this.permLimitOrgs =
+                // safari-friendly version of Array.flat()
+                Object.values(permMap).reduce((acc, val) => acc.concat(val), []);
+        });
+    }
+
+    // Format for display in the selector drop-down and input.
+    formatForDisplay(org: IdlObject): OrgDisplay {
+        return {
+            id : org.id(),
+            label : PAD_SPACE.repeat(org.ou_type().depth())
+              + org[this.displayField](),
+            disabled : false
+        };
+    }
+
+    // Fired by the typeahead to inform us of a change.
+    // TODO: this does not fire when the value is cleared :( -- implement
+    // change detection on this.selected to look specifically for NULL.
+    orgChanged(selEvent: NgbTypeaheadSelectItemEvent) {
+        // console.debug('org unit change occurred ' + selEvent.item);
+        this.onChange.emit(this.org.get(selEvent.item.id));
+    }
+
+    // Remove the tree-padding spaces when matching.
+    formatter = (result: OrgDisplay) => result.label.trim();
+
+    filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
+        return text$.pipe(
+            debounceTime(200),
+            distinctUntilChanged(),
+            merge(
+                // Inject a specifier indicating the source of the
+                // action is a user click
+                this.click$.pipe(filter(() => !this.instance.isPopupOpen()))
+                .pipe(mapTo('_CLICK_'))
+            ),
+            map(term => {
+
+                let orgs = this.org.list().filter(org =>
+                    this.hidden.filter(id => org.id() === id).length === 0
+                );
+
+                if (this.permLimitOrgs) {
+                    // Avoid showing org units where the user does
+                    // not have the requested permission.
+                    orgs = orgs.filter(org =>
+                        this.permLimitOrgs.includes(org.id()));
+                }
+
+                if (term !== '_CLICK_') {
+                    // For search-driven events, limit to the matching
+                    // org units.
+                    orgs = orgs.filter(org => {
+                        return term === '' || // show all
+                            org[this.displayField]()
+                                .toLowerCase().indexOf(term.toLowerCase()) > -1;
+
+                    });
+                }
+
+                return orgs.map(org => this.formatForDisplay(org));
+            })
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.html b/Open-ILS/src/eg2/src/app/share/print/print.component.html
new file mode 100644
index 0000000..12d05bc
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.html
@@ -0,0 +1,16 @@
+<!-- 
+Global print container.  
+There should only be one print component active in a page.
+-->
+
+<div id='eg-print-container'>
+  <!-- container for inline template compilation -->
+  <ng-container *ngIf="template">
+    <ng-container *ngTemplateOutlet="template; context:context">
+    </ng-container>
+  </ng-container>
+  <div id='eg-print-html-container'>
+  </div>
+<!-- container for pre-compiled HTML -->
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.component.ts b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
new file mode 100644
index 0000000..4f69949
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/print/print.component.ts
@@ -0,0 +1,133 @@
+import {Component, OnInit, TemplateRef, ElementRef, Renderer2} from '@angular/core';
+import {PrintService, PrintRequest} from './print.service';
+import {StoreService} from '@eg/core/store.service';
+
+ at Component({
+    selector: 'eg-print',
+    templateUrl: './print.component.html'
+})
+
+export class PrintComponent implements OnInit {
+
+    // Template that requires local processing
+    template: TemplateRef<any>;
+
+    // Context data used for processing the template.
+    context: any;
+
+    // Insertion point for externally-compiled templates
+    htmlContainer: Element;
+
+    isPrinting: boolean;
+
+    printQueue: PrintRequest[];
+
+    constructor(
+        private renderer: Renderer2,
+        private elm: ElementRef,
+        private store: StoreService,
+        private printer: PrintService) {
+        this.isPrinting = false;
+        this.printQueue = [];
+    }
+
+    ngOnInit() {
+        this.printer.onPrintRequest$.subscribe(
+            printReq => this.handlePrintRequest(printReq));
+
+        this.htmlContainer =
+            this.renderer.selectRootElement('#eg-print-html-container');
+    }
+
+    handlePrintRequest(printReq: PrintRequest) {
+
+        if (this.isPrinting) {
+            // Avoid print collisions by queuing requests as needed.
+            this.printQueue.push(printReq);
+            return;
+        }
+
+        this.isPrinting = true;
+
+        this.applyTemplate(printReq);
+
+        // Give templates a chance to render before printing
+        setTimeout(() => {
+            this.dispatchPrint(printReq);
+            this.reset();
+        });
+    }
+
+    applyTemplate(printReq: PrintRequest) {
+
+        if (printReq.template) {
+            // Inline template.  Let Angular do the interpolationwork.
+            this.template = printReq.template;
+            this.context = {$implicit: printReq.contextData};
+            return;
+        }
+
+        if (printReq.text && true /* !this.hatch.isActive */) {
+            // Insert HTML into the browser DOM for in-browser printing only.
+
+            if (printReq.contentType === 'text/plain') {
+                // Wrap text/plain content in pre's to prevent
+                // unintended html formatting.
+                printReq.text = `<pre>${printReq.text}</pre>`;
+            }
+
+            this.htmlContainer.innerHTML = printReq.text;
+        }
+    }
+
+    // Clear the print data
+    reset() {
+        this.isPrinting = false;
+        this.template = null;
+        this.context = null;
+        this.htmlContainer.innerHTML = '';
+
+        if (this.printQueue.length) {
+            this.handlePrintRequest(this.printQueue.pop());
+        }
+    }
+
+    dispatchPrint(printReq: PrintRequest) {
+
+        if (!printReq.text) {
+            // Sometimes the results come from an externally-parsed HTML
+            // template, other times they come from an in-page template.
+            printReq.text = this.elm.nativeElement.innerHTML;
+        }
+
+        // Retain a copy of each printed document in localStorage
+        // so it may be reprinted.
+        this.store.setLocalItem('eg.print.last_printed', {
+            content: printReq.text,
+            context: printReq.printContext,
+            content_type: printReq.contentType,
+            show_dialog: printReq.showDialog
+        });
+
+        if (0 /* this.hatch.isActive */) {
+            this.printViaHatch(printReq);
+        } else {
+            // Here the needed HTML is already in the page.
+            window.print();
+        }
+    }
+
+    printViaHatch(printReq: PrintRequest) {
+
+        // Send a full HTML document to Hatch
+        const html = `<html><body>${printReq.text}</body></html>`;
+
+        /*
+        this.hatch.print({
+            printContext: printReq.printContext,
+            content: html
+        });
+        */
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/print/print.service.ts b/Open-ILS/src/eg2/src/app/share/print/print.service.ts
new file mode 100644
index 0000000..5ae6844
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/print/print.service.ts
@@ -0,0 +1,41 @@
+import {Injectable, EventEmitter, TemplateRef} from '@angular/core';
+import {StoreService} from '@eg/core/store.service';
+
+export interface PrintRequest {
+    template?: TemplateRef<any>;
+    contextData?: any;
+    text?: string;
+    printContext: string;
+    contentType?: string; // defaults to text/html
+    showDialog?: boolean;
+}
+
+ at Injectable()
+export class PrintService {
+
+    onPrintRequest$: EventEmitter<PrintRequest>;
+
+    constructor(private store: StoreService) {
+        this.onPrintRequest$ = new EventEmitter<PrintRequest>();
+    }
+
+    print(printReq: PrintRequest) {
+        this.onPrintRequest$.emit(printReq);
+    }
+
+    reprintLast() {
+        const prev = this.store.getLocalItem('eg.print.last_printed');
+
+        if (prev) {
+            const req: PrintRequest = {
+                text: prev.content,
+                printContext: prev.context || 'default',
+                contentType: prev.content_type || 'text/html',
+                showDialog: Boolean(prev.show_dialog)
+            };
+
+            this.print(req);
+        }
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/string/string.component.ts b/Open-ILS/src/eg2/src/app/share/string/string.component.ts
new file mode 100644
index 0000000..f092a7e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/string/string.component.ts
@@ -0,0 +1,74 @@
+/*j
+ * <eg-string #helloStr text="Hello, {{name}}" i18n-text></eg-string>
+ *
+ * import {StringComponent} from '@eg/share/string.component';
+ * @ViewChild('helloStr') private helloStr: StringComponent;
+ * ...
+ * this.helloStr.currrent().then(s => console.log(s));
+ *
+ */
+import {Component, Input, OnInit, ElementRef, TemplateRef} from '@angular/core';
+import {StringService} from '@eg/share/string/string.service';
+
+ at Component({
+  selector: 'eg-string',
+  template: `
+    <span style='display:none'>
+    <ng-container *ngTemplateOutlet="template; context:ctx"></ng-container>
+    </span>
+  `
+})
+
+export class StringComponent implements OnInit {
+
+    // Storage key for future reference by the string service
+    @Input() key: string;
+
+    // Interpolation context
+    @Input() ctx: any;
+
+    // String template to interpolate
+    @Input() template: TemplateRef<any>;
+
+    // Static text -- no interpolation performed.
+    // This supersedes 'template'
+    @Input() text: string;
+
+    constructor(private elm: ElementRef, private strings: StringService) {
+        this.elm = elm;
+        this.strings = strings;
+    }
+
+    ngOnInit() {
+        // No key means it's an unregistered (likely static) string
+        // that does not need interpolation.
+        if (this.key) {
+            this.strings.register({
+                key: this.key,
+                resolver: (ctx: any) => {
+                    if (this.text) {
+                        // When passed text that does not require any
+                        // interpolation, just return it as-is.
+                        return Promise.resolve(this.text);
+                    } else {
+                        // Interpolate
+                        return this.current(ctx);
+                    }
+                }
+            });
+        }
+    }
+
+    // Apply the new context if provided, give our container a
+    // chance to update, then resolve with the current string.
+    // NOTE: talking to the native DOM element is not so great, but
+    // hopefully we can retire the String* code entirely once
+    // in-code translations are supported (Ang6?)
+    current(ctx?: any): Promise<string> {
+        if (ctx) { this.ctx = ctx; }
+        return new Promise(resolve => {
+            setTimeout(() => resolve(this.elm.nativeElement.textContent));
+        });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/string/string.service.ts b/Open-ILS/src/eg2/src/app/share/string/string.service.ts
new file mode 100644
index 0000000..88d0c8a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/string/string.service.ts
@@ -0,0 +1,78 @@
+import {Injectable} from '@angular/core';
+
+interface StringAssignment {
+    key: string;     // keyboard command
+    resolver: (ctx: any) => Promise<string>;
+}
+
+interface PendingInterpolation {
+    key: string;
+    ctx: any;
+    resolve: (string) => any;
+    reject: (string) => any;
+}
+
+ at Injectable()
+export class StringService {
+
+    strings: {[key: string]: StringAssignment} = {};
+
+    // This service can only interpolate one string at a time, since it
+    // maintains only one string component instance.  Avoid clobbering
+    // in-process interpolation requests by maintaining a request queue.
+    private pending: PendingInterpolation[];
+
+    constructor() {
+        this.pending = [];
+    }
+
+    register(assn: StringAssignment) {
+        this.strings[assn.key] = assn;
+    }
+
+    interpolate(key: string, ctx?: any): Promise<string> {
+
+        if (!this.strings[key]) {
+            return Promise.reject(`String key not found: "${key}"`);
+        }
+
+        return new Promise( (resolve, reject) => {
+            const pend: PendingInterpolation = {
+                key: key,
+                ctx: ctx,
+                resolve: resolve,
+                reject: reject
+            };
+
+            this.pending.push(pend);
+
+            // Avoid launching the pending string processer with >1
+            // pending, because the processor will have already started.
+            if (this.pending.length === 1) {
+                this.processPending();
+            }
+        });
+    }
+
+    processPending() {
+        const pstring = this.pending[0];
+        this.strings[pstring.key].resolver(pstring.ctx).then(
+            txt => {
+                pstring.resolve(txt);
+                this.pending.shift();
+                if (this.pending.length) {
+                    this.processPending();
+                }
+            },
+            err => {
+                pstring.reject(err);
+                this.pending.shift();
+                if (this.pending.length) {
+                    this.processPending();
+                }
+            }
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.component.css b/Open-ILS/src/eg2/src/app/share/toast/toast.component.css
new file mode 100644
index 0000000..1f70349
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/toast/toast.component.css
@@ -0,0 +1,11 @@
+#eg-toast-container {
+    min-width: 250px;
+    text-align: center;
+    border-radius: 2px;
+    padding: 10px;
+    position: fixed;
+    z-index: 1;
+    right: 15px;
+    bottom: 5px;
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.component.html b/Open-ILS/src/eg2/src/app/share/toast/toast.component.html
new file mode 100644
index 0000000..6aa1545
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/toast/toast.component.html
@@ -0,0 +1,3 @@
+<div id="eg-toast-container" *ngIf="message">
+  <ngb-alert [type]="message.style" (close)="dismiss(message)">{{message.text}}</ngb-alert>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.component.ts b/Open-ILS/src/eg2/src/app/share/toast/toast.component.ts
new file mode 100644
index 0000000..9503ffd
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/toast/toast.component.ts
@@ -0,0 +1,43 @@
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {ToastService, ToastMessage} from '@eg/share/toast/toast.service';
+
+const EG_TOAST_TIMEOUT = 3000;
+
+ at Component({
+  selector: 'eg-toast',
+  templateUrl: './toast.component.html',
+  styleUrls: ['./toast.component.css']
+})
+export class ToastComponent implements OnInit {
+
+    message: ToastMessage;
+
+    // track the most recent timeout event
+    timeout: any;
+
+    constructor(private toast: ToastService) {
+    }
+
+    ngOnInit() {
+        this.toast.messages$.subscribe(msg => this.show(msg));
+    }
+
+    show(msg: ToastMessage) {
+        this.dismiss(this.message);
+        this.message = msg;
+        this.timeout = setTimeout(
+            () => this.dismiss(this.message),
+            EG_TOAST_TIMEOUT
+        );
+    }
+
+    dismiss(msg: ToastMessage) {
+        this.message = null;
+        if (this.timeout) {
+            clearTimeout(this.timeout);
+            this.timeout = null;
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/toast/toast.service.ts b/Open-ILS/src/eg2/src/app/share/toast/toast.service.ts
new file mode 100644
index 0000000..5806592
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/toast/toast.service.ts
@@ -0,0 +1,39 @@
+import {Injectable, EventEmitter} from '@angular/core';
+
+export interface ToastMessage {
+    text: string;
+    style: string;
+}
+
+ at Injectable()
+export class ToastService {
+
+    messages$: EventEmitter<ToastMessage>;
+
+    constructor() {
+        this.messages$ = new EventEmitter<ToastMessage>();
+    }
+
+    sendMessage(msg: ToastMessage) {
+        this.messages$.emit(msg);
+    }
+
+    success(text: string) {
+        this.sendMessage({text: text, style: 'success'});
+    }
+
+    info(text: string) {
+        this.sendMessage({text: text, style: 'info'});
+    }
+
+    warning(text: string) {
+        this.sendMessage({text: text, style: 'warning'});
+    }
+
+    danger(text: string) {
+        this.sendMessage({text: text, style: 'danger'});
+    }
+
+    // Others?
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.css b/Open-ILS/src/eg2/src/app/share/tree/tree.component.css
new file mode 100644
index 0000000..0d29dd7
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/tree/tree.component.css
@@ -0,0 +1,19 @@
+
+.eg-tree-node-expandy .material-icons {
+  font-size: 16px;
+}
+
+.eg-tree-node {
+  padding: 2px;
+}
+
+.eg-tree-node.active {
+  background-color: rgba(0,0,0,.03);
+  border: 1px solid rgba(0,0,0,.125);
+  font-style: italic;
+}
+
+.eg-tree-node-nochild {
+  border-left: 2px dashed rgba(0,0,0,.125);
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.html b/Open-ILS/src/eg2/src/app/share/tree/tree.component.html
new file mode 100644
index 0000000..525fece
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/tree/tree.component.html
@@ -0,0 +1,20 @@
+
+
+<div class="eg-tree" *ngFor="let node of displayNodes()">
+  <div class="eg-tree-node-wrapper d-flex"
+    [ngStyle]="{'padding-left': (node.depth * 20) + 'px'}">
+    <div class="eg-tree-node-expandy">
+      <div *ngIf="node.children.length" (click)="node.toggleExpand()"
+        i18n-title title="Toggle Expand Node">
+        <span *ngIf="!node.expanded" class="material-icons">expand_more</span>
+        <span *ngIf="node.expanded" class="material-icons">expand_less</span>
+      </div>
+      <div *ngIf="!node.children.length" class="eg-tree-node-nochild">
+         
+      </div>
+    </div>
+    <div class="eg-tree-node" [ngClass]="{active : node.selected}">
+      <a [routerLink]="" (click)="handleNodeClick(node)">{{node.label}}</a>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts
new file mode 100644
index 0000000..d3fccda
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/tree/tree.component.ts
@@ -0,0 +1,60 @@
+import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
+import {Tree, TreeNode} from './tree';
+
+/*
+Tree Widget:
+
+<eg-tree
+    [tree]="myTree"
+    (nodeClicked)="nodeClicked($event)">
+</eg-tree>
+
+----
+
+constructor() {
+
+    const rootNode = new TreeNode({
+        id: 1,
+        label: 'Root',
+        children: [
+            new TreeNode({id: 2, label: 'Child'}),
+            new TreeNode({id: 3, label: 'Child2'})
+        ]
+    ]});
+
+    this.myTree = new Tree(rootNode);
+}
+
+nodeClicked(node: TreeNode) {
+    console.log('someone clicked on ' + node.label);
+}
+*/
+
+ at Component({
+    selector: 'eg-tree',
+    templateUrl: 'tree.component.html',
+    styleUrls: ['tree.component.css']
+})
+export class TreeComponent implements OnInit {
+
+    @Input() tree: Tree;
+    @Output() nodeClicked: EventEmitter<TreeNode>;
+
+    constructor() {
+        this.nodeClicked = new EventEmitter<TreeNode>();
+    }
+
+    ngOnInit() {}
+
+    displayNodes(): TreeNode[] {
+        return this.tree.nodeList(true);
+    }
+
+    handleNodeClick(node: TreeNode) {
+        this.tree.selectNode(node);
+        this.nodeClicked.emit(node);
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts
new file mode 100644
index 0000000..3894fcd
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/tree/tree.module.ts
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {TreeComponent} from './tree.component';
+
+ at NgModule({
+    declarations: [
+        TreeComponent
+    ],
+    imports: [
+        EgCommonModule
+    ],
+    exports: [
+        TreeComponent
+    ],
+    providers: [
+    ]
+})
+
+export class TreeModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/share/tree/tree.ts b/Open-ILS/src/eg2/src/app/share/tree/tree.ts
new file mode 100644
index 0000000..cca36d4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/tree/tree.ts
@@ -0,0 +1,133 @@
+
+export class TreeNode {
+    // Unique identifier
+    id: any;
+
+    // Display label
+    label: string;
+
+    // True if child nodes should be visible
+    expanded: boolean;
+
+    children: TreeNode[];
+
+    // Set by the tree.
+    depth: number;
+
+    // Set by the tree.
+    selected: boolean;
+
+    // Optional link to user-provided stuff.
+    // This field is ignored by the tree.
+    callerData: any;
+
+    constructor(values: {[key: string]: any}) {
+        this.children = [];
+        this.expanded = true;
+        this.depth = 0;
+        this.selected = false;
+
+        if (!values) { return; }
+
+        if ('id' in values) { this.id = values.id; }
+        if ('label' in values) { this.label = values.label; }
+        if ('children' in values) { this.children = values.children; }
+        if ('expanded' in values) { this.expanded = values.expanded; }
+        if ('callerData' in values) { this.callerData = values.callerData; }
+    }
+
+    toggleExpand() {
+        this.expanded = !this.expanded;
+    }
+}
+
+export class Tree {
+
+    rootNode: TreeNode;
+    idMap: {[id: string]: TreeNode};
+
+    constructor(rootNode?: TreeNode) {
+        this.rootNode = rootNode;
+        this.idMap = {};
+    }
+
+    // Returns a depth-first list of tree nodes
+    // Tweaks node attributes along the way to match the shape of the tree.
+    nodeList(filterHidden?: boolean): TreeNode[] {
+
+        const nodes = [];
+
+        const recurseTree =
+            (node: TreeNode, depth: number, hidden: boolean) => {
+            if (!node) { return; }
+
+            node.depth = depth++;
+            this.idMap[node.id + ''] = node;
+
+            if (hidden) {
+                // it could be confusing for a hidden node to be selected.
+                node.selected = false;
+            }
+
+            if (hidden && filterHidden) {
+                // Avoid adding hidden child nodes to the list.
+            } else {
+                nodes.push(node);
+            }
+
+            node.children.forEach(n => recurseTree(n, depth, !node.expanded));
+        };
+
+        recurseTree(this.rootNode, 0, false);
+        return nodes;
+    }
+
+    findNode(id: any): TreeNode {
+        if (this.idMap[id + '']) {
+            return this.idMap[id + ''];
+        } else {
+            // nodeList re-indexes all the nodes.
+            this.nodeList();
+            return this.idMap[id + ''];
+        }
+    }
+
+    findParentNode(node: TreeNode) {
+        const list = this.nodeList();
+        for (let idx = 0; idx < list.length; idx++) {
+            const pnode = list[idx];
+            if (pnode.children.filter(c => c.id === node.id).length) {
+                return pnode;
+            }
+        }
+        return null;
+    }
+
+    removeNode(node: TreeNode) {
+        if (!node) { return; }
+        const pnode = this.findParentNode(node);
+        if (pnode) {
+            pnode.children = pnode.children.filter(n => n.id !== node.id);
+        } else {
+            this.rootNode = null;
+        }
+    }
+
+    expandAll() {
+        this.nodeList().forEach(node => node.expanded = true);
+    }
+
+    collapseAll() {
+        this.nodeList().forEach(node => node.expanded = false);
+    }
+
+    selectedNode(): TreeNode {
+        return this.nodeList().filter(node => node.selected)[0];
+    }
+
+    selectNode(node: TreeNode) {
+        this.nodeList().forEach(n => n.selected = false);
+        node.selected = true;
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/share/util/audio.service.ts b/Open-ILS/src/eg2/src/app/share/util/audio.service.ts
new file mode 100644
index 0000000..3f3320a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/util/audio.service.ts
@@ -0,0 +1,78 @@
+/**
+ * Plays audio files (alerts, generally) by key name.  Each sound uses a
+ * dot-path to indicate  the sound.
+ *
+ * For example:
+ *
+ * this.audio.play('warning.checkout.no_item');
+ *
+ * URLs are tested in the following order until an audio file is found
+ * or no other paths are left to check.
+ *
+ * /audio/notifications/warning/checkout/not_found.wav
+ * /audio/notifications/warning/checkout.wav
+ * /audio/notifications/warning.wav
+ *
+ * Files are only played when sounds are configured to play via
+ * workstation settings.
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {ServerStoreService} from '@eg/core/server-store.service';
+const AUDIO_BASE_URL = '/audio/notifications/';
+
+ at Injectable()
+export class AudioService {
+
+    // map of requested audio path to resolved path
+    private urlCache: {[path: string]: string} = {};
+
+    constructor(private store: ServerStoreService) {}
+
+    play(path: string): void {
+        if (path) {
+            this.playUrl(path, path);
+        }
+    }
+
+    playUrl(path: string, origPath: string): void {
+        // console.debug(`audio: playUrl(${path}, ${origPath})`);
+
+        this.store.getItem('eg.audio.disable').then(audioDisabled => {
+            if (audioDisabled) { return; }
+
+            const url = this.urlCache[path] ||
+                AUDIO_BASE_URL + path.replace(/\./g, '/') + '.wav';
+
+            const player = new Audio(url);
+
+            player.onloadeddata = () => {
+                this.urlCache[origPath] = url;
+                player.play();
+                console.debug(`audio: ${url}`);
+            };
+
+            if (this.urlCache[path]) {
+                // when serving from the cache, avoid secondary URL lookups.
+                return;
+            }
+
+            player.onerror = () => {
+                // Unable to play path at the requested URL.
+
+                if (!path.match(/\./)) {
+                    // all fall-through options have been exhausted.
+                    // No path to play.
+                    console.warn(
+                        `No suitable URL found for path "${origPath}"`);
+                    return;
+                }
+
+                // Fall through to the next (more generic) option
+                path = path.replace(/\.[^\.]+$/, '');
+                this.playUrl(path, origPath);
+            };
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/share/util/pager.ts b/Open-ILS/src/eg2/src/app/share/util/pager.ts
new file mode 100644
index 0000000..267d4fc
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/share/util/pager.ts
@@ -0,0 +1,111 @@
+import {EventEmitter} from '@angular/core';
+
+/**
+ * Utility class for manage paged information.
+ */
+export class Pager {
+    offset = 0;
+    limit: number = null;
+    resultCount: number;
+    onChange$: EventEmitter<number>;
+
+    constructor() {
+        this.resultCount = null;
+        this.onChange$ = new EventEmitter<number>();
+    }
+
+    reset() {
+        this.resultCount = null;
+        this.offset = 0;
+    }
+
+    setLimit(l: number) {
+        if (l !== this.limit) {
+            this.limit = l;
+            this.setPage(1);
+        }
+    }
+
+    isFirstPage(): boolean {
+        return this.offset === 0;
+    }
+
+    isLastPage(): boolean {
+        return this.currentPage() === this.pageCount();
+    }
+
+    currentPage(): number {
+        return Math.floor(this.offset / this.limit) + 1;
+    }
+
+    increment(): void {
+        this.setPage(this.currentPage() + 1);
+    }
+
+    decrement(): void {
+        this.setPage(this.currentPage() - 1);
+    }
+
+    toFirst() {
+        if (!this.isFirstPage()) {
+            this.setPage(1);
+        }
+    }
+
+    toLast() {
+        if (!this.isLastPage()) {
+            this.setPage(this.pageCount());
+        }
+    }
+
+    setPage(page: number): void {
+        this.offset = (this.limit * (page - 1));
+        this.onChange$.emit(this.offset);
+    }
+
+    pageCount(): number {
+        if (this.resultCount === null) { return -1; }
+        let pages = this.resultCount / this.limit;
+        if (Math.floor(pages) < pages) {
+            pages = Math.floor(pages) + 1;
+        }
+        return pages;
+    }
+
+    // Returns a list of pages numbers with @pivot at the center
+    // or as close to center as possible.
+    // @pivot is 1-based for consistency with page numbers.
+    // pageRange(25, 10) => [21,22,...29,30]
+    pageRange(pivot: number, size: number): number[] {
+
+        const diff = Math.floor(size / 2);
+        let start = pivot <= diff ? 1 : pivot - diff + 1;
+
+        const pcount = this.pageCount();
+
+        if (start + size > pcount) {
+            start = pcount - size + 1;
+            if (start < 1) { start = 1; }
+        }
+
+        if (start + size > pcount) {
+            size = pcount;
+        }
+
+        return this.pageList().slice(start - 1, start - 1 + size);
+    }
+
+    pageList(): number[] {
+        const list = [];
+        for (let i = 1; i <= this.pageCount(); i++) {
+            list.push(i);
+        }
+        return list;
+    }
+
+    // Given a zero-based page-specific offset, return the where in the
+    // entire data set the row lives, 1-based for UI friendliness.
+    rowNumber(offset: number): number {
+        return this.offset + offset + 1;
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/about.component.html b/Open-ILS/src/eg2/src/app/staff/about.component.html
new file mode 100644
index 0000000..9e8e1c4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/about.component.html
@@ -0,0 +1,57 @@
+<eg-staff-banner bannerText="About Evergreen" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+  <div class="col-lg-4">
+    <div class="card">
+      <div class="card-header" i18n>Server Details</div>
+      <ul class="list-group list-group-flush">
+        <li class="list-group-item">
+          <div class="row pt-2">
+            <div class="col-lg-6" i18n>Evergreen Version</div>
+            <div class="col-lg-6">{{version}}</div>
+          </div>
+        </li>
+        <li class="list-group-item">
+          <div class="row pt-2">
+            <div class="col-lg-6" i18n>Hostname</div>
+            <div class="col-lg-6">{{server}}</div>
+          </div>
+        </li>
+      </ul>
+    </div><!-- card -->
+   </div>
+</div>
+<div class="row mt-4">
+  <div class="col-lg-8">
+    <h2 i18n>What is Evergreen?</h2>
+    <p i18n>Evergreen is library automation software that assists libraries
+       in day-to-day operations such as checking out materials, keeping
+       track of users, sharing resources among a group of libraries,
+       acquiring materials, and providing a web-based library catalog for
+       the public.
+    </p>
+    <p i18n>The open-source community developing and supporting Evergreen is
+       marked by a high degree of participation from developers and from
+       the librarians who use the software.
+    </p>
+    <p i18n>
+      More information can be found at 
+      <a href="https://evergreen-ils.org">https://evergreen-ils.org</a>. 
+      For help in using Evergreen, see our documentation at 
+      <a href="http://docs.evergreen-ils.org">http://docs.evergreen-ils.org</a>.
+    </p>
+    <p i18n>
+      Evergreen is Copyright © Georgia Public Library Service - 
+      A Unit of the University System of Georgia, and others. The 
+      Evergreen software is distributed under the 
+      <a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html">
+      GNU General Public License, Version 2.
+      </a>
+    </p>
+  </div>
+</div>
+
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/about.component.ts b/Open-ILS/src/eg2/src/app/staff/about.component.ts
new file mode 100644
index 0000000..a494956
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/about.component.ts
@@ -0,0 +1,25 @@
+import {Component, OnInit} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+
+ at Component({
+    selector: 'eg-about',
+    templateUrl: 'about.component.html'
+})
+
+export class AboutComponent implements OnInit {
+    server: string;
+    version: string;
+
+    constructor(
+        private net: NetService
+    ) {}
+
+    ngOnInit() {
+        this.server = window.location.hostname;
+        this.net.request(
+            'open-ils.actor',
+            'opensrf.open-ils.system.ils_version'
+        ).subscribe(v => this.version = v);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html
new file mode 100644
index 0000000..58fcbf6
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html
@@ -0,0 +1,60 @@
+<eg-staff-banner bannerText="Acquisitions Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="container">
+  <eg-link-table columnCount="3">
+    <eg-link-table-link i18n-label label="Cancel Reasons"
+      routerLink="/staff/admin/acq/cancel_reason"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Event Types"
+      routerLink="/staff/admin/acq/claim_event_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Policies"
+      routerLink="/staff/admin/acq/claim_policy"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Policy Actions"
+      routerLink="/staff/admin/acq/claim_policy_action"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Claim Types"
+      routerLink="/staff/admin/acq/claim_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Currency Types"
+      routerLink="/staff/admin/acq/currency_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Distribution Formulas"
+      url="/eg/staff/admin/acq/conify/distribution_formula"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/distribution_formula"></eg-link-table-link>
+      -->
+    <eg-link-table-link i18n-label label="EDI Accounts"
+      routerLink="/staff/admin/acq/edi_account"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="EDI Messages"
+      routerLink="/staff/admin/acq/edi_message"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="EDI Attribute Sets"
+      url="/eg/staff/admin/acq/edi_attr_set"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/edi_attr_set"></eg-link-table-link>
+      -->
+    <eg-link-table-link i18n-label label="Exchange Rates"
+      routerLink="/staff/admin/acq/exchange_rate"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Fund Tags"
+      routerLink="/staff/admin/acq/fund_tag"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Funding Sources"
+      url="/eg/staff/admin/acq/funding_source/list"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/funding_source"></eg-link-table-link>
+      -->
+    <!-- TODO fund admin page w/ year filter and rollover -->
+    <eg-link-table-link i18n-label label="Funds"
+      url="/eg/staff/admin/acq/fund/list"></eg-link-table-link>
+      <!-- routerLink="/staff/admin/acq/fund" -->
+    <eg-link-table-link i18n-label label="Invoice Item Types"
+      routerLink="/staff/admin/acq/invoice_item_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Invoice Payment Method"
+      routerLink="/staff/admin/acq/invoice_payment_method"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Line Item Alerts"
+      routerLink="/staff/admin/acq/lineitem_alert_text"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Line Item MARC Attribute Definitions"
+      routerLink="/staff/admin/acq/lineitem_marc_attr_definition"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Providers"
+      url="/eg/staff/admin/acq/conify/provider"></eg-link-table-link>
+      <!-- TODO
+      routerLink="/staff/admin/acq/provider"></eg-link-table-link>
+      -->
+  </eg-link-table>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts
new file mode 100644
index 0000000..dc47db9
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts
@@ -0,0 +1,11 @@
+import {Component, Input, ViewChildren,
+    AfterViewInit, QueryList} from '@angular/core';
+
+ at Component({
+    templateUrl: './admin-acq-splash.component.html'
+})
+
+export class AdminAcqSplashComponent {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts
new file mode 100644
index 0000000..5c57b3d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts
@@ -0,0 +1,24 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AdminAcqRoutingModule} from './routing.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {AdminAcqSplashComponent} from './admin-acq-splash.component';
+
+ at NgModule({
+  declarations: [
+      AdminAcqSplashComponent
+  ],
+  imports: [
+    AdminCommonModule,
+    AdminAcqRoutingModule
+  ],
+  exports: [
+  ],
+  providers: [
+  ]
+})
+
+export class AdminAcqModule {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts
new file mode 100644
index 0000000..c07dd5d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts
@@ -0,0 +1,22 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AdminAcqSplashComponent} from './admin-acq-splash.component';
+import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+
+const routes: Routes = [{
+    path: 'splash',
+    component: AdminAcqSplashComponent
+}, {
+    path: ':table',
+    component: BasicAdminPageComponent,
+    // All ACQ admin pages cover data in the acq.* schema.  No need to
+    // duplicate it within the URL path.  Pass it manually instead.
+    data: [{schema: 'acq'}]
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminAcqRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
new file mode 100644
index 0000000..0d6be84
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
@@ -0,0 +1,61 @@
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService} from '@eg/core/idl.service';
+
+/**
+ * Generic IDL class editor page.
+ */
+
+ at Component({
+    template: `
+      <eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
+      </eg-staff-banner>
+      <eg-admin-page persistKeyPfx="{{persistKeyPfx}}" idlClass="{{idlClass}}"></eg-admin-page>
+    `
+})
+
+export class BasicAdminPageComponent implements OnInit {
+
+    idlClass: string;
+    classLabel: string;
+    persistKeyPfx: string;
+
+    constructor(
+        private route: ActivatedRoute,
+        private idl: IdlService
+    ) {
+    }
+
+    ngOnInit() {
+        let schema = this.route.snapshot.paramMap.get('schema');
+        if (!schema) {
+            // Allow callers to pass the schema via static route data
+            const data = this.route.snapshot.data[0];
+            if (data) { schema = data.schema; }
+        }
+        const table = schema + '.' + this.route.snapshot.paramMap.get('table');
+
+        // Set the prefix to "server", "local", "workstation",
+        // extracted from the URL path.
+        this.persistKeyPfx = this.route.snapshot.parent.url[0].path;
+        if (this.persistKeyPfx === 'acq') {
+            // ACQ is a special case, becaus unlike 'server', 'local',
+            // 'workstation', the schema ('acq') is the root of the path.
+            this.persistKeyPfx = '';
+        }
+
+        Object.keys(this.idl.classes).forEach(class_ => {
+            const classDef = this.idl.classes[class_];
+            if (classDef.table === table) {
+                this.idlClass = class_;
+                this.classLabel = classDef.label;
+            }
+        });
+
+        if (!this.idlClass) {
+            throw new Error('Unable to find IDL class for table ' + table);
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/common.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/common.module.ts
new file mode 100644
index 0000000..5bd71d3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/common.module.ts
@@ -0,0 +1,28 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {LinkTableComponent, LinkTableLinkComponent} from '@eg/staff/share/link-table/link-table.component';
+import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+
+ at NgModule({
+  declarations: [
+    LinkTableComponent,
+    LinkTableLinkComponent,
+    BasicAdminPageComponent
+  ],
+  imports: [
+    StaffCommonModule
+  ],
+  exports: [
+    StaffCommonModule,
+    LinkTableComponent,
+    LinkTableLinkComponent,
+    BasicAdminPageComponent
+  ],
+  providers: [
+  ]
+})
+
+export class AdminCommonModule {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts
new file mode 100644
index 0000000..a93f9ee
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts
@@ -0,0 +1,23 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [{
+  path: '',
+  children : [
+  { path: 'workstation',
+   loadChildren: '@eg/staff/admin/workstation/routing.module#AdminWsRoutingModule'
+  }, {
+    path: 'server',
+    loadChildren: '@eg/staff/admin/server/admin-server.module#AdminServerModule'
+  }, {
+    path: 'acq',
+    loadChildren: '@eg/staff/admin/acq/admin-acq.module#AdminAcqModule'
+  }]
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
new file mode 100644
index 0000000..5e6058d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
@@ -0,0 +1,99 @@
+<eg-staff-banner bannerText="Server Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="container">
+  <eg-link-table columnCount="3">
+    <eg-link-table-link i18n-label label="Actor Stat Cat Sip Fields"  
+      routerLink="/staff/admin/server/actor/stat_cat_sip_fields"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Age Hold Protect Rules"  
+      routerLink="/staff/admin/server/config/rule_age_hold_protect"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Asset Stat Cat Sip Fields"  
+      routerLink="/staff/admin/server/asset/stat_cat_sip_fields"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Browse Axes"  
+      routerLink="/staff/admin/server/authority/browse_axis"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Control Sets"  
+      routerLink="/staff/admin/server/authority/control_set"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Heading Fields"  
+      routerLink="/staff/admin/server/authority/heading_field"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Authority Thesauri"  
+      routerLink="/staff/admin/server/authority/thesaurus"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Best-Hold Selection Sort Order"  
+      routerLink="/staff/admin/server/config/best_hold_order"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Billing Types"  
+      routerLink="/staff/admin/server/config/billing_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Call Number Prefixes"  
+      routerLink="/staff/admin/server/asset/call_number_prefix"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Call Number Suffixes"  
+      routerLink="/staff/admin/server/asset/call_number_suffix"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Duration Rules"  
+      routerLink="/staff/admin/server/config/rule_circ_duration"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Limit Groups"  
+      routerLink="/staff/admin/server/config/circ_limit_group"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Matchpoint Weights"  
+      routerLink="/staff/admin/server/config/circ_matrix_weights"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Max Fine Rules"  
+      routerLink="/staff/admin/server/config/rule_max_fine"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Modifiers"  
+      routerLink="/staff/admin/server/config/circ_modifier"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Circulation Recurring Fine Rules"  
+      routerLink="/staff/admin/server/config/rule_recurring_fine"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Copy Statuses"  
+      routerLink="/staff/admin/server/config/copy_status"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Copy Tag Types"  
+      routerLink="/staff/admin/server/config/copy_tag_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Custom Org Unit Trees"  
+      url="/eg/staff/admin/server/actor/org_unit_custom_tree"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Floating Groups"  
+      routerLink="/staff/admin/server/config/floating_group"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Global Flags"  
+      routerLink="/staff/admin/server/config/global_flag"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Hard Due Date Changes"  
+      routerLink="/staff/admin/server/config/hard_due_date"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Hold Matchpoint Weights"  
+      routerLink="/staff/admin/server/config/hold_matrix_weights"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Import Match Sets"  
+      routerLink="/staff/admin/server/vandelay/match_set"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Coded Value Maps"  
+      routerLink="/staff/admin/server/config/coded_value_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Import Remove Fields"  
+      routerLink="/staff/admin/server/vandelay/import_bib_trash_group"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Record Attributes"  
+      routerLink="/staff/admin/server/config/record_attr_definition"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Class FTS Maps"  
+      routerLink="/staff/admin/server/config/metabib_class_ts_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Classes"  
+      routerLink="/staff/admin/server/config/metabib_class"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Field FTS Maps"  
+      routerLink="/staff/admin/server/config/metabib_field_ts_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Search/Facet Fields"  
+      routerLink="/staff/admin/server/config/metabib_field"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="MARC Tag Tables"  
+      routerLink="/staff/admin/server/config/marc_field"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Org Unit Proximity Adjustments"  
+      routerLink="/staff/admin/server/actor/org_unit_proximity_adjustment"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Organization Types"  
+      url="/eg/staff/admin/server/legacy/actor/org_unit_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Org Unit Setting Types"  
+      routerLink="/staff/admin/server/config/org_unit_setting_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Organizational Units"  
+      url="/eg/staff/admin/server/legacy/actor/org_unit"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Permission Groups"  
+      url="/eg/staff/admin/server/legacy/permission/grp_tree"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Permissions"  
+      routerLink="/staff/admin/server/permission/perm_list"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Remote Accounts"  
+      routerLink="/staff/admin/server/config/remote_account"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="SMS Carriers"  
+      routerLink="/staff/admin/server/config/sms_carrier"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="User Activity Types"  
+      routerLink="/staff/admin/server/config/usr_activity_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="User Setting Types"  
+      routerLink="/staff/admin/server/config/usr_setting_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Weights Association"  
+      routerLink="/staff/admin/server/config/weight_assoc"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Z39.50 Index Field Maps"  
+      routerLink="/staff/admin/server/config/z3950_index_field_map"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Z39.50 Servers"  
+      routerLink="/staff/admin/server/config/z3950_source"></eg-link-table-link>
+  </eg-link-table>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts
new file mode 100644
index 0000000..9debf57
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts
@@ -0,0 +1,11 @@
+import {Component, Input, ViewChildren,
+    AfterViewInit, QueryList} from '@angular/core';
+
+ at Component({
+    templateUrl: './admin-server-splash.component.html'
+})
+
+export class AdminServerSplashComponent {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
new file mode 100644
index 0000000..1f00a8a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
@@ -0,0 +1,24 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AdminServerRoutingModule} from './routing.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {AdminServerSplashComponent} from './admin-server-splash.component';
+
+ at NgModule({
+  declarations: [
+      AdminServerSplashComponent
+  ],
+  imports: [
+    AdminCommonModule,
+    AdminServerRoutingModule
+  ],
+  exports: [
+  ],
+  providers: [
+  ]
+})
+
+export class AdminServerModule {
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
new file mode 100644
index 0000000..ceb60f2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {AdminServerSplashComponent} from './admin-server-splash.component';
+import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component';
+
+const routes: Routes = [{
+    path: 'splash',
+    component: AdminServerSplashComponent
+}, {
+    path: ':schema/:table',
+    component: BasicAdminPageComponent
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminServerRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts
new file mode 100644
index 0000000..acdb9a1
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts
@@ -0,0 +1,14 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [{
+    path: 'workstations',
+    loadChildren: '@eg/staff/admin/workstation/workstations/workstations.module#ManageWorkstationsModule'
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class AdminWsRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts
new file mode 100644
index 0000000..cebf138
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts
@@ -0,0 +1,25 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {WorkstationsComponent} from './workstations.component';
+
+// Note that we need a path value (e.g. 'manage') because without it
+// there is nothing for the router to match, unless we rely on the parent
+// module to handle all of our routing for us.
+const routes: Routes = [
+  {
+    path: 'manage',
+    component: WorkstationsComponent
+  }, {
+    path: 'remove/:remove',
+    component: WorkstationsComponent
+  }
+];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class WorkstationsRoutingModule {
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html
new file mode 100644
index 0000000..a2358d2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html
@@ -0,0 +1,92 @@
+<eg-staff-banner bannerText="Workstation Administration" i18n-bannerText>
+</eg-staff-banner>
+
+<!-- this will remain hidden until opened -->
+<eg-confirm-dialog 
+  #workstationExistsDialog 
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Workstation Exists"
+  dialogBody='Workstation "{{newName}}" already exists.  Use it anyway?'>
+</eg-confirm-dialog>
+
+<div class="row">
+  <div class="col-lg-8 offset-1 mt-3">
+    <div class="alert alert-warning" *ngIf="removeWorkstation" i18n>
+      Workstation {{removeWorkstation}} is no longer valid.  Removing registration.
+    </div>
+    <div class="alert alert-danger" *ngIf="workstations.length == 0">
+      <span i18n>Please register a workstation.</span>
+    </div>
+
+    <div class="row">
+      <div class="col" i18n>Register a New Workstation For This Browser</div>
+    </div>
+    <div class="row mt-2">
+      <div class="col-lg-2">
+        <eg-org-select 
+          [applyDefault]="true"
+          (onChange)="orgOnChange($event)"
+          [hideOrgs]="hideOrgs"
+          [disableOrgs]="disableOrgs"
+          i18n-placeholder
+          placeholder="Owner..." >
+        </eg-org-select>
+      </div>
+      <div class="col-lg-6">
+        <div class="input-group">
+          <input type='text'
+            class='form-control'
+            i18n-title
+            title="Workstation Name"
+            i18n-placeholder
+            placeholder="Workstation Name..."
+            [(ngModel)]='newName'/>
+          <div class="input-group-btn">
+            <button class="btn btn-outline-dark" 
+              [disabled]="!newName || !newOwner"
+              (click)="registerWorkstation()">
+              <span i18n>Register</span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="row mt-3 pt-3 border border-left-0 border-right-0 border-bottom-0 border-light">
+      <div class="col">
+        <span i18n>Workstations Registered With This Browser</span>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col-lg-8">
+        <select class="form-control" [(ngModel)]="selectedName">
+          <option *ngFor="let ws of workstations" value="{{ws.name}}">
+            <span *ngIf="ws.name == defaultName" i18n>
+              {{ws.name}} (Default)
+            </span>
+            <span *ngIf="ws.name != defaultName">
+              {{ws.name}}
+            </span>
+          </option>
+        </select>
+      </div>
+    </div>
+    <div class="row mt-2">
+      <div class="col-lg-6">
+        <button i18n class="btn btn-success" 
+          (click)="useNow()" [disabled]="!selected">
+          Use Now
+        </button>
+        <button i18n class="btn btn-outline-dark" 
+          (click)="setDefault()" [disabled]="!selected">
+          Mark As Default
+        </button>
+        <button i18n class="btn btn-danger"
+          (click)="removeSelected()"
+          [disabled]="!selected || !canDeleteSelected()">
+          Remove
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
new file mode 100644
index 0000000..a5c72e2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
@@ -0,0 +1,186 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {StoreService} from '@eg/core/store.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {EventService} from '@eg/core/event.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+// Slim version of the WS that's stored in the cache.
+interface Workstation {
+    id: number;
+    name: string;
+    owning_lib: number;
+}
+
+ at Component({
+  templateUrl: 'workstations.component.html'
+})
+export class WorkstationsComponent implements OnInit {
+
+    selectedName: string;
+    workstations: Workstation[] = [];
+    removeWorkstation: string;
+    newOwner: IdlObject;
+    newName: string;
+    defaultName: string;
+
+    @ViewChild('workstationExistsDialog')
+    private wsExistsDialog: ConfirmDialogComponent;
+
+    // Org selector options.
+    hideOrgs: number[];
+    disableOrgs: number[];
+    orgOnChange = (org: IdlObject): void => {
+        this.newOwner = org;
+    }
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private evt: EventService,
+        private net: NetService,
+        private store: StoreService,
+        private auth: AuthService,
+        private org: OrgService,
+        private perm: PermService
+    ) {}
+
+    ngOnInit() {
+        this.workstations = this.store.getLocalItem('eg.workstation.all') || [];
+        this.defaultName = this.store.getLocalItem('eg.workstation.default');
+        this.selectedName = this.auth.workstation() || this.defaultName;
+        const rm = this.route.snapshot.paramMap.get('remove');
+        if (rm) {
+            this.removeSelected(this.removeWorkstation = rm);
+        }
+
+        // TODO: use the org selector limitPerm option
+        this.perm.hasWorkPermAt(['REGISTER_WORKSTATION'], true)
+        .then(perms => {
+            // Disable org units that cannot have users and any
+            // that this user does not have work perms for.
+            this.disableOrgs =
+                this.org.filterList({canHaveUsers : false}, true)
+                .concat(this.org.filterList(
+                    {notInList : perms.REGISTER_WORKSTATION}, true));
+        });
+    }
+
+    selected(): Workstation {
+        return this.workstations.filter(
+          ws => ws.name === this.selectedName)[0];
+    }
+
+    useNow(): void {
+        if (this.selected()) {
+            this.router.navigate(['/staff/login'],
+                {queryParams: {workstation: this.selected().name}});
+        }
+    }
+
+    setDefault(): void {
+      if (this.selected()) {
+            this.defaultName = this.selected().name;
+            this.store.setLocalItem('eg.workstation.default', this.defaultName);
+        }
+    }
+
+    removeSelected(name?: string): void {
+        if (!name) {
+            name = this.selected().name;
+        }
+
+        this.workstations = this.workstations.filter(w => w.name !== name);
+        this.store.setLocalItem('eg.workstation.all', this.workstations);
+
+        if (this.defaultName === name) {
+            this.defaultName = null;
+            this.store.removeLocalItem('eg.workstation.default');
+        }
+    }
+
+    canDeleteSelected(): boolean {
+        return true;
+    }
+
+    registerWorkstation(): void {
+        console.log(`Registering new workstation ` +
+            `"${this.newName}" at ${this.newOwner.shortname()}`);
+
+        this.newName = this.newOwner.shortname() + '-' + this.newName;
+
+        this.registerWorkstationApi().then(
+            wsId => this.registerWorkstationLocal(wsId),
+            notOk => console.log('Workstation registration canceled/failed')
+        );
+    }
+
+    private handleCollision(): Promise<number> {
+        return new Promise((resolve, reject) => {
+            this.wsExistsDialog.open()
+            .then(
+                confirmed => {
+                    this.registerWorkstationApi(true).then(
+                        wsId => resolve(wsId),
+                        notOk => reject(notOk)
+                    );
+                },
+                dismissed => reject(dismissed)
+            );
+        });
+    }
+
+
+    private registerWorkstationApi(override?: boolean): Promise<number> {
+        let method = 'open-ils.actor.workstation.register';
+        if (override) {
+            method += '.override';
+        }
+
+        return new Promise((resolve, reject) => {
+            this.net.request(
+                'open-ils.actor', method,
+                this.auth.token(), this.newName, this.newOwner.id()
+            ).subscribe(wsId => {
+                const evt = this.evt.parse(wsId);
+                if (evt) {
+                    if (evt.textcode === 'WORKSTATION_NAME_EXISTS') {
+                        this.handleCollision().then(
+                            id => resolve(id),
+                            notOk => reject(notOk)
+                        );
+                    } else {
+                        console.error(`Registration failed ${evt}`);
+                        reject();
+                    }
+                } else {
+                   resolve(wsId);
+                }
+            });
+        });
+    }
+
+    private registerWorkstationLocal(wsId: number) {
+        const ws: Workstation = {
+            id: wsId,
+            name: this.newName,
+            owning_lib: this.newOwner.id()
+        };
+
+        this.workstations.push(ws);
+        this.store.setLocalItem('eg.workstation.all', this.workstations);
+        this.newName = '';
+        // when registering our first workstation, mark it as the
+        // default and show it as selected in the ws selector.
+        if (this.workstations.length === 1) {
+            this.selectedName = ws.name;
+            this.setDefault();
+        }
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts
new file mode 100644
index 0000000..cbd8dd6
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts
@@ -0,0 +1,18 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {WorkstationsRoutingModule} from './routing.module';
+import {WorkstationsComponent} from './workstations.component';
+
+ at NgModule({
+  declarations: [
+    WorkstationsComponent,
+  ],
+  imports: [
+    StaffCommonModule,
+    WorkstationsRoutingModule
+  ]
+})
+
+export class ManageWorkstationsModule {}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.html
new file mode 100644
index 0000000..1596454
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.html
@@ -0,0 +1,6 @@
+<!-- search form sits atop every catalog page -->
+<eg-catalog-search-form></eg-catalog-search-form>
+
+<!-- search results, record details, etc. -->
+<router-outlet></router-outlet>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts
new file mode 100644
index 0000000..8b2206c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts
@@ -0,0 +1,18 @@
+import {Component, OnInit} from '@angular/core';
+import {StaffCatalogService} from './catalog.service';
+
+ at Component({
+  templateUrl: 'catalog.component.html'
+})
+export class CatalogComponent implements OnInit {
+
+    constructor(private staffCat: StaffCatalogService) {}
+
+    ngOnInit() {
+        // Create the search context that will be used by all of my
+        // child components.  After initial creation, the context is
+        // reset and updated as needed to apply new search parameters.
+        this.staffCat.createContext();
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
new file mode 100644
index 0000000..20e17a0
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
@@ -0,0 +1,44 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
+import {CatalogRoutingModule} from './routing.module';
+import {CatalogComponent} from './catalog.component';
+import {SearchFormComponent} from './search-form.component';
+import {ResultsComponent} from './result/results.component';
+import {RecordComponent} from './record/record.component';
+import {CopiesComponent} from './record/copies.component';
+import {ResultPaginationComponent} from './result/pagination.component';
+import {ResultFacetsComponent} from './result/facets.component';
+import {ResultRecordComponent} from './result/record.component';
+import {StaffCatalogService} from './catalog.service';
+import {RecordPaginationComponent} from './record/pagination.component';
+import {RecordActionsComponent} from './record/actions.component';
+import {HoldingsService} from '@eg/staff/share/holdings.service';
+
+ at NgModule({
+  declarations: [
+    CatalogComponent,
+    ResultsComponent,
+    RecordComponent,
+    CopiesComponent,
+    SearchFormComponent,
+    ResultRecordComponent,
+    ResultFacetsComponent,
+    ResultPaginationComponent,
+    RecordPaginationComponent,
+    RecordActionsComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    CatalogCommonModule,
+    CatalogRoutingModule
+  ],
+  providers: [
+    StaffCatalogService,
+    HoldingsService
+  ]
+})
+
+export class CatalogModule {
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
new file mode 100644
index 0000000..1e50d9b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
@@ -0,0 +1,87 @@
+import {Injectable} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+
+/**
+ * Shared bits needed by the staff version of the catalog.
+ */
+
+ at Injectable()
+export class StaffCatalogService {
+
+    searchContext: CatalogSearchContext;
+    routeIndex = 0;
+    defaultSearchOrg: IdlObject;
+    defaultSearchLimit: number;
+
+    // TODO: does unapi support pref-lib for result-page copy counts?
+    prefOrg: IdlObject;
+
+    // Cache the currently selected detail record (i.g. catalog/record/123)
+    // summary so the record detail component can avoid duplicate fetches
+    // during record tab navigation.
+    currentDetailRecordSummary: any;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private org: OrgService,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService
+    ) { }
+
+    createContext(): void {
+        // Initialize the search context from the load-time URL params.
+        // Do this here so the search form and other context data are
+        // applied on every page, not just the search results page.  The
+        // search results pages will handle running the actual search.
+        this.searchContext =
+            this.catUrl.fromUrlParams(this.route.snapshot.queryParamMap);
+
+        this.searchContext.org = this.org; // service, not searchOrg
+        this.searchContext.isStaff = true;
+        this.applySearchDefaults();
+    }
+
+    applySearchDefaults(): void {
+        if (!this.searchContext.searchOrg) {
+            this.searchContext.searchOrg =
+                this.defaultSearchOrg || this.org.root();
+        }
+
+        if (!this.searchContext.pager.limit) {
+            this.searchContext.pager.limit = this.defaultSearchLimit || 20;
+        }
+    }
+
+    /**
+     * Redirect to the search results page while propagating the current
+     * search paramters into the URL.  Let the search results component
+     * execute the actual search.
+     */
+    search(): void {
+        if (!this.searchContext.isSearchable()) { return; }
+
+        const params = this.catUrl.toUrlParams(this.searchContext);
+
+        // Force a new search every time this method is called, even if
+        // it's the same as the active search.  Since router navigation
+        // exits early when the route + params is identical, add a
+        // random token to the route params to force a full navigation.
+        // This also resolves a problem where only removing secondary+
+        // versions of a query param fail to cause a route navigation.
+        // (E.g. going from two query= params to one).  Investigation
+        // pending.
+        params.ridx = '' + this.routeIndex++;
+
+        this.router.navigate(
+          ['/staff/catalog/search'], {queryParams: params});
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
new file mode 100644
index 0000000..6fd9454
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
@@ -0,0 +1,70 @@
+
+<eg-string key="catalog.record.toast.conjoined" 
+  i18n-text text="Conjoined Record Target Set"></eg-string>
+<eg-string key="catalog.record.toast.overlay" 
+  i18n-text text="Record Overlay Target Set"></eg-string>
+<eg-string key="catalog.record.toast.holdTransfer" 
+  i18n-text text="Hold Transfer Target Set"></eg-string>
+<eg-string key="catalog.record.toast.volumeTransfer" 
+  i18n-text text="Volume Transfer Target Set"></eg-string>
+<eg-string key="catalog.record.toast.cleared" 
+  text="Record Marks Cleared"></eg-string>
+
+<eg-record-bucket-dialog #recordBucketDialog [recordId]="recId">
+</eg-record-bucket-dialog>
+
+<div class="row ml-0 mr-0">
+
+  <button class="btn btn-info ml-1" (click)="addVolumes()" i18n>
+    Add Volumes
+  </button>
+
+  <div ngbDropdown placement="bottom-right" class="ml-1">
+    <button class="btn btn-info" id="actionsForDd" 
+      ngbDropdownToggle i18n>Mark For...</button>
+    <div ngbDropdownMenu aria-labelledby="actionsForDd">
+      <button class="dropdown-item" (click)="mark('conjoined')">
+        <span i18n>
+          Conjoined Items<ng-container *ngIf="targets.conjoined.current"> 
+            (Currently {{targets.conjoined.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="mark('overlay')">
+        <span i18n>
+          Overlay Target<ng-container *ngIf="targets.overlay.current"> 
+            (Currently {{targets.overlay.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="mark('holdTransfer')">
+        <span i18n>
+          Title Hold Transfer<ng-container *ngIf="targets.holdTransfer.current"> 
+            (Currently {{targets.holdTransfer.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="mark('volumeTransfer')">
+        <span i18n>
+          Volume Transfer<ng-container *ngIf="targets.volumeTransfer.current"> 
+            (Currently {{targets.volumeTransfer.current}})</ng-container>
+        </span>
+      </button>
+      <button class="dropdown-item" (click)="clearMarks()">
+        <span i18n>Reset Record Marks</span>
+      </button>
+    </div>
+  </div>
+
+  <div ngbDropdown placement="bottom-right" class="ml-1">
+    <button class="btn btn-info" id="otherActionsForDd" 
+      ngbDropdownToggle i18n>Other Actions</button>
+    <div ngbDropdownMenu aria-labelledby="otherActionsForDd">
+      <button class="dropdown-item" (click)="recordBucketDialog.open({size: 'lg'})">
+        <span i18n>Add To Bucket</span>
+      </button>
+      <a class="dropdown-item" 
+        href="/eg/staff/acq/legacy/lineitem/related/{{recId}}?target=bib">
+        <span i18n>View/Place Orders</span>
+      </a>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
new file mode 100644
index 0000000..b65bfae
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
@@ -0,0 +1,96 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router} from '@angular/router';
+import {StoreService} from '@eg/core/store.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {StaffCatalogService} from '../catalog.service';
+import {StringService} from '@eg/share/string/string.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {HoldingsService} from '@eg/staff/share/holdings.service';
+
+ at Component({
+  selector: 'eg-catalog-record-actions',
+  templateUrl: 'actions.component.html'
+})
+export class RecordActionsComponent implements OnInit {
+
+    recId: number;
+    initDone = false;
+    searchContext: CatalogSearchContext;
+
+    targets = {
+        conjoined: {
+          key: 'eg.cat.marked_conjoined_record',
+          current: null
+        },
+        overlay: {
+            key: 'eg.cat.marked_overlay_record',
+            current: null
+        },
+        holdTransfer: {
+            key: 'eg.circ.hold.title_transfer_target',
+            current: null
+        },
+        volumeTransfer: {
+            key: 'eg.cat.marked_volume_transfer_record',
+            current: null
+        }
+    };
+
+    @Input() set recordId(recId: number) {
+        this.recId = recId;
+        if (this.initDone) {
+            // Fire any record specific actions here
+        }
+    }
+
+    constructor(
+        private router: Router,
+        private store: StoreService,
+        private strings: StringService,
+        private toast: ToastService,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService,
+        private holdings: HoldingsService
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+
+        Object.keys(this.targets).forEach(name => {
+            const target = this.targets[name];
+            target.current = this.store.getLocalItem(target.key);
+        });
+    }
+
+    mark(name: string) {
+        const target = this.targets[name];
+        target.current = this.recId;
+        this.store.setLocalItem(target.key, this.recId);
+        this.strings.interpolate('catalog.record.toast.' + name)
+            .then(txt => this.toast.success(txt));
+    }
+
+    clearMarks() {
+        Object.keys(this.targets).forEach(name => {
+            const target = this.targets[name];
+            target.current = null;
+            this.store.removeLocalItem(target.key);
+        });
+        this.strings.interpolate('catalog.record.toast.cleared')
+            .then(txt => this.toast.success(txt));
+    }
+
+    // TODO: Support adding copies to existing volumes by getting
+    // selected volumes from the holdings grid.
+    // TODO: Support adding like volumes by getting selected
+    // volumes from the holdings grid.
+    addVolumes() {
+        this.holdings.spawnAddHoldingsUi(this.recId);
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html
new file mode 100644
index 0000000..e7d8249
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html
@@ -0,0 +1,53 @@
+<ng-template #cnTemplate let-copy="row">
+  {{copy.call_number_prefix_label}}
+  {{copy.call_number_label}}
+  {{copy.call_number_suffix_label}}
+</ng-template>
+
+<ng-template #barcodeTemplate let-copy="row">
+  <div>{{copy.barcode}}</div>
+  <div>
+  <a class="pl-1" href="/eg/staff/cat/item/{{copy.id}}" i18n>View</a>
+  | 
+  <a class="pl-1" href="/eg/staff/cat/item/{{copy.id}}/edit" i18n>Edit</a>
+  </div>
+</ng-template>
+
+<ng-template #holdableTemplate let-copy="row" let-context="userContext">
+  <span *ngIf="context.holdable(copy)" i18n>Yes</span>
+  <span *ngIf="!context.holdable(copy)" i18n>No</span>
+</ng-template>
+
+<div class='eg-copies w-100 mt-3'>
+  <eg-grid #copyGrid [dataSource]="gridDataSource" 
+    [sortable]="false" persistKey="catalog.record.copies">
+    <eg-grid-column i18n-label label="Copy ID" path="id" 
+      [hidden]="true" [index]="true">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Location" path="circ_lib" datatype="org_unit">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Call Number / Copy Notes" 
+      name="callnumber" [cellTemplate]="cnTemplate">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Barcode" name="barcode"
+      [cellTemplate]="barcodeTemplate">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Shelving Location" path="copy_location">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Circulation Modifier" path="circ_modifier">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Age Hold Protection" path="age_protect">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Active/Create Date" 
+      path="active_date" datatype="timestamp">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Holdable?" name="holdable" 
+      [cellTemplate]="holdableTemplate" [cellContext]="copyContext">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Status" path="copy_status">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Due Date" path="due_date" datatype="timestamp">
+    </eg-grid-column>
+  </eg-grid>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts
new file mode 100644
index 0000000..68908ec
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts
@@ -0,0 +1,91 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {of} from 'rxjs';
+import {NetService} from '@eg/core/net.service';
+import {StaffCatalogService} from '../catalog.service';
+import {Pager} from '@eg/share/util/pager';
+import {OrgService} from '@eg/core/org.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+
+ at Component({
+  selector: 'eg-catalog-copies',
+  templateUrl: 'copies.component.html'
+})
+export class CopiesComponent implements OnInit {
+
+    recId: number;
+    initDone = false;
+    gridDataSource: GridDataSource;
+    copyContext: any; // grid context
+    @ViewChild('copyGrid') copyGrid: GridComponent;
+
+    @Input() set recordId(id: number) {
+        this.recId = id;
+        // Only force new data collection when recordId()
+        // is invoked after ngInit() has already run.
+        if (this.initDone) {
+            this.copyGrid.reload();
+        }
+    }
+
+    constructor(
+        private net: NetService,
+        private org: OrgService,
+        private staffCat: StaffCatalogService,
+    ) {
+        this.gridDataSource = new GridDataSource();
+    }
+
+    ngOnInit() {
+        this.initDone = true;
+
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            // sorting not currently supported
+            return this.fetchCopies(pager);
+        };
+
+        this.copyContext = {
+            holdable: (copy: any) => {
+                return copy.holdable === 't'
+                    && copy.location_holdable === 't'
+                    && copy.status_holdable === 't';
+            }
+        };
+    }
+
+    collectData() {
+        if (!this.recId) { return; }
+    }
+
+    orgName(orgId: number): string {
+        return this.org.get(orgId).shortname();
+    }
+
+    fetchCopies(pager: Pager): Observable<any> {
+        if (!this.recId) { return of([]); }
+
+        // "Show Result from All Libraries" i.e. global search displays
+        // copies from all branches, sorted by search/pref libs.
+        const copy_depth = this.staffCat.searchContext.global ?
+            this.org.root().ou_type().depth() :
+            this.staffCat.searchContext.searchOrg.ou_type().depth();
+
+        return this.net.request(
+            'open-ils.search',
+            'open-ils.search.bib.copies.staff',
+            this.recId,
+            this.staffCat.searchContext.searchOrg.id(),
+            copy_depth,
+            pager.limit,
+            pager.offset,
+            this.staffCat.prefOrg ? this.staffCat.prefOrg.id() : null
+        ).pipe(map(copy => {
+            copy.active_date = copy.active_date || copy.create_date;
+            return copy;
+        }));
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.html
new file mode 100644
index 0000000..0edcded
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.html
@@ -0,0 +1,36 @@
+<ul class="pagination mb-0" *ngIf="index !== null">
+  <li class="page-item" [ngClass]="{disabled : index == 0}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Start" (click)="firstRecord()">
+      <span i18n>Start</span>
+    </a>
+  </li>
+  <li class="page-item" [ngClass]="{disabled : index == 0}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Previous" (click)="prevRecord()">
+      <span i18n>Previous</span>
+    </a>
+  </li>
+  <li class="page-item"
+    [ngClass]="{disabled : index >= searchContext.result.count - 1}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Next" (click)="nextRecord()">
+      <span i18n>Next</span>
+    </a>
+  </li>
+  <li class="page-item"
+      [ngClass]="{disabled : index >= searchContext.result.count - 1}">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="End" (click)="lastRecord()">
+      <span i18n>End</span>
+    </a>
+  </li>
+  <li class="page-item">
+    <a class="no-href page-link" 
+      i18n-aria-label aria-label="Back to Results" (click)="returnToSearch()">
+      <span i18n>
+        Back to Results ({{index + 1}} / {{searchContext.result.count}})
+      </span>
+    </a>
+  </li>
+</ul>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts
new file mode 100644
index 0000000..793767b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts
@@ -0,0 +1,164 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router} from '@angular/router';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {StaffCatalogService} from '../catalog.service';
+import {Pager} from '@eg/share/util/pager';
+
+
+ at Component({
+  selector: 'eg-catalog-record-pagination',
+  templateUrl: 'pagination.component.html'
+})
+export class RecordPaginationComponent implements OnInit {
+
+    id: number;
+    index: number;
+    initDone = false;
+    searchContext: CatalogSearchContext;
+
+    @Input() set recordId(id: number) {
+        this.id = id;
+        // Only apply new record data after the initial load
+        if (this.initDone) {
+            this.setIndex();
+        }
+    }
+
+    constructor(
+        private router: Router,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService,
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+        this.setIndex();
+    }
+
+    firstRecord(): void {
+        this.findRecordAtIndex(0).then(id => {
+            const params = this.catUrl.toUrlParams(this.searchContext);
+            this.router.navigate(
+                ['/staff/catalog/record/' + id], {queryParams: params});
+        });
+    }
+
+    lastRecord(): void {
+        this.findRecordAtIndex(
+            this.searchContext.result.count - 1
+        ).then(id => {
+            const params = this.catUrl.toUrlParams(this.searchContext);
+            this.router.navigate(
+                ['/staff/catalog/record/' + id], {queryParams: params});
+        });
+    }
+
+    nextRecord(): void {
+        this.findRecordAtIndex(this.index + 1).then(id => {
+            const params = this.catUrl.toUrlParams(this.searchContext);
+            this.router.navigate(
+                ['/staff/catalog/record/' + id], {queryParams: params});
+        });
+    }
+
+    prevRecord(): void {
+        this.findRecordAtIndex(this.index - 1).then(id => {
+            const params = this.catUrl.toUrlParams(this.searchContext);
+            this.router.navigate(
+                ['/staff/catalog/record/' + id], {queryParams: params});
+        });
+    }
+
+
+    // Returns the offset of the record within the search results as a whole.
+    searchIndex(idx: number): number {
+        return idx + this.searchContext.pager.offset;
+    }
+
+    // Find the position of the current record in the search results
+    // If no results are present or the record is not found, expand
+    // the search scope to find the record.
+    setIndex(): Promise<void> {
+        this.searchContext = this.staffCat.searchContext;
+        this.index = null;
+
+        return new Promise((resolve, reject) => {
+
+            this.index = this.searchContext.indexForResult(this.id);
+            if (this.index !== null) {
+                return resolve();
+            }
+
+            return this.refreshSearch().then(ok => {
+                this.index = this.searchContext.indexForResult(this.id);
+                if (this.index === null) {
+                    console.warn(
+                        'No search results found containing the focused record.');
+                }
+                resolve();
+            });
+        });
+    }
+
+    // Find the record ID at the specified search index.
+    // If no data exists for the requested index, expand the search
+    // to include data for that index.
+    findRecordAtIndex(index: number): Promise<number> {
+
+        // First see if the selected record sits in the current page
+        // of search results.
+        return new Promise((resolve, reject) => {
+            const id = this.searchContext.resultIdAt(index);
+            if (id) { return resolve(id); }
+
+            console.debug(
+                'Record paginator unable to find record at index ' + index);
+
+            // If we have to re-run the search to find the record,
+            // expand the search limit out just enough to find the
+            // requested record plus one more.
+            return this.refreshSearch(index + 2).then(
+                ok => {
+                    const rid = this.searchContext.resultIdAt(index);
+                    if (rid) {
+                        resolve(rid);
+                    } else {
+                        reject('no record found');
+                    }
+                }
+            );
+        });
+    }
+
+    refreshSearch(limit?: number): Promise<any> {
+
+        console.debug('paginator refreshing search');
+
+        if (!this.searchContext.isSearchable()) {
+            return Promise.resolve();
+        }
+
+        const origPager = this.searchContext.pager;
+        const tmpPager = new Pager();
+        tmpPager.limit = limit || 1000;
+
+        this.searchContext.pager = tmpPager;
+
+        return this.cat.search(this.searchContext)
+        .then(
+            ok => this.searchContext.pager = origPager,
+            notOk => this.searchContext.pager = origPager
+        );
+    }
+
+    returnToSearch(): void {
+        // Fire the main search.  This will direct us back to /results/
+        this.staffCat.search();
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
new file mode 100644
index 0000000..4c74316
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
@@ -0,0 +1,37 @@
+
+<div id="staff-catalog-record-container">
+  <div class="row ml-0 mr-0">
+    <div id='staff-catalog-bib-navigation'>
+      <div *ngIf="searchContext.isSearchable()">
+        <eg-catalog-record-pagination [recordId]="recordId">
+        </eg-catalog-record-pagination>
+      </div>
+    </div>
+    <!-- push the actions component to the right -->
+    <div class="flex-1"></div>
+    <div id='staff-catalog-bib-navigation'>
+      <eg-catalog-record-actions [recordId]="recordId">
+      </eg-catalog-record-actions>
+    </div>
+  </div>
+  <div id='staff-catalog-bib-summary-container' class='mt-1'>
+    <eg-bib-summary [bibSummary]="summary">
+    </eg-bib-summary>
+  </div>
+  <div id='staff-catalog-bib-tabs-container' class='mt-3'>
+    <ngb-tabset #recordTabs [activeId]="recordTab" (tabChange)="onTabChange($event)">
+      <ngb-tab title="Copy Table" i18n-title id="copy_table">
+        <ng-template ngbTabContent>
+          <eg-catalog-copies [recordId]="recordId"></eg-catalog-copies>
+        </ng-template>
+      </ngb-tab>
+      <ngb-tab title="MARC View" i18n-title id="marc_view">
+        <ng-template ngbTabContent>
+          <eg-marc-html [recordId]="recordId" recordType="bib"></eg-marc-html>
+        </ng-template>
+      </ngb-tab>
+    </ngb-tabset>
+  </div>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
new file mode 100644
index 0000000..b217e5c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
@@ -0,0 +1,84 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {StaffCatalogService} from '../catalog.service';
+import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
+
+ at Component({
+  selector: 'eg-catalog-record',
+  templateUrl: 'record.component.html'
+})
+export class RecordComponent implements OnInit {
+
+    recordId: number;
+    recordTab: string;
+    summary: BibRecordSummary;
+    searchContext: CatalogSearchContext;
+    @ViewChild('recordTabs') recordTabs: NgbTabset;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private bib: BibRecordService,
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+
+        // Watch for URL record ID changes
+        this.route.paramMap.subscribe((params: ParamMap) => {
+            this.recordTab = params.get('tab') || 'copy_table';
+            this.recordId = +params.get('id');
+            this.searchContext = this.staffCat.searchContext;
+            this.loadRecord();
+        });
+    }
+
+    // Changing a tab in the UI means changing the route.
+    // Changing the route ultimately results in changing the tab.
+    onTabChange(evt: NgbTabChangeEvent) {
+        this.recordTab = evt.nextId;
+
+        // prevent tab changing until after route navigation
+        evt.preventDefault();
+
+        let url = '/staff/catalog/record/' + this.recordId;
+        if (this.recordTab !== 'copy_table') {
+            url += '/' + this.recordTab;
+        }
+
+        // Retain search parameters
+        this.router.navigate([url], {queryParamsHandling: 'merge'});
+    }
+
+    loadRecord(): void {
+
+        // Avoid re-fetching the same record summary during tab navigation.
+        if (this.staffCat.currentDetailRecordSummary &&
+            this.recordId === this.staffCat.currentDetailRecordSummary.id) {
+            this.summary = this.staffCat.currentDetailRecordSummary;
+            return;
+        }
+
+        this.summary = null;
+        this.bib.getBibSummary(
+            this.recordId,
+            this.searchContext.searchOrg.id(),
+            this.searchContext.searchOrg.ou_type().depth()).toPromise()
+        .then(summary => {
+            this.summary =
+                this.staffCat.currentDetailRecordSummary = summary;
+            this.bib.fleshBibUsers([summary.record]);
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
new file mode 100644
index 0000000..729beea
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
@@ -0,0 +1,59 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRouteSnapshot} from '@angular/router';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {StaffCatalogService} from './catalog.service';
+
+ at Injectable()
+export class CatalogResolver implements Resolve<Promise<any[]>> {
+
+    constructor(
+        private router: Router,
+        private store: ServerStoreService,
+        private org: OrgService,
+        private net: NetService,
+        private auth: AuthService,
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Promise<any[]> {
+
+        console.debug('CatalogResolver:resolve()');
+
+        return Promise.all([
+            this.cat.fetchCcvms(),
+            this.cat.fetchCmfs(),
+            this.fetchSettings()
+        ]);
+    }
+
+    fetchSettings(): Promise<any> {
+        const promises = [];
+
+        promises.push(
+            this.store.getItem('eg.search.search_lib').then(
+                id => this.staffCat.defaultSearchOrg = this.org.get(id)
+            )
+        );
+
+        promises.push(
+            this.store.getItem('eg.search.pref_lib').then(
+                id => this.staffCat.prefOrg = this.org.get(id)
+            )
+        );
+
+        return Promise.all(promises);
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html
new file mode 100644
index 0000000..9681747
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html
@@ -0,0 +1,43 @@
+<style>
+  .facet-selected {
+    background-color: #DDD;
+  }
+  .card {
+    width: 100%;
+  }
+  .list-group-item {padding: .5rem .75rem .5rem .75rem}
+</style>
+<div *ngIf="searchContext.result.facetData">
+  <div *ngFor="let facetConf of facetConfig.display">
+    <div *ngIf="searchContext.result.facetData[facetConf.facetClass]">
+      <div *ngFor="let name of facetConf.facetOrder">
+        <div class="row"
+          *ngIf="searchContext.result.facetData[facetConf.facetClass][name]">
+          <div class="card mb-2">
+            <h4 class="card-header">
+              {{searchContext.result.facetData[facetConf.facetClass][name].cmfLabel}}
+            </h4>
+            <ul class="list-group list-group-flush">
+              <li class="list-group-item" 
+                [ngClass]="{'facet-selected' :
+                  facetIsApplied(facetConf.facetClass, name, value.value)}"
+                *ngFor="
+                  let value of searchContext.result.facetData[facetConf.facetClass][name].valueList | slice:0:facetConfig.displayCount">
+                <div class="row">
+                  <div class="col-lg-9">
+                    <a class="card-link"
+                      href='javascript:;'
+                      (click)="applyFacet(facetConf.facetClass, name, value.value)">
+                      {{value.value}}
+                    </a>
+                  </div>
+                  <div class="col-lg-3">{{value.count}}</div>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts
new file mode 100644
index 0000000..44583b8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts
@@ -0,0 +1,48 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, FacetFilter} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.service';
+
+export const FACET_CONFIG = {
+    display: [
+        {facetClass : 'author',  facetOrder : ['personal', 'corporate']},
+        {facetClass : 'subject', facetOrder : ['topic']},
+        {facetClass : 'identifier', facetOrder : ['genre']},
+        {facetClass : 'series',  facetOrder : ['seriestitle']},
+        {facetClass : 'subject', facetOrder : ['name', 'geographic']}
+    ],
+    displayCount : 5
+};
+
+ at Component({
+  selector: 'eg-catalog-result-facets',
+  templateUrl: 'facets.component.html'
+})
+export class ResultFacetsComponent implements OnInit {
+
+    searchContext: CatalogSearchContext;
+    facetConfig: any;
+
+    constructor(
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {
+        this.facetConfig = FACET_CONFIG;
+    }
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+    }
+
+    facetIsApplied(cls: string, name: string, value: string): boolean {
+        return this.searchContext.hasFacet(new FacetFilter(cls, name, value));
+    }
+
+    applyFacet(cls: string, name: string, value: string): void {
+        this.searchContext.toggleFacet(new FacetFilter(cls, name, value));
+        this.searchContext.pager.offset = 0;
+        this.staffCat.search();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.css b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.css
new file mode 100644
index 0000000..c283ff4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.css
@@ -0,0 +1,8 @@
+
+/* Bootstrap default is 20px */
+.pagination {margin: 0px 0px 0px 0px}
+
+.pagination li:not(.active) a {
+  cursor: pointer;
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.html
new file mode 100644
index 0000000..7cdda00
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.html
@@ -0,0 +1,28 @@
+<!-- 
+Using bare BS pagination instead of ng-bootstrap, which seemed 
+unnecessary given we have to track paging externally anyway.
+-->
+<ul class="pagination">
+  <li class="page-item" 
+    [ngClass]="{disabled : searchContext.pager.isFirstPage()}">
+    <a (click)="prevPage()"
+      class="page-link" 
+      i18n-aria-label
+      aria-label="Previous">
+      <span aria-hidden="true">«</span>
+    </a>
+  </li>
+  <li class="page-item" 
+    *ngFor="let page of currentPageList()"
+    [ngClass]="{active : searchContext.pager.currentPage() == page}">
+    <a class="page-link" (click)="setPage(page)">
+      {{page}} <span class="sr-only" i18n>(current)</span></a>
+  </li>
+  <li class="page-item" 
+    [ngClass]="{disabled : searchContext.pager.isLastPage()}">
+    <a (click)="nextPage()"
+      class="page-link" aria-label="Next" i18n-aria-label>
+      <span aria-hidden="true">»</span>
+    </a>
+  </li>
+</ul>
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.ts
new file mode 100644
index 0000000..15214b5
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.ts
@@ -0,0 +1,51 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.service';
+
+ at Component({
+  selector: 'eg-catalog-result-pagination',
+  styleUrls: ['pagination.component.css'],
+  templateUrl: 'pagination.component.html'
+})
+export class ResultPaginationComponent implements OnInit {
+
+    searchContext: CatalogSearchContext;
+
+    // Maximum number of jump-to-page buttons displayed.
+    @Input() numPages: number;
+
+    constructor(
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {
+        this.numPages = 10;
+    }
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+    }
+
+    currentPageList(): number[] {
+        const pgr = this.searchContext.pager;
+        return pgr.pageRange(pgr.currentPage(), this.numPages);
+    }
+
+    nextPage(): void {
+        this.searchContext.pager.increment();
+        this.staffCat.search();
+    }
+
+    prevPage(): void {
+        this.searchContext.pager.decrement();
+        this.staffCat.search();
+    }
+
+    setPage(page: number): void {
+        if (this.searchContext.pager.currentPage() === page) { return; }
+        this.searchContext.pager.setPage(page);
+        this.staffCat.search();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
new file mode 100644
index 0000000..54ad3db
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
@@ -0,0 +1,132 @@
+<!-- 
+  TODO
+  routerLink's
+  egDateFilter's
+-->
+<eg-record-bucket-dialog #addToListDialog>
+</eg-record-bucket-dialog>
+
+<div class="col-lg-12 card tight-card mb-2 bg-light">
+  <div class="card-body">
+    <div class="row">
+      <div class="col-lg-1">
+        <a href="javascript:void(0)" (click)="navigatToRecord(summary.id)">
+          <img style="height:80px"
+            src="/opac/extras/ac/jacket/small/r/{{summary.id}}"/>
+        </a>
+      </div>
+      <div class="col-lg-5">
+        <div class="row">
+          <div class="col-lg-12 font-weight-bold">
+            <!-- nbsp allows the column to take shape when no value exists -->
+            <span class="font-weight-light font-italic">
+              #{{index + 1 + searchContext.pager.offset}}
+            </span>
+            <a href="javascript:void(0)"
+              (click)="navigatToRecord(summary.id)">
+              {{summary.display.title || ' '}}
+            </a>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <!-- nbsp allows the column to take shape when no value exists -->
+            <a href="javascript:void(0)"
+              (click)="searchAuthor(summary)">
+              {{summary.display.author || ' '}}
+            </a>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <!-- only shows the first icon format -->
+            <span *ngIf="summary.attributes.icon_format && summary.attributes.icon_format[0]">
+              <img class="pr-1"
+                src="/images/format_icons/icon_format/{{summary.attributes.icon_format[0]}}.png"/>
+              <span>{{iconFormatLabel(summary.attributes.icon_format[0])}}</span>
+            </span>
+            <span class='pl-1'>{{summary.display.edition}}</span>
+            <span class='pl-1'>{{summary.display.pubdate}}</span>
+          </div>
+        </div>
+      </div>
+      <div class="col-lg-2">
+        <div class="row" [ngClass]="{'pt-2':copyIndex > 0}" 
+          *ngFor="let copyCount of summary.holdingsSummary; let copyIdx = index">
+          <div class="w-100" *ngIf="copyCount.type == 'staff'">
+            <div class="float-left text-left w-50">
+              <span class="pr-1">
+              {{copyCount.available}} / {{copyCount.count}} items
+              </span>
+            </div>
+            <div class="float-left w-50">
+              @ {{orgName(copyCount.org_unit)}}
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="col-lg-1">
+        <div class="row">
+          <div class="w-100">
+            TCN: {{summary.record.tcn_value()}}
+          </div>
+        </div>
+        <div class="row">
+          <div class="w-100">
+            Holds: {{summary.holdCount}}
+          </div>
+        </div>
+      </div>
+      <div class="col-lg-3">
+        <div class="row">
+          <div class="col-lg-12">
+            <div class="float-right small-text-1">
+              Created {{summary.record.create_date() | date:'shortDate'}} by
+              <!-- creator if fleshed after the initial data set is loaded -->
+              <a *ngIf="summary.record.creator().usrname" target="_self" 
+                href="/eg/staff/circ/patron/{{summary.record.creator().id()}}/checkout">
+                  {{summary.record.creator().usrname()}}
+              </a>
+              <!-- add a spacer pending data to reduce page shuffle -->
+              <span *ngIf="!summary.record.creator().usrname"> ... </span>
+            </div>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <div class="float-right small-text-1" i18n>
+              Edited {{summary.record.edit_date() | date:'shortDate'}} by
+              <a *ngIf="summary.record.editor().usrname" target="_self" 
+                href="/eg/staff/circ/patron/{{summary.record.editor().id()}}/checkout">
+                  {{summary.record.editor().usrname()}}
+              </a>
+              <span *ngIf="!summary.record.editor().usrname"> ... </span>
+            </div>
+          </div>
+        </div>
+        <div class="row pt-2">
+          <div class="col-lg-12">
+            <div class="float-right">
+              <span>
+                <button (click)="placeHold()"
+                  class="btn btn-sm btn-success label-with-material-icon small-text-1">
+                  <span class="material-icons">check</span>
+                  <span i18n>Place Hold</span>
+                </button>
+              </span>
+              <span class="pl-1">
+                <button 
+                  (click)="addToListDialog.recordId=summary.record.id(); addToListDialog.open({size: 'lg'})"
+                  class="btn btn-sm btn-info label-with-material-icon small-text-1">
+                  <span class="material-icons">playlist_add_check</span>
+                  <span i18n>Add to List</span>
+                </button>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div><!-- col -->
+    </div><!-- row -->
+  </div><!-- card-body -->
+</div><!-- card -->
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
new file mode 100644
index 0000000..bfcfd45
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
@@ -0,0 +1,77 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router} from '@angular/router';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {CatalogSearchContext} from '@eg/share/catalog/search-context';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {StaffCatalogService} from '../catalog.service';
+
+ at Component({
+  selector: 'eg-catalog-result-record',
+  templateUrl: 'record.component.html'
+})
+export class ResultRecordComponent implements OnInit {
+
+    @Input() index: number;  // 0-index display row
+    @Input() summary: BibRecordSummary;
+    searchContext: CatalogSearchContext;
+
+    constructor(
+        private router: Router,
+        private org: OrgService,
+        private net: NetService,
+        private bib: BibRecordService,
+        private cat: CatalogService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+        this.summary.getHoldCount();
+    }
+
+    orgName(orgId: number): string {
+        return this.org.get(orgId).shortname();
+    }
+
+    iconFormatLabel(code: string): string {
+        if (this.cat.ccvmMap) {
+            const ccvm = this.cat.ccvmMap.icon_format.filter(
+                format => format.code() === code)[0];
+            if (ccvm) {
+                return ccvm.search_label();
+            }
+        }
+    }
+
+    placeHold(): void {
+        alert('Placing hold on bib ' + this.summary.id);
+    }
+
+    addToList(): void {
+        alert('Adding to list for bib ' + this.summary.id);
+    }
+
+    searchAuthor(summary: any) {
+        this.searchContext.reset();
+        this.searchContext.fieldClass = ['author'];
+        this.searchContext.query = [summary.display.author];
+        this.staffCat.search();
+    }
+
+    /**
+     * Propagate the search params along when navigating to each record.
+     */
+    navigatToRecord(id: number) {
+        const params = this.catUrl.toUrlParams(this.searchContext);
+
+        this.router.navigate(
+          ['/staff/catalog/record/' + id], {queryParams: params});
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
new file mode 100644
index 0000000..ee9ca8d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
@@ -0,0 +1,30 @@
+
+<div id="staff-catalog-results-container" *ngIf="searchIsDone()">
+  <div class="row">
+    <div class="col-lg-2"><!--match pagination margin-->
+      <h3 i18n>Search Results ({{searchContext.result.count}})</h3>
+    </div>
+    <div class="col-lg-1"></div>
+    <div class="col-lg-9">
+      <div class="float-right">
+				<eg-catalog-result-pagination></eg-catalog-result-pagination>
+      </div>
+    </div>
+  </div>
+	<div class="row mt-2">
+		<div class="col-lg-2">
+      <eg-catalog-result-facets></eg-catalog-result-facets>
+		</div>
+		<div class="col-lg-10">
+			<div *ngIf="searchContext.result">
+				<div *ngFor="let summary of searchContext.result.records; let idx = index">
+          <div *ngIf="summary">
+					  <eg-catalog-result-record [summary]="summary" [index]="idx">
+					  </eg-catalog-result-record>
+          </div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
new file mode 100644
index 0000000..d9b7062
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
@@ -0,0 +1,84 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {ActivatedRoute, ParamMap} from '@angular/router';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService} from '@eg/share/catalog/bib-record.service';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {StaffCatalogService} from '../catalog.service';
+import {IdlObject} from '@eg/core/idl.service';
+
+ at Component({
+  selector: 'eg-catalog-results',
+  templateUrl: 'results.component.html'
+})
+export class ResultsComponent implements OnInit {
+
+    searchContext: CatalogSearchContext;
+
+    // Cache record creator/editor since this will likely be a
+    // reasonably small set of data w/ lots of repitition.
+    userCache: {[id: number]: IdlObject} = {};
+
+    constructor(
+        private route: ActivatedRoute,
+        private pcrud: PcrudService,
+        private cat: CatalogService,
+        private bib: BibRecordService,
+        private catUrl: CatalogUrlService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.searchContext = this.staffCat.searchContext;
+
+        // Our search context is initialized on page load.  Once
+        // ResultsComponent is active, it will not be reinitialized,
+        // even if the route parameters changes (unless we change the
+        // route reuse policy).  Watch for changes here to pick up new
+        // searches.
+        //
+        // This will also fire on page load.
+        this.route.queryParamMap.subscribe((params: ParamMap) => {
+
+              // TODO: Angular docs suggest using switchMap(), but
+              // it's not firing for some reason.  Also, could avoid
+              // firing unnecessary searches when a param unrelated to
+              // searching is changed by .map()'ing out only the desired
+              // params and running through .distinctUntilChanged(), but
+              // .map() is not firing either.  I'm missing something.
+              this.searchByUrl(params);
+        });
+    }
+
+    searchByUrl(params: ParamMap): void {
+        this.catUrl.applyUrlParams(this.searchContext, params);
+
+        if (this.searchContext.isSearchable()) {
+
+            this.cat.search(this.searchContext)
+            .then(ok => {
+                this.cat.fetchFacets(this.searchContext);
+                this.cat.fetchBibSummaries(this.searchContext)
+                .then(ok2 => this.fleshSearchResults());
+            });
+        }
+    }
+
+    fleshSearchResults(): void {
+        const records = this.searchContext.result.records;
+        if (!records || records.length === 0) { return; }
+
+        // Flesh the creator / editor fields with the user object.
+        this.bib.fleshBibUsers(records.map(r => r.record));
+    }
+
+    searchIsDone(): boolean {
+        return this.searchContext.searchState === CatalogSearchState.COMPLETE;
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts
new file mode 100644
index 0000000..0e3c96f
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts
@@ -0,0 +1,30 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {CatalogComponent} from './catalog.component';
+import {ResultsComponent} from './result/results.component';
+import {RecordComponent} from './record/record.component';
+import {CatalogResolver} from './resolver.service';
+
+const routes: Routes = [{
+  path: '',
+  component: CatalogComponent,
+  resolve: {catResolver : CatalogResolver},
+  children : [{
+    path: 'search',
+    component: ResultsComponent
+  }, {
+    path: 'record/:id',
+    component: RecordComponent
+  }, {
+    path: 'record/:id/:tab',
+    component: RecordComponent
+  }]
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: [CatalogResolver]
+})
+
+export class CatalogRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css
new file mode 100644
index 0000000..6201dff
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css
@@ -0,0 +1,16 @@
+
+/* filter checkbox labels move to bottom */
+.checkbox label {
+  margin-bottom: .1rem;
+}
+
+/* BS default height is 2.25rem + 2px which is quite chunky.
+ * This better matches the text input heights */
+select.form-control:not([size]):not([multiple]) {
+  padding: .355rem .55rem;
+  height: 2.2rem;
+}
+
+#staffcat-search-form {
+  border-bottom: 2px dashed rgba(0,0,0,.225);
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html
new file mode 100644
index 0000000..da54f4a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html
@@ -0,0 +1,244 @@
+<!--
+TODO focus search input
+-->
+<div id='staffcat-search-form' class='pb-2 mb-3'>
+  <div class="row"
+    *ngFor="let q of searchContext.query; let idx = index; trackBy:trackByIdx">
+    <div class="col-lg-9 d-flex">
+      <div class="flex-1">
+        <div *ngIf="idx == 0">
+          <select class="form-control" [(ngModel)]="searchContext.format">
+            <option i18n value=''>All Formats</option>
+            <option *ngFor="let fmt of ccvmMap.search_format"
+              value="{{fmt.code()}}">{{fmt.value()}}</option>
+          </select>
+        </div>
+        <div *ngIf="idx > 0">
+          <select class="form-control"
+            [(ngModel)]="searchContext.joinOp[idx]">
+            <option i18n value='&&'>And</option>
+            <option i18n value='||'>Or</option>
+          </select>
+        </div>
+      </div>
+      <div class="flex-1 pl-1">
+        <select class="form-control" 
+          [(ngModel)]="searchContext.fieldClass[idx]">
+          <option i18n value='keyword'>Keyword</option>
+          <option i18n value='title'>Title</option>
+          <option i18n value='jtitle'>Journal Title</option>
+          <option i18n value='author'>Author</option>
+          <option i18n value='subject'>Subject</option>
+          <option i18n value='series'>Series</option>
+        </select>
+      </div>
+      <div class="flex-1 pl-1">
+        <select class="form-control" 
+          [(ngModel)]="searchContext.matchOp[idx]">
+          <option i18n value='contains'>Contains</option>
+          <option i18n value='nocontains'>Does not contain</option>
+          <option i18n value='phrase'>Contains phrase</option>
+          <option i18n value='exact'>Matches exactly</option>
+          <option i18n value='starts'>Starts with</option>
+        </select>
+      </div>
+      <div class="flex-2 pl-1">
+        <div class="form-group">
+          <div *ngIf="idx == 0">
+            <input type="text" class="form-control"
+              id='first-query-input'
+              [(ngModel)]="searchContext.query[idx]"
+              (keyup.enter)="formEnter('query')"
+              placeholder="Query..."/>
+          </div>
+          <div *ngIf="idx > 0">
+            <input type="text" class="form-control"
+              [(ngModel)]="searchContext.query[idx]"
+              (keyup.enter)="formEnter('query')"
+              placeholder="Query..."/>
+          </div>
+        </div>
+      </div>
+      <div class="flex-1 pl-1">
+        <button class="btn btn-sm material-icon-button"
+          (click)="addSearchRow(idx + 1)">
+          <span class="material-icons">add_circle_outline</span>
+        </button>
+        <button class="btn btn-sm material-icon-button"
+          [disabled]="searchContext.query.length < 2"
+          (click)="delSearchRow(idx)">
+          <span class="material-icons">remove_circle_outline</span>
+        </button>
+      </div>
+    </div><!-- col -->
+    <div class="col-lg-3">
+      <div *ngIf="idx == 0" class="float-right">
+        <button class="btn btn-success mr-1" type="button"
+          [disabled]="searchIsActive()"
+          (click)="searchContext.pager.offset=0;searchByForm()">
+          Search
+        </button>
+        <button class="btn btn-warning mr-1" type="button"
+          [disabled]="searchIsActive()"
+          (click)="searchContext.reset()">
+          Clear Form
+        </button>
+        <button class="btn btn-outline-secondary" type="button"
+          *ngIf="!showAdvanced()"
+          [disabled]="searchIsActive()"
+          (click)="showAdvancedSearch=true">
+          More Filters
+        </button>
+        <button class="btn btn-outline-secondary" type="button"
+          *ngIf="showAdvanced()"
+          (click)="showAdvancedSearch=false">
+          Hide Filters
+        </button>
+      </div>
+    </div>
+  </div><!-- row -->
+
+  <div class="row">
+    <div class="col-lg-9 d-flex">
+      <div class="flex-1">
+        <eg-org-select 
+          (onChange)="orgOnChange($event)"
+          [initialOrg]="searchContext.searchOrg"
+          [placeholder]="'Library'" >
+        </eg-org-select>
+      </div>
+      <div class="flex-3 pl-1">
+        <select class="form-control" [(ngModel)]="searchContext.sort">
+          <option value='' i18n>Sort by Relevance</option>
+          <optgroup label="Sort by Title" i18n-label>
+            <option value='titlesort' i18n>Title: A to Z</option>
+            <option value='titlesort.descending' i18n>Title: Z to A</option>
+          </optgroup>
+          <optgroup label="Sort by Author" i18n-label>
+            <option value='authorsort' i18n>Author: A to Z</option>
+            <option value='authorsort.descending' i18n>Author: Z to A</option>
+          </optgroup>
+          <optgroup label="Sort by Publication Date" i18n-label>
+            <option value='pubdate' i18n>Date: A to Z</option>
+            <option value='pubdate.descending' i18n>Date: Z to A</option>
+          </optgroup>
+          <optgroup label="Sort by Popularity" i18n-label>
+            <option value='popularity' i18n>Most Popular</option>
+            <option value='poprel' i18n>Popularity Adjusted Relevance</option>
+          </optgroup>
+        </select>
+      </div>
+      <div class="flex-2 pl-2 align-self-end">
+        <div class="checkbox">
+          <label>
+            <input type="checkbox" [(ngModel)]="searchContext.available"/>
+            <span i18n>Limit to Available</span>
+          </label>
+        </div>
+      </div>
+      <div class="flex-4 pl-2 align-self-end">
+        <div class="checkbox">
+          <label>
+            <input type="checkbox" [(ngModel)]="searchContext.global"/>
+            <span i18n>Show Results from All Libraries</span>
+          </label>
+        </div>
+      </div>
+      <div class="flex-2 pl-1">
+        <!-- alignment -->
+      </div>
+    </div>
+    <div class="col-lg-3">
+      <div *ngIf="searchIsActive()">
+        <div class="progress">
+          <div class="progress-bar progress-bar-striped active w-100"
+            role="progressbar" aria-valuenow="100" 
+            aria-valuemin="0" aria-valuemax="100">
+            <span class="sr-only" i18n>Searching..</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="row pt-2" *ngIf="showAdvanced()">
+    <div class="col-lg-2">
+      <select class="form-control"  multiple="true"
+        [(ngModel)]="searchContext.ccvmFilters.item_type">
+        <option value='' i18n>All Item Types</option>
+        <option *ngFor="let itemType of ccvmMap.item_type"
+          value="{{itemType.code()}}">{{itemType.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control" multiple="true"
+        [(ngModel)]="searchContext.ccvmFilters.item_form">
+        <option value='' i18n>All Item Forms</option>
+        <option *ngFor="let itemForm of ccvmMap.item_form"
+          value="{{itemForm.code()}}">{{itemForm.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.item_lang" multiple="true">
+        <option value='' i18n>All Languages</option>
+        <option *ngFor="let lang of ccvmMap.item_lang"
+          value="{{lang.code()}}">{{lang.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.audience" multiple="true">
+        <option value='' i18n>All Audiences</option>
+        <option *ngFor="let audience of ccvmMap.audience"
+          value="{{audience.code()}}">{{audience.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control"
+        [(ngModel)]="searchContext.identQueryType">
+        <option i18n value="identifier|isbn">ISBN</option>
+        <option i18n value="identifier|issn">ISSN</option>
+        <option i18n disabled value="cnbrowse">Call Number (Shelf Browse)</option>
+        <option i18n value="identifier|lccn">LCCN</option>
+        <option i18n value="identifier|tcn">TCN</option>
+        <option i18n disabled value="item_barcode">Item Barcode</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <input id='ident-query-input' type="text" class="form-control"
+        [(ngModel)]="searchContext.identQuery"
+        (keyup.enter)="formEnter('ident')"
+        placeholder="Numeric Query..."/>
+    </div>
+  </div>
+  <div class="row pt-2" *ngIf="showAdvanced()">
+    <div class="col-lg-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.vr_format" multiple="true">
+        <option value='' i18n>All Video Formats</option>
+        <option *ngFor="let vrFormat of ccvmMap.vr_format"
+          value="{{vrFormat.code()}}">{{vrFormat.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.bib_level" multiple="true">
+        <option value='' i18n>All Bib Levels</option>
+        <option *ngFor="let bibLevel of ccvmMap.bib_level"
+          value="{{bibLevel.code()}}">{{bibLevel.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <select class="form-control" 
+        [(ngModel)]="searchContext.ccvmFilters.lit_form" multiple="true">
+        <option value='' i18n>All Literary Forms</option>
+        <option *ngFor="let litForm of ccvmMap.lit_form"
+          value="{{litForm.code()}}">{{litForm.value()}}</option>
+      </select>
+    </div>
+    <div class="col-lg-2">
+      <i>Copy location filter goes here...</i>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
new file mode 100644
index 0000000..52a26f2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
@@ -0,0 +1,137 @@
+import {Component, OnInit, AfterViewInit, Renderer2} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from './catalog.service';
+
+ at Component({
+  selector: 'eg-catalog-search-form',
+  styleUrls: ['search-form.component.css'],
+  templateUrl: 'search-form.component.html'
+})
+export class SearchFormComponent implements OnInit, AfterViewInit {
+
+    searchContext: CatalogSearchContext;
+    ccvmMap: {[ccvm: string]: IdlObject[]} = {};
+    cmfMap: {[cmf: string]: IdlObject} = {};
+    showAdvancedSearch = false;
+
+    constructor(
+        private renderer: Renderer2,
+        private org: OrgService,
+        private cat: CatalogService,
+        private staffCat: StaffCatalogService
+    ) {}
+
+    ngOnInit() {
+        this.ccvmMap = this.cat.ccvmMap;
+        this.cmfMap = this.cat.cmfMap;
+        this.searchContext = this.staffCat.searchContext;
+
+        // Start with advanced search options open
+        // if any filters are active.
+        this.showAdvancedSearch = this.hasAdvancedOptions();
+
+    }
+
+    ngAfterViewInit() {
+        // Query inputs are generated from search context data,
+        // so they are not available until after the first render.
+        // Search context data is extracted synchronously from the URL.
+
+        if (this.searchContext.identQuery) {
+            // Focus identifier query input if identQuery is in progress
+            this.renderer.selectRootElement('#ident-query-input').focus();
+        } else {
+            // Otherwise focus the main query input
+            this.renderer.selectRootElement('#first-query-input').focus();
+        }
+    }
+
+    /**
+     * Display the advanced/extended search options when asked to
+     * or if any advanced options are selected.
+     */
+    showAdvanced(): boolean {
+        return this.showAdvancedSearch;
+    }
+
+    hasAdvancedOptions(): boolean {
+        // ccvm filters may be present without any filters applied.
+        // e.g. if filters were applied then removed.
+        let show = false;
+        Object.keys(this.searchContext.ccvmFilters).forEach(ccvm => {
+            if (this.searchContext.ccvmFilters[ccvm][0] !== '') {
+                show = true;
+            }
+        });
+
+        if (this.searchContext.identQuery) {
+            show = true;
+        }
+
+        return show;
+    }
+
+    orgOnChange = (org: IdlObject): void => {
+        this.searchContext.searchOrg = org;
+    }
+
+    addSearchRow(index: number): void {
+        this.searchContext.query.splice(index, 0, '');
+        this.searchContext.fieldClass.splice(index, 0, 'keyword');
+        this.searchContext.joinOp.splice(index, 0, '&&');
+        this.searchContext.matchOp.splice(index, 0, 'contains');
+    }
+
+    delSearchRow(index: number): void {
+        this.searchContext.query.splice(index, 1);
+        this.searchContext.fieldClass.splice(index, 1);
+        this.searchContext.joinOp.splice(index, 1);
+        this.searchContext.matchOp.splice(index, 1);
+    }
+
+    formEnter(source) {
+        this.searchContext.pager.offset = 0;
+
+        switch (source) {
+
+            case 'query': // main search form query input
+
+                // Be sure a previous ident search does not take precedence
+                // over the newly entered/submitted search query
+                this.searchContext.identQuery = null;
+                break;
+
+            case 'ident': // identifier query input
+                const iq = this.searchContext.identQuery;
+                const qt = this.searchContext.identQueryType;
+                if (iq) {
+                    // Ident queries ignore search-specific filters.
+                    this.searchContext.reset();
+                    this.searchContext.identQuery = iq;
+                    this.searchContext.identQueryType = qt;
+                }
+                break;
+        }
+
+        this.searchByForm();
+    }
+
+    // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes
+    trackByIdx(index: any, item: any) {
+       return index;
+    }
+
+    searchByForm(): void {
+        this.staffCat.search();
+    }
+
+    searchIsActive(): boolean {
+        return this.searchContext.searchState === CatalogSearchState.SEARCHING;
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html
new file mode 100644
index 0000000..e83cf9e
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html
@@ -0,0 +1,19 @@
+
+<eg-staff-banner bannerText="Search for Patron by Barcode" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="col-lg-4">
+  <div class="input-group">
+    <div class="input-group-prepend">
+      <span class="input-group-text" i18n>Barcode:</span>
+    </div>
+    <input type='text' id='barcode-search-input' class="form-control" 
+      placeholder="Barcode" i18n-placeholder [ngModel]='barcode'/>
+    <div class="input-group-append">
+      <button class="btn btn-outline-secondary" 
+        (click)="findUser()" i18n>Submit</button>
+    </div>
+  </div>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts
new file mode 100644
index 0000000..dac5048
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts
@@ -0,0 +1,36 @@
+import {Component, OnInit, Renderer2} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+
+ at Component({
+  templateUrl: 'bcsearch.component.html'
+})
+
+export class BcSearchComponent implements OnInit {
+
+    barcode = '';
+
+    constructor(
+        private route: ActivatedRoute,
+        private renderer: Renderer2,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    ngOnInit() {
+
+        this.renderer.selectRootElement('#barcode-search-input').focus();
+        this.barcode = this.route.snapshot.paramMap.get('barcode');
+
+        if (this.barcode) {
+            this.findUser();
+        }
+    }
+
+    findUser(): void {
+        alert('Searching for user ' + this.barcode);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts
new file mode 100644
index 0000000..d1b16df
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts
@@ -0,0 +1,17 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {BcSearchRoutingModule} from './routing.module';
+import {BcSearchComponent} from './bcsearch.component';
+
+ at NgModule({
+  declarations: [
+    BcSearchComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    BcSearchRoutingModule,
+  ],
+})
+
+export class BcSearchModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts
new file mode 100644
index 0000000..ce6783d
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {BcSearchComponent} from './bcsearch.component';
+
+const routes: Routes = [
+  { path: '',
+    component: BcSearchComponent
+  },
+  { path: ':barcode',
+    component: BcSearchComponent
+  },
+];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class BcSearchRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
new file mode 100644
index 0000000..9033f92
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'bcsearch',
+    loadChildren: '@eg/staff/circ/patron/bcsearch/bcsearch.module#BcSearchModule'
+  }
+];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CircPatronRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
new file mode 100644
index 0000000..2409977
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'patron',
+    loadChildren: '@eg/staff/circ/patron/routing.module#CircPatronRoutingModule'
+  }
+];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CircRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts
new file mode 100644
index 0000000..e83143c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts
@@ -0,0 +1,84 @@
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {AudioService} from '@eg/share/util/audio.service';
+import {GridModule} from '@eg/share/grid/grid.module';
+import {StaffBannerComponent} from './share/staff-banner.component';
+import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
+import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
+import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
+import {OpChangeComponent} from '@eg/staff/share/op-change/op-change.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ToastComponent} from '@eg/share/toast/toast.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {RecordBucketDialogComponent} from '@eg/staff/share/buckets/record-bucket-dialog.component';
+import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
+import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
+import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
+
+/**
+ * Imports the EG common modules and adds modules common to all staff UI's.
+ */
+
+ at NgModule({
+  declarations: [
+    StaffBannerComponent,
+    ComboboxComponent,
+    ComboboxEntryComponent,
+    OrgSelectComponent,
+    AccessKeyDirective,
+    AccessKeyInfoComponent,
+    ToastComponent,
+    StringComponent,
+    OpChangeComponent,
+    FmRecordEditorComponent,
+    DateSelectComponent,
+    RecordBucketDialogComponent,
+    BibSummaryComponent,
+    TranslateComponent,
+    AdminPageComponent
+  ],
+  imports: [
+    EgCommonModule,
+    GridModule
+  ],
+  exports: [
+    EgCommonModule,
+    GridModule,
+    StaffBannerComponent,
+    ComboboxComponent,
+    ComboboxEntryComponent,
+    OrgSelectComponent,
+    AccessKeyDirective,
+    AccessKeyInfoComponent,
+    ToastComponent,
+    StringComponent,
+    OpChangeComponent,
+    FmRecordEditorComponent,
+    DateSelectComponent,
+    RecordBucketDialogComponent,
+    BibSummaryComponent,
+    TranslateComponent,
+    AdminPageComponent
+  ]
+})
+
+export class StaffCommonModule {
+    static forRoot(): ModuleWithProviders {
+        return {
+            ngModule: StaffCommonModule,
+            providers: [ // Export staff-wide services
+                AccessKeyService,
+                AudioService,
+                StringService,
+                ToastService
+            ]
+        };
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/login.component.html b/Open-ILS/src/eg2/src/app/staff/login.component.html
new file mode 100644
index 0000000..ba474f8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/login.component.html
@@ -0,0 +1,58 @@
+<div class="container">
+  <div class="col-lg-6 offset-lg-3">
+    <fieldset>
+      <legend class="mb-0" i18n>Sign In</legend>
+      <hr class="mt-1"/>
+      <form (ngSubmit)="handleSubmit()" #loginForm="ngForm" class="form-validated">
+
+        <div class="form-group row">
+          <label class="col-lg-4 text-right font-weight-bold" for="username" i18n>Username</label>
+          <input 
+            type="text" 
+            class="form-control col-lg-8"
+            id="username" 
+            name="username"
+            required
+            autocomplete="username"
+            i18n-placeholder
+            placeholder="Username" 
+            [(ngModel)]="args.username"/>
+        </div>
+
+        <div class="form-group row">
+          <label class="col-lg-4 text-right font-weight-bold" for="password" i18n>Password</label>
+          <input 
+            type="password" 
+            class="form-control col-lg-8"
+            id="password" 
+            name="password"
+            required
+            autocomplete="current-password"
+            i18n-placeholder
+            placeholder="Password" 
+            [(ngModel)]="args.password"/>
+        </div>
+
+        <div class="form-group row" *ngIf="workstations && workstations.length">
+          <label class="col-lg-4 text-right font-weight-bold" for="workstation" i18n>Workstation</label>
+          <select 
+            class="form-control col-lg-8" 
+            id="workstation" 
+            name="workstation"
+            required
+            [(ngModel)]="args.workstation">
+            <option *ngFor="let ws of workstations" [value]="ws.name">
+              {{ws.name}}
+            </option>
+          </select>
+        </div>
+
+        <div class="row">
+          <div class="col-lg-8 offset-lg-4 pl-0">
+            <button type="submit" class="btn btn-outline-dark" i18n>Sign in</button>
+          </div>
+        </div>
+      </form>
+    </fieldset>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/login.component.ts b/Open-ILS/src/eg2/src/app/staff/login.component.ts
new file mode 100644
index 0000000..2c1ac2a
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/login.component.ts
@@ -0,0 +1,96 @@
+import {Component, OnInit, Renderer2} from '@angular/core';
+import {Location} from '@angular/common';
+import {Router, ActivatedRoute} from '@angular/router';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {StoreService} from '@eg/core/store.service';
+
+ at Component({
+  templateUrl : './login.component.html'
+})
+
+export class StaffLoginComponent implements OnInit {
+
+    workstations: any[];
+
+    args = {
+      username : '',
+      password : '',
+      workstation : '',
+      type : 'staff'
+    };
+
+    constructor(
+      private router: Router,
+      private route: ActivatedRoute,
+      private ngLocation: Location,
+      private renderer: Renderer2,
+      private auth: AuthService,
+      private store: StoreService
+    ) {}
+
+    ngOnInit() {
+        // clear out any stale auth data
+        this.auth.logout();
+
+        // Focus username
+        this.renderer.selectRootElement('#username').focus();
+
+        this.workstations = this.store.getLocalItem('eg.workstation.all');
+        this.args.workstation =
+            this.store.getLocalItem('eg.workstation.default');
+        this.applyWorkstation();
+    }
+
+    applyWorkstation() {
+        const wanted = this.route.snapshot.queryParamMap.get('workstation');
+        if (!wanted) { return; } // use the default
+
+        const exists = this.workstations.filter(w => w.name === wanted)[0];
+        if (exists) {
+            this.args.workstation = wanted;
+        } else {
+            console.error(`Unknown workstation requested: ${wanted}`);
+        }
+    }
+
+    handleSubmit() {
+
+        // post-login URL
+        let url: string = this.auth.redirectUrl || '/staff/splash';
+
+        // prevent sending the user back to the login page
+        if (url.startsWith('/staff/login')) {
+            url = '/staff/splash';
+        }
+
+        const workstation: string = this.args.workstation;
+
+        this.auth.login(this.args).then(
+            ok => {
+                this.auth.redirectUrl = null;
+
+                if (this.auth.workstationState === AuthWsState.NOT_FOUND_SERVER) {
+                    // User attempted to login with a workstation that is
+                    // unknown to the server. Redirect to the WS admin page.
+                    // Reset the WS state to avoid looping back to WS removal
+                    // page before the new workstation can be activated.
+                    this.auth.workstationState = AuthWsState.PENDING;
+                    this.router.navigate(
+                        [`/staff/admin/workstation/workstations/remove/${workstation}`]);
+                } else {
+                    // Force reload of the app after a successful login.
+                    // This allows the route resolver to re-run with a
+                    // valid auth token and workstation.
+                    window.location.href =
+                        this.ngLocation.prepareExternalUrl(url);
+                }
+            },
+            notOk => {
+                // indicate failure in the UI.
+            }
+        );
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.css b/Open-ILS/src/eg2/src/app/staff/nav.component.css
new file mode 100644
index 0000000..63d3e37
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.css
@@ -0,0 +1,72 @@
+/* remove dropdown carret for icon-based entries */
+#staff-navbar .no-caret::after {
+    display:none;
+}
+
+/* move the caret closer to the dropdown text */
+#staff-navbar {
+    padding-left: 0px;
+}
+
+#staff-navbar {
+    background: -webkit-linear-gradient(#00593d, #007a54);
+    background-color: #007a54;
+    color: #fff;
+    font-size: 14px;
+}
+
+#staff-navbar .navbar-nav {
+  padding: 4px;
+}
+
+/* align top of dropdown w/ bottom of nav */
+#staff-navbar .dropdown-menu {
+    margin-top: 7px;
+}
+#staff-navbar .material-icons {
+    padding-right:3px;
+}
+#staff-navbar .dropdown-item {
+    font-size: 14px;
+    font-weight: 400;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    padding-left: 0.7rem;
+    padding-right: 0.7rem;
+    margin: -4px;
+}
+
+#staff-navbar .dropdown-item .material-icons {
+  font-size: 18px;
+}
+
+#staff-navbar .nav-link {
+    color: #fff;
+    padding-top:1px;
+    padding-bottom:1px;
+}
+#staff-navbar .nav-link:hover {
+    color: #ddd;
+    cursor: pointer;
+}
+
+#staff-navbar .navbar-nav > .open > a,
+#staff-navbar .navbar-nav > .open > a:focus,
+#staff-navbar .navbar-nav > .open > a:hover {
+    background-color: #7a7a7a;
+}
+#staff-navbar .navbar-nav>.dropdown>a .caret {
+    border-top-color: #fff;
+    border-bottom-color: #fff;
+}
+#staff-navbar .navbar-nav>.dropdown>a:hover .caret {
+    border-top-color: #ddd;
+    border-bottom-color: #ddd;
+}
+
+/* Align material-icons with sibling text; otherwise they float up */
+#staff-navbar .with-material-icon, #staff-navbar .dropdown-item {
+    display: inline-flex;
+    vertical-align: middle;
+    align-items: center;
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html
new file mode 100644
index 0000000..419f5d5
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html
@@ -0,0 +1,432 @@
+<div id="staff-navbar" class="navbar fixed-top navbar-expand navbar-default">
+  <div class="collapse navbar-collapse">
+    <div class="navbar-nav">
+      <div class="nav-item">
+        <a i18n class="nav-link with-material-icon" 
+          routerLink="/staff/splash"
+          egAccessKey keyCtx="navbar"
+          keySpec="alt+h" i18n-keySpec
+          keyDesc="Navigate Home" i18n-keyDesc>
+          <span class="material-icons">home</span>
+        </a>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+         Search
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/search">
+            <span class="material-icons">person</span>
+            <span i18n>Search for Patrons</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/cat/item/search">
+            <span class="material-icons">assignment</span>
+            <span i18n>Search for Copies by Barcode</span>
+          </a>
+          <a class="dropdown-item" routerLink="/staff/catalog/search"
+            egAccessKey keyCtx="navbar"
+            keySpec="alt+c" i18n-keySpec
+            keyDesc="Navigate To Catalog" i18n-keyDesc>
+            <span class="material-icons">search</span>
+            <span i18n>Search the Catalog</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle class="nav-link dropdown-toggle">
+         <span i18n>Circulation</span>
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/bcsearch">
+            <span class="material-icons">trending_up</span>
+            <span i18n>Check Out</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/checkin/checkin">
+            <span class="material-icons">trending_down</span>
+            <span i18n>Check In</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/checkin/capture">
+            <span class="material-icons">pin_drop</span>
+            <span i18n>Capture Holds</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/holds/pull">
+            <span class="material-icons">view_list</span>
+            <span i18n>Pull List for Hold Requests</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/renew/renew">
+            <span class="material-icons">autorenew</span>
+            <span i18n>Renew Items</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/register">
+            <span class="material-icons">person_add</span>
+            <span i18n>Register Patron</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/last">
+            <span class="material-icons">redo</span>
+            <span i18n>Retrieve Last Patron</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/search?show_recent=1">
+            <span class="material-icons">redo</span>
+            <span i18n>Retrieve Recent Patrons</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/pending/list">
+            <span class="material-icons">thumb_up</span>
+            <span i18n>Pending Patrons</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/bucket/view">
+            <span class="material-icons">list</span>
+            <span i18n>User Buckets</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/circ/patron/credentials">
+            <span class="material-icons">check_circle</span>
+            <span i18n>Verify Credentials</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/in_house_use/index">
+            <span class="material-icons">playlist_add</span>
+            <span i18n>Record In-House Use</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/circ/holds/shelf">
+            <span class="material-icons">format_list_bulleted</span>
+            <span i18n>Holds Shelf</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/cat/item/replace_barcode/index">
+            <span class="material-icons">library_books</span>
+            <span i18n>Replace Barcode</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/cat/item/search"
+            egAccessKey keyCtx="navbar"
+            keySpec="f5" i18n-keySpec
+            keyDesc="Navigate To Item Status" i18n-keyDesc>
+            <span class="material-icons">question_answer</span>
+            <span i18n>Item Status</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/cat/item/missing_pieces">
+            <span class="material-icons">grid_on</span>
+            <span i18n>Scan Item as Missing Pieces</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" (click)="reprintLast()">
+            <span class="material-icons">redo</span>
+            <span i18n>Reprint Last Receipt</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" href="/eg/staff/offline-interface">
+            <span class="material-icons">signal_wifi_off</span>
+            <span i18n>Offline Circulation</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <!-- CATALOGING -->
+    
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+         Cataloging
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+
+          <a href="/eg/staff/cat/catalog/index" class="dropdown-item">
+            <span class="material-icons">search</span>
+            <span i18n>Search the Catalog</span>
+          </a>
+          <!--
+            Link to experimental Angular staff catalog.
+            Leaving disabled until more functionality can be fleshed out.
+          -->
+          <!--
+          <a class="dropdown-item"
+              routerLink="/staff/catalog/search">
+            <span class="material-icons">search</span>
+            <span i18n>Staff Catalog (Experimental)</span>
+          </a>
+          -->
+          <a href="/eg/staff/cat/bucket/record/view" class="dropdown-item">
+            <span class="material-icons">list_alt</span>
+            <span i18n>Record Buckets</span>
+          </a>
+          <a href="/eg/staff/cat/bucket/copy/view" class="dropdown-item">
+            <span class="material-icons">list_alt</span>
+            <span i18n>Copy Buckets</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/retrieve_by_id" class="dropdown-item">
+            <span class="material-icons">collections</span>
+            <span i18n>Retrieve Bib Record by ID</span>
+          </a>
+          <a href="/eg/staff/cat/catalog/retrieve_by_tcn"
+            eg-accesskey="shift+f3" 
+            eg-accesskey-desc="Retrieve Last Bib Record" class="dropdown-item">
+            <span class="material-icons">collections_bookmark</span>
+            <span i18n>Retrieve Bib Record by TCN</span>
+          </a>
+          <a href="" ng-click="retrieveLastRecord()"
+            eg-accesskey="shift+f8" 
+            eg-accesskey-desc="Retrieve Last Bib Record" class="dropdown-item">
+            <span class="material-icons">redo</span>
+            <span i18n>Retrieve Last Bib Record</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/new_bib" class="dropdown-item">
+            <span class="material-icons">add</span>
+            <span i18n>Create New MARC Record</span>
+          </a>
+          <a href="/eg/staff/cat/z3950/index" class="dropdown-item">
+            <span class="material-icons">cloud_download</span>
+            <span i18n>Import Record from Z39.50</span>
+          </a>
+          <a href="/eg/staff/cat/catalog/vandelay" class="dropdown-item">
+            <span class="material-icons">import_export</span>
+            <span i18n>MARC Batch Import/Export</span>
+          </a>
+          <a href="/eg/staff/cat/catalog/batchEdit" class="dropdown-item">
+            <span class="material-icons">format_paint</span>
+            <span i18n>MARC Batch Edit</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/verifyURLs" class="dropdown-item">
+            <span class="material-icons">link</span>
+            <span i18n>Link Checker</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a href="/eg/staff/cat/catalog/manageAuthorities" class="dropdown-item">
+            <span class="material-icons">lock</span>
+            <span i18n>Manage Authorities</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <!-- ACQUISITIONS -->
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+          Acquisitions
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified">
+            <span class="material-icons">search</span>
+            <span i18n>General Search</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified?ca=pl">
+            <span class="material-icons">view_list</span>
+            <span i18n>My Selection Lists</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/brief_record">
+            <span class="material-icons">edit</span>
+            <span i18n>New Brief Record</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/user_request">
+            <span class="material-icons">thumb_up</span>
+            <span i18n>Patron Requests</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/bib_search">
+            <span class="material-icons">cloud_download</span>
+            <span i18n>MARC Federated Search</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/from_bib">
+            <span class="material-icons">trending_down</span>
+            <span i18n>Load Catalog Record IDs</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/picklist/upload">
+            <span class="material-icons">cloud_upload</span>
+            <span i18n>Load MARC Order Records</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified?ca=po">
+            <span class="material-icons">shopping_cart</span>
+            <span i18n>Purchase Orders</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/acq/legacy/po/create">
+            <span class="material-icons">add_shopping_cart</span>
+            <span i18n>Create Purchase Order</span>
+          </a>
+          <div class="dropdown-divider"></div>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/financial/claim_eligible">
+            <span class="material-icons">contact_phone</span>
+            <span i18n>Claim-Ready Items</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/search/unified?ca=inv">
+            <span class="material-icons">attach_money</span>
+            <span i18n>Open Invoices</span>
+          </a>
+          <a class="dropdown-item" 
+            href="/eg/staff/acq/legacy/invoice/view?create=1">
+            <span class="material-icons">monetization_on</span>
+            <span i18n>Create Invoice</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+          Booking
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/reservation">
+            <span class="material-icons">add</span>
+            <span i18n>Create Reservations</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/pull_list">
+            <span class="material-icons">list</span>
+            <span i18n>Pull List</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/capture">
+            <span class="material-icons">pin_drop</span>
+            <span i18n>Capture Resources</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/pickup">
+            <span class="material-icons">trending_up</span>
+            <span i18n>Pick Up Reservations</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/booking/legacy/booking/return">
+            <span class="material-icons">trending_down</span>
+            <span i18n>Return Reservations</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+    <div class="navbar-nav">
+      <div ngbDropdown class="nav-item dropdown">
+        <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
+          Administration
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" href="/eg/staff/admin/workstation/index">
+            <span class="material-icons">computer</span>
+            <span i18n>Workstation</span>
+          </a>
+          <!--
+          Leaving here as a reminder this UI exists.
+          <a class="dropdown-item"
+              routerLink="/staff/admin/workstation/workstations/manage">
+            <span class="material-icons">computer</span>
+            <span i18n>Registered Workstations</span>
+          </a>
+          -->
+          <a class="dropdown-item" href="/eg/staff/admin/user_perms">
+            <span class="material-icons">person</span>
+            <span i18n>User Permission Editor</span>
+          </a>
+          <!-- Angular version
+          <a class="dropdown-item"
+              routerLink="/staff/admin/server/splash">
+            <span class="material-icons">account_balance</span>
+            <span i18n>Server Administration</span>
+          </a>
+          -->
+          <a class="dropdown-item" href="/eg/staff/admin/server/index">
+            <span class="material-icons">account_balance</span>
+            <span i18n>Server Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/admin/local/index">
+            <span class="material-icons">landscape</span>
+            <span i18n>Local Administration</span>
+          </a>
+          <a class="dropdown-item"
+              routerLink="/staff/admin/acq/splash">
+            <span class="material-icons">attach_money</span>
+            <span i18n>Acquisitions Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/admin/serials/index">
+            <span class="material-icons">layers</span>
+            <span i18n>Serials Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/admin/booking/index">
+            <span class="material-icons">business_center</span>
+            <span i18n>Booking Administration</span>
+          </a>
+          <a class="dropdown-item" href="/eg/staff/reporter/legacy/main">
+            <span class="material-icons">insert_chart_outlined</span>
+            <span i18n>Reports</span>
+          </a>
+        </div>
+      </div>
+    </div>
+
+
+    <div class="navbar-nav mr-auto"></div>
+    <div class="navbar-nav" *ngIf="user()">
+      <span i18n>{{user()}} @ {{workstation()}}</span>
+    </div>
+    <div class="navbar-nav" *ngIf="locales.length > 1 && currentLocale">
+      <div ngbDropdown class="nav-item dropdown" placement="bottom-right">
+        <a ngbDropdownToggle i18n i18n-title
+          title="Select Locale"
+          class="nav-link dropdown-toggle no-caret with-material-icon">
+          <i class="material-icons">flag</i>
+          <span>{{currentLocale.name()}}</span>
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <a class="dropdown-item" (click)="setLocale(locale)" 
+            [ngClass]="{disabled: currentLocale.code() == locale.code()}"
+            *ngFor="let locale of locales">
+            <span class="material-icons">add_location</span>
+            <span i18n>{{locale.name()}}</span>
+          </a>
+        </div>
+      </div>
+    </div>
+    <div class="navbar-nav" *ngIf="user()">
+      <div ngbDropdown class="nav-item dropdown" placement="bottom-right">
+        <a ngbDropdownToggle i18n 
+          i18n-title
+          title="Log out and more..."
+          class="nav-link dropdown-toggle no-caret with-material-icon">
+          <i class="material-icons">list</i>
+        </a>
+        <div class="dropdown-menu" ngbDropdownMenu>
+          <eg-op-change #navOpChange
+            i18n-failMessage
+            i18n-successMessage
+            failMessage="Operator Change Failed"
+            successMessage="Operator Change Succeeded">
+          </eg-op-change>
+          <a class="dropdown-item" *ngIf="!opChangeActive()" 
+            (click)="navOpChange.open()">
+            <span class="material-icons">transform</span>
+            <span i18n>Change Operator</span>
+          </a>
+          <a *ngIf="opChangeActive()" class="dropdown-item" 
+            (click)="navOpChange.restore()">
+            <span class="material-icons">transform</span>
+            <span i18n>Restore Operator</span>
+          </a>
+          <a class="dropdown-item" (click)="logout()">
+            <span class="material-icons">lock_outline</span>
+            <span i18n>Logout</span>
+          </a>
+          <a class="dropdown-item" routerLink="/staff/about">
+            <span class="material-icons">info_outline</span>
+            <span i18n>About</span>
+          </a>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.ts b/Open-ILS/src/eg2/src/app/staff/nav.component.ts
new file mode 100644
index 0000000..c477c11
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/nav.component.ts
@@ -0,0 +1,72 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {Location} from '@angular/common';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {PrintService} from '@eg/share/print/print.service';
+
+ at Component({
+    selector: 'eg-staff-nav-bar',
+    styleUrls: ['nav.component.css'],
+    templateUrl: 'nav.component.html'
+})
+
+export class StaffNavComponent implements OnInit {
+
+    // Locales that have Angular staff translations
+    locales: any[];
+    currentLocale: any;
+
+    constructor(
+        private router: Router,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private locale: LocaleService,
+        private printer: PrintService
+    ) {
+        this.locales = [];
+    }
+
+    ngOnInit() {
+
+        this.locale.supportedLocales().subscribe(
+            l => this.locales.push(l),
+            err => {},
+            () => {
+                this.currentLocale = this.locales.filter(
+                    l => l.code() === this.locale.currentLocaleCode())[0];
+            }
+        );
+    }
+
+    user() {
+        return this.auth.user() ? this.auth.user().usrname() : '';
+    }
+
+    workstation() {
+        return this.auth.user() ? this.auth.workstation() : '';
+    }
+
+    setLocale(locale: any) {
+        this.locale.setLocale(locale.code());
+    }
+
+    opChangeActive(): boolean {
+        return this.auth.opChangeIsActive();
+    }
+
+    // Broadcast to all tabs that we're logging out.
+    // Redirect to the login page, which performs the remaining
+    // logout duties.
+    logout(): void {
+        this.auth.broadcastLogout();
+        this.router.navigate(['/staff/login']);
+    }
+
+    reprintLast() {
+        this.printer.reprintLast();
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
new file mode 100644
index 0000000..ccee922
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/resolver.service.ts
@@ -0,0 +1,143 @@
+import {Injectable} from '@angular/core';
+import {Location} from '@angular/common';
+import {Observable} from 'rxjs/Observable';
+import {Observer} from 'rxjs/Observer';
+import {of} from 'rxjs';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router';
+import {StoreService} from '@eg/core/store.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {PermService} from '@eg/core/perm.service';
+import {OrgService} from '@eg/core/org.service';
+import {FormatService} from '@eg/core/format.service';
+
+const LOGIN_PATH = '/staff/login';
+const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
+
+/**
+ * Load data used by all staff modules.
+ */
+ at Injectable()
+export class StaffResolver implements Resolve<Observable<any>> {
+
+    // Tracks the primary resolve observable.
+    observer: Observer<any>;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private ngLocation: Location,
+        private store: StoreService,
+        private org: OrgService,
+        private net: NetService,
+        private auth: AuthService,
+        private perm: PermService,
+        private format: FormatService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Observable<any> {
+
+        // Staff cookies stay in /$base/staff/
+        // NOTE: storing session data at '/' so it can be shared by
+        // Angularjs apps.
+        this.store.loginSessionBasePath = '/';
+        // ^-- = this.ngLocation.prepareExternalUrl('/staff');
+
+        // Not sure how to get the path without params... using this for now.
+        const path = state.url.split('?')[0];
+        if (path === '/staff/login') {
+            return of(true);
+        }
+
+        const observable: Observable<any>
+            = Observable.create(o => this.observer = o);
+
+        this.auth.testAuthToken().then(
+            tokenOk => {
+                this.confirmStaffPerms().then(
+                    hasPerms => {
+                        this.auth.verifyWorkstation().then(
+                            wsOk => {
+                                this.loadStartupData()
+                                .then(ok => this.observer.complete());
+                            },
+                            wsNotOk => this.handleInvalidWorkstation(path)
+                        );
+                    },
+                    hasNotPerms => {
+                        this.observer.error(
+                            'User does not have staff permissions');
+                    }
+                );
+            },
+            tokenNotOk => this.handleInvalidToken(state)
+        );
+
+        return observable;
+    }
+
+
+    // Confirm the user has the STAFF_LOGIN permission anywhere before
+    // allowing the staff sub-tree to load. This will prevent users
+    // with valid, non-staff authtokens from attempting to connect and
+    // subsequently getting redirected to the workstation admin page
+    // (since they won't have a valid WS either).
+    confirmStaffPerms(): Promise<any> {
+        return new Promise((resolve, reject) => {
+            this.perm.hasWorkPermAt(['STAFF_LOGIN']).then(
+                permMap => {
+                    if (permMap.STAFF_LOGIN.length) {
+                        resolve('perm check OK');
+                    } else {
+                        reject('perm check faield');
+                    }
+                }
+            );
+        });
+    }
+
+
+    // A page that's not the login page was requested without a
+    // valid auth token.  Send the caller back to the login page.
+    handleInvalidToken(state: RouterStateSnapshot): void {
+        console.debug('StaffResolver: authtoken is not valid');
+        this.auth.redirectUrl = state.url;
+        this.router.navigate([LOGIN_PATH]);
+        this.observer.error('invalid or no auth token');
+    }
+
+    handleInvalidWorkstation(path: string): void {
+
+        if (path.startsWith(WS_MANAGE_PATH)) {
+            // user is navigating to the WS admin page.
+            this.observer.complete();
+        } else {
+            this.router.navigate([WS_MANAGE_PATH]);
+            this.observer.error(`Auth session linked to no
+                workstation or a workstation unknown to this browser`);
+        }
+    }
+
+    /**
+     * Fetches data common to all staff interfaces.
+     */
+    loadStartupData(): Promise<void> {
+
+        // Fetch settings needed globally.  This will cache the values
+        // in the org service.
+        return this.org.settings([
+            'lib.timezone',
+            'webstaff.format.dates',
+            'webstaff.format.date_and_time',
+            'ui.staff.max_recent_patrons'
+        ]).then(settings => {
+            this.format.wsOrgTimezone = settings['lib.timezone'];
+            this.format.dateFormat = settings['webstaff.format.dates'];
+            this.format.dateTimeFormat = settings['webstaff.format.date_and_time'];
+        });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
new file mode 100644
index 0000000..b515f38
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/routing.module.ts
@@ -0,0 +1,52 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {StaffResolver} from './resolver.service';
+import {StaffComponent} from './staff.component';
+import {StaffLoginComponent} from './login.component';
+import {StaffSplashComponent} from './splash.component';
+import {AboutComponent} from './about.component';
+
+// Not using 'canActivate' because it's called before all resolvers,
+// even the parent resolver, but the resolvers parse the IDL, load settings,
+// etc.  Chicken, meet egg.
+
+const routes: Routes = [{
+  path: '',
+  component: StaffComponent,
+  resolve: {staffResolver : StaffResolver},
+  children: [{
+    path: '',
+    redirectTo: 'splash',
+    pathMatch: 'full',
+  }, {
+    path: 'about',
+    component: AboutComponent
+  }, {
+    path: 'login',
+    component: StaffLoginComponent
+  }, {
+    path: 'splash',
+    component: StaffSplashComponent
+  }, {
+    path: 'circ',
+    loadChildren : '@eg/staff/circ/routing.module#CircRoutingModule'
+  }, {
+    path: 'catalog',
+    loadChildren : '@eg/staff/catalog/catalog.module#CatalogModule'
+  }, {
+    path: 'sandbox',
+    loadChildren : '@eg/staff/sandbox/sandbox.module#SandboxModule'
+  }, {
+    path: 'admin',
+    loadChildren : '@eg/staff/admin/routing.module#AdminRoutingModule'
+  }]
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: [StaffResolver]
+})
+
+export class StaffRoutingModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/README b/Open-ILS/src/eg2/src/app/staff/sandbox/README
new file mode 100644
index 0000000..66e77dc
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/README
@@ -0,0 +1 @@
+Place for experimenting with code.
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts
new file mode 100644
index 0000000..144f0d2
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts
@@ -0,0 +1,16 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {SandboxComponent} from './sandbox.component';
+
+const routes: Routes = [{
+  path: '',
+  component: SandboxComponent
+}];
+
+ at NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+  providers: []
+})
+
+export class SandboxRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
new file mode 100644
index 0000000..289ed50
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
@@ -0,0 +1,133 @@
+
+<eg-staff-banner bannerText="Sandbox" i18n-bannerText>
+</eg-staff-banner>
+
+<!-- FM Editor Experiments ----------------------------- -->
+<div class="row mb-3">
+  <ng-template #descriptionTemplate 
+      let-field="field" let-record="record" let-hello="hello">
+  <!-- example custom template for editing the 'description' field -->
+    <textarea
+      placeholder="{{hello}}"
+      class="form-control"
+      name="{{field.name}}"
+      [readonly]="field.readOnly"
+      [required]="field.isRequired()"
+      [ngModel]="record[field.name]()"
+      (ngModelChange)="record[field.name]($event)">
+    </textarea>
+  </ng-template>
+  <eg-fm-record-editor #fmRecordEditor 
+      idlClass="cmrcfld" mode="create" 
+      [customFieldTemplates]="{description:{template:descriptionTemplate,context:{'hello':'goodbye'}}}"
+      recordId="1" orgDefaultAllowed="owner">
+  </eg-fm-record-editor>
+  <button class="btn btn-dark" (click)="fmRecordEditor.open({size:'lg'})">
+      Fm Record Editor
+  </button>
+</div>
+<!-- / FM Editor Experiments ----------------------------- -->
+
+<!-- Progress Dialog Experiments ----------------------------- -->
+<div class="row mb-3">
+  <div class="col-lg-3">
+    <button class="btn btn-outline-danger" (click)="progress.increment()">Increment Inline</button>
+  </div>
+  <div class="col-lg-3">
+    <eg-progress-inline [max]="100" [value]="1" #progress></eg-progress-inline>
+  </div>
+</div>
+<div class="row mb-3">
+  <div class="col-lg-4">
+    <eg-progress-dialog #progressDialog>
+    </eg-progress-dialog>
+    <button class="btn btn-light" (click)="showProgress()">Test Progress Dialog</button>
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox [allowFreeText]="true" 
+      placeholder="Combobox with static data"
+      [entries]="cbEntries"></eg-combobox>
+  </div>
+  <div class="col-lg-3">
+    <eg-combobox
+      placeholder="Combobox with dynamic data"
+      [asyncDataSource]="cbAsyncSource"></eg-combobox>
+  </div>
+</div>
+<div class="row mb-3">
+  <div class="col-lg-4">
+   <button class="btn btn-info" (click)="testToast()">Test Toast Message</button>
+  </div>
+  <div class="col-lg-2">
+    Org select with limit perms
+  </div>
+  <div class="col-lg-2">
+    <eg-org-select [limitPerms]="['REGISTER_WORKSTATION']">
+    </eg-org-select>
+  </div>
+</div>
+<!-- /Progress Dialog Experiments ----------------------------- -->
+
+<!-- eg strings -->
+<!--
+<div class="row mb-3">
+    <eg-string #helloString text="Hello, {{name}}" i18n-text></eg-string>
+    <button class="btn btn-success" (click)="testStrings()">Test Strings</button>
+</div>
+-->
+
+<div class="row mb-3">
+    <ng-template #helloStrTmpl let-name="name" i18n>Hello, {{name}}</ng-template>
+    <!--
+    <eg-string #helloStr key="helloKey" [template]="helloStrTmpl"></eg-string>
+    -->
+    <eg-string key="staff.sandbox.test" [template]="helloStrTmpl"></eg-string>
+    <button class="btn btn-success" (click)="testStrings()">Test Strings</button>
+</div>
+
+<div class="row">
+  <div class="form-group">
+    <eg-date-select (onChangeAsDate)="changeDate($event)"
+        initialYmd="2017-03-04">
+    </eg-date-select>
+  </div>
+  <div>HERE: {{testDate}}</div>
+</div>
+
+<!-- printing -->
+
+<button class="btn btn-secondary" (click)="doPrint()">Test Print</button>
+<ng-template #printTemplate let-context>Hello, {{context.world}}!</ng-template>
+
+<br/><br/>
+HERasdfE
+<div class="row">
+  <div class="col-lg-3">
+    <eg-translate #translate [idlObject]="oneBtype" fieldName="name"></eg-translate>
+    <button class="btn btn-info"
+      (click)="translate.open({size:'lg'})">Translate</button>
+  </div>
+</div>
+<br/><br/>
+
+<!-- grid stuff -->
+<ng-template #cellTmpl let-row="row" let-col="col" let-userContext="userContext">
+  HELLO {{userContext.hello}}
+  <button>{{row.id()}}</button>
+</ng-template>
+<eg-grid #cbtGrid idlClass="cbt" 
+  [dataSource]="btSource" 
+  [rowClassCallback]="btGridRowClassCallback"
+  [rowFlairIsEnabled]="true"
+  [rowFlairCallback]="btGridRowFlairCallback"
+  [cellClassCallback]="btGridCellClassCallback"
+  [sortable]="true">
+  <eg-grid-column name="test" [cellTemplate]="cellTmpl" 
+    [cellContext]="btGridTestContext" [sortable]="false">
+  </eg-grid-column>
+  <eg-grid-column [sortable]="false" path="owner.name"></eg-grid-column>
+</eg-grid>
+
+<br/><br/>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
new file mode 100644
index 0000000..92b18dc
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
@@ -0,0 +1,188 @@
+import {Component, OnInit, ViewChild, Input, TemplateRef} from '@angular/core';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringService} from '@eg/share/string/string.service';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/timer';
+import {of} from 'rxjs';
+import {map} from 'rxjs/operators/map';
+import {take} from 'rxjs/operators/take';
+import {GridDataSource, GridColumn, GridRowFlairEntry} from '@eg/share/grid/grid';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {Pager} from '@eg/share/util/pager';
+import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+ at Component({
+  templateUrl: 'sandbox.component.html'
+})
+export class SandboxComponent implements OnInit {
+
+    @ViewChild('progressDialog')
+    private progressDialog: ProgressDialogComponent;
+
+    @ViewChild('dateSelect')
+    private dateSelector: DateSelectComponent;
+
+    @ViewChild('printTemplate')
+    private printTemplate: TemplateRef<any>;
+
+    // @ViewChild('helloStr') private helloStr: StringComponent;
+
+    gridDataSource: GridDataSource = new GridDataSource();
+
+    cbEntries: ComboboxEntry[];
+    // supplier of async combobox data
+    cbAsyncSource: (term: string) => Observable<ComboboxEntry>;
+
+    btSource: GridDataSource = new GridDataSource();
+    world = 'world'; // for local template version
+    btGridTestContext: any = {hello : this.world};
+
+    renderLocal = false;
+
+    testDate: any;
+
+    testStr: string;
+    @Input() set testString(str: string) {
+        this.testStr = str;
+    }
+
+    oneBtype: IdlObject;
+
+    name = 'Jane';
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private pcrud: PcrudService,
+        private strings: StringService,
+        private toast: ToastService,
+        private printer: PrintService
+    ) {
+    }
+
+    ngOnInit() {
+
+        this.gridDataSource.data = [
+            {name: 'Jane', state: 'AZ'},
+            {name: 'Al', state: 'CA'},
+            {name: 'The Tick', state: 'TX'}
+        ];
+
+        this.pcrud.retrieveAll('cmrcfld', {order_by: {cmrcfld: 'name'}})
+        .subscribe(format => {
+            if (!this.cbEntries) { this.cbEntries = []; }
+            this.cbEntries.push({id: format.id(), label: format.name()});
+        });
+
+        this.cbAsyncSource = term => {
+            return this.pcrud.search(
+                'cmrcfld',
+                {name: {'ilike': `%${term}%`}}, // could -or search on label
+                {order_by: {cmrcfld: 'name'}}
+            ).pipe(map(marcField => {
+                return {id: marcField.id(), label: marcField.name()};
+            }));
+        };
+
+        this.btSource.getRows = (pager: Pager, sort: any[]) => {
+
+            const orderBy: any = {cbt: 'name'};
+            if (sort.length) {
+                orderBy.cbt = sort[0].name + ' ' + sort[0].dir;
+            }
+
+            return this.pcrud.retrieveAll('cbt', {
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            }).pipe(map(cbt => {
+                // example of inline fleshing
+                cbt.owner(this.org.get(cbt.owner()));
+                this.oneBtype = cbt;
+                return cbt;
+            }));
+        };
+    }
+
+    btGridRowClassCallback(row: any): string {
+        if (row.id() === 1) {
+            return 'text-uppercase font-weight-bold text-danger';
+        }
+    }
+
+    btGridRowFlairCallback(row: any): GridRowFlairEntry {
+        const flair = {icon: null, title: null};
+        if (row.id() === 2) {
+            flair.icon = 'priority_high';
+            flair.title = 'I Am ID 2';
+        } else if (row.id() === 3) {
+            flair.icon = 'not_interested';
+        }
+        return flair;
+    }
+
+    // apply to all 'name' columns regardless of row
+    btGridCellClassCallback(row: any, col: GridColumn): string {
+        if (col.name === 'name') {
+            if (row.id() === 7) {
+                return 'text-lowercase font-weight-bold text-info';
+            }
+            return 'text-uppercase font-weight-bold text-success';
+        }
+    }
+
+    doPrint() {
+        this.printer.print({
+            template: this.printTemplate,
+            contextData: {world : this.world},
+            printContext: 'default'
+        });
+
+        this.printer.print({
+            text: '<b>hello</b>',
+            printContext: 'default'
+        });
+
+    }
+
+    changeDate(date) {
+        console.log('HERE WITH ' + date);
+        this.testDate = date;
+    }
+
+    showProgress() {
+        this.progressDialog.open();
+
+        // every 250ms emit x*10 for 0-10
+        Observable.timer(0, 250).pipe(
+            map(x => x * 10),
+            take(11)
+        ).subscribe(
+            val => this.progressDialog.update({value: val, max: 100}),
+            err => {},
+            ()  => this.progressDialog.close()
+        );
+    }
+
+    testToast() {
+        this.toast.success('HELLO TOAST TEST');
+        setTimeout(() => this.toast.danger('DANGER TEST AHHH!'), 4000);
+    }
+
+    testStrings() {
+        this.strings.interpolate('staff.sandbox.test', {name : 'janey'})
+            .then(txt => this.toast.success(txt));
+
+        setTimeout(() => {
+            this.strings.interpolate('staff.sandbox.test', {name : 'johnny'})
+                .then(txt => this.toast.success(txt));
+        }, 4000);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts
new file mode 100644
index 0000000..58910dd
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {SandboxRoutingModule} from './routing.module';
+import {SandboxComponent} from './sandbox.component';
+
+ at NgModule({
+  declarations: [
+    SandboxComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    SandboxRoutingModule,
+  ],
+  providers: [
+  ]
+})
+
+export class SandboxModule {
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/share/README b/Open-ILS/src/eg2/src/app/staff/share/README
new file mode 100644
index 0000000..1d6d167
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/README
@@ -0,0 +1 @@
+Classes, services, and components shared in the staff app.
diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
new file mode 100644
index 0000000..194f06b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
@@ -0,0 +1,59 @@
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Succeessfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+  <div class="d-flex">
+    <div>
+      <div class="input-group">
+        <div class="input-group-prepend">
+          <span class="input-group-text">{{orgFieldLabel}}</span>
+        </div>
+        <eg-org-select 
+          [limitPerms]="viewPerms"
+          [initialOrg]="contextOrg"
+          (onChange)="orgOnChange($event)">
+        </eg-org-select>
+      </div>
+    </div>
+    <div class="pl-2">
+      <div class="form-check">
+        <input type="checkbox" (click)="grid.reload()" 
+          [disabled]="disableAncestorSelector()"
+          [(ngModel)]="includeOrgAncestors"
+          class="form-check-input" id="include-ancestors">
+        <label class="form-check-label" for="include-ancestors" i18n>+ Ancestors</label>
+      </div>
+      <div class="form-check">
+        <input type="checkbox" (click)="grid.reload()" 
+          [disabled]="disableDescendantSelector()"
+          [(ngModel)]="includeOrgDescendants" 
+          class="form-check-input" id="include-descendants">
+        <label class="form-check-label" for="include-descendants" i18n>+ Descendants</label>
+      </div>
+    </div>
+  </div>
+  <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" 
+    [sortable]="true" persistKey="{{persistKey}}">
+  <eg-grid-toolbar-button [disabled]="!canCreate" 
+    label="New {{idlClassDef.label}}" i18n-label [action]="createNew">
+  </eg-grid-toolbar-button>
+  <eg-grid-toolbar-button [disabled]="translatableFields.length == 0" 
+    label="Apply Translations" i18n-label [action]="translate">
+  </eg-grid-toolbar-button>
+  <eg-grid-toolbar-action label="Delete Selected" i18n-label [action]="deleteSelected">
+  </eg-grid-toolbar-action>
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}">
+</eg-fm-record-editor>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts
new file mode 100644
index 0000000..be4452b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts
@@ -0,0 +1,311 @@
+import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {Pager} from '@eg/share/util/pager';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+
+/**
+ * General purpose CRUD interface for IDL objects
+ *
+ * Object types using this component must be editable via PCRUD.
+ */
+
+ at Component({
+    selector: 'eg-admin-page',
+    templateUrl: './admin-page.component.html'
+})
+
+export class AdminPageComponent implements OnInit {
+
+    @Input() idlClass: string;
+
+    // Default sort field, used when no grid sorting is applied.
+    @Input() sortField: string;
+
+    // Data source may be provided by the caller.  This gives the caller
+    // complete control over the contents of the grid.  If no data source
+    // is provided, a generic one is create which is sufficient for data
+    // that requires no special handling, filtering, etc.
+    @Input() dataSource: GridDataSource;
+
+    // Size of create/edito dialog.  Uses large by default.
+    @Input() dialogSize: 'sm' | 'lg' = 'lg';
+
+    // If an org unit field is specified, an org unit filter
+    // is added to the top of the page.
+    @Input() orgField: string;
+
+    // Disable the auto-matic org unit field filter
+    @Input() disableOrgFilter: boolean;
+
+    // Include objects linking to org units which are ancestors
+    // of the selected org unit.
+    @Input() includeOrgAncestors: boolean;
+
+    // Ditto includeOrgAncestors, but descendants.
+    @Input() includeOrgDescendants: boolean;
+
+    // Optional grid persist key.  This is the part of the key
+    // following eg.grid.
+    @Input() persistKey: string;
+
+    // Optional path component to add to the generated grid persist key,
+    // formatted as (for example):
+    // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
+    @Input() persistKeyPfx: string;
+
+    @ViewChild('grid') grid: GridComponent;
+    @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+    @ViewChild('successString') successString: StringComponent;
+    @ViewChild('createString') createString: StringComponent;
+    @ViewChild('translator') translator: TranslateComponent;
+
+    idlClassDef: any;
+    pkeyField: string;
+    createNew: () => void;
+    deleteSelected: (rows: IdlObject[]) => void;
+
+    // True if any columns on the object support translations
+    translateRowIdx: number;
+    translateFieldIdx: number;
+    translatableFields: string[];
+    translate: () => void;
+
+    contextOrg: IdlObject;
+    orgFieldLabel: string;
+    viewPerms: string;
+    canCreate: boolean;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private perm: PermService,
+        private toast: ToastService
+    ) {
+        this.translatableFields = [];
+    }
+
+    applyOrgValues() {
+
+        if (this.disableOrgFilter) {
+            this.orgField = null;
+            return;
+        }
+
+        if (!this.orgField) {
+            // If no org unit field is specified, try to find one.
+            // If an object type has multiple org unit fields, the
+            // caller should specify one or disable org unit filter.
+            this.idlClassDef.fields.forEach(field => {
+                if (field['class'] === 'aou') {
+                    this.orgField = field.name;
+                }
+            });
+        }
+
+        if (this.orgField) {
+            this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
+            this.contextOrg = this.org.root();
+        }
+    }
+
+    ngOnInit() {
+        this.idlClassDef = this.idl.classes[this.idlClass];
+        this.pkeyField = this.idlClassDef.pkey || 'id';
+
+        this.translatableFields =
+            this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
+
+        if (!this.persistKey) {
+            this.persistKey =
+                'admin.' +
+                (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
+                this.idlClassDef.table;
+        }
+
+        // Limit the view org selector to orgs where the user has
+        // permacrud-encoded view permissions.
+        const pc = this.idlClassDef.permacrud;
+        if (pc && pc.retrieve) {
+            this.viewPerms = pc.retrieve.perms;
+        }
+
+        this.checkCreatePerms();
+        this.applyOrgValues();
+
+        // If the caller provides not data source, create a generic one.
+        if (!this.dataSource) {
+            this.initDataSource();
+        }
+
+        // TODO: pass the row activate handler via the grid markup
+        this.grid.onRowActivate.subscribe(
+            (idlThing: IdlObject) => {
+                this.editDialog.mode = 'update';
+                this.editDialog.recId = idlThing[this.pkeyField]();
+                this.editDialog.open({size: this.dialogSize}).then(
+                    ok => {
+                        this.successString.current()
+                            .then(str => this.toast.success(str));
+                        this.grid.reload();
+                    },
+                    err => {}
+                );
+            }
+        );
+
+        this.createNew = () => {
+            this.editDialog.mode = 'create';
+            this.editDialog.open({size: this.dialogSize}).then(
+                ok => {
+                    this.createString.current()
+                        .then(str => this.toast.success(str));
+                    this.grid.reload();
+                },
+                err => {}
+            );
+        };
+
+        this.deleteSelected = (idlThings: IdlObject[]) => {
+            idlThings.forEach(idlThing => idlThing.isdeleted(true));
+            this.pcrud.autoApply(idlThings).subscribe(
+                val => console.debug('deleted: ' + val),
+                err => {},
+                ()  => this.grid.reload()
+            );
+        };
+
+        // Open the field translation dialog.
+        // Link the next/previous actions to cycle through each translatable
+        // field on each row.
+        this.translate = () => {
+            this.translateRowIdx = 0;
+            this.translateFieldIdx = 0;
+            this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
+            this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
+
+            this.translator.nextString = () => {
+
+                if (this.translateFieldIdx < this.translatableFields.length - 1) {
+                    this.translateFieldIdx++;
+
+                } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
+                    this.translateRowIdx++;
+                    this.translateFieldIdx = 0;
+                }
+
+                this.translator.idlObject =
+                    this.dataSource.data[this.translateRowIdx];
+                this.translator.fieldName =
+                    this.translatableFields[this.translateFieldIdx];
+            };
+
+            this.translator.prevString = () => {
+
+                if (this.translateFieldIdx > 0) {
+                    this.translateFieldIdx--;
+
+                } else if (this.translateRowIdx > 0) {
+                    this.translateRowIdx--;
+                    this.translateFieldIdx = 0;
+                }
+
+                this.translator.idlObject =
+                    this.dataSource.data[this.translateRowIdx];
+                this.translator.fieldName =
+                    this.translatableFields[this.translateFieldIdx];
+            };
+
+            this.translator.open({size: 'lg'});
+        };
+    }
+
+    checkCreatePerms() {
+        this.canCreate = false;
+        const pc = this.idlClassDef.permacrud || {};
+        const perms = pc.create ? pc.create.perms : [];
+        if (perms.length === 0) { return; }
+
+        this.perm.hasWorkPermAt(perms, true).then(permMap => {
+            Object.keys(permMap).forEach(key => {
+                if (permMap[key].length > 0) {
+                    this.canCreate = true;
+                }
+            });
+        });
+    }
+
+    orgOnChange(org: IdlObject) {
+        this.contextOrg = org;
+        this.grid.reload();
+    }
+
+    initDataSource() {
+        this.dataSource = new GridDataSource();
+
+        this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy: any = {};
+
+            if (sort.length) {
+                // Sort specified from grid
+                orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
+
+            } else if (this.sortField) {
+                // Default sort field
+                orderBy[this.idlClass] = this.sortField;
+            }
+
+            const searchOps = {
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            };
+
+            if (this.contextOrg) {
+                // Filter rows by those linking to the context org and
+                // optionally ancestor and descendant org units.
+
+                let orgs = [this.contextOrg.id()];
+
+                if (this.includeOrgAncestors) {
+                    orgs = this.org.ancestors(this.contextOrg, true);
+                }
+
+                if (this.includeOrgDescendants) {
+                    // can result in duplicate workstation org IDs... meh
+                    orgs = orgs.concat(
+                        this.org.descendants(this.contextOrg, true));
+                }
+
+                const search = {};
+                search[this.orgField] = orgs;
+                return this.pcrud.search(this.idlClass, search, searchOps);
+            }
+
+            // No org filter -- fetch all rows
+            return this.pcrud.retrieveAll(this.idlClass, searchOps);
+        };
+    }
+
+    disableAncestorSelector(): boolean {
+        return this.contextOrg &&
+            this.contextOrg.id() === this.org.root().id();
+    }
+
+    disableDescendantSelector(): boolean {
+        return this.contextOrg && this.contextOrg.children().length === 0;
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html
new file mode 100644
index 0000000..d49de1b
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html
@@ -0,0 +1,70 @@
+
+<div class='eg-bib-summary card tight-card w-100' *ngIf="summary">
+  <div class="card-header d-flex">
+    <div class="font-weight-bold">
+      Record Summary
+    </div>
+    <div class="flex-1"></div>
+    <div>
+      <a class="with-material-icon no-href text-primary" 
+        title="Show More" i18n-title
+        *ngIf="!expandDisplay" (click)="expandDisplay=true">
+        <span class="material-icons">expand_more</span>
+      </a>
+      <a class="with-material-icon no-href text-primary" 
+        title="Show Less" i18n-title
+        *ngIf="expandDisplay" (click)="expandDisplay=false">
+        <span class="material-icons">expand_less</span>
+      </a>
+    </div>
+  </div>
+  <div class="card-body">
+    <ul class="list-group list-group-flush">
+      <li class="list-group-item">
+        <div class="d-flex">
+          <div class="flex-1 font-weight-bold" i18n>Title:</div>
+          <div class="flex-3">{{summary.display.title}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Edition:</div>
+          <div class="flex-1">{{summary.display.edition}}</div>
+          <div class="flex-1 font-weight-bold" i18n>TCN:</div>
+          <div class="flex-1">{{summary.record.tcn_value()}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Created By:</div>
+          <div class="flex-1" *ngIf="summary.record.creator().usrname">
+            <a href="/eg/staff/circ/patron/{{summary.record.creator().id()}}/checkout">
+              {{summary.record.creator().usrname()}}
+            </a>
+          </div>
+        </div>
+      </li>
+      <li class="list-group-item" *ngIf="expandDisplay">
+        <div class="d-flex">
+          <div class="flex-1 font-weight-bold" i18n>Author:</div>
+          <div class="flex-3">{{summary.display.author}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Pubdate:</div>
+          <div class="flex-1">{{summary.display.pubdate}}</div>
+          <div class="flex-1 font-weight-bold" i18n>Database ID:</div>
+          <div class="flex-1">{{summary.id}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited By:</div>
+          <div class="flex-1" *ngIf="summary.record.editor().usrname">
+            <a href="/eg/staff/circ/patron/{{summary.record.editor().id()}}/checkout">
+              {{summary.record.editor().usrname()}}
+            </a>
+          </div>
+        </div>
+      </li>
+      <li class="list-group-item" *ngIf="expandDisplay">
+        <div class="d-flex">
+          <div class="flex-1 font-weight-bold" i18n>Bib Call #:</div>
+          <div class="flex-3">{{summary.bibCallNumber}}</div>
+          <div class="flex-1 font-weight-bold" i18n>Record Owner:</div>
+          <div class="flex-1">{{orgName(summary.record.owner())}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Created On:</div>
+          <div class="flex-1">{{summary.record.create_date() | date:'short'}}</div>
+          <div class="flex-1 font-weight-bold pl-1" i18n>Last Edited On:</div>
+          <div class="flex-1">{{summary.record.edit_date() | date:'short'}}</div>
+        </div>
+      </li>
+    </ul>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts
new file mode 100644
index 0000000..78d2653
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts
@@ -0,0 +1,67 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+
+ at Component({
+  selector: 'eg-bib-summary',
+  templateUrl: 'bib-summary.component.html',
+  styles: ['.eg-bib-summary .card-header {padding: .25rem .5rem}']
+})
+export class BibSummaryComponent implements OnInit {
+
+    initDone = false;
+    expandDisplay = true;
+
+    // If provided, the record will be fetched by the component.
+    @Input() recordId: number;
+
+    // Otherwise, we'll use the provided bib summary object.
+    summary: BibRecordSummary;
+    @Input() set bibSummary(s: any) {
+        this.summary = s;
+        if (this.initDone && this.summary) {
+            this.summary.getBibCallNumber();
+        }
+    }
+
+    constructor(
+        private bib: BibRecordService,
+        private cat: CatalogService,
+        private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {}
+
+    ngOnInit() {
+        this.initDone = true;
+        if (this.summary) {
+            this.summary.getBibCallNumber();
+        } else {
+            if (this.recordId) {
+                this.loadSummary();
+            }
+        }
+    }
+
+    loadSummary(): void {
+        this.bib.getBibSummary(this.recordId).toPromise()
+        .then(summary => {
+            summary.getBibCallNumber();
+            this.bib.fleshBibUsers([summary.record]);
+            this.summary = summary;
+            console.log(this.summary.display);
+        });
+    }
+
+    orgName(orgId: number): string {
+        if (orgId) {
+            return this.org.get(orgId).shortname();
+        }
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html
new file mode 100644
index 0000000..f5e4c94
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html
@@ -0,0 +1,56 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Add To Record #{{recId}} to Bucket</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-lg-3 font-weight-bold" i18n>Name of existing bucket</div>
+      <div class="col-lg-5">
+         <select 
+          class="form-control"
+          placeholder="Existing Bucket..."
+          i18n-placeholder
+          [(ngModel)]="selectedBucket">
+          <option *ngFor="let bkt of buckets" 
+            value="{{bkt.id()}}">{{bkt.name()}}</option>
+        </select>
+      </div>
+      <div class="col-lg-4">
+        <button class="btn btn-info" (click)="addToSelected()" i18n 
+          [disabled]="!selectedBucket">
+          Add To Selected Bucket
+        </button>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-lg-3 font-weight-bold" i18n>Name of new bucket</div>
+      <div class="col-lg-5">
+        <input type="text" class="form-control" 
+          placeholder="New Bucket Name..."
+          i18n-placeholder
+          [(ngModel)]="newBucketName"/>
+      </div>
+      <div class="col-lg-4">
+        <button class="btn btn-info" (click)="addToNew()" i18n 
+          [disabled]="!newBucketName">
+          Add To New Bucket
+        </button>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-lg-3 font-weight-bold" i18n>New bucket description</div>
+      <div class="col-lg-5">
+        <textarea size="3" type="text" class="form-control" 
+          placeholder="Optional New Bucket Description..."
+          i18n-placeholder
+          [(ngModel)]="newBucketDesc">
+        </textarea>
+      </div>
+    </div>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts
new file mode 100644
index 0000000..1f127b4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts
@@ -0,0 +1,109 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Dialog for adding bib records to new and existing record buckets.
+ */
+
+ at Component({
+  selector: 'eg-record-bucket-dialog',
+  templateUrl: 'record-bucket-dialog.component.html'
+})
+
+export class RecordBucketDialogComponent
+    extends DialogComponent implements OnInit {
+
+    selectedBucket: number;
+    newBucketName: string;
+    newBucketDesc: string;
+    buckets: any[];
+
+    recId: number;
+    @Input() set recordId(id: number) {
+        this.recId = id;
+    }
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private toast: ToastService,
+        private idl: IdlService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {
+
+        this.onOpen$.subscribe(ok => {
+            // Reset data on dialog open
+
+            this.selectedBucket = null;
+            this.newBucketName = '';
+            this.newBucketDesc = '';
+
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.container.retrieve_by_class.authoritative',
+                this.auth.token(), this.auth.user().id(),
+                'biblio', 'staff_client'
+            ).subscribe(buckets => this.buckets = buckets);
+        });
+    }
+
+    addToSelected() {
+        this.addToBucket(this.selectedBucket);
+    }
+
+    // Create a new bucket then add the record
+    addToNew() {
+        const bucket = this.idl.create('cbreb');
+
+        bucket.owner(this.auth.user().id());
+        bucket.name(this.newBucketName);
+        bucket.description(this.newBucketDesc);
+        bucket.btype('staff_client');
+
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.container.create',
+            this.auth.token(), 'biblio', bucket
+        ).subscribe(bktId => {
+            const evt = this.evt.parse(bktId);
+            if (evt) {
+                this.toast.danger(evt.desc);
+            } else {
+                this.addToBucket(bktId);
+            }
+        });
+    }
+
+    // Add the record to the selected existing bucket
+    addToBucket(id: number) {
+        const item = this.idl.create('cbrebi');
+        item.bucket(id);
+        item.target_biblio_record_entry(this.recId);
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.container.item.create',
+            this.auth.token(), 'biblio', item
+        ).subscribe(resp => {
+            const evt = this.evt.parse(resp);
+            if (evt) {
+                this.toast.danger(evt.toString());
+            } else {
+                this.close();
+            }
+        });
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts
new file mode 100644
index 0000000..d2596b5
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts
@@ -0,0 +1,57 @@
+/**
+ * Common code for mananging holdings
+ */
+import {Injectable, EventEmitter} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+
+interface NewVolumeData {
+    owner: number;
+    label?: string;
+}
+
+ at Injectable()
+export class HoldingsService {
+
+    constructor(private net: NetService) {}
+
+    // Open the holdings editor UI in a new browser window/tab.
+    spawnAddHoldingsUi(
+        recordId: number,                   // Bib record ID
+        addToVols: number[] = [],           // Add copies to existing volumes
+        volumeData: NewVolumeData[] = []) { // Creating new volumes
+
+        const raw: any[] = [];
+
+        if (addToVols) {
+            addToVols.forEach(volId => raw.push({callnumber: volId}));
+        } else if (volumeData) {
+            volumeData.forEach(data => raw.push(data));
+        }
+
+        if (raw.length === 0) { raw.push({}); }
+
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'edit-these-copies', {
+                record_id: recordId,
+                raw: raw,
+                hide_vols : false,
+                hide_copies : false
+            }
+        ).subscribe(
+            key => {
+                if (!key) {
+                    console.error('Could not create holds cache key!');
+                    return;
+                }
+                setTimeout(() => {
+                    const url = `/eg/staff/cat/volcopy/${key}`;
+                    window.open(url, '_blank');
+                });
+            }
+        );
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html b/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html
new file mode 100644
index 0000000..0d82279
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html
@@ -0,0 +1,22 @@
+
+<div class="d-flex border-top border-light" 
+    *ngFor="let row of rowBuckets; let rowIdx = index">
+  <div class="flex-1 p-2" *ngFor="let col of colList">
+    <ng-container *ngIf="row[col]">
+      <!-- avoid mixing [href] and [routerLink] in one link, 
+          because routerLink will take precedence, even if it's empty -->
+      <ng-container *ngIf="row[col].url">
+        <a [href]="row[col].url" class="with-material-icon">
+          <span class="material-icons">edit</span>
+          <span>{{row[col].label}}</span>
+        </a>
+      </ng-container>
+      <ng-container *ngIf="row[col].routerLink">
+        <a [routerLink]="row[col].routerLink" class="with-material-icon">
+          <span class="material-icons">edit</span>
+          <span>{{row[col].label}}</span>
+        </a>
+      </ng-container>
+    </ng-container>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts b/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts
new file mode 100644
index 0000000..9b06c92
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts
@@ -0,0 +1,73 @@
+import {Component, Input, OnInit, AfterViewInit, Host} from '@angular/core';
+
+interface LinkTableLink {
+    label: string;
+    url?: string;
+    routerLink?: string;
+}
+
+ at Component({
+    selector: 'eg-link-table',
+    templateUrl: './link-table.component.html'
+})
+
+export class LinkTableComponent implements AfterViewInit {
+    @Input() columnCount: number;
+    links: LinkTableLink[];
+    rowBuckets: any[];
+    colList: number[];
+    colWidth: number;
+
+    constructor() {
+        this.links = [];
+        this.rowBuckets = [];
+        this.colList = [];
+    }
+
+    ngAfterViewInit() {
+        // table-ize the links
+        const rowCount = Math.ceil(this.links.length / this.columnCount);
+        this.colWidth = Math.floor(12 / this.columnCount); // Bootstrap 12-grid
+
+        for (let col = 0; col < this.columnCount; col++) {
+            this.colList.push(col);
+        }
+
+        // Modifying values in AfterViewInit without other activity
+        // happening can result in the modified values not getting
+        // displayed until some action occurs.  Modifing after
+        // via timeout works though.
+        setTimeout(() => {
+            for (let row = 0; row < rowCount; row++) {
+                this.rowBuckets[row] = [
+                    this.links[row],
+                    this.links[row + Number(rowCount)],
+                    this.links[row + Number(rowCount * 2)]
+                ];
+            }
+        });
+    }
+}
+
+ at Component({
+    selector: 'eg-link-table-link',
+    template: '<ng-template></ng-template>'
+})
+
+export class LinkTableLinkComponent implements OnInit {
+    @Input() label: string;
+    @Input() url: string;
+    @Input() routerLink: string;
+
+    constructor(@Host() private linkTable: LinkTableComponent) {}
+
+    ngOnInit() {
+        this.linkTable.links.push({
+            label : this.label,
+            url: this.url,
+            routerLink: this.routerLink
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html
new file mode 100644
index 0000000..e5a6f49
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html
@@ -0,0 +1,65 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Change Operator</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <form class="form-validated">
+      <div class="form-group row">
+        <label class="col-lg-4 text-right font-weight-bold" for="username" i18n>Username</label>
+        <input 
+          type="text" 
+          class="form-control col-lg-7"
+          id="username" 
+          name="username"
+          required
+          (keyup.enter)="login()"
+          autocomplete="username"
+          i18n-placeholder
+          placeholder="Username..." 
+          [(ngModel)]="username"/>
+      </div>
+
+      <div class="form-group row">
+        <label class="col-lg-4 text-right font-weight-bold" 
+            for="password" i18n>Password</label>
+        <input 
+          type="password" 
+          class="form-control col-lg-7"
+          id="password" 
+          name="password"
+          required
+          (keyup.enter)="login()"
+          autocomplete="current-password"
+          i18n-placeholder
+          placeholder="Password..." 
+          [(ngModel)]="password"/>
+      </div>
+
+      <div class="form-group row">
+        <label class="col-lg-4 text-right font-weight-bold" 
+            for="loginType" i18n>Login Type</label>
+        <select 
+          class="form-control col-lg-7" 
+          id="loginType" 
+          name="loginType"
+          placeholder="Login Type..."
+          i18n-placeholder
+          required
+          [(ngModel)]="loginType">
+          <option value="temp" selected i18n>Temporary</option>                   
+          <option value="staff" i18n>Staff</option>             
+          <option value="persist" i18n>Persistent</option>      
+        </select>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <button (click)="login()" class="btn btn-info" i18n>OK/Continue</button>
+    <button (click)="dismiss('canceled')" class="btn btn-warning ml-2" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts
new file mode 100644
index 0000000..95d4db8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts
@@ -0,0 +1,77 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+ at Component({
+  selector: 'eg-op-change',
+  templateUrl: 'op-change.component.html'
+})
+
+export class OpChangeComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() username: string;
+    @Input() password: string;
+    @Input() loginType = 'temp';
+
+    @Input() successMessage: string;
+    @Input() failMessage: string;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private toast: ToastService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+
+        // Focus the username any time the dialog is opened.
+        this.onOpen$.subscribe(
+            val => this.renderer.selectRootElement('#username').focus()
+        );
+    }
+
+    login(): Promise<any> {
+        if (!(this.username && this.password)) {
+            return Promise.reject('Missing Params');
+        }
+
+        return this.auth.login(
+            {   username    : this.username,
+                password    : this.password,
+                workstation : this.auth.workstation(),
+                type        : this.loginType
+            },  true        // isOpChange
+        ).then(
+            ok => {
+                this.password = '';
+                this.username = '';
+
+                // Fetch the user object
+                this.auth.testAuthToken().then(
+                    ok2 => {
+                        this.close();
+                        this.toast.success(this.successMessage);
+                    }
+                );
+            },
+            notOk => {
+                this.password = '';
+                this.toast.danger(this.failMessage);
+            }
+        );
+    }
+
+    restore(): Promise<any> {
+        return this.auth.undoOpChange().then(
+            ok => this.toast.success(this.successMessage),
+            err => this.toast.danger(this.failMessage)
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts
new file mode 100644
index 0000000..13ac684
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts
@@ -0,0 +1,15 @@
+import {Component, OnInit, Input} from '@angular/core';
+
+ at Component({
+  selector: 'eg-staff-banner',
+  template:
+    '<div class="lead alert alert-primary text-center pt-1 pb-1" role="alert">' +
+      '<span>{{bannerText}}</span>' +
+    '</div>'
+})
+
+export class StaffBannerComponent {
+    @Input() public bannerText: string;
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html
new file mode 100644
index 0000000..7aa59b4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html
@@ -0,0 +1,63 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>
+      {{idlClassDef.label}}
+    </h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">×</span>
+    </button>
+  </div>
+  <div class="modal-body form-common form-validated" *ngIf="idlObj">
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Field Name</label>
+      <input 
+        type="text" 
+        [disabled]="true"
+        class="form-control col-lg-7"
+        value="{{idlClassDef.field_map[field].label}}">
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Current Value</label>
+      <input 
+        type="text" 
+        [disabled]="true"
+        class="form-control col-lg-7"
+        value="{{idlObj[field]()}}">
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" 
+        i18n>Select Locale</label>
+      <select class="form-control col-lg-7" 
+        (change)="localeChanged($event)"
+        [(ngModel)]="selectedLocale">
+        <option value="{{locale.code()}}" *ngFor="let locale of locales">
+          {{locale.name()}}
+        </option>
+      </select>
+    </div>
+    <div class="form-group row">
+      <label class="col-lg-4 text-right font-weight-bold" i18n>Translation</label>
+      <input 
+        id='translation-input'
+        type="text" 
+        class="form-control col-lg-7"
+        required
+        i18n-placeholder
+        (keyup.enter)="translate()"
+        placeholder="Translation..." 
+        [(ngModel)]="translatedValue"/>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button *ngIf="prevString" (click)="prevString()" 
+      class="btn btn-info" i18n>Prev String</button>
+    <button *ngIf="nextString" (click)="nextString()" 
+      class="btn btn-info mr-3" i18n>Next String</button>
+    <button (click)="translate()" class="btn btn-info" i18n>Apply</button>
+    <button (click)="dismiss('canceled')" class="btn btn-warning ml-2" i18n>Cancel</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts
new file mode 100644
index 0000000..9c7361c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts
@@ -0,0 +1,145 @@
+import {Component, OnInit, Input, Renderer2} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {LocaleService} from '@eg/core/locale.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+ at Component({
+  selector: 'eg-translate',
+  templateUrl: 'translate.component.html'
+})
+
+export class TranslateComponent
+    extends DialogComponent implements OnInit {
+
+    idlClassDef: any;
+    locales: IdlObject[];
+    selectedLocale: string;
+    translatedValue: string;
+    existingTranslation: IdlObject;
+
+    // These actions should update the idlObject and/or fieldName values,
+    // forcing the dialog to load a new string to translate.  When set,
+    // applying a translation in the dialog will leave the dialog window open
+    // so the next/prev buttons can be used to fetch the next string.
+    nextString: () => void;
+    prevString: () => void;
+
+    idlObj: IdlObject;
+    @Input() set idlObject(o: IdlObject) {
+        if (o) {
+            this.idlObj = o;
+            this.idlClassDef = this.idl.classes[o.classname];
+            this.fetchTranslation();
+        }
+    }
+
+    field: string;
+    @Input() set fieldName(n: string) {
+        this.field = n;
+    }
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private renderer: Renderer2,
+        private idl: IdlService,
+        private toast: ToastService,
+        private locale: LocaleService,
+        private pcrud: PcrudService,
+        private auth: AuthService) {
+        super(modal);
+    }
+
+    ngOnInit() {
+        // Default to the login locale
+        this.selectedLocale = this.locale.currentLocaleCode();
+        this.locales = [];
+        this.locale.supportedLocales().subscribe(l => this.locales.push(l));
+
+        this.onOpen$.subscribe(() => {
+            const elm = this.renderer.selectRootElement('#translation-input');
+            if (elm) {
+                elm.focus();
+                elm.select();
+            }
+        });
+    }
+
+    localeChanged(code: string) {
+        this.fetchTranslation();
+    }
+
+    fetchTranslation() {
+        const exist = this.existingTranslation;
+
+        if (exist
+            && exist.fq_field() === this.fqField()
+            && exist.identity_value() === this.identValue()) {
+            // Already have the current translation object.
+            return;
+        }
+
+        this.translatedValue = '';
+        this.existingTranslation = null;
+
+        this.pcrud.search('i18n', {
+            translation: this.selectedLocale,
+            fq_field : this.fqField(),
+            identity_value: this.identValue()
+        }).subscribe(tr => {
+            this.existingTranslation = tr;
+            this.translatedValue = tr.string();
+            console.debug('found existing translation ', tr);
+        });
+    }
+
+    fqField(): string {
+        return this.idlClassDef.classname + '.' + this.field;
+    }
+
+    identValue(): string {
+        return this.idlObj[this.idlClassDef.pkey || 'id']();
+    }
+
+    translate() {
+        if (!this.translatedValue) { return; }
+
+        let entry;
+
+        if (this.existingTranslation) {
+            entry = this.existingTranslation;
+            entry.string(this.translatedValue);
+
+            this.pcrud.update(entry).toPromise().then(
+                ok => {
+                    if (!this.nextString) {
+                        this.close('Translation updated');
+                    }
+                },
+                err => console.error(err)
+            );
+
+            return;
+        }
+
+        entry = this.idl.create('i18n');
+        entry.fq_field(this.fqField());
+        entry.identity_value(this.identValue());
+        entry.translation(this.selectedLocale);
+        entry.string(this.translatedValue);
+
+        this.pcrud.create(entry).toPromise().then(
+            ok => {
+                if (!this.nextString) {
+                    this.close('Translation created');
+                }
+            },
+            err => console.error('Translation creation failed')
+        );
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/splash.component.html b/Open-ILS/src/eg2/src/app/staff/splash.component.html
new file mode 100644
index 0000000..b37bec3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/splash.component.html
@@ -0,0 +1,128 @@
+
+
+<style>
+    /* TODO change BS color scheme so this isn't necessary */
+    .bg-evergreen {
+      background: -webkit-linear-gradient(#00593d, #007a54);
+      background-color: #007a54;
+      color: #fff;
+    }
+
+    /* Match the ang1 splash page */
+    .card-header {
+        color: #3c763d;
+        background-color: #dff0d8;
+        border-color: #d6e9c6;
+    }
+</style>
+
+<div class="container">
+
+  <!-- header icon -->
+  <div class="row mb-3">
+    <div class="col-lg-12 text-center">
+      <img src="/images/portal/logo.png"/>
+    </div>
+  </div>
+
+  <div class="row" id="splash-nav">
+    <div class="col-lg-4">
+      <div class="card">
+        <div class="card-header">
+          <div class="panel-title text-center" i18n>Circulation and Patrons</div>
+        </div>
+        <div class="card-body">
+          <div class="list-group">
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/forward.png"/>
+              <a href="/eg/staff/circ/patron/bcsearch" i18n>Check Out Items</a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/back.png"/>
+              <a href="/eg/staff/circ/checkin/index" i18n>Check In Items</a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/retreivepatron.png"/>
+              <a href="/eg/staff/circ/patron/search" i18n>Search For Patron By Name</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-lg-4">
+      <div class="card">
+        <div class="card-header">
+          <div class="panel-title text-center" i18n>Item Search and Cataloging</div>
+        </div>
+        <div class="card-body">
+          <div class="list-group">
+            <div class="list-group-item border-0 p-2">
+              <div class="input-group">
+                <input type="text" class="form-control" 
+                  [(ngModel)]="catSearchQuery"
+                  id='catalog-search-input'
+                  (keyup.enter)="searchCatalog()"
+                  i18n-placeholder placeholder="Search for...">
+                <span class="input-group-btn">
+                  <button class="btn btn-outline-secondary" 
+                    (click)="searchCatalog()" type="button" i18n>
+                    Search
+                  </button>
+                </span>
+                  <!--
+                  <input focus-me="focus_search" 
+                      class="form-control" ng-model="cat_query" type="text" 
+                      ng-keypress="catalog_search($event)"
+                      placeholder="Search catalog for..."/>
+                  <button class='btn btn-light' ng-click="catalog_search()">
+                      Search
+                  </button>
+                  -->
+              </div>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/bucket.png"/>
+              <a href="/eg/staff/cat/bucket/record/" i18n>Record Buckets</a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/bucket.png"/>
+              <a href="/eg/staff/cat/bucket/copy/" i18n>Copy Buckets</a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-lg-4">
+      <div class="card">
+        <div class="card-header">
+          <div class="panel-title text-center" i18n>Administration</div>
+        </div>
+        <div class="card-body">
+          <div class="list-group">
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/helpdesk.png"/>
+              <a target="_top" href="http://docs.evergreen-ils.org/" i18n>
+                Evergreen Documentation
+              </a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/helpdesk.png"/>
+              <a target="_top" href="/eg/staff/admin/workstation/index" i18n>
+                Workstation Administration
+              </a>
+            </div>
+            <div class="list-group-item border-0 p-2">
+              <img src="/images/portal/reports.png"/>
+              <a target="_top" href="/eg/staff/reporter/legacy/main" i18n>
+                Reports
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/splash.component.ts b/Open-ILS/src/eg2/src/app/staff/splash.component.ts
new file mode 100644
index 0000000..af6b647
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/splash.component.ts
@@ -0,0 +1,40 @@
+import {Component, OnInit, Renderer2} from '@angular/core';
+import {Router} from '@angular/router';
+
+ at Component({
+    templateUrl: 'splash.component.html'
+})
+
+export class StaffSplashComponent implements OnInit {
+
+    catSearchQuery: string;
+
+    constructor(
+        private renderer: Renderer2,
+        private router: Router
+    ) {}
+
+    ngOnInit() {
+
+        // Focus catalog search form
+        this.renderer.selectRootElement('#catalog-search-input').focus();
+    }
+
+    searchCatalog(): void {
+        if (!this.catSearchQuery) { return; }
+
+        /* Route to angular6 catalog
+        this.router.navigate(
+            ['/staff/catalog/search'],
+            {queryParams: {query : this.catSearchQuery}}
+        );
+        */
+
+        // Route to AngularJS / TPAC catalog
+        window.location.href =
+            '/eg/staff/cat/catalog/results?query=' +
+            encodeURIComponent(this.catSearchQuery);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.component.css b/Open-ILS/src/eg2/src/app/staff/staff.component.css
new file mode 100644
index 0000000..508d879
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/staff.component.css
@@ -0,0 +1,8 @@
+#staff-content-container {
+  width: 95%;
+  margin-top:56px;
+  padding-right: 10px;
+  padding-left: 10px;
+  margin-right: auto;
+  margin-left: auto;
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.component.html b/Open-ILS/src/eg2/src/app/staff/staff.component.html
new file mode 100644
index 0000000..2a2539c
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/staff.component.html
@@ -0,0 +1,19 @@
+<!-- top navigation bar -->
+<eg-staff-nav-bar></eg-staff-nav-bar>
+
+<div id='staff-content-container'>
+  <!-- page content -->
+  <router-outlet></router-outlet>
+</div>
+
+<!-- EgAccessKey Info Panel -->
+<eg-accesskey-info #egAccessKeyInfo></eg-accesskey-info>
+<a egAccessKey keyCtx="base"
+    keySpec="ctrl+h" i18n-keySpec
+    keyDesc="Display AccessKey Info Dialog" i18n-keyDesc
+    (click)="egAccessKeyInfo.open()">
+</a>
+
+<!-- global toast alerts -->
+<eg-toast></eg-toast>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.component.ts b/Open-ILS/src/eg2/src/app/staff/staff.component.ts
new file mode 100644
index 0000000..492b1df
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/staff.component.ts
@@ -0,0 +1,118 @@
+import {Component, OnInit, NgZone, HostListener} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
+import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
+
+const LOGIN_PATH = '/staff/login';
+const WS_BASE_PATH = '/staff/admin/workstation/workstations/';
+const WS_MANAGE_PATH = '/staff/admin/workstation/workstations/manage';
+
+ at Component({
+  templateUrl: 'staff.component.html',
+  styleUrls: ['staff.component.css']
+})
+
+export class StaffComponent implements OnInit {
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private zone: NgZone,
+        private net: NetService,
+        private auth: AuthService,
+        private keys: AccessKeyService
+    ) {}
+
+    ngOnInit() {
+
+        // Fires on all in-staff-app router navigation, but not initial
+        // page load.
+        this.router.events.subscribe(routeEvent => {
+            if (routeEvent instanceof NavigationEnd) {
+                // console.debug(`StaffComponent routing to ${routeEvent.url}`);
+                this.preventForbiddenNavigation(routeEvent.url);
+            }
+        });
+
+        // Redirect to the login page on any auth timeout events.
+        this.net.authExpired$.subscribe(expireEvent => {
+
+            // If the expired authtoken was identified locally (i.e.
+            // in this browser tab) notify all tabs of imminent logout.
+            if (!expireEvent.viaExternal) {
+                this.auth.broadcastLogout();
+            }
+
+            console.debug('Auth session has expired. Redirecting to login');
+            this.auth.redirectUrl = this.router.url;
+
+            // https://github.com/angular/angular/issues/18254
+            // When a tab redirects to a login page as a result of
+            // another tab broadcasting a logout, ngOnInit() fails to
+            // fire in the login component, until the user interacts
+            // with the page.  Fix it by wrapping it in zone.run().
+            // This is the only navigate() where I have seen this happen.
+            this.zone.run(() => {
+                this.router.navigate([LOGIN_PATH]);
+            });
+        });
+
+        this.route.data.subscribe((data: {staffResolver: any}) => {
+            // Data fetched via StaffResolver is available here.
+        });
+
+
+    }
+
+    /**
+     * Prevent the user from leaving the login page when they don't have
+     * a valid authoken.
+     *
+     * Prevent the user from leaving the workstation admin page when
+     * they don't have a valid workstation.
+     *
+     * This does not verify auth tokens with the server on every route,
+     * because that would be overkill.  This is only here to keep
+     * people boxed in with their authenication state was already
+     * known to be less then 100%.
+     */
+    preventForbiddenNavigation(url: string): void {
+
+        // No auth checks needed for login page.
+        if (url.startsWith(LOGIN_PATH)) {
+            return;
+        }
+
+        // We lost our authtoken, go back to the login page.
+        if (!this.auth.token()) {
+            this.router.navigate([LOGIN_PATH]);
+        }
+
+        // No workstation checks needed for workstation admin page.
+        if (url.startsWith(WS_BASE_PATH)) {
+            return;
+        }
+
+        if (this.auth.workstationState !== AuthWsState.VALID) {
+            this.router.navigate([WS_MANAGE_PATH]);
+        }
+    }
+
+    /**
+     * Listen for keyboard events here -- the root directive --  and pass
+     * events down to the key service for processing.
+     */
+    @HostListener('window:keydown', ['$event']) onKeyDown(evt: KeyboardEvent) {
+        this.keys.fire(evt);
+    }
+
+    /*
+    @ViewChild('egAccessKeyInfo')
+    private keyComponent: AccessKeyInfoComponent;
+    */
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/staff.module.ts b/Open-ILS/src/eg2/src/app/staff/staff.module.ts
new file mode 100644
index 0000000..dd22f93
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/staff/staff.module.ts
@@ -0,0 +1,26 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+
+import {StaffComponent} from './staff.component';
+import {StaffRoutingModule} from './routing.module';
+import {StaffNavComponent} from './nav.component';
+import {StaffLoginComponent} from './login.component';
+import {StaffSplashComponent} from './splash.component';
+import {AboutComponent} from './about.component';
+
+ at NgModule({
+  declarations: [
+    StaffComponent,
+    StaffNavComponent,
+    StaffSplashComponent,
+    StaffLoginComponent,
+    AboutComponent
+  ],
+  imports: [
+    StaffCommonModule.forRoot(),
+    StaffRoutingModule
+  ]
+})
+
+export class StaffModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/welcome.component.html b/Open-ILS/src/eg2/src/app/welcome.component.html
new file mode 100644
index 0000000..eaa1c71
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/welcome.component.html
@@ -0,0 +1,11 @@
+<div class="jumbotron">
+  <h1 i18n class="display-3">Welcome to Webby</h1>
+  <p i18n class="lead">
+    If you see this page, you're probably in good shape...
+  </p>
+  <hr class="my-4"/>
+  <p i18n>
+    But maybe you meant to go to the 
+    <a routerLink="/staff/splash">staff page</a>
+  </p>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/welcome.component.ts b/Open-ILS/src/eg2/src/app/welcome.component.ts
new file mode 100644
index 0000000..a588661
--- /dev/null
+++ b/Open-ILS/src/eg2/src/app/welcome.component.ts
@@ -0,0 +1,13 @@
+import { Component, OnInit } from '@angular/core';
+
+ at Component({
+  templateUrl : './welcome.component.html'
+})
+
+export class WelcomeComponent implements OnInit {
+    ngOnInit() {
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/assets/.gitkeep b/Open-ILS/src/eg2/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Open-ILS/src/eg2/src/environments/environment.prod.ts b/Open-ILS/src/eg2/src/environments/environment.prod.ts
new file mode 100644
index 0000000..50385bf
--- /dev/null
+++ b/Open-ILS/src/eg2/src/environments/environment.prod.ts
@@ -0,0 +1,4 @@
+export const environment = {
+  production: true,
+  locales: ['en-US', 'fr-CA']
+};
diff --git a/Open-ILS/src/eg2/src/environments/environment.ts b/Open-ILS/src/eg2/src/environments/environment.ts
new file mode 100644
index 0000000..113cbe3
--- /dev/null
+++ b/Open-ILS/src/eg2/src/environments/environment.ts
@@ -0,0 +1,10 @@
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `.angular-cli.json`.
+
+export const environment = {
+  production: false,
+  // currently locales are only supported in production builds.
+  locales: ['en-US']
+};
diff --git a/Open-ILS/src/eg2/src/favicon.ico b/Open-ILS/src/eg2/src/favicon.ico
new file mode 100644
index 0000000..8081c7c
Binary files /dev/null and b/Open-ILS/src/eg2/src/favicon.ico differ
diff --git a/Open-ILS/src/eg2/src/index.html b/Open-ILS/src/eg2/src/index.html
new file mode 100644
index 0000000..aee6bf8
--- /dev/null
+++ b/Open-ILS/src/eg2/src/index.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title i18n="Page Title">AngEG</title>
+  <base href="/eg2">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <!-- see notes in styles.css regarding locally served fonts -->
+  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+</head>
+<body>
+  <eg-root></eg-root>
+  <script src="/IDL2js"></script>
+  <script src="/js/dojo/opensrf/JSON_v1.js"></script>
+  <script src="/js/dojo/opensrf/opensrf.js"></script>
+  <script src="/js/dojo/opensrf/opensrf_ws.js"></script>
+</body>
+</html>
diff --git a/Open-ILS/src/eg2/src/locale/.gitkeep b/Open-ILS/src/eg2/src/locale/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Open-ILS/src/eg2/src/main.ts b/Open-ILS/src/eg2/src/main.ts
new file mode 100644
index 0000000..2e303cf
--- /dev/null
+++ b/Open-ILS/src/eg2/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { BaseModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+  enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(BaseModule)
+  .catch(err => console.log(err));
diff --git a/Open-ILS/src/eg2/src/polyfills.ts b/Open-ILS/src/eg2/src/polyfills.ts
new file mode 100644
index 0000000..e073082
--- /dev/null
+++ b/Open-ILS/src/eg2/src/polyfills.ts
@@ -0,0 +1,80 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ *      file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/weak-map';
+// import 'core-js/es6/set';
+
+// PhantomJS needs these
+import 'core-js/es6/array';
+import 'core-js/es6/string';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js';  // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+import 'core-js/es7/reflect';
+
+
+/**
+ * Required to support Web Animations `@angular/platform-browser/animations`.
+ * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
+ **/
+// import 'web-animations-js';  // Run `npm install --save web-animations-js`.
+
+
+
+/***************************************************************************************************
+ * Zone JS is required by Angular itself.
+ */
+import 'zone.js/dist/zone';  // Included with Angular CLI.
+
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+
+/**
+ * Date, currency, decimal and percent pipes.
+ * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
+ */
+// import 'intl';  // Run `npm install --save intl`.
+/**
+ * Need to import at least one locale-data with intl.
+ */
+// import 'intl/locale-data/jsonp/en';
diff --git a/Open-ILS/src/eg2/src/styles.css b/Open-ILS/src/eg2/src/styles.css
new file mode 100644
index 0000000..b87ad78
--- /dev/null
+++ b/Open-ILS/src/eg2/src/styles.css
@@ -0,0 +1,161 @@
+/* You can add global styles to this file, and also import other style files */
+
+/* bootstrap CSS only -- JS bits come from ng-bootstrap */
+ at import '~bootstrap-css-only/css/bootstrap.min.css';
+
+/* Locally served material icon fonts.
+ * Note when I first tested these after installing the fonts
+ * via:  npm install --save material-design-icons
+ * some of the icons exhibited odd behavior, adding a lot of
+ * excess space to the left or right.  It only affected certain
+ * icons.  More research needed.
+ * /
+/*
+ at import '~material-design-icons/iconfont/material-icons.css'; 
+*/
+
+/** BS default fonts are huge */
+body, .form-control, .btn, .input-group-text {
+  /* This more or less matches the font size of the angularjs client.
+   * The default BS4 font of 1rem is comically large. 
+   */
+  font-size: .88rem;
+}
+h2 {font-size: 1.25rem}
+h3 {font-size: 1.15rem}
+h4 {font-size: 1.05rem}
+h5 {font-size: .95rem}
+
+.small-text-1 {font-size: 85%}
+
+
+/** Ang5 routes on clicks to href's with no values, so we can't have
+ * bare href's to force anchor styling.  Use this for anchors w/ no href.
+ * TODO: should we style all of them?  a:not([href]) ....
+ * */
+.no-href {
+  cursor: pointer;
+  color: #007bff;
+}
+
+
+/** BS has flex utility classes, but none for specifying flex widths.
+ *  BS class="col" is roughly equivelent to flex-1, but col-2 is not
+ *  equivalent to flex-2, since col-2 really means 2/12 width. */
+.flex-1 {flex: 1}
+.flex-2 {flex: 2}
+.flex-3 {flex: 3}
+.flex-4 {flex: 4}
+.flex-5 {flex: 5}
+
+
+/* usefuf for mat-icon buttons without any background or borders */
+.material-icon-button {
+  /* Transparent background */
+  border: none;
+  background-color: rgba(0, 0, 0, 0.0);
+  padding-left: .25rem;
+  padding-right: .25rem; /* default .5rem */
+}
+
+.mat-icon-in-button {
+    line-height: inherit;
+}
+
+.material-icons {
+  /** default is 24px which is pretty chunky */
+  font-size: 22px;
+}
+
+/* allow spans/labels to vertically orient with material icons */
+.label-with-material-icon {
+    display: inline-flex;
+    vertical-align: middle;
+    align-items: center;
+}
+
+/* dropdown menu link/button with no downward carrot icon */
+.no-dropdown-caret::after {
+    display: none;
+}
+
+/* Default .card padding is extreme */
+.tight-card .card-body,
+.tight-card .list-group-item {
+  padding: .25rem;
+}
+.tight-card .card-header {
+  padding: .5rem;
+}
+
+ at media all and (min-width: 800px) {                                            
+    /* scrollable typeahead menus for full-size screens */                               
+    ngb-typeahead-window {
+        height: auto;                                                          
+        max-height: 200px;                                                     
+        overflow-x: hidden;                                                    
+    }
+}
+
+/* --------------------------------------------------------------------------
+/* Form Validation CSS - https://angular.io/guide/form-validation
+ * TODO: these colors don't fit the EG color scheme
+ * Required valid fields are left-border styled in green-ish.
+ * Invalid fields are left-border styled in red-ish.
+ */
+.form-validated .ng-valid[required], .form-validated .ng-valid.required {
+  border-left: 5px solid #78FA89;
+}
+.form-validated .ng-invalid:not(form) {
+  border-left: 5px solid #FA787E;
+}
+
+/* Typical form CSS.
+ * Brings font size down 5% to squeeze a bit more in.
+ * Bold labels
+ * Fixes some bootstrap margin funkiness with checkboxes for
+ * better vertical alignment.
+ * Optional faint odd or even row striping.
+ */
+.common-form {
+  font-size: 95%;
+}
+.common-form .row {
+  margin: 5px;
+  padding: 3px;
+}
+
+.common-form label {
+  font-weight: bold;
+}
+.common-form input[type="checkbox"] {
+  /* BS adds a negative left margin */
+  margin-left: 0px;
+}
+.common-form.striped-even .row:nth-child(even) {
+  background-color: rgba(0,0,0,.03);
+  border-top: 1px solid rgba(0,0,0,.125);
+  border-bottom: 1px solid rgba(0,0,0,.125);
+}
+.common-form.striped-odd .row:nth-child(odd) {
+  background-color: rgba(0,0,0,.03);
+  border-top: 1px solid rgba(0,0,0,.125);
+  border-bottom: 1px solid rgba(0,0,0,.125);
+}
+
+
+/**
+ * Only display the print container when printing 
+ */
+#eg-print-container {
+    display: none;
+}
+ at media print {
+  head {display: none} /* just to be safe */
+  body div:not([id="eg-print-container"]) {display: none}
+  div {display: none}
+  #eg-print-container {display: block}
+  #eg-print-container div {display: block}
+  #eg-print-container pre {border: none}
+}
+
diff --git a/Open-ILS/src/eg2/src/test.ts b/Open-ILS/src/eg2/src/test.ts
new file mode 100644
index 0000000..cd612ee
--- /dev/null
+++ b/Open-ILS/src/eg2/src/test.ts
@@ -0,0 +1,32 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/long-stack-trace-zone';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test';
+import 'zone.js/dist/jasmine-patch';
+import 'zone.js/dist/async-test';
+import 'zone.js/dist/fake-async-test';
+import { getTestBed } from '@angular/core/testing';
+import {
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
+declare const __karma__: any;
+declare const require: any;
+
+// Prevent Karma from running prematurely.
+__karma__.loaded = function () {};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+  BrowserDynamicTestingModule,
+  platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
+// Finally, start Karma to run the tests.
+__karma__.start();
diff --git a/Open-ILS/src/eg2/src/test_data/eg_mock.js b/Open-ILS/src/eg2/src/test_data/eg_mock.js
new file mode 100644
index 0000000..3db3579
--- /dev/null
+++ b/Open-ILS/src/eg2/src/test_data/eg_mock.js
@@ -0,0 +1,52 @@
+/**
+ * Mock data required by multiple unit tests.
+ */
+
+window._eg_mock_data = {
+
+    // builds a mock org unit tree fleshed with ou_types and
+    // absorbs the tree into egEnv
+    generateOrgTree : function(idlService, orgService) {
+        var type1 = idlService.create('aout');
+        type1.id(1);
+        type1.depth(0);
+
+        var type2 = idlService.create('aout');
+        type2.id(2);
+        type2.depth(1);
+        type2.parent(1);
+
+        var type3 = idlService.create('aout');
+        type3.id(3);
+        type3.depth(2);
+        type3.parent(2);
+
+        var org1 = idlService.create('aou'); 
+        org1.id(1);
+        org1.ou_type(type1);
+        org1.shortname('ROOT');
+
+        var org2 = idlService.create('aou'); 
+        org2.id(2); 
+        org2.parent_ou(1);
+        org2.ou_type(type2);
+
+        var org3 = idlService.create('aou'); 
+        org3.id(3); 
+        org3.parent_ou(1);
+        org3.ou_type(type2);
+
+        var org4 = idlService.create('aou'); 
+        org4.id(4); 
+        org4.parent_ou(2);
+        org4.ou_type(type3);
+
+        org1.children([org2, org3]);
+        org2.children([org4]);
+        org3.children([]);
+        org4.children([]);
+
+        orgService.orgTree = org1;
+        orgService.absorbTree();
+    }
+}
diff --git a/Open-ILS/src/eg2/src/test_data/idl2js.pl b/Open-ILS/src/eg2/src/test_data/idl2js.pl
new file mode 100644
index 0000000..fa3f2a4
--- /dev/null
+++ b/Open-ILS/src/eg2/src/test_data/idl2js.pl
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+use strict; use warnings;
+use XML::LibXML;
+use XML::LibXSLT;
+my $out_file = 'IDL2js.js';
+my $idl_file = '../../../../examples/fm_IDL.xml';
+my $xsl_file = '../../../../xsl/fm_IDL2js.xsl'; 
+
+my $xslt = XML::LibXSLT->new();
+my $style_doc = XML::LibXML->load_xml(location => $xsl_file, no_cdata=>1);
+my $stylesheet = $xslt->parse_stylesheet($style_doc);
+my $idl_string = preprocess_idl_file($idl_file);
+my $idl_doc = XML::LibXML->load_xml(string => $idl_string);
+my $results = $stylesheet->transform($idl_doc);
+my $output = $stylesheet->output_as_bytes($results);
+
+open(IDL, ">$out_file") or die "Cannot open IDL2js file $out_file : $!\n";
+
+print IDL $output;
+
+close(IDL);
+
+
+sub preprocess_idl_file {
+       my $file = shift;
+       open my $idl_fh, '<', $file or die "Unable to open IDL file $file : $!\n";
+       local $/ = undef;
+       my $xml = <$idl_fh>;
+       close($idl_fh);
+       # These substitutions are taken from OpenILS::WWW::IDL2js
+       $xml =~ s/<!--.*?-->//sg;     # filter out XML comments ...
+       $xml =~ s/(?:^|\s+)--.*$//mg; # and SQL comments ...
+       $xml =~ s/^\s+/ /mg;          # and extra leading spaces ...
+       $xml =~ s/\R*//g;             # and newlines
+       return $xml;
+}
diff --git a/Open-ILS/src/eg2/src/tsconfig.app.json b/Open-ILS/src/eg2/src/tsconfig.app.json
new file mode 100644
index 0000000..39ba8db
--- /dev/null
+++ b/Open-ILS/src/eg2/src/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/app",
+    "baseUrl": "./",
+    "module": "es2015",
+    "types": []
+  },
+  "exclude": [
+    "test.ts",
+    "**/*.spec.ts"
+  ]
+}
diff --git a/Open-ILS/src/eg2/src/tsconfig.spec.json b/Open-ILS/src/eg2/src/tsconfig.spec.json
new file mode 100644
index 0000000..18bad40
--- /dev/null
+++ b/Open-ILS/src/eg2/src/tsconfig.spec.json
@@ -0,0 +1,21 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../out-tsc/spec",
+    "baseUrl": "./",
+    "module": "commonjs",
+    "target": "es5",
+    "types": [
+      "jasmine",
+      "node"
+    ]
+  },
+  "files": [
+    "test.ts",
+    "polyfills.ts"
+  ],
+  "include": [
+    "**/*.spec.ts",
+    "**/*.d.ts"
+  ]
+}
diff --git a/Open-ILS/src/eg2/src/typings.d.ts b/Open-ILS/src/eg2/src/typings.d.ts
new file mode 100644
index 0000000..ef5c7bd
--- /dev/null
+++ b/Open-ILS/src/eg2/src/typings.d.ts
@@ -0,0 +1,5 @@
+/* SystemJS module definition */
+declare var module: NodeModule;
+interface NodeModule {
+  id: string;
+}
diff --git a/Open-ILS/src/eg2/tsconfig.json b/Open-ILS/src/eg2/tsconfig.json
new file mode 100644
index 0000000..14a504d
--- /dev/null
+++ b/Open-ILS/src/eg2/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "compileOnSave": false,
+  "compilerOptions": {
+    "outDir": "./dist/out-tsc",
+    "sourceMap": true,
+    "declaration": false,
+    "moduleResolution": "node",
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "target": "es5",
+    "baseUrl": "src",
+    "paths": {
+        "@eg/*": ["app/*"],
+        "@env/*": ["environments/*"]
+    },
+    "typeRoots": [
+      "node_modules/@types"
+    ],
+    "lib": [
+      "es2017",
+      "dom"
+    ]
+  }
+}
diff --git a/Open-ILS/src/eg2/tslint.json b/Open-ILS/src/eg2/tslint.json
new file mode 100644
index 0000000..e8fbb40
--- /dev/null
+++ b/Open-ILS/src/eg2/tslint.json
@@ -0,0 +1,136 @@
+{
+  "rulesDirectory": [
+    "node_modules/codelyzer"
+  ],
+  "rules": {
+    "arrow-return-shorthand": true,
+    "callable-types": true,
+    "class-name": true,
+    "comment-format": [
+      true,
+      "check-space"
+    ],
+    "curly": true,
+    "eofline": true,
+    "forin": true,
+    "import-blacklist": [
+      true,
+      "rxjs/Rx"
+    ],
+    "import-spacing": true,
+    "indent": [
+      true,
+      "spaces"
+    ],
+    "interface-over-type-literal": true,
+    "label-position": true,
+    "max-line-length": [
+      true,
+      140
+    ],
+    "member-access": false,
+    "member-ordering": [
+      true,
+      {
+        "order": [
+          "static-field",
+          "instance-field",
+          "static-method",
+          "instance-method"
+        ]
+      }
+    ],
+    "no-arg": true,
+    "no-bitwise": true,
+    "no-console": [
+      true,
+      "time",
+      "timeEnd",
+      "trace"
+    ],
+    "no-construct": true,
+    "no-debugger": true,
+    "no-duplicate-super": true,
+    "no-empty": false,
+    "no-empty-interface": true,
+    "no-eval": true,
+    "no-inferrable-types": [
+      true,
+      "ignore-params"
+    ],
+    "no-misused-new": true,
+    "no-non-null-assertion": true,
+    "no-shadowed-variable": true,
+    "no-string-literal": false,
+    "no-string-throw": true,
+    "no-switch-case-fall-through": true,
+    "no-trailing-whitespace": true,
+    "no-unnecessary-initializer": true,
+    "no-unused-expression": true,
+    "no-use-before-declare": true,
+    "no-var-keyword": true,
+    "object-literal-sort-keys": false,
+    "one-line": [
+      true,
+      "check-open-brace",
+      "check-catch",
+      "check-else",
+      "check-whitespace"
+    ],
+    "prefer-const": true,
+    "quotemark": [
+      true,
+      "single"
+    ],
+    "radix": true,
+    "semicolon": [
+      true,
+      "always"
+    ],
+    "triple-equals": [
+      true,
+      "allow-null-check"
+    ],
+    "typedef-whitespace": [
+      true,
+      {
+        "call-signature": "nospace",
+        "index-signature": "nospace",
+        "parameter": "nospace",
+        "property-declaration": "nospace",
+        "variable-declaration": "nospace"
+      }
+    ],
+    "unified-signatures": true,
+    "variable-name": false,
+    "whitespace": [
+      true,
+      "check-branch",
+      "check-decl",
+      "check-operator",
+      "check-separator",
+      "check-type"
+    ],
+    "directive-selector": [
+      true,
+      "attribute",
+      "eg",
+      "camelCase"
+    ],
+    "component-selector": [
+      true,
+      "element",
+      "eg",
+      "kebab-case"
+    ],
+    "use-input-property-decorator": true,
+    "use-output-property-decorator": true,
+    "use-host-property-decorator": true,
+    "no-input-rename": true,
+    "no-output-rename": true,
+    "use-life-cycle-interface": true,
+    "use-pipe-transform-interface": true,
+    "component-class-suffix": true,
+    "directive-class-suffix": true
+  }
+}

commit 6bcefced08f07d783b1d46bb4ee441ecde70df02
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 16:02:56 2018 -0400

    LP#1775466 Public API wrapper for mk_copy_query
    
    Adds 2 new APIs:
    
    open-ils.search.bib.copies
    open-ils.search.bib.copies.staff
    
    Used by the in-progress Angular staff catalog.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index d0c6bc4..159ecd7 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -2663,6 +2663,86 @@ sub copies_by_cn_label {
     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
 }
 
+__PACKAGE__->register_method(
+    method   => 'bib_copies',
+    api_name => 'open-ils.search.bib.copies',
+    stream => 1
+);
+__PACKAGE__->register_method(
+    method   => 'bib_copies',
+    api_name => 'open-ils.search.bib.copies.staff',
+    stream => 1
+);
+
+sub bib_copies {
+    my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
+    my $is_staff = ($self->api_name =~ /staff/);
+
+    my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
+    my $req = $cstore->request(
+        'open-ils.cstore.json_query', mk_copy_query(
+        $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
+
+    my $resp;
+    while ($resp = $req->recv) {
+        $client->respond($resp->content); 
+    }
+
+    return undef;
+}
+
+# TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
+# Refactor to share
+sub mk_copy_query {
+    my $rec_id = shift;
+    my $org = shift;
+    my $depth = shift;
+    my $copy_limit = shift;
+    my $copy_offset = shift;
+    my $pref_ou = shift;
+    my $is_staff = shift;
+
+    my $query = $U->basic_opac_copy_query(
+        $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
+    );
+
+    if ($org) { # TODO: root org test
+        # no need to add the org join filter if we're not actually filtering
+        $query->{from}->{acp}->[1] = { aou => {
+            fkey => 'circ_lib',
+            field => 'id',
+            filter => {
+                id => {
+                    in => {
+                        select => {aou => [{
+                            column => 'id', 
+                            transform => 'actor.org_unit_descendants',
+                            result_field => 'id', 
+                            params => [$depth]
+                        }]},
+                        from => 'aou',
+                        where => {id => $org}
+                    }
+                }
+            }
+        }};
+    };
+
+    # Unsure if we want these in the shared function, leaving here for now
+    unshift(@{$query->{order_by}},
+        { class => "aou", field => 'id',
+          transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
+        }
+    );
+    push(@{$query->{order_by}},
+        { class => "acp", field => 'id',
+          transform => 'evergreen.rank_cp'
+        }
+    );
+
+    return $query;
+}
+
 
 1;
 

commit b0f99db6c9cf141fe10addccce075b95a26e6595
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 5 16:00:51 2018 -0400

    LP#1775466 Record entry flat display attrs link repair
    
    Fleshing 'mattrs' on a bib record should return an array (i.e. replace
    might_have with has_many).
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 790a337..e9b2323 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3238,7 +3238,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<link field="authority_links" reltype="has_many" key="bib" map="" class="abl"/>
 			<link field="subscriptions" reltype="has_many" key="record_entry" map="" class="ssub"/>
 			<link field="attrs" reltype="might_have" key="id" map="" class="mra"/>
-			<link field="mattrs" reltype="might_have" key="id" map="" class="mraf"/>
+			<link field="mattrs" reltype="has_many" key="id" map="" class="mraf"/>
 			<link field="source" reltype="has_a" key="id" map="" class="cbs"/>
 			<link field="display_entries" reltype="has_many" key="source" map="" class="mde"/>
 			<link field="flat_display_entries" reltype="has_many" key="source" map="" class="mfde"/>

-----------------------------------------------------------------------

Summary of changes:
 .gitignore                                         |    1 +
 Open-ILS/examples/apache/eg_vhost.conf.in          |   31 +
 Open-ILS/examples/apache_24/eg_vhost.conf.in       |   30 +
 Open-ILS/examples/fm_IDL.xml                       |    2 +-
 Open-ILS/src/eg2/.editorconfig                     |   13 +
 Open-ILS/src/eg2/.gitignore                        |   49 +
 Open-ILS/src/eg2/CHEAT_SHEET.adoc                  |   31 +
 Open-ILS/src/eg2/angular.json                      |  155 +
 Open-ILS/src/eg2/e2e/app.e2e-spec.ts               |   14 +
 Open-ILS/src/eg2/e2e/app.po.ts                     |   11 +
 Open-ILS/src/eg2/e2e/tsconfig.e2e.json             |   14 +
 Open-ILS/src/eg2/karma.conf.js                     |   43 +
 Open-ILS/src/eg2/package-lock.json                 |10689 ++++++++++++++++++++
 Open-ILS/src/eg2/package.json                      |   84 +
 Open-ILS/src/eg2/protractor.conf.js                |   28 +
 Open-ILS/src/eg2/src/app/app.component.ts          |   11 +
 Open-ILS/src/eg2/src/app/app.module.ts             |   33 +
 Open-ILS/src/eg2/src/app/common.module.ts          |   71 +
 Open-ILS/src/eg2/src/app/core/README               |    9 +
 Open-ILS/src/eg2/src/app/core/auth.service.ts      |  341 +
 Open-ILS/src/eg2/src/app/core/event.service.ts     |   55 +
 Open-ILS/src/eg2/src/app/core/event.spec.ts        |   47 +
 Open-ILS/src/eg2/src/app/core/format.service.ts    |  103 +
 Open-ILS/src/eg2/src/app/core/format.spec.ts       |   90 +
 Open-ILS/src/eg2/src/app/core/idl.service.ts       |  137 +
 Open-ILS/src/eg2/src/app/core/idl.spec.ts          |   28 +
 Open-ILS/src/eg2/src/app/core/locale.service.ts    |   69 +
 Open-ILS/src/eg2/src/app/core/net.service.ts       |  187 +
 Open-ILS/src/eg2/src/app/core/org.service.ts       |  278 +
 Open-ILS/src/eg2/src/app/core/org.spec.ts          |   66 +
 Open-ILS/src/eg2/src/app/core/pcrud.service.ts     |  305 +
 Open-ILS/src/eg2/src/app/core/perm.service.ts      |   59 +
 .../src/eg2/src/app/core/server-store.service.ts   |  114 +
 Open-ILS/src/eg2/src/app/core/store.service.ts     |  107 +
 Open-ILS/src/eg2/src/app/core/store.spec.ts        |   22 +
 Open-ILS/src/eg2/src/app/resolver.service.ts       |   36 +
 Open-ILS/src/eg2/src/app/routing.module.ts         |   29 +
 Open-ILS/src/eg2/src/app/share/README              |    6 +
 .../share/accesskey/accesskey-info.component.html  |   26 +
 .../share/accesskey/accesskey-info.component.ts    |   25 +
 .../src/app/share/accesskey/accesskey.directive.ts |   56 +
 .../src/app/share/accesskey/accesskey.service.ts   |   67 +
 .../src/app/share/catalog/bib-record.service.ts    |  249 +
 .../src/app/share/catalog/catalog-common.module.ts |   28 +
 .../src/app/share/catalog/catalog-url.service.ts   |  143 +
 .../eg2/src/app/share/catalog/catalog.service.ts   |  210 +
 .../src/app/share/catalog/marc-html.component.ts   |   90 +
 .../eg2/src/app/share/catalog/search-context.ts    |  266 +
 .../src/eg2/src/app/share/catalog/unapi.service.ts |   54 +
 .../app/share/combobox/combobox-entry.component.ts |   25 +
 .../src/app/share/combobox/combobox.component.html |   27 +
 .../src/app/share/combobox/combobox.component.ts   |  241 +
 .../share/date-select/date-select.component.html   |   21 +
 .../app/share/date-select/date-select.component.ts |   70 +
 .../src/app/share/dialog/confirm.component.html    |   17 +
 .../eg2/src/app/share/dialog/confirm.component.ts  |   17 +
 .../eg2/src/app/share/dialog/dialog.component.ts   |   80 +
 .../app/share/dialog/progress-inline.component.css |    5 +
 .../share/dialog/progress-inline.component.html    |   28 +
 .../app/share/dialog/progress-inline.component.ts  |   92 +
 .../src/app/share/dialog/progress.component.css    |    5 +
 .../src/app/share/dialog/progress.component.html   |   33 +
 .../eg2/src/app/share/dialog/progress.component.ts |  108 +
 .../eg2/src/app/share/dialog/prompt.component.html |   22 +
 .../eg2/src/app/share/dialog/prompt.component.ts   |   19 +
 .../app/share/fm-editor/fm-editor.component.html   |  146 +
 .../src/app/share/fm-editor/fm-editor.component.ts |  302 +
 .../app/share/grid/grid-body-cell.component.html   |   20 +
 .../src/app/share/grid/grid-body-cell.component.ts |   57 +
 .../src/app/share/grid/grid-body.component.html    |   39 +
 .../eg2/src/app/share/grid/grid-body.component.ts  |   77 +
 .../share/grid/grid-column-config.component.html   |   69 +
 .../app/share/grid/grid-column-config.component.ts |   16 +
 .../share/grid/grid-column-width.component.html    |   20 +
 .../app/share/grid/grid-column-width.component.ts  |   32 +
 .../src/app/share/grid/grid-column.component.ts    |   57 +
 .../src/app/share/grid/grid-header.component.html  |   32 +
 .../src/app/share/grid/grid-header.component.ts    |   85 +
 .../src/app/share/grid/grid-print.component.html   |   30 +
 .../eg2/src/app/share/grid/grid-print.component.ts |   45 +
 .../share/grid/grid-toolbar-action.component.ts    |   33 +
 .../share/grid/grid-toolbar-button.component.ts    |   43 +
 .../share/grid/grid-toolbar-checkbox.component.ts  |   37 +
 .../src/app/share/grid/grid-toolbar.component.html |  152 +
 .../src/app/share/grid/grid-toolbar.component.ts   |   86 +
 .../src/eg2/src/app/share/grid/grid.component.css  |  142 +
 .../src/eg2/src/app/share/grid/grid.component.html |   27 +
 .../src/eg2/src/app/share/grid/grid.component.ts   |  149 +
 Open-ILS/src/eg2/src/app/share/grid/grid.module.ts |   50 +
 Open-ILS/src/eg2/src/app/share/grid/grid.ts        |  972 ++
 .../app/share/org-select/org-select.component.html |   17 +
 .../app/share/org-select/org-select.component.ts   |  212 +
 .../eg2/src/app/share/print/print.component.html   |   16 +
 .../src/eg2/src/app/share/print/print.component.ts |  133 +
 .../src/eg2/src/app/share/print/print.service.ts   |   41 +
 .../eg2/src/app/share/string/string.component.ts   |   74 +
 .../src/eg2/src/app/share/string/string.service.ts |   78 +
 .../eg2/src/app/share/toast/toast.component.css    |   11 +
 .../eg2/src/app/share/toast/toast.component.html   |    3 +
 .../src/eg2/src/app/share/toast/toast.component.ts |   43 +
 .../src/eg2/src/app/share/toast/toast.service.ts   |   39 +
 .../src/eg2/src/app/share/tree/tree.component.css  |   19 +
 .../src/eg2/src/app/share/tree/tree.component.html |   20 +
 .../src/eg2/src/app/share/tree/tree.component.ts   |   60 +
 Open-ILS/src/eg2/src/app/share/tree/tree.module.ts |   20 +
 Open-ILS/src/eg2/src/app/share/tree/tree.ts        |  133 +
 .../src/eg2/src/app/share/util/audio.service.ts    |   78 +
 Open-ILS/src/eg2/src/app/share/util/pager.ts       |  111 +
 .../src/eg2/src/app/staff/about.component.html     |   57 +
 Open-ILS/src/eg2/src/app/staff/about.component.ts  |   25 +
 .../admin/acq/admin-acq-splash.component.html      |   60 +
 .../staff/admin/acq/admin-acq-splash.component.ts  |   11 +
 .../src/app/staff/admin/acq/admin-acq.module.ts    |   24 +
 .../eg2/src/app/staff/admin/acq/routing.module.ts  |   22 +
 .../app/staff/admin/basic-admin-page.component.ts  |   61 +
 .../src/eg2/src/app/staff/admin/common.module.ts   |   28 +
 .../src/eg2/src/app/staff/admin/routing.module.ts  |   23 +
 .../server/admin-server-splash.component.html      |   99 +
 .../admin/server/admin-server-splash.component.ts  |   11 +
 .../app/staff/admin/server/admin-server.module.ts  |   24 +
 .../src/app/staff/admin/server/routing.module.ts   |   19 +
 .../app/staff/admin/workstation/routing.module.ts  |   14 +
 .../workstation/workstations/routing.module.ts     |   25 +
 .../workstations/workstations.component.html       |   92 +
 .../workstations/workstations.component.ts         |  186 +
 .../workstations/workstations.module.ts            |   18 +
 .../src/app/staff/catalog/catalog.component.html   |    6 +
 .../eg2/src/app/staff/catalog/catalog.component.ts |   18 +
 .../eg2/src/app/staff/catalog/catalog.module.ts    |   44 +
 .../eg2/src/app/staff/catalog/catalog.service.ts   |   87 +
 .../staff/catalog/record/actions.component.html    |   70 +
 .../app/staff/catalog/record/actions.component.ts  |   96 +
 .../app/staff/catalog/record/copies.component.html |   53 +
 .../app/staff/catalog/record/copies.component.ts   |   91 +
 .../staff/catalog/record/pagination.component.html |   36 +
 .../staff/catalog/record/pagination.component.ts   |  164 +
 .../app/staff/catalog/record/record.component.html |   37 +
 .../app/staff/catalog/record/record.component.ts   |   84 +
 .../eg2/src/app/staff/catalog/resolver.service.ts  |   59 +
 .../app/staff/catalog/result/facets.component.html |   43 +
 .../app/staff/catalog/result/facets.component.ts   |   48 +
 .../staff/catalog/result/pagination.component.css  |    8 +
 .../staff/catalog/result/pagination.component.html |   28 +
 .../staff/catalog/result/pagination.component.ts   |   51 +
 .../app/staff/catalog/result/record.component.html |  132 +
 .../app/staff/catalog/result/record.component.ts   |   77 +
 .../staff/catalog/result/results.component.html    |   30 +
 .../app/staff/catalog/result/results.component.ts  |   84 +
 .../eg2/src/app/staff/catalog/routing.module.ts    |   30 +
 .../app/staff/catalog/search-form.component.css    |   16 +
 .../app/staff/catalog/search-form.component.html   |  244 +
 .../src/app/staff/catalog/search-form.component.ts |  137 +
 .../circ/patron/bcsearch/bcsearch.component.html   |   19 +
 .../circ/patron/bcsearch/bcsearch.component.ts     |   36 +
 .../staff/circ/patron/bcsearch/bcsearch.module.ts  |   17 +
 .../staff/circ/patron/bcsearch/routing.module.ts   |   19 +
 .../src/app/staff/circ/patron/routing.module.ts    |   15 +
 .../src/eg2/src/app/staff/circ/routing.module.ts   |   15 +
 Open-ILS/src/eg2/src/app/staff/common.module.ts    |   84 +
 .../src/eg2/src/app/staff/login.component.html     |   58 +
 Open-ILS/src/eg2/src/app/staff/login.component.ts  |   96 +
 Open-ILS/src/eg2/src/app/staff/nav.component.css   |   72 +
 Open-ILS/src/eg2/src/app/staff/nav.component.html  |  436 +
 Open-ILS/src/eg2/src/app/staff/nav.component.ts    |   72 +
 Open-ILS/src/eg2/src/app/staff/resolver.service.ts |  143 +
 Open-ILS/src/eg2/src/app/staff/routing.module.ts   |   52 +
 Open-ILS/src/eg2/src/app/staff/sandbox/README      |    1 +
 .../eg2/src/app/staff/sandbox/routing.module.ts    |   16 +
 .../src/app/staff/sandbox/sandbox.component.html   |  133 +
 .../eg2/src/app/staff/sandbox/sandbox.component.ts |  188 +
 .../eg2/src/app/staff/sandbox/sandbox.module.ts    |   20 +
 Open-ILS/src/eg2/src/app/staff/share/README        |    1 +
 .../share/admin-page/admin-page.component.html     |   59 +
 .../staff/share/admin-page/admin-page.component.ts |  311 +
 .../share/bib-summary/bib-summary.component.html   |   70 +
 .../share/bib-summary/bib-summary.component.ts     |   67 +
 .../buckets/record-bucket-dialog.component.html    |   56 +
 .../buckets/record-bucket-dialog.component.ts      |  109 +
 .../eg2/src/app/staff/share/holdings.service.ts    |   57 +
 .../share/link-table/link-table.component.html     |   22 +
 .../staff/share/link-table/link-table.component.ts |   73 +
 .../staff/share/op-change/op-change.component.html |   65 +
 .../staff/share/op-change/op-change.component.ts   |   77 +
 .../src/app/staff/share/staff-banner.component.ts  |   15 +
 .../staff/share/translate/translate.component.html |   63 +
 .../staff/share/translate/translate.component.ts   |  145 +
 .../src/eg2/src/app/staff/splash.component.html    |  128 +
 Open-ILS/src/eg2/src/app/staff/splash.component.ts |   40 +
 Open-ILS/src/eg2/src/app/staff/staff.component.css |    8 +
 .../src/eg2/src/app/staff/staff.component.html     |   19 +
 Open-ILS/src/eg2/src/app/staff/staff.component.ts  |  118 +
 Open-ILS/src/eg2/src/app/staff/staff.module.ts     |   26 +
 Open-ILS/src/eg2/src/app/welcome.component.html    |   11 +
 Open-ILS/src/eg2/src/app/welcome.component.ts      |   13 +
 .../oils/__init__.py => eg2/src/assets/.gitkeep}   |    0
 .../src/eg2/src/environments/environment.prod.ts   |    4 +
 Open-ILS/src/eg2/src/environments/environment.ts   |   10 +
 Open-ILS/src/eg2/src/favicon.ico                   |  Bin 0 -> 5430 bytes
 Open-ILS/src/eg2/src/index.html                    |   19 +
 .../oils/__init__.py => eg2/src/locale/.gitkeep}   |    0
 Open-ILS/src/eg2/src/main.ts                       |   12 +
 Open-ILS/src/eg2/src/polyfills.ts                  |   80 +
 Open-ILS/src/eg2/src/styles.css                    |  161 +
 Open-ILS/src/eg2/src/test.ts                       |   32 +
 Open-ILS/src/eg2/src/test_data/eg_mock.js          |   52 +
 Open-ILS/src/eg2/src/test_data/idl2js.pl           |   36 +
 Open-ILS/src/eg2/src/tsconfig.app.json             |   13 +
 Open-ILS/src/eg2/src/tsconfig.spec.json            |   21 +
 Open-ILS/src/eg2/src/typings.d.ts                  |    5 +
 Open-ILS/src/eg2/tsconfig.json                     |   24 +
 Open-ILS/src/eg2/tslint.json                       |  136 +
 Open-ILS/src/extras/Makefile.install               |    2 +-
 Open-ILS/src/extras/install/Makefile.common        |    2 +-
 .../lib/OpenILS/Application/Search/Biblio.pm       |   80 +
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  101 +
 .../sql/Pg/upgrade/1129.data.acq-grid-settings.sql |  106 +
 Open-ILS/src/templates/staff/navbar.tt2            |   14 +-
 Open-ILS/web/js/ui/default/staff/services/auth.js  |    4 +
 Open-ILS/web/js/ui/default/staff/services/hatch.js |   22 +-
 Open-ILS/web/js/ui/default/staff/services/print.js |   37 +-
 Open-ILS/xul/staff_client/Makefile.am              |   22 +-
 build/tools/make_release                           |   16 +-
 .../Architecture/angular6-app.adoc                 |   70 +
 .../Client/Disabling_Legacy_Staff_client.adoc      |   13 +
 docs/installation/server_installation.adoc         |   76 +-
 docs/installation/server_upgrade.adoc              |   14 +-
 227 files changed, 25998 insertions(+), 53 deletions(-)
 create mode 100644 Open-ILS/src/eg2/.editorconfig
 create mode 100644 Open-ILS/src/eg2/.gitignore
 create mode 100644 Open-ILS/src/eg2/CHEAT_SHEET.adoc
 create mode 100644 Open-ILS/src/eg2/angular.json
 create mode 100644 Open-ILS/src/eg2/e2e/app.e2e-spec.ts
 create mode 100644 Open-ILS/src/eg2/e2e/app.po.ts
 create mode 100644 Open-ILS/src/eg2/e2e/tsconfig.e2e.json
 create mode 100644 Open-ILS/src/eg2/karma.conf.js
 create mode 100644 Open-ILS/src/eg2/package-lock.json
 create mode 100644 Open-ILS/src/eg2/package.json
 create mode 100644 Open-ILS/src/eg2/protractor.conf.js
 create mode 100644 Open-ILS/src/eg2/src/app/app.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/app.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/common.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/README
 create mode 100644 Open-ILS/src/eg2/src/app/core/auth.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/event.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/event.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/format.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/format.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/idl.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/idl.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/locale.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/net.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/org.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/org.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/pcrud.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/perm.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/server-store.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/store.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/core/store.spec.ts
 create mode 100644 Open-ILS/src/eg2/src/app/resolver.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/README
 create mode 100644 Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/accesskey/accesskey.directive.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/accesskey/accesskey.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/catalog-common.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/catalog-url.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/marc-html.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/unapi.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/combobox/combobox-entry.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/confirm.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/confirm.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/progress-inline.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/progress.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/progress.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/progress.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/prompt.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/dialog/prompt.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-body.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-body.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-column-config.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-column-width.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-column.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-header.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-header.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-print.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-print.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-button.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid-toolbar.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/grid/grid.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/print/print.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/print/print.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/print/print.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/string/string.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/string/string.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/toast/toast.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/share/toast/toast.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/toast/toast.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/toast/toast.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/tree/tree.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/share/tree/tree.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/share/tree/tree.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/tree/tree.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/tree/tree.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/util/audio.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/share/util/pager.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/about.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/about.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq-splash.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/admin-acq.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/acq/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/basic-admin-page.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/common.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/workstation/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/actions.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/copies.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/pagination.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/resolver.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/facets.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/pagination.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/result/results.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/search-form.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/common.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/login.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/login.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/nav.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/staff/nav.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/nav.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/resolver.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/sandbox/README
 create mode 100644 Open-ILS/src/eg2/src/app/staff/sandbox/routing.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/README
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/admin-page/admin-page.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/buckets/record-bucket-dialog.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings.service.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/link-table/link-table.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/op-change/op-change.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/staff-banner.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/share/translate/translate.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/splash.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/splash.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/staff.component.css
 create mode 100644 Open-ILS/src/eg2/src/app/staff/staff.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/staff/staff.component.ts
 create mode 100644 Open-ILS/src/eg2/src/app/staff/staff.module.ts
 create mode 100644 Open-ILS/src/eg2/src/app/welcome.component.html
 create mode 100644 Open-ILS/src/eg2/src/app/welcome.component.ts
 copy Open-ILS/src/{python/oils/__init__.py => eg2/src/assets/.gitkeep} (100%)
 create mode 100644 Open-ILS/src/eg2/src/environments/environment.prod.ts
 create mode 100644 Open-ILS/src/eg2/src/environments/environment.ts
 create mode 100644 Open-ILS/src/eg2/src/favicon.ico
 create mode 100644 Open-ILS/src/eg2/src/index.html
 copy Open-ILS/src/{python/oils/__init__.py => eg2/src/locale/.gitkeep} (100%)
 create mode 100644 Open-ILS/src/eg2/src/main.ts
 create mode 100644 Open-ILS/src/eg2/src/polyfills.ts
 create mode 100644 Open-ILS/src/eg2/src/styles.css
 create mode 100644 Open-ILS/src/eg2/src/test.ts
 create mode 100644 Open-ILS/src/eg2/src/test_data/eg_mock.js
 create mode 100644 Open-ILS/src/eg2/src/test_data/idl2js.pl
 create mode 100644 Open-ILS/src/eg2/src/tsconfig.app.json
 create mode 100644 Open-ILS/src/eg2/src/tsconfig.spec.json
 create mode 100644 Open-ILS/src/eg2/src/typings.d.ts
 create mode 100644 Open-ILS/src/eg2/tsconfig.json
 create mode 100644 Open-ILS/src/eg2/tslint.json
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1129.data.acq-grid-settings.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Architecture/angular6-app.adoc
 create mode 100644 docs/RELEASE_NOTES_NEXT/Client/Disabling_Legacy_Staff_client.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list