aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Tatiyants <atatiyan@gmail.com>2016-01-03 17:17:48 -0800
committerAlex Tatiyants <atatiyan@gmail.com>2016-01-03 17:17:48 -0800
commit5310ac7d8eb1838a6297117bc7f9fca70291f46a (patch)
tree28f54b184cb85f04e6d6720dd03258f3728fedde
initial commit
-rw-r--r--.editorconfig15
-rw-r--r--.gitignore39
-rw-r--r--.jshintrc25
-rw-r--r--LICENSE21
-rw-r--r--README.md1
-rw-r--r--app/assets/css/styles.css4
-rw-r--r--app/assets/fonts/FontAwesome.otfbin0 -> 109688 bytes
-rw-r--r--app/assets/fonts/fontawesome-webfont.eotbin0 -> 70807 bytes
-rw-r--r--app/assets/fonts/fontawesome-webfont.ttfbin0 -> 142072 bytes
-rw-r--r--app/assets/fonts/fontawesome-webfont.woffbin0 -> 83588 bytes
-rw-r--r--app/assets/fonts/fontawesome-webfont.woff2bin0 -> 66624 bytes
-rw-r--r--app/assets/sass/_buttons.scss47
-rw-r--r--app/assets/sass/_common.scss53
-rw-r--r--app/assets/sass/_footer.scss14
-rw-r--r--app/assets/sass/_forms.scss33
-rw-r--r--app/assets/sass/_menu.scss65
-rw-r--r--app/assets/sass/_modal.scss52
-rw-r--r--app/assets/sass/_nav.scss10
-rw-r--r--app/assets/sass/_page.scss17
-rw-r--r--app/assets/sass/_plan-node.scss111
-rw-r--r--app/assets/sass/_plan.scss121
-rw-r--r--app/assets/sass/_table.scss12
-rw-r--r--app/assets/sass/_variables.scss51
-rw-r--r--app/assets/sass/font-awesome/_animated.scss34
-rw-r--r--app/assets/sass/font-awesome/_bordered-pulled.scss25
-rw-r--r--app/assets/sass/font-awesome/_core.scss12
-rw-r--r--app/assets/sass/font-awesome/_fixed-width.scss6
-rw-r--r--app/assets/sass/font-awesome/_icons.scss697
-rw-r--r--app/assets/sass/font-awesome/_larger.scss13
-rw-r--r--app/assets/sass/font-awesome/_list.scss19
-rw-r--r--app/assets/sass/font-awesome/_mixins.scss26
-rw-r--r--app/assets/sass/font-awesome/_path.scss15
-rw-r--r--app/assets/sass/font-awesome/_rotated-flipped.scss20
-rw-r--r--app/assets/sass/font-awesome/_stacked.scss20
-rw-r--r--app/assets/sass/font-awesome/_styles.scss17
-rw-r--r--app/assets/sass/font-awesome/_variables.scss708
-rw-r--r--app/assets/sass/styles.scss16
-rw-r--r--app/assets/styles.css4
-rw-r--r--app/bootstrap.ts9
-rw-r--r--app/components/app/app.html6
-rw-r--r--app/components/app/app.ts22
-rw-r--r--app/components/plan-list/plan-list.html38
-rw-r--r--app/components/plan-list/plan-list.ts47
-rw-r--r--app/components/plan-new/plan-new.html18
-rw-r--r--app/components/plan-new/plan-new.ts26
-rw-r--r--app/components/plan-node/plan-node.html50
-rw-r--r--app/components/plan-node/plan-node.ts176
-rw-r--r--app/components/plan-view/plan-view.html72
-rw-r--r--app/components/plan-view/plan-view.ts96
-rw-r--r--app/enums.ts11
-rw-r--r--app/index.html32
-rw-r--r--app/interfaces/iplan.ts7
-rw-r--r--app/pipes.ts10
-rw-r--r--app/sample-plans/plan1.json334
-rw-r--r--app/services/plan-service.ts166
-rw-r--r--gulpfile.ts83
-rw-r--r--karma.conf.js103
-rw-r--r--package.json93
-rw-r--r--test-main.js53
-rw-r--r--tools/config.ts101
-rw-r--r--tools/tasks/build.bundles.ts26
-rw-r--r--tools/tasks/build.deps.ts20
-rw-r--r--tools/tasks/build.docs.ts27
-rw-r--r--tools/tasks/build.fonts.dev.ts15
-rw-r--r--tools/tasks/build.html_css.prod.ts24
-rw-r--r--tools/tasks/build.img.dev.ts14
-rw-r--r--tools/tasks/build.index.ts34
-rw-r--r--tools/tasks/build.js.dev.ts25
-rw-r--r--tools/tasks/build.js.prod.ts22
-rw-r--r--tools/tasks/build.sass.dev.ts22
-rw-r--r--tools/tasks/build.test.ts21
-rw-r--r--tools/tasks/check.versions.ts35
-rw-r--r--tools/tasks/clean.ts34
-rw-r--r--tools/tasks/karma.start.ts11
-rw-r--r--tools/tasks/npm.ts5
-rw-r--r--tools/tasks/serve.docs.ts7
-rw-r--r--tools/tasks/server.start.ts7
-rw-r--r--tools/tasks/tsd.ts7
-rw-r--r--tools/tasks/tslint.ts21
-rw-r--r--tools/tasks/watch.dev.ts8
-rw-r--r--tools/tasks/watch.serve.ts12
-rw-r--r--tools/tasks/watch.test.ts8
-rw-r--r--tools/typings/connect-livereload.d.ts5
-rw-r--r--tools/typings/gulp-load-plugins.d.ts42
-rw-r--r--tools/typings/karma.d.ts12
-rw-r--r--tools/typings/merge-stream.d.ts8
-rw-r--r--tools/typings/open.d.ts8
-rw-r--r--tools/typings/run-sequence.d.ts5
-rw-r--r--tools/typings/slash.d.ts5
-rw-r--r--tools/typings/systemjs-builder.d.ts10
-rw-r--r--tools/typings/tiny-lr.d.ts10
-rw-r--r--tools/typings/yargs.d.ts9
-rw-r--r--tools/utils.ts11
-rw-r--r--tools/utils/server.ts45
-rw-r--r--tools/utils/tasks_tools.ts64
-rw-r--r--tools/utils/template-injectables.ts25
-rw-r--r--tools/utils/template-locals.ts13
-rw-r--r--tsconfig.json17
-rw-r--r--tsd.json63
-rw-r--r--tslint.json36
100 files changed, 4669 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f1cc3ad
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+# http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7548caf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+node_modules
+.sass-cache
+
+# Users Environment Variables
+.lock-wscript
+.tsdrc
+
+#IDE configuration files
+.idea
+.vscode
+
+dist
+dev
+docs
+lib
+test
+tools/typings/tsd
+tmp
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..0f847e2
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,25 @@
+{
+ "bitwise": true,
+ "immed": true,
+ "newcap": true,
+ "noarg": true,
+ "noempty": true,
+ "nonew": true,
+ "trailing": true,
+ "maxlen": 200,
+ "boss": true,
+ "eqnull": true,
+ "expr": true,
+ "globalstrict": true,
+ "laxbreak": true,
+ "loopfunc": true,
+ "sub": true,
+ "undef": true,
+ "indent": 2,
+ "unused": true,
+
+ "node": true,
+ "globals": {
+ "System": true
+ }
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..69a333e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Alex Tatiyants
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d81c36e
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Postgres Explain Visualizer (PEV)
diff --git a/app/assets/css/styles.css b/app/assets/css/styles.css
new file mode 100644
index 0000000..ec0a393
--- /dev/null
+++ b/app/assets/css/styles.css
@@ -0,0 +1,4 @@
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}/*!
+ * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.5.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.5.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.5.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.5.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:0.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}html{height:100%}body{font-size:13px;font-weight:300;color:#4d525a;height:100%;width:100%;background-color:#f7f7f7;line-height:1.3}strong{font-weight:600}body,input,a,button,textarea{font-family:"noto sans";font-weight:300}.text-muted{color:#999ea7}.hero-container{margin:30px;font-size:22px;text-align:center}.pull-right{float:right}.align-right{text-align:right}a{color:#00B5E2;text-decoration:none}.fa{margin-right:3px}.clickable{cursor:pointer}.btn{border-radius:3px;padding:6px 10px;font-size:13px;line-height:1.2;text-decoration:none;text-transform:uppercase}.btn-default{border:1px solid #00B5E2;color:#00B5E2;background-color:#fff}.btn-default:hover{background-color:#65DDFB}.btn-lg{padding:6px 20px;font-size:16px}.btn-danger{border:1px solid #AF2F11;color:#AF2F11;text-transform:uppercase;background-color:#fff}.btn-danger:hover{background-color:#FB8165}.btn-primary{border:0;background:#00B5E2;box-shadow:1px 1px 1px #ababab;color:#ffffff}.btn-primary:hover{background:#008CAF}.input-box:-moz-placeholder{color:#ababab;font-size:18px}.input-box::-moz-placeholder{color:#ababab;font-size:18px}.input-box:-ms-input-placeholder{color:#ababab;font-size:18px}.input-box::-webkit-input-placeholder{color:#ababab;font-size:18px}.input-box:focus{box-shadow:0 0 5px #51cbee}.input-box-main{font-size:18px;width:700px;border:0;border-bottom:2px solid #00B5E2;padding:10px;margin-top:10px;margin-bottom:10px}.input-box-lg{width:100%;height:280px;margin-bottom:6px;margin-bottom:10px;border-radius:3px;border:1px solid #dedede;padding:10px}nav{font-size:17px;background-color:#fff;padding:15px}nav .nav-container{width:1000px;margin:auto}.plan{padding-bottom:30px;overflow:auto;height:100%;width:100%}.plan ul{display:flex;padding-top:12px;position:relative;margin:auto;transition:all 0.5s;margin-top:-5px}.plan ul ul::before{content:'';position:absolute;top:0;left:50%;border-left:2px solid #c4c4c4;height:12px;width:0}.plan ul li{float:left;text-align:center;list-style-type:none;position:relative;padding:12px 3px 0 3px;transition:all 0.5s}.plan ul li:before,.plan ul li:after{content:'';position:absolute;top:0;right:50%;border-top:2px solid #c4c4c4;width:50%;height:12px}.plan ul li:after{right:auto;left:50%;border-left:2px solid #c4c4c4}.plan ul li:only-child{padding-top:0}.plan ul li:only-child:after,.plan ul li:only-child:before{display:none}.plan ul li:first-child::before,.plan ul li:last-child::after{border:0 none}.plan ul li:last-child::before{border-right:2px solid #c4c4c4;border-radius:0 6px 0 0}.plan ul li:first-child::after{border-radius:6px 0 0 0}.plan ul li .plan-node:hover+ul::before{border-color:#00B5E2}.plan ul li .plan-node:hover+ul li::after,.plan ul li .plan-node:hover+ul li::before,.plan ul li .plan-node:hover+ul ul::before{border-color:#008CAF}.plan-stats{display:flex;font-size:13px;margin:0 auto 10px auto;padding-bottom:10px;border-bottom:1px solid #dedede;border-radius:12px;width:650px;position:relative}.plan-stats div{padding-right:10px;flex-grow:1}.plan-stats .stat-value{display:block;text-align:center;font-size:17px}.plan-stats .stat-label{display:block;text-align:center;font-size:12px}.plan-stats:after{content:'';position:absolute;top:100%;left:50%;margin-left:-9px;width:0;height:0;border-top:solid 9px #dedede;border-left:solid 9px transparent;border-right:solid 9px transparent}.plan-node{text-decoration:none;color:#4d525a;display:inline-block;transition:all 0.5s;position:relative;padding:6px 10px;background-color:#fff;font-size:12px;border:2px solid #dedede;border-radius:3px;overflow:hidden;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;min-width:220px}.plan-node header{margin-bottom:6px;overflow:hidden;cursor:pointer}.plan-node header:hover{background-color:#f7f7f7}.plan-node header h4{font-size:13px;float:left;font-weight:600}.plan-node header .node-duration{float:right;margin-left:10px;font-size:13px}.plan-node .prop-list{float:left;text-align:left;width:400px;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;margin-top:10px;margin-bottom:6px}.plan-node .relation-name{text-align:left;max-width:220px}.plan-node .planner-estimate{float:left;clear:both;text-align:left;margin-top:6px;width:100%}.plan-node .tags{margin-top:6px;text-align:left}.plan-node .tags span{display:inline-block;background-color:#FB4418;color:#fff;font-size:10px;font-weight:600;margin-right:3px;padding:3px;border-radius:3px;line-height:1.1}.plan-node:hover{border-color:#00B5E2}.node-bar-container{float:left;height:5px;margin-top:6px;margin-left:auto;margin-right:auto;border:1px solid #dedede;border-radius:3px;color:#fff;background-color:#454545;position:relative}.node-bar-container .node-bar{height:100%;text-align:left;position:absolute;left:0;top:0}.node-bar-label{text-align:left;display:block}.menu{width:190px;height:200px;position:absolute;font-size:12px;top:115px;left:0;background-color:#777;box-shadow:1px 1px 2px 1px rgba(0,0,0,0.5);color:#fff;border-top-right-radius:3px;border-bottom-right-radius:3px;z-index:1;transition:all 0.3s}.menu header h3{padding-top:10px;margin-bottom:20px;font-size:16px;font-weight:600;line-height:2;text-align:right;padding-right:20px}.menu ul{margin-left:10px}.menu ul li{line-height:2.5}.menu-toggle{font-size:26px;float:left;padding-left:10px;line-height:2;cursor:pointer}.menu-hidden{width:50px;height:50px;border-top-right-radius:50%;border-bottom-right-radius:50%}.menu-hidden ul,.menu-hidden h3{visibility:hidden}.menu button{border:1px solid #fff;background-color:#544D4D;color:#fff;padding:2px;border-radius:3px}.page,.page-stretch{padding-top:10px;margin:auto;width:1000px;min-height:600px}.page h2,.page-stretch h2{font-size:26px;margin-bottom:6px}.page-stretch{margin:auto;width:95%}.table{width:100%}.table td{border-bottom:1px solid #dedede;padding:6px}.table tr:hover{background-color:#f7f7f7}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000;opacity:0.7}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050}.modal .modal-dialog{position:relative;transform:translate(0);margin:30px auto;width:500px;opacity:1}.modal .modal-dialog .modal-content{padding:30px;position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;box-shadow:0 3px 9px rgba(0,0,0,0.5);display:block}.modal .modal-dialog .modal-content .modal-body{padding:3px}.modal .modal-dialog .modal-content .modal-footer{text-align:right}.modal .modal-dialog .modal-content .modal-footer button{margin-left:3px}footer{border-top:2px solid #dedede;margin:20px;padding:20px;color:#ababab;text-align:center;width:600px;margin:auto}footer .fa{font-size:17px;margin-left:6px}
diff --git a/app/assets/fonts/FontAwesome.otf b/app/assets/fonts/FontAwesome.otf
new file mode 100644
index 0000000..3ed7f8b
--- /dev/null
+++ b/app/assets/fonts/FontAwesome.otf
Binary files differ
diff --git a/app/assets/fonts/fontawesome-webfont.eot b/app/assets/fonts/fontawesome-webfont.eot
new file mode 100644
index 0000000..9b6afae
--- /dev/null
+++ b/app/assets/fonts/fontawesome-webfont.eot
Binary files differ
diff --git a/app/assets/fonts/fontawesome-webfont.ttf b/app/assets/fonts/fontawesome-webfont.ttf
new file mode 100644
index 0000000..26dea79
--- /dev/null
+++ b/app/assets/fonts/fontawesome-webfont.ttf
Binary files differ
diff --git a/app/assets/fonts/fontawesome-webfont.woff b/app/assets/fonts/fontawesome-webfont.woff
new file mode 100644
index 0000000..dc35ce3
--- /dev/null
+++ b/app/assets/fonts/fontawesome-webfont.woff
Binary files differ
diff --git a/app/assets/fonts/fontawesome-webfont.woff2 b/app/assets/fonts/fontawesome-webfont.woff2
new file mode 100644
index 0000000..500e517
--- /dev/null
+++ b/app/assets/fonts/fontawesome-webfont.woff2
Binary files differ
diff --git a/app/assets/sass/_buttons.scss b/app/assets/sass/_buttons.scss
new file mode 100644
index 0000000..760d5ec
--- /dev/null
+++ b/app/assets/sass/_buttons.scss
@@ -0,0 +1,47 @@
+.btn {
+ border-radius: $border-radius-base;
+ padding: $padding-base $padding-lg;
+ font-size: $font-size-base;
+ line-height: 1.2;
+ text-decoration: none;
+ text-transform: uppercase;
+
+ &-default {
+ border: 1px solid $blue;
+ color: $blue;
+ background-color: #fff;
+
+ &:hover {
+ background-color: $light-blue;
+ }
+ }
+
+ &-lg {
+ padding: $padding-base $padding-lg*2;
+ font-size: round($font-size-base * 1.2);
+ }
+
+ &-danger {
+ border: 1px solid $red;
+ color: $red;
+ text-transform: uppercase;
+ background-color: #fff;
+
+ &:hover {
+ background-color: $light-red;
+ }
+ }
+
+ &-primary {
+ border: 0;
+ background: $blue;
+ // background-image: linear-gradient(to bottom, $blue, $dark-blue);
+ box-shadow: 1px 1px 1px $gray;
+ color: #ffffff;
+
+ &:hover {
+ background: $dark-blue;
+ // background-image: linear-gradient(to bottom, $dark-blue, $blue);
+ }
+ }
+}
diff --git a/app/assets/sass/_common.scss b/app/assets/sass/_common.scss
new file mode 100644
index 0000000..5d38773
--- /dev/null
+++ b/app/assets/sass/_common.scss
@@ -0,0 +1,53 @@
+html {
+ height: 100%;
+}
+
+body {
+ font-size: $font-size-base;
+ font-weight: 300;
+ color: $text-color;
+ height: 100%;
+ width: 100%;
+ background-color: $bg-color;
+ line-height: $line-height-base;
+}
+
+strong {
+ font-weight: 600;
+}
+
+body, input, a, button, textarea {
+ font-family: $font-family-sans-serif;
+ font-weight: 300;
+}
+
+.text-muted {
+ color: $text-color-light;
+}
+
+.hero-container {
+ margin: $padding-lg * 3;
+ font-size: $font-size-xl;
+ text-align: center;
+}
+
+.pull-right {
+ float: right;
+}
+
+.align-right {
+ text-align: right;
+}
+
+a {
+ color: $link-color;
+ text-decoration: none;
+}
+
+.fa {
+ margin-right: $padding-sm;
+}
+
+.clickable {
+ cursor: pointer;
+}
diff --git a/app/assets/sass/_footer.scss b/app/assets/sass/_footer.scss
new file mode 100644
index 0000000..603ff5a
--- /dev/null
+++ b/app/assets/sass/_footer.scss
@@ -0,0 +1,14 @@
+footer {
+ border-top: 2px solid $gray-light;
+ margin: round($padding-lg * 2);
+ padding: round($padding-lg * 2);
+ color: $gray;
+ text-align: center;
+ width: 600px;
+ margin: auto;
+
+ .fa {
+ font-size: $font-size-lg;
+ margin-left: $padding-base;
+ }
+}
diff --git a/app/assets/sass/_forms.scss b/app/assets/sass/_forms.scss
new file mode 100644
index 0000000..4ab4dfe
--- /dev/null
+++ b/app/assets/sass/_forms.scss
@@ -0,0 +1,33 @@
+@import "compass/css3/user-interface";
+
+.input-box {
+ @include input-placeholder {
+ color: $gray;
+ font-size: round($font-size-base * 1.4);
+ }
+
+ &:focus {
+ box-shadow: 0 0 5px rgba(81, 203, 238, 1);
+ }
+
+ &-main {
+ font-size: round($font-size-base * 1.4);
+ width: 700px;
+ border: 0;
+ border-bottom: 2px solid $blue;
+ padding: $padding-lg;
+ margin-top: $padding-lg;
+ margin-bottom: $padding-lg;
+ }
+
+ &-lg {
+ width: 100%;
+ height: 280px;
+ margin-bottom: $padding-base;
+ margin-bottom: $padding-lg;
+ border-radius: $border-radius-base;
+ border: 1px solid $line-color;
+ padding: $padding-lg;
+ }
+
+}
diff --git a/app/assets/sass/_menu.scss b/app/assets/sass/_menu.scss
new file mode 100644
index 0000000..d294686
--- /dev/null
+++ b/app/assets/sass/_menu.scss
@@ -0,0 +1,65 @@
+$menu-offset: 120px;
+$menu-toggle-height: 45px;
+
+.menu {
+ width: 190px;
+ height: 200px;
+ position: absolute;
+ font-size: $font-size-sm;
+ top: $menu-offset - 5;
+ left: 0;
+ background-color: $gray-dark;
+ box-shadow: 1px 1px 2px 1px rgba(0,0,0,0.5);
+ color: #fff;
+ border-top-right-radius: $border-radius-base;
+ border-bottom-right-radius: $border-radius-base;
+ z-index: 1;
+ transition: all 0.3s;
+
+ header {
+ h3 {
+ padding-top: $padding-lg;
+ margin-bottom: $padding-lg * 2;
+ font-size: round($font-size-base * 1.2);
+ font-weight: 600;
+ line-height: 2;
+ text-align: right;
+ padding-right: $padding-lg * 2;
+ }
+ }
+
+ ul {
+ margin-left: $padding-lg;
+
+ li {
+ line-height: 2.5;
+ }
+ }
+
+ &-toggle {
+ font-size: round($font-size-lg * 1.5);
+ float: left;
+ padding-left: $padding-lg;
+ line-height: 2;
+ cursor: pointer;
+ }
+
+ &-hidden {
+ width: 50px;
+ height: 50px;
+ border-top-right-radius: 50%;
+ border-bottom-right-radius: 50%;
+
+ ul, h3 {
+ visibility: hidden;
+ }
+ }
+}
+
+.menu button {
+ border: 1px solid #fff;
+ background-color: #544D4D;
+ color: #fff;
+ padding: 2px;
+ border-radius: 3px;
+}
diff --git a/app/assets/sass/_modal.scss b/app/assets/sass/_modal.scss
new file mode 100644
index 0000000..19d4bfc
--- /dev/null
+++ b/app/assets/sass/_modal.scss
@@ -0,0 +1,52 @@
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1040;
+ background-color: #000;
+ opacity: 0.7;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1050;
+
+ .modal-dialog {
+ position: relative;
+ transform: translate(0);
+ margin: 30px auto;
+ width: 500px;
+ opacity: 1;
+
+ .modal-content {
+ padding: 30px;
+ position: relative;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #999;
+ border: 1px solid rgba(0,0,0,.2);
+ border-radius: 6px;
+ outline: 0;
+ box-shadow: 0 3px 9px rgba(0,0,0,.5);
+ display: block;
+
+ .modal-body {
+ padding: $padding-sm;
+ }
+
+ .modal-footer {
+ text-align: right;
+
+ button {
+ margin-left: $padding-sm;
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/sass/_nav.scss b/app/assets/sass/_nav.scss
new file mode 100644
index 0000000..496213b
--- /dev/null
+++ b/app/assets/sass/_nav.scss
@@ -0,0 +1,10 @@
+nav {
+ font-size: round($font-size-base * 1.3);
+ background-color: #fff;
+ padding: round($padding-lg * 1.5);
+
+ .nav-container {
+ width: $page-width;
+ margin: auto;
+ }
+}
diff --git a/app/assets/sass/_page.scss b/app/assets/sass/_page.scss
new file mode 100644
index 0000000..64e1891
--- /dev/null
+++ b/app/assets/sass/_page.scss
@@ -0,0 +1,17 @@
+.page {
+ padding-top: $padding-lg;
+ margin: auto;
+ width: $page-width;
+ min-height: 600px;
+
+ h2 {
+ font-size: round($font-size-base * 2);
+ margin-bottom: $padding-base;
+ }
+}
+
+.page-stretch {
+ @extend .page;
+ margin: auto;
+ width: 95%;
+}
diff --git a/app/assets/sass/_plan-node.scss b/app/assets/sass/_plan-node.scss
new file mode 100644
index 0000000..25b3a05
--- /dev/null
+++ b/app/assets/sass/_plan-node.scss
@@ -0,0 +1,111 @@
+.plan-node {
+ text-decoration: none;
+ color: $text-color;
+ display: inline-block;
+ transition: all 0.5s;
+ position: relative;
+ padding: $padding-base $padding-lg;
+ background-color: #fff;
+ font-size: $font-size-sm;
+ border: 2px solid $line-color;
+ border-radius: $border-radius-base;
+ overflow: hidden;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-all;
+ min-width: 220px;
+
+ header {
+ margin-bottom: $padding-base;
+ overflow: hidden;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $gray-lightest;
+ }
+
+ h4 {
+ font-size: $font-size-base;
+ float: left;
+ font-weight: 600;
+ }
+
+ .node-duration {
+ float: right;
+ margin-left: $padding-lg;
+ font-size: $font-size-base;
+ }
+ }
+
+ .prop-list {
+ float: left;
+ text-align: left;
+ width: 400px;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-all;
+ margin-top: $padding-lg;
+ margin-bottom: $padding-base;
+ }
+
+ .relation-name {
+ text-align: left;
+ max-width: 220px;
+ }
+
+ .planner-estimate {
+ float: left;
+ clear: both;
+ text-align: left;
+ margin-top: $padding-base;
+ width: 100%;
+ }
+
+ .tags {
+ margin-top: $padding-base;
+ text-align: left;
+
+ span {
+ display: inline-block;
+ background-color: $alert-color;
+ color: #fff;
+ font-size: round($font-size-sm * 0.8);
+ font-weight: 600;
+ margin-right: $padding-sm;
+ padding: $padding-sm;
+ border-radius: $border-radius-base;
+ line-height: 1.1;
+ }
+ }
+
+ //hovers
+ &:hover {
+ border-color: $highlight-color;
+ }
+}
+
+.node-bar-container {
+ float: left;
+ height: 5px;
+ margin-top: $padding-base;
+ margin-left: auto;
+ margin-right: auto;
+ border: 1px solid $line-color;
+ border-radius: $border-radius-base;
+ color: #fff;
+ background-color: $gray-darkest;
+ position: relative;
+
+ .node-bar {
+ height: 100%;
+ text-align: left;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+}
+
+.node-bar-label {
+ text-align: left;
+ display: block;
+}
diff --git a/app/assets/sass/_plan.scss b/app/assets/sass/_plan.scss
new file mode 100644
index 0000000..7b8808d
--- /dev/null
+++ b/app/assets/sass/_plan.scss
@@ -0,0 +1,121 @@
+$connector-height: 12px;
+$connector-line: 2px solid darken($line-color, 10%);
+
+.plan {
+ padding-bottom: $padding-lg * 3;
+ overflow: auto;
+ height: 100%;
+ width: 100%;
+
+ ul {
+ display: flex;
+ padding-top: $connector-height;
+ position: relative;
+ margin: auto;
+ transition: all 0.5s;
+ margin-top: -5px;
+
+ // vertical connector
+ ul::before {
+ content: '';
+ position: absolute; top: 0; left: 50%;
+ border-left: $connector-line;
+ height: $connector-height;
+ width: 0;
+ }
+
+ li {
+ float: left; text-align: center;
+ list-style-type: none;
+ position: relative;
+ padding: $connector-height $padding-sm 0 $padding-sm;
+ transition: all 0.5s;
+
+ // connectors
+ &:before, &:after {
+ content: '';
+ position: absolute; top: 0; right: 50%;
+ border-top: $connector-line;
+ width: 50%; height: $connector-height;
+ }
+
+ &:after {
+ right: auto; left: 50%;
+ border-left: $connector-line;
+ }
+
+ &:only-child {
+ padding-top: 0;
+ &:after, &:before {
+ display: none;
+ }
+ }
+
+ &:first-child::before, &:last-child::after {
+ border: 0 none;
+ }
+
+ &:last-child::before {
+ border-right: $connector-line;
+ border-radius: 0 $border-radius-lg 0 0;
+ }
+
+ &:first-child::after {
+ border-radius: $border-radius-lg 0 0 0;
+ }
+
+ //hovers
+ .plan-node:hover+ul::before {
+ border-color: $highlight-color;
+ }
+
+ .plan-node:hover+ul li::after,
+ .plan-node:hover+ul li::before,
+ .plan-node:hover+ul ul::before{
+ border-color: $highlight-color-dark;
+ }
+ }
+ }
+}
+
+.plan-stats {
+ display: flex;
+ font-size: $font-size-base;
+ margin: 0 auto $padding-lg auto;
+ padding-bottom: $padding-lg;
+ border-bottom: 1px solid $line-color;
+ border-radius: $border-radius-lg*2;
+ width: 650px;
+ position: relative;
+
+ div {
+ padding-right: $padding-lg;
+ flex-grow: 1;
+ }
+
+ .stat-value {
+ display: block;
+ text-align: center;
+ font-size: $font-size-lg;
+ }
+
+ .stat-label {
+ display: block;
+ text-align: center;
+ font-size: $font-size-sm;
+ }
+
+ $triangle-size: 9px;
+ &:after {
+ content:'';
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: $triangle-size*-1;
+ width: 0;
+ height: 0;
+ border-top: solid $triangle-size $line-color;
+ border-left: solid $triangle-size transparent;
+ border-right: solid $triangle-size transparent;
+ }
+}
diff --git a/app/assets/sass/_table.scss b/app/assets/sass/_table.scss
new file mode 100644
index 0000000..d3e6e63
--- /dev/null
+++ b/app/assets/sass/_table.scss
@@ -0,0 +1,12 @@
+.table {
+ width: 100%;
+
+ td {
+ border-bottom: 1px solid $line-color;
+ padding: $padding-base;
+ }
+
+ tr:hover {
+ background-color: $gray-lightest;
+ }
+}
diff --git a/app/assets/sass/_variables.scss b/app/assets/sass/_variables.scss
new file mode 100644
index 0000000..cfdb166
--- /dev/null
+++ b/app/assets/sass/_variables.scss
@@ -0,0 +1,51 @@
+//vars
+$page-width: 1000px;
+
+$padding-base: 6px;
+$padding-sm: 3px;
+$padding-lg: 10px;
+
+$font-size-base: 13px;
+$font-size-xs: round($font-size-base * 0.7);
+$font-size-sm: round($font-size-base * 0.9);
+$font-size-lg: round($font-size-base * 1.3);
+$font-size-xl: round($font-size-base * 1.7);
+
+$font-family-sans-serif: 'noto sans';
+
+$line-height-base: 1.3;
+
+$gray-lightest: #f7f7f7;
+$gray-light: darken($gray-lightest, 10%);
+$gray: darken(#f7f7f7, 30%);
+$gray-dark: darken(#f7f7f7, 50%);
+$gray-darkest: darken($gray-lightest, 70%);
+
+$blue: #00B5E2;
+$dark-blue: #008CAF;
+$light-blue: #65DDFB;
+
+$red: #AF2F11;
+$dark-red: #7C210C;
+$light-red: #FB8165;
+
+$bg-color: $gray-lightest;
+
+$text-color: #4d525a;
+$text-color-light: lighten($text-color, 30%);
+
+$line-color: $gray-light;
+$line-color-light: lighten($gray-light, 20%);
+
+$link-color: $blue;
+
+$border-radius-base: 3px;
+$border-radius-lg: 6px;
+
+$main-color: $blue;
+$main-color-dark: $blue;
+
+$highlight-color: $blue;
+$highlight-color-dark: $dark-blue;
+
+$alert-color: #FB4418;
diff --git a/app/assets/sass/font-awesome/_animated.scss b/app/assets/sass/font-awesome/_animated.scss
new file mode 100644
index 0000000..8a020db
--- /dev/null
+++ b/app/assets/sass/font-awesome/_animated.scss
@@ -0,0 +1,34 @@
+// Spinning Icons
+// --------------------------
+
+.#{$fa-css-prefix}-spin {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear;
+}
+
+.#{$fa-css-prefix}-pulse {
+ -webkit-animation: fa-spin 1s infinite steps(8);
+ animation: fa-spin 1s infinite steps(8);
+}
+
+@-webkit-keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
diff --git a/app/assets/sass/font-awesome/_bordered-pulled.scss b/app/assets/sass/font-awesome/_bordered-pulled.scss
new file mode 100644
index 0000000..d4b85a0
--- /dev/null
+++ b/app/assets/sass/font-awesome/_bordered-pulled.scss
@@ -0,0 +1,25 @@
+// Bordered & Pulled
+// -------------------------
+
+.#{$fa-css-prefix}-border {
+ padding: .2em .25em .15em;
+ border: solid .08em $fa-border-color;
+ border-radius: .1em;
+}
+
+.#{$fa-css-prefix}-pull-left { float: left; }
+.#{$fa-css-prefix}-pull-right { float: right; }
+
+.#{$fa-css-prefix} {
+ &.#{$fa-css-prefix}-pull-left { margin-right: .3em; }
+ &.#{$fa-css-prefix}-pull-right { margin-left: .3em; }
+}
+
+/* Deprecated as of 4.4.0 */
+.pull-right { float: right; }
+.pull-left { float: left; }
+
+.#{$fa-css-prefix} {
+ &.pull-left { margin-right: .3em; }
+ &.pull-right { margin-left: .3em; }
+}
diff --git a/app/assets/sass/font-awesome/_core.scss b/app/assets/sass/font-awesome/_core.scss
new file mode 100644
index 0000000..7425ef8
--- /dev/null
+++ b/app/assets/sass/font-awesome/_core.scss
@@ -0,0 +1,12 @@
+// Base Class Definition
+// -------------------------
+
+.#{$fa-css-prefix} {
+ display: inline-block;
+ font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
+ font-size: inherit; // can't have font-size inherit on line above, so need to override
+ text-rendering: auto; // optimizelegibility throws things off #1094
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+}
diff --git a/app/assets/sass/font-awesome/_fixed-width.scss b/app/assets/sass/font-awesome/_fixed-width.scss
new file mode 100644
index 0000000..b221c98
--- /dev/null
+++ b/app/assets/sass/font-awesome/_fixed-width.scss
@@ -0,0 +1,6 @@
+// Fixed Width Icons
+// -------------------------
+.#{$fa-css-prefix}-fw {
+ width: (18em / 14);
+ text-align: center;
+}
diff --git a/app/assets/sass/font-awesome/_icons.scss b/app/assets/sass/font-awesome/_icons.scss
new file mode 100644
index 0000000..6f93759
--- /dev/null
+++ b/app/assets/sass/font-awesome/_icons.scss
@@ -0,0 +1,697 @@
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+ readers do not read off random characters that represent icons */
+
+.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; }
+.#{$fa-css-prefix}-music:before { content: $fa-var-music; }
+.#{$fa-css-prefix}-search:before { content: $fa-var-search; }
+.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; }
+.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; }
+.#{$fa-css-prefix}-star:before { content: $fa-var-star; }
+.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; }
+.#{$fa-css-prefix}-user:before { content: $fa-var-user; }
+.#{$fa-css-prefix}-film:before { content: $fa-var-film; }
+.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; }
+.#{$fa-css-prefix}-th:before { content: $fa-var-th; }
+.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; }
+.#{$fa-css-prefix}-check:before { content: $fa-var-check; }
+.#{$fa-css-prefix}-remove:before,
+.#{$fa-css-prefix}-close:before,
+.#{$fa-css-prefix}-times:before { content: $fa-var-times; }
+.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; }
+.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; }
+.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; }
+.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; }
+.#{$fa-css-prefix}-gear:before,
+.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; }
+.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; }
+.#{$fa-css-prefix}-home:before { content: $fa-var-home; }
+.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; }
+.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; }
+.#{$fa-css-prefix}-road:before { content: $fa-var-road; }
+.#{$fa-css-prefix}-download:before { content: $fa-var-download; }
+.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; }
+.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; }
+.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; }
+.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; }
+.#{$fa-css-prefix}-rotate-right:before,
+.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; }
+.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; }
+.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; }
+.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; }
+.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; }
+.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; }
+.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; }
+.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; }
+.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; }
+.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; }
+.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; }
+.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; }
+.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; }
+.#{$fa-css-prefix}-book:before { content: $fa-var-book; }
+.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; }
+.#{$fa-css-prefix}-print:before { content: $fa-var-print; }
+.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; }
+.#{$fa-css-prefix}-font:before { content: $fa-var-font; }
+.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; }
+.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; }
+.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; }
+.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; }
+.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; }
+.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; }
+.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; }
+.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; }
+.#{$fa-css-prefix}-list:before { content: $fa-var-list; }
+.#{$fa-css-prefix}-dedent:before,
+.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; }
+.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; }
+.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; }
+.#{$fa-css-prefix}-photo:before,
+.#{$fa-css-prefix}-image:before,
+.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; }
+.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; }
+.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; }
+.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; }
+.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; }
+.#{$fa-css-prefix}-edit:before,
+.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; }
+.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; }
+.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; }
+.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; }
+.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; }
+.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; }
+.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; }
+.#{$fa-css-prefix}-play:before { content: $fa-var-play; }
+.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; }
+.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; }
+.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; }
+.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; }
+.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; }
+.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; }
+.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; }
+.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; }
+.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; }
+.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; }
+.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; }
+.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; }
+.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; }
+.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; }
+.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; }
+.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; }
+.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; }
+.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; }
+.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; }
+.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; }
+.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; }
+.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; }
+.#{$fa-css-prefix}-mail-forward:before,
+.#{$fa-css-prefix}-share:before { content: $fa-var-share; }
+.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; }
+.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; }
+.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; }
+.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; }
+.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; }
+.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; }
+.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; }
+.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; }
+.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; }
+.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; }
+.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; }
+.#{$fa-css-prefix}-warning:before,
+.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; }
+.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; }
+.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; }
+.#{$fa-css-prefix}-random:before { content: $fa-var-random; }
+.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; }
+.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; }
+.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; }
+.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; }
+.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; }
+.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; }
+.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; }
+.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; }
+.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; }
+.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; }
+.#{$fa-css-prefix}-bar-chart-o:before,
+.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; }
+.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; }
+.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; }
+.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; }
+.#{$fa-css-prefix}-key:before { content: $fa-var-key; }
+.#{$fa-css-prefix}-gears:before,
+.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; }
+.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; }
+.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; }
+.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; }
+.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; }
+.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; }
+.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; }
+.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; }
+.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; }
+.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; }
+.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; }
+.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; }
+.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; }
+.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; }
+.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; }
+.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; }
+.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; }
+.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; }
+.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; }
+.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; }
+.#{$fa-css-prefix}-facebook-f:before,
+.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; }
+.#{$fa-css-prefix}-github:before { content: $fa-var-github; }
+.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; }
+.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; }
+.#{$fa-css-prefix}-feed:before,
+.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; }
+.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; }
+.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; }
+.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; }
+.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; }
+.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; }
+.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; }
+.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; }
+.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; }
+.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; }
+.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; }
+.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; }
+.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; }
+.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; }
+.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; }
+.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; }
+.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; }
+.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; }
+.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; }
+.#{$fa-css-prefix}-group:before,
+.#{$fa-css-prefix}-users:before { content: $fa-var-users; }
+.#{$fa-css-prefix}-chain:before,
+.#{$fa-css-prefix}-link:before { content: $fa-var-link; }
+.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; }
+.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; }
+.#{$fa-css-prefix}-cut:before,
+.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; }
+.#{$fa-css-prefix}-copy:before,
+.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; }
+.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; }
+.#{$fa-css-prefix}-save:before,
+.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; }
+.#{$fa-css-prefix}-square:before { content: $fa-var-square; }
+.#{$fa-css-prefix}-navicon:before,
+.#{$fa-css-prefix}-reorder:before,
+.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; }
+.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; }
+.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; }
+.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; }
+.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; }
+.#{$fa-css-prefix}-table:before { content: $fa-var-table; }
+.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; }
+.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; }
+.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; }
+.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; }
+.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; }
+.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; }
+.#{$fa-css-prefix}-money:before { content: $fa-var-money; }
+.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; }
+.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; }
+.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; }
+.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; }
+.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; }
+.#{$fa-css-prefix}-unsorted:before,
+.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; }
+.#{$fa-css-prefix}-sort-down:before,
+.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; }
+.#{$fa-css-prefix}-sort-up:before,
+.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; }
+.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; }
+.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; }
+.#{$fa-css-prefix}-rotate-left:before,
+.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; }
+.#{$fa-css-prefix}-legal:before,
+.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; }
+.#{$fa-css-prefix}-dashboard:before,
+.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; }
+.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; }
+.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; }
+.#{$fa-css-prefix}-flash:before,
+.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; }
+.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; }
+.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; }
+.#{$fa-css-prefix}-paste:before,
+.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; }
+.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; }
+.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; }
+.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; }
+.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; }
+.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; }
+.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; }
+.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; }
+.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; }
+.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; }
+.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; }
+.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; }
+.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; }
+.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; }
+.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; }
+.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; }
+.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; }
+.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; }
+.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; }
+.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; }
+.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; }
+.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; }
+.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; }
+.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; }
+.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; }
+.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; }
+.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; }
+.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; }
+.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; }
+.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; }
+.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; }
+.#{$fa-css-prefix}-mobile-phone:before,
+.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; }
+.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; }
+.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; }
+.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; }
+.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; }
+.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; }
+.#{$fa-css-prefix}-mail-reply:before,
+.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; }
+.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; }
+.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; }
+.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; }
+.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; }
+.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; }
+.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; }
+.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; }
+.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; }
+.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; }
+.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; }
+.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; }
+.#{$fa-css-prefix}-code:before { content: $fa-var-code; }
+.#{$fa-css-prefix}-mail-reply-all:before,
+.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; }
+.#{$fa-css-prefix}-star-half-empty:before,
+.#{$fa-css-prefix}-star-half-full:before,
+.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; }
+.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; }
+.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; }
+.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; }
+.#{$fa-css-prefix}-unlink:before,
+.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; }
+.#{$fa-css-prefix}-question:before { content: $fa-var-question; }
+.#{$fa-css-prefix}-info:before { content: $fa-var-info; }
+.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; }
+.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; }
+.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; }
+.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; }
+.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; }
+.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; }
+.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; }
+.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; }
+.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; }
+.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; }
+.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; }
+.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; }
+.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; }
+.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; }
+.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; }
+.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; }
+.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; }
+.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; }
+.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; }
+.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; }
+.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; }
+.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; }
+.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; }
+.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; }
+.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; }
+.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; }
+.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; }
+.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; }
+.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; }
+.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; }
+.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; }
+.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; }
+.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; }
+.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; }
+.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; }
+.#{$fa-css-prefix}-toggle-down:before,
+.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; }
+.#{$fa-css-prefix}-toggle-up:before,
+.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; }
+.#{$fa-css-prefix}-toggle-right:before,
+.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; }
+.#{$fa-css-prefix}-euro:before,
+.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; }
+.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; }
+.#{$fa-css-prefix}-dollar:before,
+.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; }
+.#{$fa-css-prefix}-rupee:before,
+.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; }
+.#{$fa-css-prefix}-cny:before,
+.#{$fa-css-prefix}-rmb:before,
+.#{$fa-css-prefix}-yen:before,
+.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; }
+.#{$fa-css-prefix}-ruble:before,
+.#{$fa-css-prefix}-rouble:before,
+.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; }
+.#{$fa-css-prefix}-won:before,
+.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; }
+.#{$fa-css-prefix}-bitcoin:before,
+.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; }
+.#{$fa-css-prefix}-file:before { content: $fa-var-file; }
+.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; }
+.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; }
+.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; }
+.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; }
+.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; }
+.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; }
+.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; }
+.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; }
+.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; }
+.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; }
+.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; }
+.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; }
+.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; }
+.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; }
+.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; }
+.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; }
+.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; }
+.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; }
+.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; }
+.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; }
+.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; }
+.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; }
+.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; }
+.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; }
+.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; }
+.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; }
+.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; }
+.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; }
+.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; }
+.#{$fa-css-prefix}-android:before { content: $fa-var-android; }
+.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; }
+.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; }
+.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; }
+.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; }
+.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; }
+.#{$fa-css-prefix}-female:before { content: $fa-var-female; }
+.#{$fa-css-prefix}-male:before { content: $fa-var-male; }
+.#{$fa-css-prefix}-gittip:before,
+.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; }
+.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; }
+.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; }
+.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; }
+.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; }
+.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; }
+.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; }
+.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; }
+.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; }
+.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; }
+.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; }
+.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; }
+.#{$fa-css-prefix}-toggle-left:before,
+.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; }
+.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; }
+.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; }
+.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; }
+.#{$fa-css-prefix}-turkish-lira:before,
+.#{$fa-css-prefix}-try:before { content: $fa-var-try; }
+.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; }
+.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; }
+.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; }
+.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; }
+.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; }
+.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; }
+.#{$fa-css-prefix}-institution:before,
+.#{$fa-css-prefix}-bank:before,
+.#{$fa-css-prefix}-university:before { content: $fa-var-university; }
+.#{$fa-css-prefix}-mortar-board:before,
+.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; }
+.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; }
+.#{$fa-css-prefix}-google:before { content: $fa-var-google; }
+.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; }
+.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; }
+.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; }
+.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; }
+.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; }
+.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; }
+.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
+.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; }
+.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; }
+.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; }
+.#{$fa-css-prefix}-language:before { content: $fa-var-language; }
+.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; }
+.#{$fa-css-prefix}-building:before { content: $fa-var-building; }
+.#{$fa-css-prefix}-child:before { content: $fa-var-child; }
+.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; }
+.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; }
+.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; }
+.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; }
+.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; }
+.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; }
+.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; }
+.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; }
+.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; }
+.#{$fa-css-prefix}-automobile:before,
+.#{$fa-css-prefix}-car:before { content: $fa-var-car; }
+.#{$fa-css-prefix}-cab:before,
+.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; }
+.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; }
+.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; }
+.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; }
+.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; }
+.#{$fa-css-prefix}-database:before { content: $fa-var-database; }
+.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; }
+.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; }
+.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; }
+.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; }
+.#{$fa-css-prefix}-file-photo-o:before,
+.#{$fa-css-prefix}-file-picture-o:before,
+.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; }
+.#{$fa-css-prefix}-file-zip-o:before,
+.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; }
+.#{$fa-css-prefix}-file-sound-o:before,
+.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; }
+.#{$fa-css-prefix}-file-movie-o:before,
+.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; }
+.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; }
+.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; }
+.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; }
+.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; }
+.#{$fa-css-prefix}-life-bouy:before,
+.#{$fa-css-prefix}-life-buoy:before,
+.#{$fa-css-prefix}-life-saver:before,
+.#{$fa-css-prefix}-support:before,
+.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; }
+.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; }
+.#{$fa-css-prefix}-ra:before,
+.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; }
+.#{$fa-css-prefix}-ge:before,
+.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; }
+.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; }
+.#{$fa-css-prefix}-git:before { content: $fa-var-git; }
+.#{$fa-css-prefix}-y-combinator-square:before,
+.#{$fa-css-prefix}-yc-square:before,
+.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; }
+.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; }
+.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; }
+.#{$fa-css-prefix}-wechat:before,
+.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; }
+.#{$fa-css-prefix}-send:before,
+.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; }
+.#{$fa-css-prefix}-send-o:before,
+.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; }
+.#{$fa-css-prefix}-history:before { content: $fa-var-history; }
+.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; }
+.#{$fa-css-prefix}-header:before { content: $fa-var-header; }
+.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; }
+.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; }
+.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; }
+.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; }
+.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; }
+.#{$fa-css-prefix}-soccer-ball-o:before,
+.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; }
+.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; }
+.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; }
+.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; }
+.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; }
+.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; }
+.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; }
+.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; }
+.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; }
+.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; }
+.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; }
+.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; }
+.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; }
+.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; }
+.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; }
+.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; }
+.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; }
+.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; }
+.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; }
+.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; }
+.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; }
+.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; }
+.#{$fa-css-prefix}-at:before { content: $fa-var-at; }
+.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; }
+.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; }
+.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; }
+.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; }
+.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; }
+.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; }
+.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; }
+.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; }
+.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; }
+.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; }
+.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; }
+.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; }
+.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; }
+.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; }
+.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; }
+.#{$fa-css-prefix}-shekel:before,
+.#{$fa-css-prefix}-sheqel:before,
+.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; }
+.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; }
+.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; }
+.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; }
+.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; }
+.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; }
+.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; }
+.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; }
+.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; }
+.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; }
+.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; }
+.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; }
+.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; }
+.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; }
+.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; }
+.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; }
+.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; }
+.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; }
+.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; }
+.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; }
+.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; }
+.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; }
+.#{$fa-css-prefix}-intersex:before,
+.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; }
+.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; }
+.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; }
+.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; }
+.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; }
+.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; }
+.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; }
+.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; }
+.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; }
+.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; }
+.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; }
+.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; }
+.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; }
+.#{$fa-css-prefix}-server:before { content: $fa-var-server; }
+.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; }
+.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; }
+.#{$fa-css-prefix}-hotel:before,
+.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; }
+.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; }
+.#{$fa-css-prefix}-train:before { content: $fa-var-train; }
+.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; }
+.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; }
+.#{$fa-css-prefix}-yc:before,
+.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; }
+.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; }
+.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; }
+.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; }
+.#{$fa-css-prefix}-battery-4:before,
+.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; }
+.#{$fa-css-prefix}-battery-3:before,
+.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; }
+.#{$fa-css-prefix}-battery-2:before,
+.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; }
+.#{$fa-css-prefix}-battery-1:before,
+.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; }
+.#{$fa-css-prefix}-battery-0:before,
+.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; }
+.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; }
+.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; }
+.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; }
+.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; }
+.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; }
+.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; }
+.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; }
+.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; }
+.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; }
+.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; }
+.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; }
+.#{$fa-css-prefix}-hourglass-1:before,
+.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; }
+.#{$fa-css-prefix}-hourglass-2:before,
+.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; }
+.#{$fa-css-prefix}-hourglass-3:before,
+.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; }
+.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; }
+.#{$fa-css-prefix}-hand-grab-o:before,
+.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; }
+.#{$fa-css-prefix}-hand-stop-o:before,
+.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; }
+.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; }
+.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; }
+.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; }
+.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; }
+.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; }
+.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; }
+.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; }
+.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; }
+.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; }
+.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; }
+.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; }
+.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; }
+.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; }
+.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; }
+.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; }
+.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; }
+.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; }
+.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; }
+.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; }
+.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; }
+.#{$fa-css-prefix}-tv:before,
+.#{$fa-css-prefix}-television:before { content: $fa-var-television; }
+.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; }
+.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; }
+.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; }
+.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; }
+.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; }
+.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; }
+.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; }
+.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; }
+.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; }
+.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; }
+.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; }
+.#{$fa-css-prefix}-map:before { content: $fa-var-map; }
+.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; }
+.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; }
+.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; }
+.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; }
+.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; }
+.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; }
+.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; }
+.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; }
+.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; }
+.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; }
+.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; }
+.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; }
+.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; }
+.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; }
+.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; }
+.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; }
+.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; }
+.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; }
+.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; }
+.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; }
+.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; }
+.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; }
+.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; }
+.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; }
+.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; }
+.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; }
diff --git a/app/assets/sass/font-awesome/_larger.scss b/app/assets/sass/font-awesome/_larger.scss
new file mode 100644
index 0000000..41e9a81
--- /dev/null
+++ b/app/assets/sass/font-awesome/_larger.scss
@@ -0,0 +1,13 @@
+// Icon Sizes
+// -------------------------
+
+/* makes the font 33% larger relative to the icon container */
+.#{$fa-css-prefix}-lg {
+ font-size: (4em / 3);
+ line-height: (3em / 4);
+ vertical-align: -15%;
+}
+.#{$fa-css-prefix}-2x { font-size: 2em; }
+.#{$fa-css-prefix}-3x { font-size: 3em; }
+.#{$fa-css-prefix}-4x { font-size: 4em; }
+.#{$fa-css-prefix}-5x { font-size: 5em; }
diff --git a/app/assets/sass/font-awesome/_list.scss b/app/assets/sass/font-awesome/_list.scss
new file mode 100644
index 0000000..7d1e4d5
--- /dev/null
+++ b/app/assets/sass/font-awesome/_list.scss
@@ -0,0 +1,19 @@
+// List Icons
+// -------------------------
+
+.#{$fa-css-prefix}-ul {
+ padding-left: 0;
+ margin-left: $fa-li-width;
+ list-style-type: none;
+ > li { position: relative; }
+}
+.#{$fa-css-prefix}-li {
+ position: absolute;
+ left: -$fa-li-width;
+ width: $fa-li-width;
+ top: (2em / 14);
+ text-align: center;
+ &.#{$fa-css-prefix}-lg {
+ left: -$fa-li-width + (4em / 14);
+ }
+}
diff --git a/app/assets/sass/font-awesome/_mixins.scss b/app/assets/sass/font-awesome/_mixins.scss
new file mode 100644
index 0000000..f96719b
--- /dev/null
+++ b/app/assets/sass/font-awesome/_mixins.scss
@@ -0,0 +1,26 @@
+// Mixins
+// --------------------------
+
+@mixin fa-icon() {
+ display: inline-block;
+ font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
+ font-size: inherit; // can't have font-size inherit on line above, so need to override
+ text-rendering: auto; // optimizelegibility throws things off #1094
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+}
+
+@mixin fa-icon-rotate($degrees, $rotation) {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
+ -webkit-transform: rotate($degrees);
+ -ms-transform: rotate($degrees);
+ transform: rotate($degrees);
+}
+
+@mixin fa-icon-flip($horiz, $vert, $rotation) {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
+ -webkit-transform: scale($horiz, $vert);
+ -ms-transform: scale($horiz, $vert);
+ transform: scale($horiz, $vert);
+}
diff --git a/app/assets/sass/font-awesome/_path.scss b/app/assets/sass/font-awesome/_path.scss
new file mode 100644
index 0000000..bb457c2
--- /dev/null
+++ b/app/assets/sass/font-awesome/_path.scss
@@ -0,0 +1,15 @@
+/* FONT PATH
+ * -------------------------- */
+
+@font-face {
+ font-family: 'FontAwesome';
+ src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}');
+ src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
+ url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
+ url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
+ url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
+ url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg');
+// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/app/assets/sass/font-awesome/_rotated-flipped.scss b/app/assets/sass/font-awesome/_rotated-flipped.scss
new file mode 100644
index 0000000..a3558fd
--- /dev/null
+++ b/app/assets/sass/font-awesome/_rotated-flipped.scss
@@ -0,0 +1,20 @@
+// Rotated & Flipped Icons
+// -------------------------
+
+.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); }
+.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); }
+.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); }
+
+.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); }
+.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); }
+
+// Hook for IE8-9
+// -------------------------
+
+:root .#{$fa-css-prefix}-rotate-90,
+:root .#{$fa-css-prefix}-rotate-180,
+:root .#{$fa-css-prefix}-rotate-270,
+:root .#{$fa-css-prefix}-flip-horizontal,
+:root .#{$fa-css-prefix}-flip-vertical {
+ filter: none;
+}
diff --git a/app/assets/sass/font-awesome/_stacked.scss b/app/assets/sass/font-awesome/_stacked.scss
new file mode 100644
index 0000000..aef7403
--- /dev/null
+++ b/app/assets/sass/font-awesome/_stacked.scss
@@ -0,0 +1,20 @@
+// Stacked Icons
+// -------------------------
+
+.#{$fa-css-prefix}-stack {
+ position: relative;
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ line-height: 2em;
+ vertical-align: middle;
+}
+.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+.#{$fa-css-prefix}-stack-1x { line-height: inherit; }
+.#{$fa-css-prefix}-stack-2x { font-size: 2em; }
+.#{$fa-css-prefix}-inverse { color: $fa-inverse; }
diff --git a/app/assets/sass/font-awesome/_styles.scss b/app/assets/sass/font-awesome/_styles.scss
new file mode 100644
index 0000000..f4668a5
--- /dev/null
+++ b/app/assets/sass/font-awesome/_styles.scss
@@ -0,0 +1,17 @@
+/*!
+ * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+
+@import "variables";
+@import "mixins";
+@import "path";
+@import "core";
+@import "larger";
+@import "fixed-width";
+@import "list";
+@import "bordered-pulled";
+@import "animated";
+@import "rotated-flipped";
+@import "stacked";
+@import "icons";
diff --git a/app/assets/sass/font-awesome/_variables.scss b/app/assets/sass/font-awesome/_variables.scss
new file mode 100644
index 0000000..0a47110
--- /dev/null
+++ b/app/assets/sass/font-awesome/_variables.scss
@@ -0,0 +1,708 @@
+// Variables
+// --------------------------
+
+$fa-font-path: "../fonts" !default;
+$fa-font-size-base: 14px !default;
+$fa-line-height-base: 1 !default;
+//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.5.0/fonts" !default; // for referencing Bootstrap CDN font files directly
+$fa-css-prefix: fa !default;
+$fa-version: "4.5.0" !default;
+$fa-border-color: #eee !default;
+$fa-inverse: #fff !default;
+$fa-li-width: (30em / 14) !default;
+
+$fa-var-500px: "\f26e";
+$fa-var-adjust: "\f042";
+$fa-var-adn: "\f170";
+$fa-var-align-center: "\f037";
+$fa-var-align-justify: "\f039";
+$fa-var-align-left: "\f036";
+$fa-var-align-right: "\f038";
+$fa-var-amazon: "\f270";
+$fa-var-ambulance: "\f0f9";
+$fa-var-anchor: "\f13d";
+$fa-var-android: "\f17b";
+$fa-var-angellist: "\f209";
+$fa-var-angle-double-down: "\f103";
+$fa-var-angle-double-left: "\f100";
+$fa-var-angle-double-right: "\f101";
+$fa-var-angle-double-up: "\f102";
+$fa-var-angle-down: "\f107";
+$fa-var-angle-left: "\f104";
+$fa-var-angle-right: "\f105";
+$fa-var-angle-up: "\f106";
+$fa-var-apple: "\f179";
+$fa-var-archive: "\f187";
+$fa-var-area-chart: "\f1fe";
+$fa-var-arrow-circle-down: "\f0ab";
+$fa-var-arrow-circle-left: "\f0a8";
+$fa-var-arrow-circle-o-down: "\f01a";
+$fa-var-arrow-circle-o-left: "\f190";
+$fa-var-arrow-circle-o-right: "\f18e";
+$fa-var-arrow-circle-o-up: "\f01b";
+$fa-var-arrow-circle-right: "\f0a9";
+$fa-var-arrow-circle-up: "\f0aa";
+$fa-var-arrow-down: "\f063";
+$fa-var-arrow-left: "\f060";
+$fa-var-arrow-right: "\f061";
+$fa-var-arrow-up: "\f062";
+$fa-var-arrows: "\f047";
+$fa-var-arrows-alt: "\f0b2";
+$fa-var-arrows-h: "\f07e";
+$fa-var-arrows-v: "\f07d";
+$fa-var-asterisk: "\f069";
+$fa-var-at: "\f1fa";
+$fa-var-automobile: "\f1b9";
+$fa-var-backward: "\f04a";
+$fa-var-balance-scale: "\f24e";
+$fa-var-ban: "\f05e";
+$fa-var-bank: "\f19c";
+$fa-var-bar-chart: "\f080";
+$fa-var-bar-chart-o: "\f080";
+$fa-var-barcode: "\f02a";
+$fa-var-bars: "\f0c9";
+$fa-var-battery-0: "\f244";
+$fa-var-battery-1: "\f243";
+$fa-var-battery-2: "\f242";
+$fa-var-battery-3: "\f241";
+$fa-var-battery-4: "\f240";
+$fa-var-battery-empty: "\f244";
+$fa-var-battery-full: "\f240";
+$fa-var-battery-half: "\f242";
+$fa-var-battery-quarter: "\f243";
+$fa-var-battery-three-quarters: "\f241";
+$fa-var-bed: "\f236";
+$fa-var-beer: "\f0fc";
+$fa-var-behance: "\f1b4";
+$fa-var-behance-square: "\f1b5";
+$fa-var-bell: "\f0f3";
+$fa-var-bell-o: "\f0a2";
+$fa-var-bell-slash: "\f1f6";
+$fa-var-bell-slash-o: "\f1f7";
+$fa-var-bicycle: "\f206";
+$fa-var-binoculars: "\f1e5";
+$fa-var-birthday-cake: "\f1fd";
+$fa-var-bitbucket: "\f171";
+$fa-var-bitbucket-square: "\f172";
+$fa-var-bitcoin: "\f15a";
+$fa-var-black-tie: "\f27e";
+$fa-var-bluetooth: "\f293";
+$fa-var-bluetooth-b: "\f294";
+$fa-var-bold: "\f032";
+$fa-var-bolt: "\f0e7";
+$fa-var-bomb: "\f1e2";
+$fa-var-book: "\f02d";
+$fa-var-bookmark: "\f02e";
+$fa-var-bookmark-o: "\f097";
+$fa-var-briefcase: "\f0b1";
+$fa-var-btc: "\f15a";
+$fa-var-bug: "\f188";
+$fa-var-building: "\f1ad";
+$fa-var-building-o: "\f0f7";
+$fa-var-bullhorn: "\f0a1";
+$fa-var-bullseye: "\f140";
+$fa-var-bus: "\f207";
+$fa-var-buysellads: "\f20d";
+$fa-var-cab: "\f1ba";
+$fa-var-calculator: "\f1ec";
+$fa-var-calendar: "\f073";
+$fa-var-calendar-check-o: "\f274";
+$fa-var-calendar-minus-o: "\f272";
+$fa-var-calendar-o: "\f133";
+$fa-var-calendar-plus-o: "\f271";
+$fa-var-calendar-times-o: "\f273";
+$fa-var-camera: "\f030";
+$fa-var-camera-retro: "\f083";
+$fa-var-car: "\f1b9";
+$fa-var-caret-down: "\f0d7";
+$fa-var-caret-left: "\f0d9";
+$fa-var-caret-right: "\f0da";
+$fa-var-caret-square-o-down: "\f150";
+$fa-var-caret-square-o-left: "\f191";
+$fa-var-caret-square-o-right: "\f152";
+$fa-var-caret-square-o-up: "\f151";
+$fa-var-caret-up: "\f0d8";
+$fa-var-cart-arrow-down: "\f218";
+$fa-var-cart-plus: "\f217";
+$fa-var-cc: "\f20a";
+$fa-var-cc-amex: "\f1f3";
+$fa-var-cc-diners-club: "\f24c";
+$fa-var-cc-discover: "\f1f2";
+$fa-var-cc-jcb: "\f24b";
+$fa-var-cc-mastercard: "\f1f1";
+$fa-var-cc-paypal: "\f1f4";
+$fa-var-cc-stripe: "\f1f5";
+$fa-var-cc-visa: "\f1f0";
+$fa-var-certificate: "\f0a3";
+$fa-var-chain: "\f0c1";
+$fa-var-chain-broken: "\f127";
+$fa-var-check: "\f00c";
+$fa-var-check-circle: "\f058";
+$fa-var-check-circle-o: "\f05d";
+$fa-var-check-square: "\f14a";
+$fa-var-check-square-o: "\f046";
+$fa-var-chevron-circle-down: "\f13a";
+$fa-var-chevron-circle-left: "\f137";
+$fa-var-chevron-circle-right: "\f138";
+$fa-var-chevron-circle-up: "\f139";
+$fa-var-chevron-down: "\f078";
+$fa-var-chevron-left: "\f053";
+$fa-var-chevron-right: "\f054";
+$fa-var-chevron-up: "\f077";
+$fa-var-child: "\f1ae";
+$fa-var-chrome: "\f268";
+$fa-var-circle: "\f111";
+$fa-var-circle-o: "\f10c";
+$fa-var-circle-o-notch: "\f1ce";
+$fa-var-circle-thin: "\f1db";
+$fa-var-clipboard: "\f0ea";
+$fa-var-clock-o: "\f017";
+$fa-var-clone: "\f24d";
+$fa-var-close: "\f00d";
+$fa-var-cloud: "\f0c2";
+$fa-var-cloud-download: "\f0ed";
+$fa-var-cloud-upload: "\f0ee";
+$fa-var-cny: "\f157";
+$fa-var-code: "\f121";
+$fa-var-code-fork: "\f126";
+$fa-var-codepen: "\f1cb";
+$fa-var-codiepie: "\f284";
+$fa-var-coffee: "\f0f4";
+$fa-var-cog: "\f013";
+$fa-var-cogs: "\f085";
+$fa-var-columns: "\f0db";
+$fa-var-comment: "\f075";
+$fa-var-comment-o: "\f0e5";
+$fa-var-commenting: "\f27a";
+$fa-var-commenting-o: "\f27b";
+$fa-var-comments: "\f086";
+$fa-var-comments-o: "\f0e6";
+$fa-var-compass: "\f14e";
+$fa-var-compress: "\f066";
+$fa-var-connectdevelop: "\f20e";
+$fa-var-contao: "\f26d";
+$fa-var-copy: "\f0c5";
+$fa-var-copyright: "\f1f9";
+$fa-var-creative-commons: "\f25e";
+$fa-var-credit-card: "\f09d";
+$fa-var-credit-card-alt: "\f283";
+$fa-var-crop: "\f125";
+$fa-var-crosshairs: "\f05b";
+$fa-var-css3: "\f13c";
+$fa-var-cube: "\f1b2";
+$fa-var-cubes: "\f1b3";
+$fa-var-cut: "\f0c4";
+$fa-var-cutlery: "\f0f5";
+$fa-var-dashboard: "\f0e4";
+$fa-var-dashcube: "\f210";
+$fa-var-database: "\f1c0";
+$fa-var-dedent: "\f03b";
+$fa-var-delicious: "\f1a5";
+$fa-var-desktop: "\f108";
+$fa-var-deviantart: "\f1bd";
+$fa-var-diamond: "\f219";
+$fa-var-digg: "\f1a6";
+$fa-var-dollar: "\f155";
+$fa-var-dot-circle-o: "\f192";
+$fa-var-download: "\f019";
+$fa-var-dribbble: "\f17d";
+$fa-var-dropbox: "\f16b";
+$fa-var-drupal: "\f1a9";
+$fa-var-edge: "\f282";
+$fa-var-edit: "\f044";
+$fa-var-eject: "\f052";
+$fa-var-ellipsis-h: "\f141";
+$fa-var-ellipsis-v: "\f142";
+$fa-var-empire: "\f1d1";
+$fa-var-envelope: "\f0e0";
+$fa-var-envelope-o: "\f003";
+$fa-var-envelope-square: "\f199";
+$fa-var-eraser: "\f12d";
+$fa-var-eur: "\f153";
+$fa-var-euro: "\f153";
+$fa-var-exchange: "\f0ec";
+$fa-var-exclamation: "\f12a";
+$fa-var-exclamation-circle: "\f06a";
+$fa-var-exclamation-triangle: "\f071";
+$fa-var-expand: "\f065";
+$fa-var-expeditedssl: "\f23e";
+$fa-var-external-link: "\f08e";
+$fa-var-external-link-square: "\f14c";
+$fa-var-eye: "\f06e";
+$fa-var-eye-slash: "\f070";
+$fa-var-eyedropper: "\f1fb";
+$fa-var-facebook: "\f09a";
+$fa-var-facebook-f: "\f09a";
+$fa-var-facebook-official: "\f230";
+$fa-var-facebook-square: "\f082";
+$fa-var-fast-backward: "\f049";
+$fa-var-fast-forward: "\f050";
+$fa-var-fax: "\f1ac";
+$fa-var-feed: "\f09e";
+$fa-var-female: "\f182";
+$fa-var-fighter-jet: "\f0fb";
+$fa-var-file: "\f15b";
+$fa-var-file-archive-o: "\f1c6";
+$fa-var-file-audio-o: "\f1c7";
+$fa-var-file-code-o: "\f1c9";
+$fa-var-file-excel-o: "\f1c3";
+$fa-var-file-image-o: "\f1c5";
+$fa-var-file-movie-o: "\f1c8";
+$fa-var-file-o: "\f016";
+$fa-var-file-pdf-o: "\f1c1";
+$fa-var-file-photo-o: "\f1c5";
+$fa-var-file-picture-o: "\f1c5";
+$fa-var-file-powerpoint-o: "\f1c4";
+$fa-var-file-sound-o: "\f1c7";
+$fa-var-file-text: "\f15c";
+$fa-var-file-text-o: "\f0f6";
+$fa-var-file-video-o: "\f1c8";
+$fa-var-file-word-o: "\f1c2";
+$fa-var-file-zip-o: "\f1c6";
+$fa-var-files-o: "\f0c5";
+$fa-var-film: "\f008";
+$fa-var-filter: "\f0b0";
+$fa-var-fire: "\f06d";
+$fa-var-fire-extinguisher: "\f134";
+$fa-var-firefox: "\f269";
+$fa-var-flag: "\f024";
+$fa-var-flag-checkered: "\f11e";
+$fa-var-flag-o: "\f11d";
+$fa-var-flash: "\f0e7";
+$fa-var-flask: "\f0c3";
+$fa-var-flickr: "\f16e";
+$fa-var-floppy-o: "\f0c7";
+$fa-var-folder: "\f07b";
+$fa-var-folder-o: "\f114";
+$fa-var-folder-open: "\f07c";
+$fa-var-folder-open-o: "\f115";
+$fa-var-font: "\f031";
+$fa-var-fonticons: "\f280";
+$fa-var-fort-awesome: "\f286";
+$fa-var-forumbee: "\f211";
+$fa-var-forward: "\f04e";
+$fa-var-foursquare: "\f180";
+$fa-var-frown-o: "\f119";
+$fa-var-futbol-o: "\f1e3";
+$fa-var-gamepad: "\f11b";
+$fa-var-gavel: "\f0e3";
+$fa-var-gbp: "\f154";
+$fa-var-ge: "\f1d1";
+$fa-var-gear: "\f013";
+$fa-var-gears: "\f085";
+$fa-var-genderless: "\f22d";
+$fa-var-get-pocket: "\f265";
+$fa-var-gg: "\f260";
+$fa-var-gg-circle: "\f261";
+$fa-var-gift: "\f06b";
+$fa-var-git: "\f1d3";
+$fa-var-git-square: "\f1d2";
+$fa-var-github: "\f09b";
+$fa-var-github-alt: "\f113";
+$fa-var-github-square: "\f092";
+$fa-var-gittip: "\f184";
+$fa-var-glass: "\f000";
+$fa-var-globe: "\f0ac";
+$fa-var-google: "\f1a0";
+$fa-var-google-plus: "\f0d5";
+$fa-var-google-plus-square: "\f0d4";
+$fa-var-google-wallet: "\f1ee";
+$fa-var-graduation-cap: "\f19d";
+$fa-var-gratipay: "\f184";
+$fa-var-group: "\f0c0";
+$fa-var-h-square: "\f0fd";
+$fa-var-hacker-news: "\f1d4";
+$fa-var-hand-grab-o: "\f255";
+$fa-var-hand-lizard-o: "\f258";
+$fa-var-hand-o-down: "\f0a7";
+$fa-var-hand-o-left: "\f0a5";
+$fa-var-hand-o-right: "\f0a4";
+$fa-var-hand-o-up: "\f0a6";
+$fa-var-hand-paper-o: "\f256";
+$fa-var-hand-peace-o: "\f25b";
+$fa-var-hand-pointer-o: "\f25a";
+$fa-var-hand-rock-o: "\f255";
+$fa-var-hand-scissors-o: "\f257";
+$fa-var-hand-spock-o: "\f259";
+$fa-var-hand-stop-o: "\f256";
+$fa-var-hashtag: "\f292";
+$fa-var-hdd-o: "\f0a0";
+$fa-var-header: "\f1dc";
+$fa-var-headphones: "\f025";
+$fa-var-heart: "\f004";
+$fa-var-heart-o: "\f08a";
+$fa-var-heartbeat: "\f21e";
+$fa-var-history: "\f1da";
+$fa-var-home: "\f015";
+$fa-var-hospital-o: "\f0f8";
+$fa-var-hotel: "\f236";
+$fa-var-hourglass: "\f254";
+$fa-var-hourglass-1: "\f251";
+$fa-var-hourglass-2: "\f252";
+$fa-var-hourglass-3: "\f253";
+$fa-var-hourglass-end: "\f253";
+$fa-var-hourglass-half: "\f252";
+$fa-var-hourglass-o: "\f250";
+$fa-var-hourglass-start: "\f251";
+$fa-var-houzz: "\f27c";
+$fa-var-html5: "\f13b";
+$fa-var-i-cursor: "\f246";
+$fa-var-ils: "\f20b";
+$fa-var-image: "\f03e";
+$fa-var-inbox: "\f01c";
+$fa-var-indent: "\f03c";
+$fa-var-industry: "\f275";
+$fa-var-info: "\f129";
+$fa-var-info-circle: "\f05a";
+$fa-var-inr: "\f156";
+$fa-var-instagram: "\f16d";
+$fa-var-institution: "\f19c";
+$fa-var-internet-explorer: "\f26b";
+$fa-var-intersex: "\f224";
+$fa-var-ioxhost: "\f208";
+$fa-var-italic: "\f033";
+$fa-var-joomla: "\f1aa";
+$fa-var-jpy: "\f157";
+$fa-var-jsfiddle: "\f1cc";
+$fa-var-key: "\f084";
+$fa-var-keyboard-o: "\f11c";
+$fa-var-krw: "\f159";
+$fa-var-language: "\f1ab";
+$fa-var-laptop: "\f109";
+$fa-var-lastfm: "\f202";
+$fa-var-lastfm-square: "\f203";
+$fa-var-leaf: "\f06c";
+$fa-var-leanpub: "\f212";
+$fa-var-legal: "\f0e3";
+$fa-var-lemon-o: "\f094";
+$fa-var-level-down: "\f149";
+$fa-var-level-up: "\f148";
+$fa-var-life-bouy: "\f1cd";
+$fa-var-life-buoy: "\f1cd";
+$fa-var-life-ring: "\f1cd";
+$fa-var-life-saver: "\f1cd";
+$fa-var-lightbulb-o: "\f0eb";
+$fa-var-line-chart: "\f201";
+$fa-var-link: "\f0c1";
+$fa-var-linkedin: "\f0e1";
+$fa-var-linkedin-square: "\f08c";
+$fa-var-linux: "\f17c";
+$fa-var-list: "\f03a";
+$fa-var-list-alt: "\f022";
+$fa-var-list-ol: "\f0cb";
+$fa-var-list-ul: "\f0ca";
+$fa-var-location-arrow: "\f124";
+$fa-var-lock: "\f023";
+$fa-var-long-arrow-down: "\f175";
+$fa-var-long-arrow-left: "\f177";
+$fa-var-long-arrow-right: "\f178";
+$fa-var-long-arrow-up: "\f176";
+$fa-var-magic: "\f0d0";
+$fa-var-magnet: "\f076";
+$fa-var-mail-forward: "\f064";
+$fa-var-mail-reply: "\f112";
+$fa-var-mail-reply-all: "\f122";
+$fa-var-male: "\f183";
+$fa-var-map: "\f279";
+$fa-var-map-marker: "\f041";
+$fa-var-map-o: "\f278";
+$fa-var-map-pin: "\f276";
+$fa-var-map-signs: "\f277";
+$fa-var-mars: "\f222";
+$fa-var-mars-double: "\f227";
+$fa-var-mars-stroke: "\f229";
+$fa-var-mars-stroke-h: "\f22b";
+$fa-var-mars-stroke-v: "\f22a";
+$fa-var-maxcdn: "\f136";
+$fa-var-meanpath: "\f20c";
+$fa-var-medium: "\f23a";
+$fa-var-medkit: "\f0fa";
+$fa-var-meh-o: "\f11a";
+$fa-var-mercury: "\f223";
+$fa-var-microphone: "\f130";
+$fa-var-microphone-slash: "\f131";
+$fa-var-minus: "\f068";
+$fa-var-minus-circle: "\f056";
+$fa-var-minus-square: "\f146";
+$fa-var-minus-square-o: "\f147";
+$fa-var-mixcloud: "\f289";
+$fa-var-mobile: "\f10b";
+$fa-var-mobile-phone: "\f10b";
+$fa-var-modx: "\f285";
+$fa-var-money: "\f0d6";
+$fa-var-moon-o: "\f186";
+$fa-var-mortar-board: "\f19d";
+$fa-var-motorcycle: "\f21c";
+$fa-var-mouse-pointer: "\f245";
+$fa-var-music: "\f001";
+$fa-var-navicon: "\f0c9";
+$fa-var-neuter: "\f22c";
+$fa-var-newspaper-o: "\f1ea";
+$fa-var-object-group: "\f247";
+$fa-var-object-ungroup: "\f248";
+$fa-var-odnoklassniki: "\f263";
+$fa-var-odnoklassniki-square: "\f264";
+$fa-var-opencart: "\f23d";
+$fa-var-openid: "\f19b";
+$fa-var-opera: "\f26a";
+$fa-var-optin-monster: "\f23c";
+$fa-var-outdent: "\f03b";
+$fa-var-pagelines: "\f18c";
+$fa-var-paint-brush: "\f1fc";
+$fa-var-paper-plane: "\f1d8";
+$fa-var-paper-plane-o: "\f1d9";
+$fa-var-paperclip: "\f0c6";
+$fa-var-paragraph: "\f1dd";
+$fa-var-paste: "\f0ea";
+$fa-var-pause: "\f04c";
+$fa-var-pause-circle: "\f28b";
+$fa-var-pause-circle-o: "\f28c";
+$fa-var-paw: "\f1b0";
+$fa-var-paypal: "\f1ed";
+$fa-var-pencil: "\f040";
+$fa-var-pencil-square: "\f14b";
+$fa-var-pencil-square-o: "\f044";
+$fa-var-percent: "\f295";
+$fa-var-phone: "\f095";
+$fa-var-phone-square: "\f098";
+$fa-var-photo: "\f03e";
+$fa-var-picture-o: "\f03e";
+$fa-var-pie-chart: "\f200";
+$fa-var-pied-piper: "\f1a7";
+$fa-var-pied-piper-alt: "\f1a8";
+$fa-var-pinterest: "\f0d2";
+$fa-var-pinterest-p: "\f231";
+$fa-var-pinterest-square: "\f0d3";
+$fa-var-plane: "\f072";
+$fa-var-play: "\f04b";
+$fa-var-play-circle: "\f144";
+$fa-var-play-circle-o: "\f01d";
+$fa-var-plug: "\f1e6";
+$fa-var-plus: "\f067";
+$fa-var-plus-circle: "\f055";
+$fa-var-plus-square: "\f0fe";
+$fa-var-plus-square-o: "\f196";
+$fa-var-power-off: "\f011";
+$fa-var-print: "\f02f";
+$fa-var-product-hunt: "\f288";
+$fa-var-puzzle-piece: "\f12e";
+$fa-var-qq: "\f1d6";
+$fa-var-qrcode: "\f029";
+$fa-var-question: "\f128";
+$fa-var-question-circle: "\f059";
+$fa-var-quote-left: "\f10d";
+$fa-var-quote-right: "\f10e";
+$fa-var-ra: "\f1d0";
+$fa-var-random: "\f074";
+$fa-var-rebel: "\f1d0";
+$fa-var-recycle: "\f1b8";
+$fa-var-reddit: "\f1a1";
+$fa-var-reddit-alien: "\f281";
+$fa-var-reddit-square: "\f1a2";
+$fa-var-refresh: "\f021";
+$fa-var-registered: "\f25d";
+$fa-var-remove: "\f00d";
+$fa-var-renren: "\f18b";
+$fa-var-reorder: "\f0c9";
+$fa-var-repeat: "\f01e";
+$fa-var-reply: "\f112";
+$fa-var-reply-all: "\f122";
+$fa-var-retweet: "\f079";
+$fa-var-rmb: "\f157";
+$fa-var-road: "\f018";
+$fa-var-rocket: "\f135";
+$fa-var-rotate-left: "\f0e2";
+$fa-var-rotate-right: "\f01e";
+$fa-var-rouble: "\f158";
+$fa-var-rss: "\f09e";
+$fa-var-rss-square: "\f143";
+$fa-var-rub: "\f158";
+$fa-var-ruble: "\f158";
+$fa-var-rupee: "\f156";
+$fa-var-safari: "\f267";
+$fa-var-save: "\f0c7";
+$fa-var-scissors: "\f0c4";
+$fa-var-scribd: "\f28a";
+$fa-var-search: "\f002";
+$fa-var-search-minus: "\f010";
+$fa-var-search-plus: "\f00e";
+$fa-var-sellsy: "\f213";
+$fa-var-send: "\f1d8";
+$fa-var-send-o: "\f1d9";
+$fa-var-server: "\f233";
+$fa-var-share: "\f064";
+$fa-var-share-alt: "\f1e0";
+$fa-var-share-alt-square: "\f1e1";
+$fa-var-share-square: "\f14d";
+$fa-var-share-square-o: "\f045";
+$fa-var-shekel: "\f20b";
+$fa-var-sheqel: "\f20b";
+$fa-var-shield: "\f132";
+$fa-var-ship: "\f21a";
+$fa-var-shirtsinbulk: "\f214";
+$fa-var-shopping-bag: "\f290";
+$fa-var-shopping-basket: "\f291";
+$fa-var-shopping-cart: "\f07a";
+$fa-var-sign-in: "\f090";
+$fa-var-sign-out: "\f08b";
+$fa-var-signal: "\f012";
+$fa-var-simplybuilt: "\f215";
+$fa-var-sitemap: "\f0e8";
+$fa-var-skyatlas: "\f216";
+$fa-var-skype: "\f17e";
+$fa-var-slack: "\f198";
+$fa-var-sliders: "\f1de";
+$fa-var-slideshare: "\f1e7";
+$fa-var-smile-o: "\f118";
+$fa-var-soccer-ball-o: "\f1e3";
+$fa-var-sort: "\f0dc";
+$fa-var-sort-alpha-asc: "\f15d";
+$fa-var-sort-alpha-desc: "\f15e";
+$fa-var-sort-amount-asc: "\f160";
+$fa-var-sort-amount-desc: "\f161";
+$fa-var-sort-asc: "\f0de";
+$fa-var-sort-desc: "\f0dd";
+$fa-var-sort-down: "\f0dd";
+$fa-var-sort-numeric-asc: "\f162";
+$fa-var-sort-numeric-desc: "\f163";
+$fa-var-sort-up: "\f0de";
+$fa-var-soundcloud: "\f1be";
+$fa-var-space-shuttle: "\f197";
+$fa-var-spinner: "\f110";
+$fa-var-spoon: "\f1b1";
+$fa-var-spotify: "\f1bc";
+$fa-var-square: "\f0c8";
+$fa-var-square-o: "\f096";
+$fa-var-stack-exchange: "\f18d";
+$fa-var-stack-overflow: "\f16c";
+$fa-var-star: "\f005";
+$fa-var-star-half: "\f089";
+$fa-var-star-half-empty: "\f123";
+$fa-var-star-half-full: "\f123";
+$fa-var-star-half-o: "\f123";
+$fa-var-star-o: "\f006";
+$fa-var-steam: "\f1b6";
+$fa-var-steam-square: "\f1b7";
+$fa-var-step-backward: "\f048";
+$fa-var-step-forward: "\f051";
+$fa-var-stethoscope: "\f0f1";
+$fa-var-sticky-note: "\f249";
+$fa-var-sticky-note-o: "\f24a";
+$fa-var-stop: "\f04d";
+$fa-var-stop-circle: "\f28d";
+$fa-var-stop-circle-o: "\f28e";
+$fa-var-street-view: "\f21d";
+$fa-var-strikethrough: "\f0cc";
+$fa-var-stumbleupon: "\f1a4";
+$fa-var-stumbleupon-circle: "\f1a3";
+$fa-var-subscript: "\f12c";
+$fa-var-subway: "\f239";
+$fa-var-suitcase: "\f0f2";
+$fa-var-sun-o: "\f185";
+$fa-var-superscript: "\f12b";
+$fa-var-support: "\f1cd";
+$fa-var-table: "\f0ce";
+$fa-var-tablet: "\f10a";
+$fa-var-tachometer: "\f0e4";
+$fa-var-tag: "\f02b";
+$fa-var-tags: "\f02c";
+$fa-var-tasks: "\f0ae";
+$fa-var-taxi: "\f1ba";
+$fa-var-television: "\f26c";
+$fa-var-tencent-weibo: "\f1d5";
+$fa-var-terminal: "\f120";
+$fa-var-text-height: "\f034";
+$fa-var-text-width: "\f035";
+$fa-var-th: "\f00a";
+$fa-var-th-large: "\f009";
+$fa-var-th-list: "\f00b";
+$fa-var-thumb-tack: "\f08d";
+$fa-var-thumbs-down: "\f165";
+$fa-var-thumbs-o-down: "\f088";
+$fa-var-thumbs-o-up: "\f087";
+$fa-var-thumbs-up: "\f164";
+$fa-var-ticket: "\f145";
+$fa-var-times: "\f00d";
+$fa-var-times-circle: "\f057";
+$fa-var-times-circle-o: "\f05c";
+$fa-var-tint: "\f043";
+$fa-var-toggle-down: "\f150";
+$fa-var-toggle-left: "\f191";
+$fa-var-toggle-off: "\f204";
+$fa-var-toggle-on: "\f205";
+$fa-var-toggle-right: "\f152";
+$fa-var-toggle-up: "\f151";
+$fa-var-trademark: "\f25c";
+$fa-var-train: "\f238";
+$fa-var-transgender: "\f224";
+$fa-var-transgender-alt: "\f225";
+$fa-var-trash: "\f1f8";
+$fa-var-trash-o: "\f014";
+$fa-var-tree: "\f1bb";
+$fa-var-trello: "\f181";
+$fa-var-tripadvisor: "\f262";
+$fa-var-trophy: "\f091";
+$fa-var-truck: "\f0d1";
+$fa-var-try: "\f195";
+$fa-var-tty: "\f1e4";
+$fa-var-tumblr: "\f173";
+$fa-var-tumblr-square: "\f174";
+$fa-var-turkish-lira: "\f195";
+$fa-var-tv: "\f26c";
+$fa-var-twitch: "\f1e8";
+$fa-var-twitter: "\f099";
+$fa-var-twitter-square: "\f081";
+$fa-var-umbrella: "\f0e9";
+$fa-var-underline: "\f0cd";
+$fa-var-undo: "\f0e2";
+$fa-var-university: "\f19c";
+$fa-var-unlink: "\f127";
+$fa-var-unlock: "\f09c";
+$fa-var-unlock-alt: "\f13e";
+$fa-var-unsorted: "\f0dc";
+$fa-var-upload: "\f093";
+$fa-var-usb: "\f287";
+$fa-var-usd: "\f155";
+$fa-var-user: "\f007";
+$fa-var-user-md: "\f0f0";
+$fa-var-user-plus: "\f234";
+$fa-var-user-secret: "\f21b";
+$fa-var-user-times: "\f235";
+$fa-var-users: "\f0c0";
+$fa-var-venus: "\f221";
+$fa-var-venus-double: "\f226";
+$fa-var-venus-mars: "\f228";
+$fa-var-viacoin: "\f237";
+$fa-var-video-camera: "\f03d";
+$fa-var-vimeo: "\f27d";
+$fa-var-vimeo-square: "\f194";
+$fa-var-vine: "\f1ca";
+$fa-var-vk: "\f189";
+$fa-var-volume-down: "\f027";
+$fa-var-volume-off: "\f026";
+$fa-var-volume-up: "\f028";
+$fa-var-warning: "\f071";
+$fa-var-wechat: "\f1d7";
+$fa-var-weibo: "\f18a";
+$fa-var-weixin: "\f1d7";
+$fa-var-whatsapp: "\f232";
+$fa-var-wheelchair: "\f193";
+$fa-var-wifi: "\f1eb";
+$fa-var-wikipedia-w: "\f266";
+$fa-var-windows: "\f17a";
+$fa-var-won: "\f159";
+$fa-var-wordpress: "\f19a";
+$fa-var-wrench: "\f0ad";
+$fa-var-xing: "\f168";
+$fa-var-xing-square: "\f169";
+$fa-var-y-combinator: "\f23b";
+$fa-var-y-combinator-square: "\f1d4";
+$fa-var-yahoo: "\f19e";
+$fa-var-yc: "\f23b";
+$fa-var-yc-square: "\f1d4";
+$fa-var-yelp: "\f1e9";
+$fa-var-yen: "\f157";
+$fa-var-youtube: "\f167";
+$fa-var-youtube-play: "\f16a";
+$fa-var-youtube-square: "\f166";
+
diff --git a/app/assets/sass/styles.scss b/app/assets/sass/styles.scss
new file mode 100644
index 0000000..4dec9e5
--- /dev/null
+++ b/app/assets/sass/styles.scss
@@ -0,0 +1,16 @@
+@import "compass/reset";
+@import "font-awesome/styles";
+
+@import "variables";
+@import "common";
+@import "buttons";
+@import "forms";
+@import "nav";
+
+@import "plan";
+@import "plan-node";
+@import "menu";
+@import "page";
+@import "table";
+@import "modal";
+@import "footer";
diff --git a/app/assets/styles.css b/app/assets/styles.css
new file mode 100644
index 0000000..ec0a393
--- /dev/null
+++ b/app/assets/styles.css
@@ -0,0 +1,4 @@
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}/*!
+ * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.5.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.5.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.5.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.5.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:0.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}html{height:100%}body{font-size:13px;font-weight:300;color:#4d525a;height:100%;width:100%;background-color:#f7f7f7;line-height:1.3}strong{font-weight:600}body,input,a,button,textarea{font-family:"noto sans";font-weight:300}.text-muted{color:#999ea7}.hero-container{margin:30px;font-size:22px;text-align:center}.pull-right{float:right}.align-right{text-align:right}a{color:#00B5E2;text-decoration:none}.fa{margin-right:3px}.clickable{cursor:pointer}.btn{border-radius:3px;padding:6px 10px;font-size:13px;line-height:1.2;text-decoration:none;text-transform:uppercase}.btn-default{border:1px solid #00B5E2;color:#00B5E2;background-color:#fff}.btn-default:hover{background-color:#65DDFB}.btn-lg{padding:6px 20px;font-size:16px}.btn-danger{border:1px solid #AF2F11;color:#AF2F11;text-transform:uppercase;background-color:#fff}.btn-danger:hover{background-color:#FB8165}.btn-primary{border:0;background:#00B5E2;box-shadow:1px 1px 1px #ababab;color:#ffffff}.btn-primary:hover{background:#008CAF}.input-box:-moz-placeholder{color:#ababab;font-size:18px}.input-box::-moz-placeholder{color:#ababab;font-size:18px}.input-box:-ms-input-placeholder{color:#ababab;font-size:18px}.input-box::-webkit-input-placeholder{color:#ababab;font-size:18px}.input-box:focus{box-shadow:0 0 5px #51cbee}.input-box-main{font-size:18px;width:700px;border:0;border-bottom:2px solid #00B5E2;padding:10px;margin-top:10px;margin-bottom:10px}.input-box-lg{width:100%;height:280px;margin-bottom:6px;margin-bottom:10px;border-radius:3px;border:1px solid #dedede;padding:10px}nav{font-size:17px;background-color:#fff;padding:15px}nav .nav-container{width:1000px;margin:auto}.plan{padding-bottom:30px;overflow:auto;height:100%;width:100%}.plan ul{display:flex;padding-top:12px;position:relative;margin:auto;transition:all 0.5s;margin-top:-5px}.plan ul ul::before{content:'';position:absolute;top:0;left:50%;border-left:2px solid #c4c4c4;height:12px;width:0}.plan ul li{float:left;text-align:center;list-style-type:none;position:relative;padding:12px 3px 0 3px;transition:all 0.5s}.plan ul li:before,.plan ul li:after{content:'';position:absolute;top:0;right:50%;border-top:2px solid #c4c4c4;width:50%;height:12px}.plan ul li:after{right:auto;left:50%;border-left:2px solid #c4c4c4}.plan ul li:only-child{padding-top:0}.plan ul li:only-child:after,.plan ul li:only-child:before{display:none}.plan ul li:first-child::before,.plan ul li:last-child::after{border:0 none}.plan ul li:last-child::before{border-right:2px solid #c4c4c4;border-radius:0 6px 0 0}.plan ul li:first-child::after{border-radius:6px 0 0 0}.plan ul li .plan-node:hover+ul::before{border-color:#00B5E2}.plan ul li .plan-node:hover+ul li::after,.plan ul li .plan-node:hover+ul li::before,.plan ul li .plan-node:hover+ul ul::before{border-color:#008CAF}.plan-stats{display:flex;font-size:13px;margin:0 auto 10px auto;padding-bottom:10px;border-bottom:1px solid #dedede;border-radius:12px;width:650px;position:relative}.plan-stats div{padding-right:10px;flex-grow:1}.plan-stats .stat-value{display:block;text-align:center;font-size:17px}.plan-stats .stat-label{display:block;text-align:center;font-size:12px}.plan-stats:after{content:'';position:absolute;top:100%;left:50%;margin-left:-9px;width:0;height:0;border-top:solid 9px #dedede;border-left:solid 9px transparent;border-right:solid 9px transparent}.plan-node{text-decoration:none;color:#4d525a;display:inline-block;transition:all 0.5s;position:relative;padding:6px 10px;background-color:#fff;font-size:12px;border:2px solid #dedede;border-radius:3px;overflow:hidden;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;min-width:220px}.plan-node header{margin-bottom:6px;overflow:hidden;cursor:pointer}.plan-node header:hover{background-color:#f7f7f7}.plan-node header h4{font-size:13px;float:left;font-weight:600}.plan-node header .node-duration{float:right;margin-left:10px;font-size:13px}.plan-node .prop-list{float:left;text-align:left;width:400px;overflow-wrap:break-word;word-wrap:break-word;word-break:break-all;margin-top:10px;margin-bottom:6px}.plan-node .relation-name{text-align:left;max-width:220px}.plan-node .planner-estimate{float:left;clear:both;text-align:left;margin-top:6px;width:100%}.plan-node .tags{margin-top:6px;text-align:left}.plan-node .tags span{display:inline-block;background-color:#FB4418;color:#fff;font-size:10px;font-weight:600;margin-right:3px;padding:3px;border-radius:3px;line-height:1.1}.plan-node:hover{border-color:#00B5E2}.node-bar-container{float:left;height:5px;margin-top:6px;margin-left:auto;margin-right:auto;border:1px solid #dedede;border-radius:3px;color:#fff;background-color:#454545;position:relative}.node-bar-container .node-bar{height:100%;text-align:left;position:absolute;left:0;top:0}.node-bar-label{text-align:left;display:block}.menu{width:190px;height:200px;position:absolute;font-size:12px;top:115px;left:0;background-color:#777;box-shadow:1px 1px 2px 1px rgba(0,0,0,0.5);color:#fff;border-top-right-radius:3px;border-bottom-right-radius:3px;z-index:1;transition:all 0.3s}.menu header h3{padding-top:10px;margin-bottom:20px;font-size:16px;font-weight:600;line-height:2;text-align:right;padding-right:20px}.menu ul{margin-left:10px}.menu ul li{line-height:2.5}.menu-toggle{font-size:26px;float:left;padding-left:10px;line-height:2;cursor:pointer}.menu-hidden{width:50px;height:50px;border-top-right-radius:50%;border-bottom-right-radius:50%}.menu-hidden ul,.menu-hidden h3{visibility:hidden}.menu button{border:1px solid #fff;background-color:#544D4D;color:#fff;padding:2px;border-radius:3px}.page,.page-stretch{padding-top:10px;margin:auto;width:1000px;min-height:600px}.page h2,.page-stretch h2{font-size:26px;margin-bottom:6px}.page-stretch{margin:auto;width:95%}.table{width:100%}.table td{border-bottom:1px solid #dedede;padding:6px}.table tr:hover{background-color:#f7f7f7}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000;opacity:0.7}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050}.modal .modal-dialog{position:relative;transform:translate(0);margin:30px auto;width:500px;opacity:1}.modal .modal-dialog .modal-content{padding:30px;position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;box-shadow:0 3px 9px rgba(0,0,0,0.5);display:block}.modal .modal-dialog .modal-content .modal-body{padding:3px}.modal .modal-dialog .modal-content .modal-footer{text-align:right}.modal .modal-dialog .modal-content .modal-footer button{margin-left:3px}footer{border-top:2px solid #dedede;margin:20px;padding:20px;color:#ababab;text-align:center;width:600px;margin:auto}footer .fa{font-size:17px;margin-left:6px}
diff --git a/app/bootstrap.ts b/app/bootstrap.ts
new file mode 100644
index 0000000..b3303f3
--- /dev/null
+++ b/app/bootstrap.ts
@@ -0,0 +1,9 @@
+import {provide} from 'angular2/core';
+import {bootstrap} from 'angular2/platform/browser';
+import {ROUTER_PROVIDERS, LocationStrategy, HashLocationStrategy} from 'angular2/router';
+import {App} from './components/app/app';
+
+bootstrap(App, [
+ ROUTER_PROVIDERS,
+ provide(LocationStrategy, { useClass: HashLocationStrategy })
+]);
diff --git a/app/components/app/app.html b/app/components/app/app.html
new file mode 100644
index 0000000..a8a1969
--- /dev/null
+++ b/app/components/app/app.html
@@ -0,0 +1,6 @@
+<router-outlet></router-outlet>
+<footer>PEV is made by
+ <a href="http://tatiyants.com/">Alex Tatiyants</a>
+ <a href="https://twitter.com/AlexTatiyants"><i class="fa fa-twitter"></i></a>
+ <a href="https://github.com/AlexTatiyants"><i class="fa fa-github"></i></a>
+</footer>
diff --git a/app/components/app/app.ts b/app/components/app/app.ts
new file mode 100644
index 0000000..f9c83e0
--- /dev/null
+++ b/app/components/app/app.ts
@@ -0,0 +1,22 @@
+import {Component, ViewEncapsulation} from 'angular2/core';
+import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
+
+import {PlanView} from '../plan-view/plan-view';
+import {PlanList} from '../plan-list/plan-list';
+import {PlanNew} from '../plan-new/plan-new';
+
+@Component({
+ selector: 'app',
+ templateUrl: './components/app/app.html',
+ encapsulation: ViewEncapsulation.None,
+ directives: [ROUTER_DIRECTIVES]
+})
+
+@RouteConfig([
+ { path: '/', redirectTo: ['/PlanList'] },
+ { path: '/plans', component: PlanList, name: 'PlanList' },
+ { path: '/plans/new', component: PlanNew, name: 'PlanNew' },
+ { path: '/plans/:id', component: PlanView, name: 'PlanView' }
+])
+
+export class App { }
diff --git a/app/components/plan-list/plan-list.html b/app/components/plan-list/plan-list.html
new file mode 100644
index 0000000..a2f1a9d
--- /dev/null
+++ b/app/components/plan-list/plan-list.html
@@ -0,0 +1,38 @@
+<nav>
+ <div class="nav-container">
+ <a class="btn btn-primary btn-lg pull-right" [routerLink]="['/PlanNew']">create</a>
+ <a [routerLink]="['PlanList']">plans</a>
+ </div>
+</nav>
+
+<div class="page">
+ <div class="hero-container" *ngIf="plans.length === 0">
+ Welcome to PEV! Please <a [routerLink]="['/PlanNew']">submit</a> a plan for visualization
+ </div>
+
+ <table class="table">
+ <tr *ngFor="#plan of plans">
+ <td><a [routerLink]="['/PlanView', {id: plan.id}]">{{plan.name}}</a></td>
+ <td>created on {{plan.createdOn | momentDate }}</td>
+ <td class="align-right"><button class="btn btn-danger" (click)="requestDelete()">
+ <i class="fa fa-trash"></i>delete</button>
+ <!-- this is a hack that should be converted to a proper dialog once that is available in angular 2-->
+ <div *ngIf="openDialog">
+ <div class="modal-backdrop"></div>
+
+ <div class="modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-body">You're about to delete this plan. Are you sure?</div>
+ <div class="modal-footer">
+ <button class="btn btn-primary" (click)="deletePlan(plan)">Yes</button>
+ <button class="btn btn-default" (click)="cancelDelete()">No</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </td>
+ </tr>
+ </table>
+</div>
diff --git a/app/components/plan-list/plan-list.ts b/app/components/plan-list/plan-list.ts
new file mode 100644
index 0000000..888e2e2
--- /dev/null
+++ b/app/components/plan-list/plan-list.ts
@@ -0,0 +1,47 @@
+import {Component, OnInit} from 'angular2/core';
+import {ROUTER_DIRECTIVES} from 'angular2/router';
+
+import {IPlan} from '../../interfaces/iplan';
+import {PlanService} from '../../services/plan-service';
+import {PlanNew} from '../plan-new/plan-new';
+
+import {MomentDatePipe} from '../../pipes';
+
+@Component({
+ selector: 'plan-list',
+ templateUrl: './components/plan-list/plan-list.html',
+ providers: [PlanService],
+ directives: [ROUTER_DIRECTIVES, PlanNew],
+ pipes: [MomentDatePipe]
+})
+export class PlanList {
+ plans: Array<IPlan>;
+ newPlanName: string;
+ newPlanContent: any;
+ newPlanId: string;
+ openDialog: boolean = false;
+
+ constructor(private _planService: PlanService) { }
+
+ ngOnInit() {
+ this.plans = this._planService.getPlans();
+ }
+
+ requestDelete() {
+ this.openDialog = true;
+ }
+
+ deletePlan(plan) {
+ this.openDialog = false;
+ this._planService.deletePlan(plan);
+ this.plans = this._planService.getPlans();
+ }
+
+ cancelDelete() {
+ this.openDialog = false;
+ }
+
+ deleteAllPlans() {
+ this._planService.deleteAllPlans();
+ }
+}
diff --git a/app/components/plan-new/plan-new.html b/app/components/plan-new/plan-new.html
new file mode 100644
index 0000000..f9babb6
--- /dev/null
+++ b/app/components/plan-new/plan-new.html
@@ -0,0 +1,18 @@
+<nav>
+ <div class="nav-container">
+ <a [routerLink]="['PlanList']">plans</a>
+ <span class="text-muted"> | </span>
+ create plan
+ </div>
+</nav>
+
+<div class="page">
+ <span class="text-muted">For best results, use EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON)</span>
+ <div>
+ <input placeholder="name (optional)" class="input-box input-box-main" type="text" [(ngModel)]="newPlanName">
+ <button class="btn btn-primary btn-lg pull-right" (click)="submitPlan()">submit</button>
+ </div>
+
+ <textarea placeholder="paste execution plan" class="input-box input-box-lg" [(ngModel)]="newPlanContent"></textarea>
+ <textarea placeholder="paste corresponding SQL query" class="input-box input-box-lg" [(ngModel)]="newPlanQuery"></textarea>
+</div>
diff --git a/app/components/plan-new/plan-new.ts b/app/components/plan-new/plan-new.ts
new file mode 100644
index 0000000..4a748ea
--- /dev/null
+++ b/app/components/plan-new/plan-new.ts
@@ -0,0 +1,26 @@
+import {Component, OnInit} from 'angular2/core';
+import {Router, ROUTER_DIRECTIVES} from 'angular2/router';
+import {IPlan} from '../../interfaces/iplan';
+
+import {PlanService} from '../../services/plan-service';
+
+@Component({
+ selector: 'plan-new',
+ templateUrl: './components/plan-new/plan-new.html',
+ providers: [PlanService],
+ directives: [ROUTER_DIRECTIVES]
+})
+export class PlanNew {
+ planIds: string[];
+ newPlanName: string;
+ newPlanContent: string;
+ newPlanQuery: string;
+ newPlan: IPlan;
+
+ constructor( private _router: Router, private _planService: PlanService) { }
+
+ submitPlan() {
+ this.newPlan = this._planService.createPlan(this.newPlanName, this.newPlanContent, this.newPlanQuery);
+ this._router.navigate( ['PlanView', { id: this.newPlan.id }] );
+ }
+}
diff --git a/app/components/plan-node/plan-node.html b/app/components/plan-node/plan-node.html
new file mode 100644
index 0000000..49380a2
--- /dev/null
+++ b/app/components/plan-node/plan-node.html
@@ -0,0 +1,50 @@
+<div class="plan-node">
+ <header (click)="showDetails = !showDetails">
+ <h4>{{node['Node Type'] | uppercase}}</h4>
+ <span class="node-duration">{{duration}}<span class="text-muted">{{durationUnit}} | </span>
+ <strong>{{executionTimePercent}}</strong><span class="text-muted">%</span>
+ </span>
+ </header>
+
+ <div class="relation-name" *ngIf="node['Relation Name']"><span class="text-muted">on </span>
+ <span *ngIf="node['Schema']">{{node['Schema']}}.</span>{{node['Relation Name']}}
+ <span *ngIf="node['Alias']"> ({{node['Alias']}})</span>
+ </div>
+
+ <div class="relation-name" *ngIf="node['Group Key']"><span class="text-muted">by</span> {{node['Group Key']}}</div>
+ <div class="relation-name" *ngIf="node['Sort Key']"><span class="text-muted">by</span> {{node['Sort Key']}}</div>
+ <div class="relation-name" *ngIf="node['Join Type']">{{node['Join Type']}} <span class="text-muted">join</span></div>
+ <div class="relation-name" *ngIf="node['Index Name']"><span class="text-muted">using</span> {{node['Index Name']}}</div>
+
+ <div class="tags" *ngIf="viewOptions.showTags && tags.length > 0">
+ <span *ngFor="#tag of tags">{{tag}}</span>
+ </div>
+
+ <div *ngIf="currentHighlightType !== highlightTypes.NONE">
+ <div class="node-bar-container" [style.width]="(MAX_WIDTH+3)+'px'">
+ <span class="node-bar" [style.width]="width+'px'" [style.backgroundColor]="backgroundColor"></span>
+ </div>
+ <span class="node-bar-label">
+ <span class="text-muted">{{viewOptions.highlightType}}:</span> {{highlightValue | number:'.0-2'}}
+ </span>
+ </div>
+
+ <div class="planner-estimate" *ngIf="viewOptions.showPlannerEstimate">
+ <span *ngIf="plannerRowEstimateDirection === estimateDirections.over"><strong>over</strong> estimated rows</span>
+ <span *ngIf="plannerRowEstimateDirection === estimateDirections.under"><strong>under</strong> estimated rows</span>
+ <span> by <strong>{{plannerRowEstimateValue}}</strong>x</span>
+ </div>
+
+ <table *ngIf="showDetails" class="table prop-list">
+ <tr *ngFor="#prop of props">
+ <td width="40%">{{prop.key}}</td>
+ <td>{{prop.value}}</td>
+ <tr>
+ </table>
+</div>
+
+<ul *ngIf="node.Plans">
+ <li *ngFor="#subNode of node.Plans">
+ <plan-node [node]="subNode" [viewOptions]="viewOptions" [planStats]="planStats"></plan-node>
+ </li>
+</ul>
diff --git a/app/components/plan-node/plan-node.ts b/app/components/plan-node/plan-node.ts
new file mode 100644
index 0000000..3562ea6
--- /dev/null
+++ b/app/components/plan-node/plan-node.ts
@@ -0,0 +1,176 @@
+import {Component, OnInit} from 'angular2/core';
+import {HighlightType, EstimateDirection} from '../../enums';
+import {PlanService} from '../../services/plan-service';
+/// <reference path="lodash.d.ts" />
+
+@Component({
+ selector: 'plan-node',
+ inputs: ['node', 'planStats', 'viewOptions'],
+ templateUrl: './components/plan-node/plan-node.html',
+ directives: [PlanNode],
+ providers: [PlanService]
+})
+
+export class PlanNode {
+ // consts
+ MAX_WIDTH: number = 220;
+ MIN_ESTIMATE_MISS: number = 100;
+ COSTLY_TAG: string = 'costliest';
+ SLOW_TAG: string = 'slowest';
+ LARGE_TAG: string = 'largest';
+ ESTIMATE_TAG: string = 'bad estimate';
+
+ // inputs
+ node: any;
+ planStats: any;
+ viewOptions: any;
+
+ // calculated properties
+ duration: string;
+ durationUnit: string;
+ executionTimePercent: number;
+ backgroundColor: string;
+ highlightValue: number;
+ width: number;
+ props: Array<any>;
+ tags: Array<string>;
+ plannerRowEstimateValue: number;
+ plannerRowEstimateDirection: EstimateDirection;
+ currentHighlightType: string;
+
+ // expose enum to view
+ estimateDirections = EstimateDirection;
+ highlightTypes = HighlightType;
+
+ constructor(private _planService: PlanService) { }
+
+ ngOnInit() {
+ this.currentHighlightType = this.viewOptions.highlightType;
+ this.calculateBar();
+ this.calculateProps();
+ this.calculateDuration();
+ this.calculateTags();
+
+ this.plannerRowEstimateDirection = this.node[this._planService.PLANNER_ESIMATE_DIRECTION];
+ this.plannerRowEstimateValue = _.round(this.node[this._planService.PLANNER_ESTIMATE_FACTOR]);
+ }
+
+ ngDoCheck() {
+ // console.log("check", this.currentHighlightType, this.viewOptions.highlightType);
+ if (this.currentHighlightType !== this.viewOptions.highlightType) {
+ this.currentHighlightType = this.viewOptions.highlightType;
+ this.calculateBar();
+ }
+ }
+
+ calculateBar() {
+ switch (this.currentHighlightType) {
+ case HighlightType.DURATION:
+ this.highlightValue = (this.node[this._planService.ACTUAL_DURATION_PROP]);
+ this.width = Math.round((this.highlightValue / this.planStats.maxDuration) * this.MAX_WIDTH);
+ break;
+ case HighlightType.ROWS:
+ this.highlightValue = (this.node[this._planService.ACTUAL_ROWS_PROP]);
+ this.width = Math.round((this.highlightValue / this.planStats.maxRows) * this.MAX_WIDTH);
+ break;
+ case HighlightType.COST:
+ this.highlightValue = (this.node[this._planService.ACTUAL_COST_PROP]);
+ this.width = Math.round((this.highlightValue / this.planStats.maxCost) * this.MAX_WIDTH);
+ break;
+ }
+
+ if (this.width < 1) { this.width = 1 }
+ this.backgroundColor = this.numberToColorHsl(1 - this.width / this.MAX_WIDTH);
+ }
+
+ calculateDuration() {
+ var dur: number = _.round(this.node[this._planService.ACTUAL_DURATION_PROP]);
+ // convert duration into approriate units
+ if (dur < 1) {
+ this.duration = "<1";
+ this.durationUnit = 'ms';
+ } else if (dur > 1 && dur < 1000) {
+ this.duration = dur.toString();
+ this.durationUnit = 'ms';
+ } else {
+ this.duration = _.round(dur / 1000, 2).toString();
+ this.durationUnit = 'mins';
+ }
+ this.executionTimePercent = (_.round((dur / this.planStats.executionTime) * 100));
+ }
+
+ // create an array of node propeties so that they can be displayed in the view
+ calculateProps() {
+ this.props = _.chain(this.node)
+ .omit('Plans')
+ .map((value, key) => {
+ return { key: key, value: value };
+ })
+ .value();
+ }
+
+ calculateTags() {
+ this.tags = [];
+ if (this.node[this._planService.SLOWEST_NODE_PROP]) {
+ this.tags.push(this.SLOW_TAG);
+ }
+ if (this.node[this._planService.COSTLIEST_NODE_PROP]) {
+ this.tags.push(this.COSTLY_TAG);
+ }
+ if (this.node[this._planService.LARGEST_NODE_PROP]) {
+ this.tags.push(this.LARGE_TAG);
+ }
+ if (this.node[this._planService.PLANNER_ESTIMATE_FACTOR] >= this.MIN_ESTIMATE_MISS) {
+ this.tags.push(this.ESTIMATE_TAG);
+ }
+ }
+
+ /**
+ * http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
+ *
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param Number h The hue
+ * @param Number s The saturation
+ * @param Number l The lightness
+ * @return Array The RGB representation
+ */
+ hslToRgb(h, s, l) {
+ var r, g, b;
+
+ if (s == 0) {
+ r = g = b = l; // achromatic
+ } else {
+ function hue2rgb(p, q, t) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ }
+
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+
+ return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)];
+ }
+
+ // convert a number to a color using hsl
+ numberToColorHsl(i) {
+ // as the function expects a value between 0 and 1, and red = 0° and green = 120°
+ // we convert the input to the appropriate hue value
+ var hue = i * 100 * 1.2 / 360;
+ // we convert hsl to rgb (saturation 100%, lightness 50%)
+ var rgb = this.hslToRgb(hue, .9, .4);
+ // we format to css value and return
+ return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')';
+ }
+}
diff --git a/app/components/plan-view/plan-view.html b/app/components/plan-view/plan-view.html
new file mode 100644
index 0000000..ab9b7dc
--- /dev/null
+++ b/app/components/plan-view/plan-view.html
@@ -0,0 +1,72 @@
+<div class="menu" [class.menu-hidden]="hideMenu">
+ <header>
+ <i class="fa fa-cogs menu-toggle" (click)="hideMenu = !hideMenu"></i>
+ <h3>display options</h3>
+ </header>
+
+ <ul>
+ <li>
+ <input id="showPlanStats" type="checkbox" [(ngModel)]="viewOptions.showPlanStats">
+ <label class="clickable" for="showPlanStats"> show plan stats</label>
+ </li>
+
+ <li>
+ <input id="showPlannerEstimate" type="checkbox" [(ngModel)]="viewOptions.showPlannerEstimate">
+ <label class="clickable" for="showPlannerEstimate"> show planner estimate</label>
+ </li>
+
+ <li>
+ <input id="showTags" type="checkbox" [(ngModel)]="viewOptions.showTags">
+ <label class="clickable" for="showTags"> show analysis tags</label>
+ </li>
+
+ <li>
+ <label>graph metric: </label>
+ <select [(ngModel)]="viewOptions.highlightType">
+ <option value="{{highlightTypes.NONE}}">{{highlightTypes.NONE}}</option>
+ <option value="{{highlightTypes.DURATION}}">{{highlightTypes.DURATION}}</option>
+ <option value="{{highlightTypes.ROWS}}">{{highlightTypes.ROWS}}</option>
+ <option value="{{highlightTypes.COST}}">{{highlightTypes.COST}}</option>
+ </select>
+ </li>
+ </ul>
+</div>
+
+<nav>
+ <div class="nav-container">
+ <a [routerLink]="['PlanList']">plans</a><span class="text-muted"> / </span>{{plan.name}}
+ </div>
+</nav>
+
+<div class="page page-stretch">
+ <div *ngIf="viewOptions.showPlanStats" class="plan-stats">
+ <div>
+ <span class="stat-value">{{executionTime}}</span>
+ <span class="stat-label">execution time ({{executionTimeUnit}})</span>
+ </div>
+ <div *ngIf="planStats.planningTime">
+ <span class="stat-value">{{planStats.planningTime | number:'.0-2'}}</span>
+ <span class="stat-label">planning time (ms)</span>
+ </div>
+ <div *ngIf="planStats.maxDuration">
+ <span class="stat-value">{{planStats.maxDuration | number:'.0-2'}}</span>
+ <span class="stat-label">slowest node (ms)</span>
+ </div>
+ <div *ngIf="planStats.maxRows">
+ <span class="stat-value">{{planStats.maxRows | number:'.0-2'}}</span>
+ <span class="stat-label">largest node (rows)</span>
+ </div>
+ <div *ngIf="planStats.maxCost">
+ <span class="stat-value">{{planStats.maxCost | number:'.0-2'}}</span>
+ <span class="stat-label">costliest node</span>
+ </div>
+ </div>
+
+ <div class="plan">
+ <ul>
+ <li>
+ <plan-node [node]="rootContainer.Plan" [planStats]="planStats" [viewOptions]="viewOptions"></plan-node>
+ </li>
+ </ul>
+ </div>
+</div>
diff --git a/app/components/plan-view/plan-view.ts b/app/components/plan-view/plan-view.ts
new file mode 100644
index 0000000..ab4e6a3
--- /dev/null
+++ b/app/components/plan-view/plan-view.ts
@@ -0,0 +1,96 @@
+import {Component, OnInit} from 'angular2/core';
+import {RouteParams} from 'angular2/router';
+import {ROUTER_DIRECTIVES} from 'angular2/router';
+
+import {IPlan} from '../../interfaces/iplan';
+import {PlanService} from '../../services/plan-service';
+import {HighlightType} from '../../enums';
+import {PlanNode} from '../plan-node/plan-node';
+
+@Component({
+ selector: 'plan-view',
+ templateUrl: './components/plan-view/plan-view.html',
+ directives: [ROUTER_DIRECTIVES, PlanNode],
+ providers: [PlanService]
+})
+export class PlanView {
+ id: string;
+ plan: IPlan;
+ rootContainer: any;
+ executionTime: string;
+ executionTimeUnit: string;
+ hideMenu: boolean = true;
+
+ planStats: any = {
+ executionTime: 0,
+ maxRows: 0,
+ maxCost: 0,
+ maxDuration: 0
+ };
+
+ viewOptions: any = {
+ showPlanStats: true,
+ showHighlightBar: true,
+ showPlannerEstimate: false,
+ showTags: true,
+ highlightType: HighlightType.NONE
+ };
+
+ showPlannerEstimate: boolean = true;
+ showMenu: boolean = false;
+
+ highlightTypes = HighlightType; // exposing the enum to the view
+
+ constructor(private _planService: PlanService, routeParams: RouteParams) {
+ this.id = routeParams.get('id');
+ }
+
+ getPlan() {
+ if (!this.id) {
+ return;
+ }
+
+ this.plan = this._planService.getPlan(this.id);
+ this.rootContainer = this.plan.content;
+
+ var executionTime: number = this.rootContainer['Execution Time'] || this.rootContainer['Total Runtime'];
+ [this.executionTime, this.executionTimeUnit] = this.calculateDuration(executionTime);
+
+ this.planStats = {
+ executionTime: executionTime,
+ planningTime: this.rootContainer['Planning Time'],
+ maxRows: this.rootContainer[this._planService.MAXIMUM_ROWS_PROP],
+ maxCost: this.rootContainer[this._planService.MAXIMUM_COSTS_PROP],
+ maxDuration: this.rootContainer[this._planService.MAXIMUM_DURATION_PROP]
+ }
+ }
+
+ ngOnInit() {
+ this.getPlan();
+ }
+
+ toggleHighlight(type: HighlightType) {
+ this.viewOptions.highlightType = type;
+ }
+
+ analyzePlan() {
+ this._planService.analyzePlan(this.plan);
+ }
+
+ calculateDuration(originalValue: number) {
+ var duration: string = '';
+ var unit: string = '';
+
+ if (originalValue < 1) {
+ duration = "<1";
+ unit = 'ms';
+ } else if (originalValue > 1 && originalValue < 1000) {
+ duration = originalValue.toString();
+ unit = 'ms';
+ } else {
+ duration = _.round(originalValue / 1000, 2).toString();
+ unit = 'mins';
+ }
+ return [duration, unit];
+ }
+}
diff --git a/app/enums.ts b/app/enums.ts
new file mode 100644
index 0000000..d7784d8
--- /dev/null
+++ b/app/enums.ts
@@ -0,0 +1,11 @@
+export class HighlightType {
+ static NONE: string = "none";
+ static DURATION: string = "duration";
+ static ROWS: string = "rows";
+ static COST: string = "cost";
+}
+
+export enum EstimateDirection {
+ over,
+ under
+}
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..a43262e
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title><%= APP_TITLE %></title>
+ <meta name="description" content="">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <!-- inject:css -->
+ <!-- endinject -->
+</head>
+<body>
+
+ <app>Loading...</app>
+
+ <!-- shims:js -->
+ <!-- endinject -->
+
+ <script>System.config(<%= JSON.stringify(SYSTEM_CONFIG) %>)</script>
+
+ <!-- libs:js -->
+ <!-- endinject -->
+
+ <script>
+ System.import('bootstrap');
+ </script>
+
+ <!-- inject:js -->
+ <!-- endinject -->
+
+</body>
+</html>
diff --git a/app/interfaces/iplan.ts b/app/interfaces/iplan.ts
new file mode 100644
index 0000000..bd08478
--- /dev/null
+++ b/app/interfaces/iplan.ts
@@ -0,0 +1,7 @@
+export interface IPlan {
+ id: string;
+ name: string;
+ content: any;
+ query: string;
+ createdOn: Date;
+}
diff --git a/app/pipes.ts b/app/pipes.ts
new file mode 100644
index 0000000..d9f7ab5
--- /dev/null
+++ b/app/pipes.ts
@@ -0,0 +1,10 @@
+import {Pipe} from 'angular2/core';
+/// <reference path="moment.d.ts" />
+
+@Pipe({name: 'momentDate'})
+
+export class MomentDatePipe {
+ transform(value:string, args:string[]) : any {
+ return moment(value).format('LLLL');
+ }
+}
diff --git a/app/sample-plans/plan1.json b/app/sample-plans/plan1.json
new file mode 100644
index 0000000..3be6615
--- /dev/null
+++ b/app/sample-plans/plan1.json
@@ -0,0 +1,334 @@
+[
+ {
+ "Plan": {
+ "Node Type": "Limit",
+ "Startup Cost": 60970.08,
+ "Total Cost": 60970.11,
+ "Plan Rows": 10,
+ "Plan Width": 468,
+ "Actual Startup Time": 1941.751,
+ "Actual Total Time": 1941.780,
+ "Actual Rows": 10,
+ "Actual Loops": 1,
+ "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"],
+ "Shared Hit Blocks": 260587,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Sort",
+ "Parent Relationship": "Outer",
+ "Startup Cost": 60970.08,
+ "Total Cost": 60977.40,
+ "Plan Rows": 2927,
+ "Plan Width": 468,
+ "Actual Startup Time": 1941.747,
+ "Actual Total Time": 1941.757,
+ "Actual Rows": 10,
+ "Actual Loops": 1,
+ "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"],
+ "Sort Key": ["(sum(s.total_alerts))"],
+ "Sort Method": "top-N heapsort",
+ "Sort Space Used": 27,
+ "Sort Space Type": "Memory",
+ "Shared Hit Blocks": 260587,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Nested Loop",
+ "Parent Relationship": "Outer",
+ "Join Type": "Left",
+ "Startup Cost": 37552.36,
+ "Total Cost": 60906.83,
+ "Plan Rows": 2927,
+ "Plan Width": 468,
+ "Actual Startup Time": 522.250,
+ "Actual Total Time": 1865.979,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["a.id", "COALESCE(a.display_name, a.alert_name)", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "percenttotal((sum(s.total_followed)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_overridden)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_ignored)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_unknown)), (sum(s.total_alerts)))", "((sum(s.total_alerts)) * a.estimated_cost)"],
+ "Shared Hit Blocks": 260587,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Nested Loop",
+ "Parent Relationship": "Outer",
+ "Join Type": "Left",
+ "Startup Cost": 37552.22,
+ "Total Cost": 56750.41,
+ "Plan Rows": 2927,
+ "Plan Width": 461,
+ "Actual Startup Time": 522.141,
+ "Actual Total Time": 1161.187,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.subtype_id", "altt.code", "altt.name"],
+ "Shared Hit Blocks": 197115,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Nested Loop",
+ "Parent Relationship": "Outer",
+ "Join Type": "Inner",
+ "Startup Cost": 37552.07,
+ "Total Cost": 56230.56,
+ "Plan Rows": 2927,
+ "Plan Width": 405,
+ "Actual Startup Time": 522.131,
+ "Actual Total Time": 918.895,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.type_id", "a.subtype_id"],
+ "Shared Hit Blocks": 132411,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Aggregate",
+ "Strategy": "Hashed",
+ "Parent Relationship": "Outer",
+ "Startup Cost": 37551.64,
+ "Total Cost": 37624.82,
+ "Plan Rows": 2927,
+ "Plan Width": 56,
+ "Actual Startup Time": 522.107,
+ "Actual Total Time": 587.536,
+ "Actual Rows": 32352,
+ "Actual Loops": 1,
+ "Output": ["s.alert_id", "sum(s.total_alerts)", "sum(s.total_followed)", "sum(s.total_overridden)", "sum(s.total_ignored)", "sum(s.total_not_seen)", "sum(s.total_unknown)"],
+ "Group Key": ["s.alert_id"],
+ "Shared Hit Blocks": 2784,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Bitmap Heap Scan",
+ "Parent Relationship": "Outer",
+ "Relation Name": "alert_daily_summaries",
+ "Schema": "analytics",
+ "Alias": "s",
+ "Startup Cost": 3265.35,
+ "Total Cost": 35524.50,
+ "Plan Rows": 115837,
+ "Plan Width": 56,
+ "Actual Startup Time": 10.745,
+ "Actual Total Time": 165.318,
+ "Actual Rows": 140451,
+ "Actual Loops": 1,
+ "Output": ["s.id", "s.org_id", "s.alert_id", "s.alert_day", "s.total_alerts", "s.total_followed", "s.total_overridden", "s.total_ignored", "s.total_not_seen", "s.total_unknown", "s.override_comments"],
+ "Recheck Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))",
+ "Rows Removed by Index Recheck": 0,
+ "Exact Heap Blocks": 2243,
+ "Lossy Heap Blocks": 0,
+ "Shared Hit Blocks": 2784,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000,
+ "Plans": [
+ {
+ "Node Type": "Bitmap Index Scan",
+ "Parent Relationship": "Outer",
+ "Index Name": "alert_daily_summaries_org_id_alert_day_idx",
+ "Startup Cost": 0.00,
+ "Total Cost": 3236.39,
+ "Plan Rows": 115837,
+ "Plan Width": 0,
+ "Actual Startup Time": 10.445,
+ "Actual Total Time": 10.445,
+ "Actual Rows": 140451,
+ "Actual Loops": 1,
+ "Index Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))",
+ "Shared Hit Blocks": 541,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "Node Type": "Index Scan",
+ "Parent Relationship": "Inner",
+ "Scan Direction": "Forward",
+ "Index Name": "alerts_pkey1",
+ "Relation Name": "alerts",
+ "Schema": "analytics",
+ "Alias": "a",
+ "Startup Cost": 0.42,
+ "Total Cost": 6.34,
+ "Plan Rows": 1,
+ "Plan Width": 213,
+ "Actual Startup Time": 0.004,
+ "Actual Total Time": 0.005,
+ "Actual Rows": 1,
+ "Actual Loops": 32352,
+ "Output": ["a.id", "a.org_id", "a.external_alert_id", "a.alert_name", "a.display_name", "a.estimated_cost", "a.description", "a.source", "a.venue_code", "a.action_expected", "a.cancel_expected", "a.responsible_provider_type", "a.status", "a.version", "a.created_by", "a.created_date", "a.modified_by", "a.modified_date", "a.tracking_id", "a.record_date", "a.released_date", "a.released", "a.comments", "a.default_lockout_hours", "a.deploy_mode", "a.stop_type", "a.importance_level", "a.type_id", "a.subtype_id"],
+ "Index Cond": "(a.id = s.alert_id)",
+ "Rows Removed by Index Recheck": 0,
+ "Shared Hit Blocks": 129627,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ },
+ {
+ "Node Type": "Index Scan",
+ "Parent Relationship": "Inner",
+ "Scan Direction": "Forward",
+ "Index Name": "alert_types_id_org_id_key",
+ "Relation Name": "alert_types",
+ "Schema": "analytics",
+ "Alias": "altt",
+ "Startup Cost": 0.15,
+ "Total Cost": 0.17,
+ "Plan Rows": 1,
+ "Plan Width": 72,
+ "Actual Startup Time": 0.002,
+ "Actual Total Time": 0.003,
+ "Actual Rows": 1,
+ "Actual Loops": 32352,
+ "Output": ["altt.id", "altt.version", "altt.code", "altt.name", "altt.org_id"],
+ "Index Cond": "(a.type_id = altt.id)",
+ "Rows Removed by Index Recheck": 0,
+ "Shared Hit Blocks": 64704,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ },
+ {
+ "Node Type": "Index Scan",
+ "Parent Relationship": "Inner",
+ "Scan Direction": "Forward",
+ "Index Name": "alert_subtypes_id_org_id_key",
+ "Relation Name": "alert_subtypes",
+ "Schema": "analytics",
+ "Alias": "altst",
+ "Startup Cost": 0.14,
+ "Total Cost": 0.16,
+ "Plan Rows": 1,
+ "Plan Width": 23,
+ "Actual Startup Time": 0.002,
+ "Actual Total Time": 0.003,
+ "Actual Rows": 1,
+ "Actual Loops": 32352,
+ "Output": ["altst.id", "altst.version", "altst.code", "altst.name", "altst.org_id"],
+ "Index Cond": "(a.subtype_id = altst.id)",
+ "Rows Removed by Index Recheck": 0,
+ "Shared Hit Blocks": 63472,
+ "Shared Read Blocks": 0,
+ "Shared Dirtied Blocks": 0,
+ "Shared Written Blocks": 0,
+ "Local Hit Blocks": 0,
+ "Local Read Blocks": 0,
+ "Local Dirtied Blocks": 0,
+ "Local Written Blocks": 0,
+ "Temp Read Blocks": 0,
+ "Temp Written Blocks": 0,
+ "I/O Read Time": 0.000,
+ "I/O Write Time": 0.000
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "Planning Time": 0.507,
+ "Triggers": [
+ ],
+ "Execution Time": 1953.684
+ }
+]
diff --git a/app/services/plan-service.ts b/app/services/plan-service.ts
new file mode 100644
index 0000000..5d7dc3c
--- /dev/null
+++ b/app/services/plan-service.ts
@@ -0,0 +1,166 @@
+import {IPlan} from '../interfaces/iplan';
+import {HighlightType, EstimateDirection} from '../enums';
+/// <reference path="moment.d.ts" />
+/// <reference path="lodash.d.ts" />
+
+export class PlanService {
+ // plan property keys
+ NODE_TYPE_PROP: string = 'Node Type';
+ ACTUAL_ROWS_PROP: string = 'Actual Rows';
+ PLAN_ROWS_PROP: string = 'Plan Rows';
+ ACTUAL_TOTAL_TIME_PROP: string = 'Actual Total Time';
+ ACTUAL_LOOPS_PROP: string = 'Actual Loops';
+ TOTAL_COST_PROP: string = 'Total Cost';
+ PLANS_PROP: string = 'Plans';
+
+
+ // computed by pev
+ COMPUTED_TAGS_PROP: string = "*Tags";
+
+ COSTLIEST_NODE_PROP: string = "*Costiest Node (by cost)";
+ LARGEST_NODE_PROP: string = "*Largest Node (by rows)";
+ SLOWEST_NODE_PROP: string = "*Slowest Node (by duration)";
+
+ MAXIMUM_COSTS_PROP: string = '*Most Expensive Node (cost)';
+ MAXIMUM_ROWS_PROP: string = '*Largest Node (rows)';
+ MAXIMUM_DURATION_PROP: string = '*Slowest Node (time)';
+ ACTUAL_DURATION_PROP: string = '*Actual Duration';
+ ACTUAL_COST_PROP: string = '*Actual Cost';
+ PLANNER_ESTIMATE_FACTOR: string = '*Planner Row Estimate Factor';
+ PLANNER_ESIMATE_DIRECTION: string = '*Planner Row Estimate Direction';
+
+ ARRAY_INDEX_KEY: string = 'arrayIndex';
+
+ private _maxRows: number = 0;
+ private _maxCost: number = 0;
+ private _maxDuration: number = 0;
+
+ getPlans(): Array<IPlan> {
+ var plans: Array<IPlan> = [];
+
+ for (var i in localStorage) {
+ plans.push(JSON.parse(localStorage[i]));
+ }
+
+ return plans;
+ }
+
+ getPlan(id: string): IPlan {
+ return JSON.parse(localStorage.getItem(id));
+ }
+
+ createPlan(planName: string, planContent: string, planQuery): IPlan {
+ var plan: IPlan = {
+ id: 'plan_' + new Date().getTime().toString(),
+ name: planName || 'plan created on ' + moment().format('LLL'),
+ createdOn: new Date(),
+ content: JSON.parse(planContent)[0],
+ query: planQuery
+ };
+
+ this.analyzePlan(plan);
+ return plan;
+ }
+
+ analyzePlan(plan: IPlan) {
+ this.processNode(plan.content.Plan);
+ plan.content[this.MAXIMUM_ROWS_PROP] = this._maxRows;
+ plan.content[this.MAXIMUM_COSTS_PROP] = this._maxCost;
+ plan.content[this.MAXIMUM_DURATION_PROP] = this._maxDuration;
+
+ this.findOutlierNodes(plan.content.Plan);
+
+ localStorage.setItem(plan.id, JSON.stringify(plan));
+ }
+
+ deletePlan(plan: IPlan) {
+ localStorage.removeItem(plan.id);
+ }
+
+ deleteAllPlans() {
+ localStorage.clear();
+ }
+
+ // recursively walk down the plan to compute various metrics
+ processNode(node) {
+ this.calculatePlannerEstimate(node);
+ this.calculateActuals(node);
+
+ _.each(node, (value, key) => {
+ this.calculateMaximums(node, key, value);
+
+ if (key === this.PLANS_PROP) {
+ _.each(value, (value) => {
+ this.processNode(value);
+ })
+ }
+ });
+ }
+
+ calculateMaximums(node, key, value) {
+ if (key === this.ACTUAL_ROWS_PROP && this._maxRows < value) {
+ this._maxRows = value;
+ }
+ if (key === this.ACTUAL_COST_PROP && this._maxCost < value) {
+ this._maxCost = value;
+ }
+
+ if (key === this.ACTUAL_DURATION_PROP && this._maxDuration < value) {
+ this._maxDuration = value;
+ }
+ }
+
+ findOutlierNodes(node) {
+ node[this.SLOWEST_NODE_PROP] = false;
+ node[this.LARGEST_NODE_PROP] = false;
+ node[this.COSTLIEST_NODE_PROP] = false;
+
+ if (node[this.ACTUAL_COST_PROP] === this._maxCost) {
+ node[this.COSTLIEST_NODE_PROP] = true;
+ }
+ if (node[this.ACTUAL_ROWS_PROP] === this._maxRows) {
+ node[this.LARGEST_NODE_PROP] = true;
+ }
+ if (node[this.ACTUAL_DURATION_PROP] === this._maxDuration) {
+ node[this.SLOWEST_NODE_PROP] = true;
+ }
+
+ _.each(node, (value, key) => {
+ if (key === this.PLANS_PROP) {
+ _.each(value, (value) => {
+ this.findOutlierNodes(value);
+ })
+ }
+ });
+ }
+
+ // actual duration and actual cost are calculated by subtracting child values from the total
+ calculateActuals(node) {
+ node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_TOTAL_TIME_PROP];
+ node[this.ACTUAL_COST_PROP] = node[this.TOTAL_COST_PROP];
+
+ _.each(node.Plans, subPlan => {
+ node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] - subPlan[this.ACTUAL_TOTAL_TIME_PROP];
+ node[this.ACTUAL_COST_PROP] = node[this.ACTUAL_COST_PROP] - subPlan[this.TOTAL_COST_PROP];
+ });
+
+ if (node[this.ACTUAL_COST_PROP] < 0) {
+ node[this.ACTUAL_COST_PROP] = 0;
+ }
+
+ // since time is reported for an invidual loop, actual duration must be adjusted by number of loops
+ node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] * node[this.ACTUAL_LOOPS_PROP];
+ }
+
+ // figure out order of magnitude by which the planner mis-estimated how many rows would be
+ // invloved in this node
+ calculatePlannerEstimate(node) {
+ node[this.PLANNER_ESTIMATE_FACTOR] = node[this.ACTUAL_ROWS_PROP] / node[this.PLAN_ROWS_PROP];
+ node[this.PLANNER_ESIMATE_DIRECTION] = EstimateDirection.under;
+
+ if (node[this.PLANNER_ESTIMATE_FACTOR] < 1) {
+ node[this.PLANNER_ESIMATE_DIRECTION] = EstimateDirection.over;
+ node[this.PLANNER_ESTIMATE_FACTOR] = node[this.PLAN_ROWS_PROP] / node[this.ACTUAL_ROWS_PROP];
+ }
+ }
+}
diff --git a/gulpfile.ts b/gulpfile.ts
new file mode 100644
index 0000000..673804c
--- /dev/null
+++ b/gulpfile.ts
@@ -0,0 +1,83 @@
+import * as gulp from 'gulp';
+import {runSequence, task} from './tools/utils';
+
+// --------------
+// Clean (override).
+gulp.task('clean', task('clean', 'all'));
+gulp.task('clean.dist', task('clean', 'dist'));
+gulp.task('clean.test', task('clean', 'test'));
+gulp.task('clean.tmp', task('clean', 'tmp'));
+
+gulp.task('check.versions', task('check.versions'));
+
+// --------------
+// Postinstall.
+gulp.task('postinstall', done =>
+ runSequence('clean',
+ 'npm',
+ done));
+
+// --------------
+// Build dev.
+gulp.task('build.dev', done =>
+ runSequence('clean.dist',
+ 'tslint',
+ 'build.sass.dev',
+ 'build.img.dev',
+ 'build.fonts.dev',
+ 'build.js.dev',
+ 'build.index',
+ done));
+
+// --------------
+// Build prod.
+gulp.task('build.prod', done =>
+ runSequence('clean.dist',
+ 'clean.tmp',
+ 'tslint',
+ 'build.sass.dev',
+ 'build.img.dev',
+ 'build.fonts.dev',
+ 'build.html_css.prod',
+ 'build.deps',
+ 'build.js.prod',
+ 'build.bundles',
+ 'build.index',
+ done));
+
+// --------------
+// Watch.
+gulp.task('build.dev.watch', done =>
+ runSequence('build.dev',
+ 'watch.dev',
+ done));
+
+gulp.task('build.test.watch', done =>
+ runSequence('build.test',
+ 'watch.test',
+ done));
+
+// --------------
+// Test.
+gulp.task('test', done =>
+ runSequence('clean.test',
+ 'tslint',
+ 'build.test',
+ 'karma.start',
+ done));
+
+// --------------
+// Serve.
+gulp.task('serve', done =>
+ runSequence('build.dev',
+ 'server.start',
+ 'watch.serve',
+ done));
+
+// --------------
+// Docs
+// Disabled until https://github.com/sebastian-lenz/typedoc/issues/162 gets resolved
+// gulp.task('docs', done =>
+// runSequence('build.docs',
+// 'serve.docs',
+// done));
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..7f7e5cc
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,103 @@
+// Karma configuration
+// Generated on Wed Jul 15 2015 09:44:02 GMT+0200 (Romance Daylight Time)
+'use strict';
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: './',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'node_modules/zone.js/dist/zone-microtask.js',
+ 'node_modules/zone.js/dist/long-stack-trace-zone.js',
+ 'node_modules/zone.js/dist/jasmine-patch.js',
+ 'node_modules/es6-module-loader/dist/es6-module-loader.js',
+ 'node_modules/traceur/bin/traceur-runtime.js', // Required by PhantomJS2, otherwise it shouts ReferenceError: Can't find variable: require
+ 'node_modules/traceur/bin/traceur.js',
+ 'node_modules/systemjs/dist/system.src.js',
+ 'node_modules/reflect-metadata/Reflect.js',
+
+ { pattern: 'node_modules/angular2/**/*.js', included: false, watched: false },
+ { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false },
+ { pattern: 'test/**/*.js', included: false, watched: true },
+ { pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: false, watched: false }, // PhantomJS2 (and possibly others) might require it
+
+ 'test-main.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ 'node_modules/angular2/**/*_spec.js'
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['mocha'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: [
+ 'PhantomJS2',
+ 'Chrome'
+ ],
+
+
+ customLaunchers: {
+ Chrome_travis_ci: {
+ base: 'Chrome',
+ flags: ['--no-sandbox']
+ }
+ },
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false
+ });
+
+ if (process.env.APPVEYOR) {
+ config.browsers = ['IE'];
+ config.singleRun = true;
+ config.browserNoActivityTimeout = 90000; // Note: default value (10000) is not enough
+ }
+
+ if (process.env.TRAVIS || process.env.CIRCLECI) {
+ config.browsers = ['Chrome_travis_ci'];
+ config.singleRun = true;
+ }
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2f2334b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "pev",
+ "version": "0.0.0",
+ "description": "Postgres Explain Visualizer",
+ "repository": {
+ "url": "http://www.tatiyants.com"
+ },
+ "scripts": {
+ "build.dev": "gulp build.dev",
+ "build.dev.watch": "gulp build.dev.watch",
+ "build.prod": "gulp build.prod --env prod",
+ "build.test": "gulp build.test",
+ "build.test.watch": "gulp build.test.watch",
+ "docs": "npm run gulp -- build.docs && npm run gulp -- serve.docs",
+ "gulp": "gulp",
+ "karma": "karma",
+ "karma.start": "karma start",
+ "lint": "gulp tslint",
+ "postinstall": "tsd reinstall --clean && tsd link && tsd rebundle && gulp check.versions && gulp postinstall",
+ "reinstall": "rimraf node_modules && npm cache clean && npm install",
+ "start": "gulp serve --env dev",
+ "serve.dev": "gulp serve --env dev",
+ "tasks.list": "gulp --tasks-simple",
+ "test": "gulp test",
+ "tsd": "tsd"
+ },
+ "author": "Alex Tatiyants",
+ "license": "MIT",
+ "devDependencies": {
+ "async": "^1.4.2",
+ "chalk": "^1.1.1",
+ "connect-livereload": "^0.5.3",
+ "del": "^1.1.1",
+ "express": "~4.13.1",
+ "extend": "^3.0.0",
+ "gulp": "^3.9.0",
+ "gulp-compass": "^2.1.0",
+ "gulp-concat": "^2.5.2",
+ "gulp-filter": "^2.0.2",
+ "gulp-inject": "^1.3.1",
+ "gulp-inline-ng2-template": "^0.0.7",
+ "gulp-load-plugins": "^0.10.0",
+ "gulp-minify-css": "^1.1.6",
+ "gulp-minify-html": "^1.0.3",
+ "gulp-plumber": "~1.0.1",
+ "gulp-sass": "^2.0.4",
+ "gulp-shell": "~0.4.3",
+ "gulp-sourcemaps": "~1.5.2",
+ "gulp-template": "^3.0.0",
+ "gulp-tslint": "^3.3.0",
+ "gulp-tslint-stylish": "^1.0.4",
+ "gulp-typescript": "~2.8.2",
+ "gulp-uglify": "^1.2.0",
+ "gulp-util": "^3.0.7",
+ "gulp-watch": "^4.2.4",
+ "jasmine-core": "~2.3.4",
+ "karma": "~0.13.15",
+ "karma-chrome-launcher": "~0.2.0",
+ "karma-ie-launcher": "^0.2.0",
+ "karma-jasmine": "~0.3.6",
+ "karma-mocha-reporter": "^1.1.1",
+ "karma-phantomjs2-launcher": "^0.3.2",
+ "merge-stream": "^1.0.0",
+ "open": "0.0.5",
+ "rimraf": "^2.4.3",
+ "run-sequence": "^1.1.0",
+ "semver": "^5.0.3",
+ "serve-static": "^1.9.2",
+ "slash": "~1.0.0",
+ "stream-series": "^0.1.1",
+ "systemjs-builder": "^0.14.8",
+ "tiny-lr": "^0.2.1",
+ "traceur": "^0.0.91",
+ "ts-node": "^0.5.4",
+ "tsd": "^0.6.4",
+ "typedoc": "^0.3.12",
+ "typescript": "~1.7.3",
+ "typescript-register": "^1.1.0",
+ "yargs": "^3.25.0"
+ },
+ "dependencies": {
+ "angular2": "2.0.0-beta.0",
+ "bootstrap": "^3.3.5",
+ "es6-module-loader": "^0.17.8",
+ "es6-shim": "^0.33.3",
+ "lodash": "^3.10.1",
+ "moment": "^2.10.6",
+ "reflect-metadata": "^0.1.2",
+ "rxjs": "5.0.0-beta.0",
+ "systemjs": "^0.19.4",
+ "zone.js": "0.5.10"
+ }
+}
diff --git a/test-main.js b/test-main.js
new file mode 100644
index 0000000..457ee32
--- /dev/null
+++ b/test-main.js
@@ -0,0 +1,53 @@
+// Turn on full stack traces in errors to help debugging
+Error.stackTraceLimit=Infinity;
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 100;
+
+// Cancel Karma's synchronous start,
+// we will call `__karma__.start()` later, once all the specs are loaded.
+__karma__.loaded = function() {};
+
+System.config({
+ baseURL: '/base/',
+ defaultJSExtensions: true,
+ paths: {
+ 'angular2/*': 'node_modules/angular2/*.js',
+ 'rxjs/*': 'node_modules/rxjs/*.js'
+ }
+});
+
+System.import('angular2/src/platform/browser/browser_adapter').then(function(browser_adapter) {
+ browser_adapter.BrowserDomAdapter.makeCurrent();
+}).then(function() {
+ return Promise.all(
+ Object.keys(window.__karma__.files) // All files served by Karma.
+ .filter(onlySpecFiles)
+ .map(file2moduleName)
+ .map(function(path) {
+ return System.import(path).then(function(module) {
+ if (module.hasOwnProperty('main')) {
+ module.main();
+ } else {
+ throw new Error('Module ' + path + ' does not implement main() method.');
+ }
+ });
+ }));
+})
+.then(function() {
+ __karma__.start();
+}, function(error) {
+ console.error(error.stack || error);
+ __karma__.start();
+});
+
+
+function onlySpecFiles(path) {
+ return /[\.|_]spec\.js$/.test(path);
+}
+
+// Normalize paths to module names.
+function file2moduleName(filePath) {
+ return filePath.replace(/\\/g, '/')
+ .replace(/^\/base\//, '')
+ .replace(/\.js/, '');
+}
diff --git a/tools/config.ts b/tools/config.ts
new file mode 100644
index 0000000..9c5559a
--- /dev/null
+++ b/tools/config.ts
@@ -0,0 +1,101 @@
+import {readFileSync} from 'fs';
+import {argv} from 'yargs';
+
+
+// --------------
+// Configuration.
+export const ENV = argv['env'] || 'dev';
+export const DEBUG = argv['debug'] || false;
+export const PORT = argv['port'] || 5555;
+export const LIVE_RELOAD_PORT = argv['reload-port'] || 4002;
+export const DOCS_PORT = argv['docs-port'] || 4003;
+export const APP_BASE = argv['base'] || '/';
+
+export const APP_TITLE = 'My Angular2 App';
+
+export const APP_SRC = 'app';
+export const ASSETS_SRC = `${APP_SRC}/assets`;
+
+export const TOOLS_DIR = 'tools';
+export const TMP_DIR = 'tmp';
+export const TEST_DEST = 'test';
+export const DOCS_DEST = 'docs';
+export const APP_DEST = `dist/${ENV}`;
+export const ASSETS_DEST = `${APP_DEST}/assets`;
+export const BUNDLES_DEST = `${APP_DEST}/bundles`;
+export const CSS_DEST = `${APP_DEST}/css`;
+export const FONTS_DEST = `${APP_DEST}/fonts`;
+export const LIB_DEST = `${APP_DEST}/lib`;
+export const APP_ROOT = ENV === 'dev' ? `${APP_BASE}${APP_DEST}/` : `${APP_BASE}`;
+export const VERSION = appVersion();
+
+export const VERSION_NPM = '2.14.7';
+export const VERSION_NODE = '4.0.0';
+
+// Declare NPM dependencies (Note that globs should not be injected).
+export const NPM_DEPENDENCIES = [
+ { src: 'systemjs/dist/system-polyfills.js', dest: LIB_DEST },
+
+ { src: 'es6-shim/es6-shim.min.js', inject: 'shims', dest: LIB_DEST },
+ { src: 'reflect-metadata/Reflect.js', inject: 'shims', dest: LIB_DEST },
+ { src: 'systemjs/dist/system.src.js', inject: 'shims', dest: LIB_DEST },
+ { src: 'angular2/bundles/angular2-polyfills.js', inject: 'shims', dest: LIB_DEST },
+
+ // Faster dev page load
+ { src: 'rxjs/bundles/Rx.min.js', inject: 'libs', dest: LIB_DEST },
+ { src: 'angular2/bundles/angular2.min.js', inject: 'libs', dest: LIB_DEST },
+ { src: 'angular2/bundles/router.js', inject: 'libs', dest: LIB_DEST }, // use router.min.js with alpha47
+ { src: 'angular2/bundles/http.min.js', inject: 'libs', dest: LIB_DEST },
+
+ { src: 'lodash/index.js', inject: 'libs', dest: LIB_DEST },
+ { src: 'moment/moment.js', inject: 'libs', dest: LIB_DEST }
+];
+
+// Declare local files that needs to be injected
+export const APP_ASSETS = [
+ { src: `${ASSETS_SRC}/css/styles.css`, inject: true, dest: CSS_DEST}
+];
+
+NPM_DEPENDENCIES
+ .filter(d => !/\*/.test(d.src)) // Skip globs
+ .forEach(d => d.src = require.resolve(d.src));
+
+export const DEPENDENCIES = NPM_DEPENDENCIES.concat(APP_ASSETS);
+
+
+// ----------------
+// SystemsJS Configuration.
+const SYSTEM_CONFIG_DEV = {
+ defaultJSExtensions: true,
+ paths: {
+ 'bootstrap': `${APP_ROOT}bootstrap`,
+ '*': `${APP_BASE}node_modules/*`
+ }
+};
+
+const SYSTEM_CONFIG_PROD = {
+ defaultJSExtensions: true,
+ bundles: {
+ 'bundles/app': ['bootstrap']
+ }
+};
+
+export const SYSTEM_CONFIG = ENV === 'dev' ? SYSTEM_CONFIG_DEV : SYSTEM_CONFIG_PROD;
+
+// This is important to keep clean module names as 'module name == module uri'.
+export const SYSTEM_CONFIG_BUILDER = {
+ defaultJSExtensions: true,
+ paths: {
+ '*': `${TMP_DIR}/*`,
+ 'angular2/*': 'node_modules/angular2/*',
+ 'rxjs/*': 'node_modules/rxjs/*'
+ }
+};
+
+
+// --------------
+// Private.
+function appVersion(): number|string {
+ var pkg = JSON.parse(readFileSync('package.json').toString());
+ return pkg.version;
+}
diff --git a/tools/tasks/build.bundles.ts b/tools/tasks/build.bundles.ts
new file mode 100644
index 0000000..52eb381
--- /dev/null
+++ b/tools/tasks/build.bundles.ts
@@ -0,0 +1,26 @@
+import {parallel} from 'async';
+import {join} from 'path';
+import * as Builder from 'systemjs-builder';
+import {BUNDLES_DEST, SYSTEM_CONFIG_BUILDER} from '../config';
+
+const BUNDLE_OPTS = {
+ minify: true,
+ sourceMaps: true,
+ format: 'cjs'
+};
+
+export = function bundles(gulp, plugins) {
+ return function (done) {
+ let builder = new Builder(SYSTEM_CONFIG_BUILDER);
+
+ parallel([
+ bundleApp
+ ], () => done());
+
+ function bundleApp(done) {
+ builder.bundle(
+ 'bootstrap - angular2/*',
+ join(BUNDLES_DEST, 'app.js'), BUNDLE_OPTS).then(done);
+ }
+ };
+};
diff --git a/tools/tasks/build.deps.ts b/tools/tasks/build.deps.ts
new file mode 100644
index 0000000..071f4cd
--- /dev/null
+++ b/tools/tasks/build.deps.ts
@@ -0,0 +1,20 @@
+import * as merge from 'merge-stream';
+import {DEPENDENCIES} from '../config';
+
+export = function buildDepsProd(gulp, plugins) {
+ return function () {
+ let stream = merge();
+
+ DEPENDENCIES.forEach(dep => {
+ stream.add(addStream(dep));
+ });
+
+ return stream;
+
+ function addStream(dep) {
+ let stream = gulp.src(dep.src);
+ stream.pipe(gulp.dest(dep.dest));
+ return stream;
+ }
+ };
+};
diff --git a/tools/tasks/build.docs.ts b/tools/tasks/build.docs.ts
new file mode 100644
index 0000000..a464c67
--- /dev/null
+++ b/tools/tasks/build.docs.ts
@@ -0,0 +1,27 @@
+import {join} from 'path';
+import {APP_SRC, APP_TITLE, DOCS_DEST} from '../config';
+
+export = function buildDocs(gulp, plugins, option) {
+ return function() {
+
+ let src = [
+ join(APP_SRC, '**/*.ts'),
+ '!' + join(APP_SRC, '**/*_spec.ts')
+ ];
+
+ return gulp.src(src)
+ .pipe(plugins.typedoc({
+ // TypeScript options (see typescript docs)
+ module: 'commonjs',
+ target: 'es5',
+ includeDeclarations: true,
+ // Output options (see typedoc docs)
+ out: DOCS_DEST,
+ json: join(DOCS_DEST , 'data/docs.json' ),
+ name: APP_TITLE,
+ ignoreCompilerErrors: false,
+ experimentalDecorators: true,
+ version: true
+ }));
+ };
+}
diff --git a/tools/tasks/build.fonts.dev.ts b/tools/tasks/build.fonts.dev.ts
new file mode 100644
index 0000000..6aa0b92
--- /dev/null
+++ b/tools/tasks/build.fonts.dev.ts
@@ -0,0 +1,15 @@
+import {join} from 'path';
+import {APP_SRC, APP_DEST} from '../config';
+
+export = function buildFontsDev(gulp, plugins) {
+ return function () {
+ return gulp.src([
+ join(APP_SRC, '**/*.eot'),
+ join(APP_SRC, '**/*.ttf'),
+ join(APP_SRC, '**/*.woff'),
+ join(APP_SRC, '**/*.woff2'),
+ join(APP_SRC, '**/*.otf')
+ ])
+ .pipe(gulp.dest(APP_DEST));
+ };
+}
diff --git a/tools/tasks/build.html_css.prod.ts b/tools/tasks/build.html_css.prod.ts
new file mode 100644
index 0000000..93d0aa9
--- /dev/null
+++ b/tools/tasks/build.html_css.prod.ts
@@ -0,0 +1,24 @@
+import * as merge from 'merge-stream';
+import {join} from 'path';
+import {APP_SRC, TMP_DIR} from '../config';
+
+// const HTML_MINIFIER_OPTS = { empty: true };
+
+export = function buildJSDev(gulp, plugins) {
+ return function () {
+
+ return merge(minifyHtml(), minifyCss());
+
+ function minifyHtml() {
+ return gulp.src(join(APP_SRC, '**/*.html'))
+ // .pipe(plugins.minifyHtml(HTML_MINIFIER_OPTS))
+ .pipe(gulp.dest(TMP_DIR));
+ }
+
+ function minifyCss() {
+ return gulp.src(join(APP_SRC, '**/*.css'))
+ .pipe(plugins.minifyCss())
+ .pipe(gulp.dest(TMP_DIR));
+ }
+ };
+};
diff --git a/tools/tasks/build.img.dev.ts b/tools/tasks/build.img.dev.ts
new file mode 100644
index 0000000..28a7897
--- /dev/null
+++ b/tools/tasks/build.img.dev.ts
@@ -0,0 +1,14 @@
+import {join} from 'path';
+import {APP_SRC, APP_DEST} from '../config';
+
+export = function buildImagesDev(gulp, plugins) {
+ return function () {
+ return gulp.src([
+ join(APP_SRC, '**/*.gif'),
+ join(APP_SRC, '**/*.jpg'),
+ join(APP_SRC, '**/*.png'),
+ join(APP_SRC, '**/*.svg')
+ ])
+ .pipe(gulp.dest(APP_DEST));
+ };
+}
diff --git a/tools/tasks/build.index.ts b/tools/tasks/build.index.ts
new file mode 100644
index 0000000..bbf98ee
--- /dev/null
+++ b/tools/tasks/build.index.ts
@@ -0,0 +1,34 @@
+import {join, sep} from 'path';
+import {APP_SRC, APP_DEST, DEPENDENCIES, ENV} from '../config';
+import {transformPath, templateLocals} from '../utils';
+
+export = function buildIndexDev(gulp, plugins) {
+ return function () {
+ return gulp.src(join(APP_SRC, 'index.html'))
+ // NOTE: There might be a way to pipe in loop.
+ .pipe(inject('shims'))
+ .pipe(inject('libs'))
+ .pipe(inject())
+ .pipe(plugins.template(templateLocals()))
+ .pipe(gulp.dest(APP_DEST));
+ };
+
+
+ function inject(name?: string) {
+ return plugins.inject(gulp.src(getInjectablesDependenciesRef(name), { read: false }), {
+ name,
+ transform: transformPath(plugins, 'dev')
+ });
+ }
+
+ function getInjectablesDependenciesRef(name?: string) {
+ return DEPENDENCIES
+ .filter(dep => dep['inject'] && dep['inject'] === (name || true))
+ .map(mapPath);
+ }
+
+ function mapPath(dep) {
+ let prodPath = join(dep.dest, dep.src.split(sep).pop());
+ return ('prod' === ENV ? prodPath : dep.src );
+ }
+};
diff --git a/tools/tasks/build.js.dev.ts b/tools/tasks/build.js.dev.ts
new file mode 100644
index 0000000..dfe9539
--- /dev/null
+++ b/tools/tasks/build.js.dev.ts
@@ -0,0 +1,25 @@
+import {join} from 'path';
+import {APP_SRC, APP_DEST} from '../config';
+import {templateLocals, tsProjectFn} from '../utils';
+
+export = function buildJSDev(gulp, plugins) {
+ let tsProject = tsProjectFn(plugins);
+ return function () {
+ let src = [
+ join(APP_SRC, '**/*.ts'),
+ '!' + join(APP_SRC, '**/*_spec.ts')
+ ];
+
+ let result = gulp.src(src)
+ .pipe(plugins.plumber())
+ // Won't be required for non-production build after the change
+ .pipe(plugins.inlineNg2Template({ base: APP_SRC }))
+ .pipe(plugins.sourcemaps.init())
+ .pipe(plugins.typescript(tsProject));
+
+ return result.js
+ .pipe(plugins.sourcemaps.write())
+ .pipe(plugins.template(templateLocals()))
+ .pipe(gulp.dest(APP_DEST));
+ };
+};
diff --git a/tools/tasks/build.js.prod.ts b/tools/tasks/build.js.prod.ts
new file mode 100644
index 0000000..54ed21a
--- /dev/null
+++ b/tools/tasks/build.js.prod.ts
@@ -0,0 +1,22 @@
+import {join} from 'path';
+import {APP_SRC, TMP_DIR} from '../config';
+import {templateLocals, tsProjectFn} from '../utils';
+
+export = function buildJSDev(gulp, plugins) {
+ return function () {
+ let tsProject = tsProjectFn(plugins);
+ let src = [
+ join(APP_SRC, '**/*.ts'),
+ '!' + join(APP_SRC, '**/*_spec.ts')
+ ];
+
+ let result = gulp.src(src)
+ .pipe(plugins.plumber())
+ .pipe(plugins.inlineNg2Template({ base: TMP_DIR }))
+ .pipe(plugins.typescript(tsProject));
+
+ return result.js
+ .pipe(plugins.template(templateLocals()))
+ .pipe(gulp.dest(TMP_DIR));
+ };
+};
diff --git a/tools/tasks/build.sass.dev.ts b/tools/tasks/build.sass.dev.ts
new file mode 100644
index 0000000..a2127be
--- /dev/null
+++ b/tools/tasks/build.sass.dev.ts
@@ -0,0 +1,22 @@
+import {join} from 'path';
+import {APP_SRC} from '../config';
+
+export = function buildSassDev(gulp, plugins, option) {
+ return function() {
+ return gulp.src(join(APP_SRC, '**', '*.scss'))
+ .pipe(plugins.plumber({
+ // this allows gulp not to crash on sass compilation errors
+ errorHandler: function(error) {
+ console.log(error.message);
+ this.emit('end');
+ }
+ }))
+ .pipe(plugins.compass({
+ // config_file: './config.rb',
+ style: 'compressed',
+ css: 'app/assets/css',
+ sass: join(APP_SRC, 'assets/sass'),
+ }))
+ .pipe(gulp.dest(join(APP_SRC, 'assets')));
+ };
+}
diff --git a/tools/tasks/build.test.ts b/tools/tasks/build.test.ts
new file mode 100644
index 0000000..3e089cd
--- /dev/null
+++ b/tools/tasks/build.test.ts
@@ -0,0 +1,21 @@
+import {join} from 'path';
+import {APP_SRC, TEST_DEST} from '../config';
+import {tsProjectFn} from '../utils';
+
+export = function buildTest(gulp, plugins) {
+ return function () {
+ let tsProject = tsProjectFn(plugins);
+ let src = [
+ join(APP_SRC, '**/*.ts'),
+ '!' + join(APP_SRC, 'bootstrap.ts')
+ ];
+
+ let result = gulp.src(src)
+ .pipe(plugins.plumber())
+ .pipe(plugins.inlineNg2Template({ base: APP_SRC }))
+ .pipe(plugins.typescript(tsProject));
+
+ return result.js
+ .pipe(gulp.dest(TEST_DEST));
+ };
+};
diff --git a/tools/tasks/check.versions.ts b/tools/tasks/check.versions.ts
new file mode 100644
index 0000000..211d5ed
--- /dev/null
+++ b/tools/tasks/check.versions.ts
@@ -0,0 +1,35 @@
+import {VERSION_NPM, VERSION_NODE} from '../config';
+
+function reportError(message: string) {
+ console.error(require('chalk').white.bgRed.bold(message));
+ process.exit(1);
+}
+
+module.exports = function check(gulp, plugins) {
+ return function () {
+ let exec = require('child_process').exec;
+ let semver = require('semver');
+
+ exec('npm --version',
+ function (error, stdout, stderr) {
+ if (error !== null) {
+ reportError('npm preinstall error: ' + error + stderr);
+ }
+
+ if (!semver.gte(stdout, VERSION_NPM)) {
+ reportError('NPM is not in required version! Required is ' + VERSION_NPM + ' and you\'re using ' + stdout);
+ }
+ });
+
+ exec('node --version',
+ function (error, stdout, stderr) {
+ if (error !== null) {
+ reportError('npm preinstall error: ' + error + stderr);
+ }
+
+ if (!semver.gte(stdout, VERSION_NODE)) {
+ reportError('NODE is not in required version! Required is ' + VERSION_NODE + ' and you\'re using ' + stdout);
+ }
+ });
+ };
+};
diff --git a/tools/tasks/clean.ts b/tools/tasks/clean.ts
new file mode 100644
index 0000000..9f0ebf2
--- /dev/null
+++ b/tools/tasks/clean.ts
@@ -0,0 +1,34 @@
+import * as async from 'async';
+import * as del from 'del';
+import {APP_DEST, TEST_DEST, TMP_DIR} from '../config';
+
+export = function clean(gulp, plugins, option) {
+ return function (done) {
+
+ switch(option) {
+ case 'all' : cleanAll(done); break;
+ case 'dist' : cleanDist(done); break;
+ case 'test' : cleanTest(done); break;
+ case 'tmp' : cleanTmp(done); break;
+ default: done();
+ }
+
+ };
+};
+
+function cleanAll(done) {
+ async.parallel([
+ cleanDist,
+ cleanTest,
+ cleanTmp
+ ], done);
+}
+function cleanDist(done) {
+ del(APP_DEST, done);
+}
+function cleanTest(done) {
+ del(TEST_DEST, done);
+}
+function cleanTmp(done) {
+ del(TMP_DIR, done);
+}
diff --git a/tools/tasks/karma.start.ts b/tools/tasks/karma.start.ts
new file mode 100644
index 0000000..313aacd
--- /dev/null
+++ b/tools/tasks/karma.start.ts
@@ -0,0 +1,11 @@
+import * as karma from 'karma';
+import {join} from 'path';
+
+export = function karmaStart() {
+ return function (done) {
+ new (<any>karma).Server({
+ configFile: join(process.cwd(), 'karma.conf.js'),
+ singleRun: true
+ }).start(done);
+ };
+};
diff --git a/tools/tasks/npm.ts b/tools/tasks/npm.ts
new file mode 100644
index 0000000..2ab5e76
--- /dev/null
+++ b/tools/tasks/npm.ts
@@ -0,0 +1,5 @@
+export = function npm(gulp, plugins) {
+ return plugins.shell.task([
+ 'npm prune'
+ ]);
+};
diff --git a/tools/tasks/serve.docs.ts b/tools/tasks/serve.docs.ts
new file mode 100644
index 0000000..2aa5f05
--- /dev/null
+++ b/tools/tasks/serve.docs.ts
@@ -0,0 +1,7 @@
+import {serveDocs} from '../utils';
+
+export = function serverStart(gulp, plugins) {
+ return function () {
+ serveDocs();
+ };
+};
diff --git a/tools/tasks/server.start.ts b/tools/tasks/server.start.ts
new file mode 100644
index 0000000..e733e92
--- /dev/null
+++ b/tools/tasks/server.start.ts
@@ -0,0 +1,7 @@
+import {serveSPA} from '../utils';
+
+export = function serverStart(gulp, plugins) {
+ return function () {
+ serveSPA();
+ };
+};
diff --git a/tools/tasks/tsd.ts b/tools/tasks/tsd.ts
new file mode 100644
index 0000000..bf38ad0
--- /dev/null
+++ b/tools/tasks/tsd.ts
@@ -0,0 +1,7 @@
+export = function tsd(gulp, plugins) {
+ return plugins.shell.task([
+ 'tsd reinstall --clean',
+ 'tsd link',
+ 'tsd rebundle'
+ ]);
+};
diff --git a/tools/tasks/tslint.ts b/tools/tasks/tslint.ts
new file mode 100644
index 0000000..ccea264
--- /dev/null
+++ b/tools/tasks/tslint.ts
@@ -0,0 +1,21 @@
+import {join} from 'path';
+import {APP_SRC, TOOLS_DIR} from '../config';
+
+export = function tslint(gulp, plugins) {
+ return function () {
+ let src = [
+ join(APP_SRC, '**/*.ts'),
+ '!' + join(APP_SRC, '**/*.d.ts'),
+ join(TOOLS_DIR, '**/*.ts'),
+ '!' + join(TOOLS_DIR, '**/*.d.ts')
+ ];
+
+ return gulp.src(src)
+ .pipe(plugins.tslint())
+ .pipe(plugins.tslint.report(plugins.tslintStylish, {
+ emitError: false,
+ sort: true,
+ bell: true
+ }));
+ };
+};
diff --git a/tools/tasks/watch.dev.ts b/tools/tasks/watch.dev.ts
new file mode 100644
index 0000000..bfa37a3
--- /dev/null
+++ b/tools/tasks/watch.dev.ts
@@ -0,0 +1,8 @@
+import {join} from 'path';
+import {APP_SRC} from '../config';
+
+export = function watchDev(gulp, plugins) {
+ return function () {
+ plugins.watch(join(APP_SRC, '**/*'), () => gulp.start('build.dev'));
+ };
+};
diff --git a/tools/tasks/watch.serve.ts b/tools/tasks/watch.serve.ts
new file mode 100644
index 0000000..2bb18f1
--- /dev/null
+++ b/tools/tasks/watch.serve.ts
@@ -0,0 +1,12 @@
+import * as runSequence from 'run-sequence';
+import {join} from 'path';
+import {APP_SRC} from '../config';
+import {notifyLiveReload} from '../utils';
+
+export = function watchServe(gulp, plugins) {
+ return function () {
+ plugins.watch(join(APP_SRC, '**'), e =>
+ runSequence('build.dev', () => notifyLiveReload(e))
+ );
+ };
+};
diff --git a/tools/tasks/watch.test.ts b/tools/tasks/watch.test.ts
new file mode 100644
index 0000000..2a8b55b
--- /dev/null
+++ b/tools/tasks/watch.test.ts
@@ -0,0 +1,8 @@
+import {join} from 'path';
+import {APP_SRC} from '../config';
+
+export = function watchTest(gulp, plugins) {
+ return function () {
+ plugins.watch(join(APP_SRC, '**/*.ts'), () => gulp.start('build.test'));
+ };
+};
diff --git a/tools/typings/connect-livereload.d.ts b/tools/typings/connect-livereload.d.ts
new file mode 100644
index 0000000..1b1ef21
--- /dev/null
+++ b/tools/typings/connect-livereload.d.ts
@@ -0,0 +1,5 @@
+declare module 'connect-livereload' {
+ function connectLivereload(options?: any): any;
+ module connectLivereload {}
+ export = connectLivereload;
+}
diff --git a/tools/typings/gulp-load-plugins.d.ts b/tools/typings/gulp-load-plugins.d.ts
new file mode 100644
index 0000000..809c305
--- /dev/null
+++ b/tools/typings/gulp-load-plugins.d.ts
@@ -0,0 +1,42 @@
+// Type definitions for gulp-load-plugins
+// Project: https://github.com/jackfranklin/gulp-load-plugins
+// Definitions by: Joe Skeen <http://github.com/joeskeen>
+// Definitions: https://github.com/borisyankov/DefinitelyTyped
+
+// Does not support ES2015 import (import * as open from 'open').
+
+/** Loads in any gulp plugins and attaches them to an object, freeing you up from having to manually require each gulp plugin. */
+declare module 'gulp-load-plugins' {
+
+ interface IOptions {
+ /** the glob(s) to search for, default ['gulp-*', 'gulp.*'] */
+ pattern?: string[];
+ /** where to find the plugins, searched up from process.cwd(), default 'package.json' */
+ config?: string;
+ /** which keys in the config to look within, default ['dependencies', 'devDependencies', 'peerDependencies'] */
+ scope?: string[];
+ /** what to remove from the name of the module when adding it to the context, default /^gulp(-|\.)/ */
+ replaceString?: RegExp;
+ /** if true, transforms hyphenated plugin names to camel case, default true */
+ camelize?: boolean;
+ /** whether the plugins should be lazy loaded on demand, default true */
+ lazy?: boolean;
+ /** a mapping of plugins to rename, the key being the NPM name of the package, and the value being an alias you define */
+ rename?: IPluginNameMappings;
+ }
+
+ interface IPluginNameMappings {
+ [npmPackageName: string]: string;
+ }
+
+ /** Loads in any gulp plugins and attaches them to an object, freeing you up from having to manually require each gulp plugin. */
+ function gulpLoadPlugins<T extends IGulpPlugins>(options?: IOptions): T;
+ module gulpLoadPlugins {}
+ export = gulpLoadPlugins;
+}
+
+/**
+ * Extend this interface to use Gulp plugins in your gulpfile.js
+ */
+interface IGulpPlugins {
+}
diff --git a/tools/typings/karma.d.ts b/tools/typings/karma.d.ts
new file mode 100644
index 0000000..5d2cd42
--- /dev/null
+++ b/tools/typings/karma.d.ts
@@ -0,0 +1,12 @@
+// Use this minimalistic definition file as bluebird dependency
+// generate a lot of errors.
+
+declare module 'karma' {
+ var karma: IKarma;
+ export = karma;
+ interface IKarma {
+ server: {
+ start(options: any, callback: Function): void
+ };
+ }
+}
diff --git a/tools/typings/merge-stream.d.ts b/tools/typings/merge-stream.d.ts
new file mode 100644
index 0000000..7425d35
--- /dev/null
+++ b/tools/typings/merge-stream.d.ts
@@ -0,0 +1,8 @@
+declare module 'merge-stream' {
+ function mergeStream(...streams: NodeJS.ReadWriteStream[]): MergeStream;
+ interface MergeStream extends NodeJS.ReadWriteStream {
+ add(stream: NodeJS.ReadWriteStream): MergeStream;
+ }
+ module mergeStream {}
+ export = mergeStream;
+} \ No newline at end of file
diff --git a/tools/typings/open.d.ts b/tools/typings/open.d.ts
new file mode 100644
index 0000000..89d4b76
--- /dev/null
+++ b/tools/typings/open.d.ts
@@ -0,0 +1,8 @@
+// https://github.com/borisyankov/DefinitelyTyped/tree/master/open
+// Does not support ES2015 import (import * as open from 'open').
+
+declare module 'open' {
+ function open(target: string, app?: string): void;
+ module open {}
+ export = open;
+}
diff --git a/tools/typings/run-sequence.d.ts b/tools/typings/run-sequence.d.ts
new file mode 100644
index 0000000..c4fada3
--- /dev/null
+++ b/tools/typings/run-sequence.d.ts
@@ -0,0 +1,5 @@
+declare module 'run-sequence' {
+ function runSequence(...args: any[]): void;
+ module runSequence {}
+ export = runSequence;
+}
diff --git a/tools/typings/slash.d.ts b/tools/typings/slash.d.ts
new file mode 100644
index 0000000..10ec0b5
--- /dev/null
+++ b/tools/typings/slash.d.ts
@@ -0,0 +1,5 @@
+declare module 'slash' {
+ function slash(path: string): string;
+ module slash {}
+ export = slash;
+}
diff --git a/tools/typings/systemjs-builder.d.ts b/tools/typings/systemjs-builder.d.ts
new file mode 100644
index 0000000..4644816
--- /dev/null
+++ b/tools/typings/systemjs-builder.d.ts
@@ -0,0 +1,10 @@
+declare module 'systemjs-builder' {
+ class Builder {
+ constructor(configObject?: any, baseUrl?: string, configPath?: string);
+ bundle(source: string, target: string, options?: any): Promise<any>;
+ buildStatic(source: string, target: string, options?: any): Promise<any>;
+ }
+
+ module Builder {}
+ export = Builder;
+}
diff --git a/tools/typings/tiny-lr.d.ts b/tools/typings/tiny-lr.d.ts
new file mode 100644
index 0000000..8a492d5
--- /dev/null
+++ b/tools/typings/tiny-lr.d.ts
@@ -0,0 +1,10 @@
+declare module 'tiny-lr' {
+ function tinylr(): ITinylr;
+ module tinylr {}
+ export = tinylr;
+
+ interface ITinylr {
+ changed(options: any): void;
+ listen(port: number): void;
+ }
+}
diff --git a/tools/typings/yargs.d.ts b/tools/typings/yargs.d.ts
new file mode 100644
index 0000000..8351942
--- /dev/null
+++ b/tools/typings/yargs.d.ts
@@ -0,0 +1,9 @@
+declare module 'yargs' {
+ var yargs: IYargs;
+ export = yargs;
+
+ // Minimalistic but serves the usage in the seed.
+ interface IYargs {
+ argv: any;
+ }
+}
diff --git a/tools/utils.ts b/tools/utils.ts
new file mode 100644
index 0000000..81e3bb2
--- /dev/null
+++ b/tools/utils.ts
@@ -0,0 +1,11 @@
+export * from './utils/template-injectables';
+export * from './utils/template-locals';
+export * from './utils/server';
+export * from './utils/tasks_tools';
+
+
+export function tsProjectFn(plugins) {
+ return plugins.typescript.createProject('tsconfig.json', {
+ typescript: require('typescript')
+ });
+}
diff --git a/tools/utils/server.ts b/tools/utils/server.ts
new file mode 100644
index 0000000..9e185d3
--- /dev/null
+++ b/tools/utils/server.ts
@@ -0,0 +1,45 @@
+import * as connectLivereload from 'connect-livereload';
+import * as express from 'express';
+import * as tinylrFn from 'tiny-lr';
+import * as openResource from 'open';
+import * as serveStatic from 'serve-static';
+import {resolve} from 'path';
+import {APP_BASE, APP_DEST, DOCS_DEST, LIVE_RELOAD_PORT, DOCS_PORT, PORT} from '../config';
+
+let tinylr = tinylrFn();
+
+
+export function serveSPA() {
+ let server = express();
+ tinylr.listen(LIVE_RELOAD_PORT);
+
+ server.use(
+ APP_BASE,
+ connectLivereload({ port: LIVE_RELOAD_PORT }),
+ express.static(process.cwd())
+ );
+
+ server.listen(PORT, () =>
+ openResource('http://localhost:' + PORT + APP_BASE + APP_DEST)
+ );
+}
+
+export function notifyLiveReload(e) {
+ let fileName = e.path;
+ tinylr.changed({
+ body: { files: [fileName] }
+ });
+}
+
+export function serveDocs() {
+ let server = express();
+
+ server.use(
+ APP_BASE,
+ serveStatic(resolve(process.cwd(), DOCS_DEST))
+ );
+
+ server.listen(DOCS_PORT, () =>
+ openResource('http://localhost:' + DOCS_PORT + APP_BASE)
+ );
+}
diff --git a/tools/utils/tasks_tools.ts b/tools/utils/tasks_tools.ts
new file mode 100644
index 0000000..af1a069
--- /dev/null
+++ b/tools/utils/tasks_tools.ts
@@ -0,0 +1,64 @@
+import * as gulp from 'gulp';
+import * as util from 'gulp-util';
+import * as chalk from 'chalk';
+import * as gulpLoadPlugins from 'gulp-load-plugins';
+import * as _runSequence from 'run-sequence';
+import {readdirSync, existsSync, lstatSync} from 'fs';
+import {join} from 'path';
+import {TOOLS_DIR} from '../config';
+
+const TASKS_PATH = join(TOOLS_DIR, 'tasks');
+
+// NOTE: Remove if no issues with runSequence function below.
+// export function loadTasks(): void {
+// scanDir(TASKS_PATH, (taskname) => registerTask(taskname));
+// }
+
+export function task(taskname: string, option?: string) {
+ util.log('Loading task', chalk.yellow(taskname, option || ''));
+ return require(join('..', 'tasks', taskname))(gulp, gulpLoadPlugins(), option);
+}
+
+export function runSequence(...sequence: any[]) {
+ let tasks = [];
+ let _sequence = sequence.slice(0);
+ sequence.pop();
+
+ scanDir(TASKS_PATH, taskname => tasks.push(taskname));
+
+ sequence.forEach(task => {
+ if (tasks.indexOf(task) > -1) { registerTask(task); }
+ });
+
+ return _runSequence(..._sequence);
+}
+
+// ----------
+// Private.
+
+function registerTask(taskname: string, filename?: string, option: string = ''): void {
+ gulp.task(taskname, task(filename || taskname, option));
+}
+
+// TODO: add recursive lookup ? or enforce pattern file + folder (ie ./tools/utils & ./tools/utils.ts )
+function scanDir(root: string, cb: (taskname: string) => void) {
+ if (!existsSync(root)) return;
+
+ walk(root);
+
+ function walk(path) {
+ let files = readdirSync(path);
+ for (let i = 0; i < files.length; i += 1) {
+ let file = files[i];
+ let curPath = join(path, file);
+ // if (lstatSync(curPath).isDirectory()) { // recurse
+ // path = file;
+ // walk(curPath);
+ // }
+ if (lstatSync(curPath).isFile() && /\.ts$/.test(file)) {
+ let taskname = file.replace(/(\.ts)/, '');
+ cb(taskname);
+ }
+ }
+ }
+}
diff --git a/tools/utils/template-injectables.ts b/tools/utils/template-injectables.ts
new file mode 100644
index 0000000..83ac315
--- /dev/null
+++ b/tools/utils/template-injectables.ts
@@ -0,0 +1,25 @@
+import * as slash from 'slash';
+import {join} from 'path';
+import {APP_BASE, APP_DEST, ENV} from '../config';
+
+let injectables: string[] = [];
+
+export function injectableAssetsRef() {
+ return injectables;
+}
+
+export function registerInjectableAssetsRef(paths: string[], target: string = '') {
+ injectables = injectables.concat(
+ paths
+ .filter(path => !/(\.map)$/.test(path))
+ .map(path => join(target, slash(path).split('/').pop()))
+ );
+}
+
+export function transformPath(plugins, env) {
+ return function (filepath) {
+ filepath = ENV === 'prod' ? filepath.replace(`/${APP_DEST}`, '') : filepath;
+ arguments[0] = join(APP_BASE, filepath);
+ return slash(plugins.inject.transform.apply(plugins.inject.transform, arguments));
+ };
+}
diff --git a/tools/utils/template-locals.ts b/tools/utils/template-locals.ts
new file mode 100644
index 0000000..be6b669
--- /dev/null
+++ b/tools/utils/template-locals.ts
@@ -0,0 +1,13 @@
+import {APP_BASE, APP_DEST, APP_ROOT, APP_TITLE, SYSTEM_CONFIG, VERSION} from '../config';
+
+// TODO: Add an interface to register more template locals.
+export function templateLocals() {
+ return {
+ APP_BASE,
+ APP_DEST,
+ APP_ROOT,
+ APP_TITLE,
+ SYSTEM_CONFIG,
+ VERSION
+ };
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..999d975
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "declaration": false,
+ "noImplicitAny": false,
+ "removeComments": true,
+ "noLib": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "sourceMap": true
+ },
+ "exclude": [
+ "node_modules"
+ ],
+ "compileOnSave": false
+}
diff --git a/tsd.json b/tsd.json
new file mode 100644
index 0000000..4fae99b
--- /dev/null
+++ b/tsd.json
@@ -0,0 +1,63 @@
+{
+ "version": "v4",
+ "repo": "DefinitelyTyped/DefinitelyTyped",
+ "ref": "master",
+ "path": "tools/typings/tsd",
+ "bundle": "tools/typings/tsd/tsd.d.ts",
+ "installed": {
+ "systemjs/systemjs.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "gulp/gulp.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "q/Q.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "orchestrator/orchestrator.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "gulp-shell/gulp-shell.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "mime/mime.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "express/express.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "serve-static/serve-static.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "del/del.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "glob/glob.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "minimatch/minimatch.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "async/async.d.ts": {
+ "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f"
+ },
+ "es6-promise/es6-promise.d.ts": {
+ "commit": "923c5431d9447db9d5cf41adc5914e3c94c1ff10"
+ },
+ "node/node.d.ts": {
+ "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff"
+ },
+ "gulp-util/gulp-util.d.ts": {
+ "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff"
+ },
+ "vinyl/vinyl.d.ts": {
+ "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff"
+ },
+ "through2/through2.d.ts": {
+ "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff"
+ },
+ "chalk/chalk.d.ts": {
+ "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff"
+ }
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..b2cd447
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,36 @@
+{
+ "rules": {
+ "class-name": true,
+ "curly": false,
+ "eofline": true,
+ "indent": "spaces",
+ "max-line-length": [true, 140],
+ "member-ordering": [true,
+ "public-before-private",
+ "static-before-instance",
+ "variables-before-functions"
+ ],
+ "no-arg": true,
+ "no-construct": true,
+ "no-duplicate-key": true,
+ "no-duplicate-variable": true,
+ "no-empty": true,
+ "no-eval": true,
+ "no-trailing-comma": true,
+ "no-trailing-whitespace": true,
+ "no-unused-expression": true,
+ "no-unused-variable": true,
+ "no-unreachable": true,
+ "no-use-before-declare": true,
+ "one-line": [true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "quotemark": [true, "single"],
+ "semicolon": true,
+ "triple-equals": true,
+ "variable-name": false
+ }
+}