summaryrefslogtreecommitdiff
path: root/web-ui/public
diff options
context:
space:
mode:
Diffstat (limited to 'web-ui/public')
-rw-r--r--web-ui/public/404.html157
-rw-r--r--web-ui/public/Interstitial.js61
-rw-r--r--web-ui/public/dummy.json1
-rw-r--r--web-ui/public/favicon.ico0
-rw-r--r--web-ui/public/favicon.pngbin0 -> 592 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-Bold.woffbin0 -> 14504 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-BoldItalic.woffbin0 -> 15488 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-Extrabold.woffbin0 -> 15312 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-ExtraboldItalic.woffbin0 -> 15932 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-Italic.woffbin0 -> 15768 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-Light.woffbin0 -> 15048 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-Semibold.woffbin0 -> 15236 bytes
-rw-r--r--web-ui/public/fonts/OpenSans-SemiboldItalic.woffbin0 -> 15736 bytes
-rw-r--r--web-ui/public/fonts/OpenSans.woffbin0 -> 14604 bytes
-rw-r--r--web-ui/public/fonts/OpenSansLight-Italic.woffbin0 -> 15956 bytes
-rw-r--r--web-ui/public/fonts/icomoon.ttfbin0 -> 1272 bytes
-rw-r--r--web-ui/public/fonts/icomoon.woffbin0 -> 1348 bytes
-rw-r--r--web-ui/public/hive-bg.pngbin0 -> 3356 bytes
-rw-r--r--web-ui/public/jquery-2.1.3.min.js4
-rw-r--r--web-ui/public/js/dispatchers/left_pane_dispatcher.js62
-rw-r--r--web-ui/public/js/dispatchers/middle_pane_dispatcher.js74
-rw-r--r--web-ui/public/js/dispatchers/right_pane_dispatcher.js117
-rw-r--r--web-ui/public/js/features/features.js61
-rw-r--r--web-ui/public/js/feedback/feedback_cache.js35
-rw-r--r--web-ui/public/js/feedback/feedback_trigger.js39
-rw-r--r--web-ui/public/js/foundation/initialize_foundation.js5
-rw-r--r--web-ui/public/js/foundation/off_canvas.js46
-rw-r--r--web-ui/public/js/helpers/browser.js36
-rw-r--r--web-ui/public/js/helpers/contenttype.js184
-rw-r--r--web-ui/public/js/helpers/iterator.js60
-rw-r--r--web-ui/public/js/helpers/monitored_ajax.js67
-rw-r--r--web-ui/public/js/helpers/sanitizer.js126
-rw-r--r--web-ui/public/js/helpers/triggering.js29
-rw-r--r--web-ui/public/js/helpers/view_helper.js163
-rw-r--r--web-ui/public/js/lib/highlightRegex.js127
-rw-r--r--web-ui/public/js/lib/html4-defs.js640
-rw-r--r--web-ui/public/js/mail_list/domain/refresher.js43
-rw-r--r--web-ui/public/js/mail_list/ui/mail_item_factory.js59
-rw-r--r--web-ui/public/js/mail_list/ui/mail_items/draft_item.js55
-rw-r--r--web-ui/public/js/mail_list/ui/mail_items/generic_mail_item.js97
-rw-r--r--web-ui/public/js/mail_list/ui/mail_items/mail_item.js88
-rw-r--r--web-ui/public/js/mail_list/ui/mail_items/sent_item.js61
-rw-r--r--web-ui/public/js/mail_list/ui/mail_list.js182
-rw-r--r--web-ui/public/js/mail_list_actions/ui/archive_many_trigger.js29
-rw-r--r--web-ui/public/js/mail_list_actions/ui/compose_trigger.js57
-rw-r--r--web-ui/public/js/mail_list_actions/ui/delete_many_trigger.js47
-rw-r--r--web-ui/public/js/mail_list_actions/ui/mail_list_actions.js93
-rw-r--r--web-ui/public/js/mail_list_actions/ui/mark_as_unread_trigger.js47
-rw-r--r--web-ui/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js47
-rw-r--r--web-ui/public/js/mail_list_actions/ui/pagination_trigger.js66
-rw-r--r--web-ui/public/js/mail_list_actions/ui/recover_many_trigger.js47
-rw-r--r--web-ui/public/js/mail_list_actions/ui/refresh_trigger.js44
-rw-r--r--web-ui/public/js/mail_list_actions/ui/toggle_check_all_trigger.js49
-rw-r--r--web-ui/public/js/mail_view/data/feedback_sender.js49
-rw-r--r--web-ui/public/js/mail_view/data/mail_builder.js102
-rw-r--r--web-ui/public/js/mail_view/data/mail_sender.js93
-rw-r--r--web-ui/public/js/mail_view/ui/attachment_icon.js61
-rw-r--r--web-ui/public/js/mail_view/ui/attachment_list.js210
-rw-r--r--web-ui/public/js/mail_view/ui/compose_box.js84
-rw-r--r--web-ui/public/js/mail_view/ui/draft_box.js109
-rw-r--r--web-ui/public/js/mail_view/ui/draft_save_status.js42
-rw-r--r--web-ui/public/js/mail_view/ui/feedback_box.js69
-rw-r--r--web-ui/public/js/mail_view/ui/forward_box.js97
-rw-r--r--web-ui/public/js/mail_view/ui/mail_actions.js84
-rw-r--r--web-ui/public/js/mail_view/ui/mail_view.js255
-rw-r--r--web-ui/public/js/mail_view/ui/no_mails_available_pane.js50
-rw-r--r--web-ui/public/js/mail_view/ui/no_message_selected_pane.js41
-rw-r--r--web-ui/public/js/mail_view/ui/recipients/recipient.js112
-rw-r--r--web-ui/public/js/mail_view/ui/recipients/recipients.js193
-rw-r--r--web-ui/public/js/mail_view/ui/recipients/recipients_input.js180
-rw-r--r--web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js59
-rw-r--r--web-ui/public/js/mail_view/ui/reply_box.js116
-rw-r--r--web-ui/public/js/mail_view/ui/reply_section.js129
-rw-r--r--web-ui/public/js/mail_view/ui/send_button.js130
-rw-r--r--web-ui/public/js/main.js84
-rw-r--r--web-ui/public/js/mixins/with_auto_refresh.js47
-rw-r--r--web-ui/public/js/mixins/with_compose_inline.js84
-rw-r--r--web-ui/public/js/mixins/with_enable_disable_on_event.js48
-rw-r--r--web-ui/public/js/mixins/with_feature_toggle.js40
-rw-r--r--web-ui/public/js/mixins/with_hide_and_show.js31
-rw-r--r--web-ui/public/js/mixins/with_mail_edit_base.js263
-rw-r--r--web-ui/public/js/mixins/with_mail_sandbox.js80
-rw-r--r--web-ui/public/js/mixins/with_mail_tagging.js69
-rw-r--r--web-ui/public/js/monkey_patching/all.js17
-rw-r--r--web-ui/public/js/monkey_patching/array.js27
-rw-r--r--web-ui/public/js/monkey_patching/post_message.js32
-rw-r--r--web-ui/public/js/page/default.js146
-rw-r--r--web-ui/public/js/page/events.js222
-rw-r--r--web-ui/public/js/page/logout.js43
-rw-r--r--web-ui/public/js/page/logout_shortcut.js33
-rw-r--r--web-ui/public/js/page/pane_contract_expand.js51
-rw-r--r--web-ui/public/js/page/pix_logo.js62
-rw-r--r--web-ui/public/js/page/router.js71
-rw-r--r--web-ui/public/js/page/router/url_params.js57
-rw-r--r--web-ui/public/js/page/unread_count_title.js53
-rw-r--r--web-ui/public/js/page/version.js41
-rw-r--r--web-ui/public/js/sandbox.js11
-rw-r--r--web-ui/public/js/search/results_highlighter.js97
-rw-r--r--web-ui/public/js/search/search_trigger.js81
-rw-r--r--web-ui/public/js/services/delete_service.js59
-rw-r--r--web-ui/public/js/services/mail_service.js335
-rw-r--r--web-ui/public/js/services/model/mail.js126
-rw-r--r--web-ui/public/js/services/recover_service.js38
-rw-r--r--web-ui/public/js/style_guide/main.js33
-rw-r--r--web-ui/public/js/tags/data/tags.js66
-rw-r--r--web-ui/public/js/tags/ui/tag.js154
-rw-r--r--web-ui/public/js/tags/ui/tag_base.js68
-rw-r--r--web-ui/public/js/tags/ui/tag_list.js105
-rw-r--r--web-ui/public/js/user_alerts/ui/user_alerts.js57
-rw-r--r--web-ui/public/js/user_settings/data/user_settings.js52
-rw-r--r--web-ui/public/js/user_settings/ui/user_settings_box.js77
-rw-r--r--web-ui/public/js/user_settings/ui/user_settings_icon.js57
-rw-r--r--web-ui/public/js/views/i18n.js62
-rw-r--r--web-ui/public/js/views/recipientListFormatter.js33
-rw-r--r--web-ui/public/js/views/templates.js85
-rw-r--r--web-ui/public/locales/en_US/translation.json72
-rw-r--r--web-ui/public/locales/pt_BR/translation.json72
-rw-r--r--web-ui/public/locales/sv_SE/translation.json42
-rw-r--r--web-ui/public/normalize.min.css1
-rw-r--r--web-ui/public/opensans.css69
-rw-r--r--web-ui/public/pixelated-logo-orange.svg29
-rw-r--r--web-ui/public/pixelated.css128
-rw-r--r--web-ui/public/robots.txt3
-rw-r--r--web-ui/public/sandbox.html13
-rw-r--r--web-ui/public/scss/_mixins.scss71
-rw-r--r--web-ui/public/scss/_others.scss72
-rw-r--r--web-ui/public/scss/base/_colors.scss64
-rw-r--r--web-ui/public/scss/base/_fonts.scss68
-rw-r--r--web-ui/public/scss/base/_scaffolding.scss10
-rw-r--r--web-ui/public/scss/mixins/_position-helpers.scss9
-rw-r--r--web-ui/public/scss/mixins/_tags.scss110
-rw-r--r--web-ui/public/scss/sandbox.scss27
-rw-r--r--web-ui/public/scss/style.scss39
-rw-r--r--web-ui/public/scss/templates/_no-content-placeholder.scss5
-rw-r--r--web-ui/public/scss/templates/_unread-count.scss14
-rw-r--r--web-ui/public/scss/vendor/_customfont.scss9
-rw-r--r--web-ui/public/scss/vendor/_foundation.scss2066
-rw-r--r--web-ui/public/scss/vendor/_reset.scss421
-rw-r--r--web-ui/public/scss/vendor/_scut.scss1518
-rw-r--r--web-ui/public/scss/views/_action-bar.scss159
-rw-r--r--web-ui/public/scss/views/_close-button.scss22
-rw-r--r--web-ui/public/scss/views/_compose-button.scss27
-rw-r--r--web-ui/public/scss/views/_compose-view.scss451
-rw-r--r--web-ui/public/scss/views/_mail-list.scss124
-rw-r--r--web-ui/public/scss/views/_message-panel.scss26
-rw-r--r--web-ui/public/scss/views/_navigation.scss589
-rw-r--r--web-ui/public/scss/views/_no-mails-available.scss3
-rw-r--r--web-ui/public/scss/views/_no-message-selected.scss14
-rw-r--r--web-ui/public/scss/views/_read-view.scss165
-rw-r--r--web-ui/public/scss/views/_security-labels.scss67
-rw-r--r--web-ui/public/signup.css174
-rw-r--r--web-ui/public/signup.html19
-rw-r--r--web-ui/public/snap.svg-min.js20
-rw-r--r--web-ui/public/templates/compose/attachment_item.hbs4
-rw-r--r--web-ui/public/templates/compose/attachment_upload_item.hbs5
-rw-r--r--web-ui/public/templates/compose/attachments_list.hbs14
-rw-r--r--web-ui/public/templates/compose/compose_box.hbs32
-rw-r--r--web-ui/public/templates/compose/feedback_box.hbs18
-rw-r--r--web-ui/public/templates/compose/fixed_recipient.hbs8
-rw-r--r--web-ui/public/templates/compose/inline_box.hbs20
-rw-r--r--web-ui/public/templates/compose/no_mails_available.hbs7
-rw-r--r--web-ui/public/templates/compose/no_message_selected.hbs3
-rw-r--r--web-ui/public/templates/compose/recipient_input.hbs1
-rw-r--r--web-ui/public/templates/compose/recipients.hbs33
-rw-r--r--web-ui/public/templates/compose/reply_section.hbs6
-rw-r--r--web-ui/public/templates/compose/upload_attachment_failed.hbs6
-rw-r--r--web-ui/public/templates/feedback/feedback_trigger.hbs8
-rw-r--r--web-ui/public/templates/mail_actions/actions_box.hbs7
-rw-r--r--web-ui/public/templates/mail_actions/compose_trigger.hbs3
-rw-r--r--web-ui/public/templates/mail_actions/pagination_trigger.hbs3
-rw-r--r--web-ui/public/templates/mail_actions/refresh_trigger.hbs3
-rw-r--r--web-ui/public/templates/mail_actions/trash_actions_box.hbs5
-rw-r--r--web-ui/public/templates/mails/draft.hbs41
-rw-r--r--web-ui/public/templates/mails/full_view.hbs83
-rw-r--r--web-ui/public/templates/mails/mail_actions.hbs6
-rw-r--r--web-ui/public/templates/mails/sent.hbs36
-rw-r--r--web-ui/public/templates/mails/single.hbs28
-rw-r--r--web-ui/public/templates/mails/trash.hbs32
-rw-r--r--web-ui/public/templates/page/logout.hbs9
-rw-r--r--web-ui/public/templates/page/logout_shortcut.hbs6
-rw-r--r--web-ui/public/templates/page/user_settings_box.hbs10
-rw-r--r--web-ui/public/templates/page/user_settings_icon.hbs8
-rw-r--r--web-ui/public/templates/page/version.hbs2
-rw-r--r--web-ui/public/templates/search/search_trigger.hbs3
-rw-r--r--web-ui/public/templates/tags/shortcut.hbs9
-rw-r--r--web-ui/public/templates/tags/tag.hbs3
-rw-r--r--web-ui/public/templates/tags/tag_inner.hbs4
-rw-r--r--web-ui/public/templates/tags/tag_list.hbs6
-rw-r--r--web-ui/public/templates/user_alerts/message.hbs1
189 files changed, 15932 insertions, 0 deletions
diff --git a/web-ui/public/404.html b/web-ui/public/404.html
new file mode 100644
index 00000000..fdace4ab
--- /dev/null
+++ b/web-ui/public/404.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Page Not Found :(</title>
+ <style>
+ ::-moz-selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+
+ ::selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+
+ html {
+ padding: 30px 10px;
+ font-size: 20px;
+ line-height: 1.4;
+ color: #737373;
+ background: #f0f0f0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+
+ html,
+ input {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ }
+
+ body {
+ max-width: 500px;
+ _width: 500px;
+ padding: 30px 20px 50px;
+ border: 1px solid #b3b3b3;
+ border-radius: 4px;
+ margin: 0 auto;
+ box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
+ background: #fcfcfc;
+ }
+
+ h1 {
+ margin: 0 10px;
+ font-size: 50px;
+ text-align: center;
+ }
+
+ h1 span {
+ color: #bbb;
+ }
+
+ h3 {
+ margin: 1.5em 0 0.5em;
+ }
+
+ p {
+ margin: 1em 0;
+ }
+
+ ul {
+ padding: 0 0 0 40px;
+ margin: 1em 0;
+ }
+
+ .container {
+ max-width: 380px;
+ _width: 380px;
+ margin: 0 auto;
+ }
+
+ /* google search */
+
+ #goog-fixurl ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ #goog-fixurl form {
+ margin: 0;
+ }
+
+ #goog-wm-qt,
+ #goog-wm-sb {
+ border: 1px solid #bbb;
+ font-size: 16px;
+ line-height: normal;
+ vertical-align: top;
+ color: #444;
+ border-radius: 2px;
+ }
+
+ #goog-wm-qt {
+ width: 220px;
+ height: 20px;
+ padding: 5px;
+ margin: 5px 10px 0 0;
+ box-shadow: inset 0 1px 1px #ccc;
+ }
+
+ #goog-wm-sb {
+ display: inline-block;
+ height: 32px;
+ padding: 0 10px;
+ margin: 5px 0 0;
+ white-space: nowrap;
+ cursor: pointer;
+ background-color: #f5f5f5;
+ background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ *overflow: visible;
+ *display: inline;
+ *zoom: 1;
+ }
+
+ #goog-wm-sb:hover,
+ #goog-wm-sb:focus {
+ border-color: #aaa;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ background-color: #f8f8f8;
+ }
+
+ #goog-wm-qt:hover,
+ #goog-wm-qt:focus {
+ border-color: #105cb6;
+ outline: 0;
+ color: #222;
+ }
+
+ input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <h1>Not found <span>:(</span></h1>
+ <p>Sorry, but the page you were trying to view does not exist.</p>
+ <p>It looks like this was the result of either:</p>
+ <ul>
+ <li>a mistyped address</li>
+ <li>an out-of-date link</li>
+ </ul>
+ <script>
+ var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
+ </script>
+ <script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
+ </div>
+ </body>
+</html>
diff --git a/web-ui/public/Interstitial.js b/web-ui/public/Interstitial.js
new file mode 100644
index 00000000..05c5ff29
--- /dev/null
+++ b/web-ui/public/Interstitial.js
@@ -0,0 +1,61 @@
+if ($('#hive').length) {
+ var hive = new Snap('#hive');
+ var img_width = $('#hive').width();
+ var left_pos = img_width * 0.5;
+
+ var pixelated = hive.path("M12.4,20.3v31.8l28,15.8l28-15.8V20.3l-28-15.8L12.4,20.3z M39.2,56.4l-16.3-9V27.9l16.3,9.3L39.2,56.4z M57.7,47.4l-16.1,9l0-19.2l16.1-9.4V47.4z M57.7,25.2L40.4,35.5L22.9,25.2l17.5-9.4L57.7,25.2z").transform("translate(319, 50)").attr("fill", "#908e8e");
+ var all = hive.group().transform("matrix(2, 0, 0, 2, -100, -100)");
+
+ var height = 50;
+ var width = 58;
+ var rows = (($(window).height() / height) / 2) + 1;
+ var cols = (($(window).width() / width) / 2) + 1;
+
+
+ for (var j = 0; j < rows; j++) {
+ for (var i = 0; i < cols; i++) {
+ var x = i * width + (j%2*width/2);
+ var y = j * height;
+ all.add(pixelated.clone().transform("translate("+x+","+y+")"));
+ }
+ }
+
+ all.add(pixelated);
+
+ var brightenLogo = function () {
+ 'use strict';
+ var glowPosition = Math.floor(Math.random()*rows*cols);
+
+ all[glowPosition].animate({fill: "#FFF"}, 1000, function() {
+ darkenLogo(all[glowPosition]);
+ });
+ };
+
+ var darkenLogo = function (el) {
+ 'use strict';
+ el.animate({fill: "#908e8e"}, 1000, brightenLogo);
+ };
+
+ brightenLogo();
+
+}
+
+$(function () {
+ 'use strict';
+ var handler = setInterval(function () {
+ $.ajax({
+ method: 'GET',
+ url: '/'
+ }).success(function (data) {
+ if (/Pixelated Mail/g.test(data)) {
+ window.location="/";
+ }
+ });
+ }, 2000);
+
+ $('#hive-section').height($(window).height());
+
+ $(window).resize(function() {
+ window.location.reload(true);
+ });
+});
diff --git a/web-ui/public/dummy.json b/web-ui/public/dummy.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/web-ui/public/dummy.json
@@ -0,0 +1 @@
+{}
diff --git a/web-ui/public/favicon.ico b/web-ui/public/favicon.ico
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/web-ui/public/favicon.ico
diff --git a/web-ui/public/favicon.png b/web-ui/public/favicon.png
new file mode 100644
index 00000000..e14841c7
--- /dev/null
+++ b/web-ui/public/favicon.png
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-Bold.woff b/web-ui/public/fonts/OpenSans-Bold.woff
new file mode 100644
index 00000000..dacf3c9c
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-Bold.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-BoldItalic.woff b/web-ui/public/fonts/OpenSans-BoldItalic.woff
new file mode 100644
index 00000000..a4e29c0f
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-BoldItalic.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-Extrabold.woff b/web-ui/public/fonts/OpenSans-Extrabold.woff
new file mode 100644
index 00000000..7a2e352b
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-Extrabold.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-ExtraboldItalic.woff b/web-ui/public/fonts/OpenSans-ExtraboldItalic.woff
new file mode 100644
index 00000000..ce3ab2e7
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-ExtraboldItalic.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-Italic.woff b/web-ui/public/fonts/OpenSans-Italic.woff
new file mode 100644
index 00000000..c5f6bac1
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-Italic.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-Light.woff b/web-ui/public/fonts/OpenSans-Light.woff
new file mode 100644
index 00000000..eb601d70
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-Light.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-Semibold.woff b/web-ui/public/fonts/OpenSans-Semibold.woff
new file mode 100644
index 00000000..56c44944
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-Semibold.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans-SemiboldItalic.woff b/web-ui/public/fonts/OpenSans-SemiboldItalic.woff
new file mode 100644
index 00000000..3a439fc3
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans-SemiboldItalic.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSans.woff b/web-ui/public/fonts/OpenSans.woff
new file mode 100644
index 00000000..77706fa6
--- /dev/null
+++ b/web-ui/public/fonts/OpenSans.woff
Binary files differ
diff --git a/web-ui/public/fonts/OpenSansLight-Italic.woff b/web-ui/public/fonts/OpenSansLight-Italic.woff
new file mode 100644
index 00000000..3f9f088f
--- /dev/null
+++ b/web-ui/public/fonts/OpenSansLight-Italic.woff
Binary files differ
diff --git a/web-ui/public/fonts/icomoon.ttf b/web-ui/public/fonts/icomoon.ttf
new file mode 100644
index 00000000..61315d04
--- /dev/null
+++ b/web-ui/public/fonts/icomoon.ttf
Binary files differ
diff --git a/web-ui/public/fonts/icomoon.woff b/web-ui/public/fonts/icomoon.woff
new file mode 100644
index 00000000..82f11748
--- /dev/null
+++ b/web-ui/public/fonts/icomoon.woff
Binary files differ
diff --git a/web-ui/public/hive-bg.png b/web-ui/public/hive-bg.png
new file mode 100644
index 00000000..77316967
--- /dev/null
+++ b/web-ui/public/hive-bg.png
Binary files differ
diff --git a/web-ui/public/jquery-2.1.3.min.js b/web-ui/public/jquery-2.1.3.min.js
new file mode 100644
index 00000000..25714ed2
--- /dev/null
+++ b/web-ui/public/jquery-2.1.3.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=mb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=nb(b);function qb(){}qb.prototype=d.filters=d.pseudos,d.setFilters=new qb,g=gb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?gb.error(a):z(a,i).slice(0)};function rb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)
+},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),f.removeChild(c),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
+},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec=/#.*$/,fc=/([?&])_=[^&]*/,gc=/^(.*?):[ \t]*([^\r\n]*)$/gm,hc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,ic=/^(?:GET|HEAD)$/,jc=/^\/\//,kc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,lc={},mc={},nc="*/".concat("*"),oc=a.location.href,pc=kc.exec(oc.toLowerCase())||[];function qc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function rc(a,b,c,d){var e={},f=a===mc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function sc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function tc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function uc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:oc,type:"GET",isLocal:hc.test(pc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":nc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?sc(sc(a,n.ajaxSettings),b):sc(n.ajaxSettings,a)},ajaxPrefilter:qc(lc),ajaxTransport:qc(mc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=gc.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||oc)+"").replace(ec,"").replace(jc,pc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=kc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===pc[1]&&h[2]===pc[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(pc[3]||("http:"===pc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),rc(lc,k,b,v),2===t)return v;i=n.event&&k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!ic.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=fc.test(d)?d.replace(fc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+nc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=rc(mc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=tc(k,v,f)),u=uc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var vc=/%20/g,wc=/\[\]$/,xc=/\r?\n/g,yc=/^(?:submit|button|image|reset|file)$/i,zc=/^(?:input|select|textarea|keygen)/i;function Ac(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||wc.test(a)?d(a,e):Ac(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Ac(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Ac(c,a[c],b,e);return d.join("&").replace(vc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&zc.test(this.nodeName)&&!yc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(xc,"\r\n")}}):{name:b.name,value:c.replace(xc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Bc=0,Cc={},Dc={0:200,1223:204},Ec=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Cc)Cc[a]()}),k.cors=!!Ec&&"withCredentials"in Ec,k.ajax=Ec=!!Ec,n.ajaxTransport(function(a){var b;return k.cors||Ec&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Bc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Cc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Dc[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Cc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Fc=[],Gc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Fc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Gc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Gc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Gc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Fc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Hc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Hc)return Hc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Ic=a.document.documentElement;function Jc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Jc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Ic;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Ic})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Jc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Kc=a.jQuery,Lc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Lc),b&&a.jQuery===n&&(a.jQuery=Kc),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/web-ui/public/js/dispatchers/left_pane_dispatcher.js b/web-ui/public/js/dispatchers/left_pane_dispatcher.js
new file mode 100644
index 00000000..0037a88f
--- /dev/null
+++ b/web-ui/public/js/dispatchers/left_pane_dispatcher.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'page/router/url_params',
+ 'page/events'
+ ],
+
+ function(defineComponent, urlParams, events) {
+ 'use strict';
+
+ return defineComponent(leftPaneDispatcher);
+
+ function leftPaneDispatcher() {
+ var initialized = false;
+
+ this.refreshTagList = function (ev, data) {
+ this.trigger(document, events.tags.want, { caller: this.$node, skipMailListRefresh: data.skipMailListRefresh });
+ };
+
+ this.loadTags = function (ev, data) {
+ this.trigger(document, events.ui.tagList.load, data);
+ };
+
+ this.selectTag = function (ev, data) {
+ var tag = (data && data.tag) || urlParams.getTag();
+ this.trigger(document, events.ui.tag.select, { tag: tag, skipMailListRefresh: data.skipMailListRefresh });
+ };
+
+ this.pushUrlState = function (ev, data) {
+ if (initialized) {
+ this.trigger(document, events.router.pushState, data);
+ }
+ initialized = true;
+ };
+
+ this.after('initialize', function () {
+ //this.on(this.$node, events.tags.received, this.loadTags);
+ this.on(document, events.dispatchers.tags.refreshTagList, this.refreshTagList);
+ this.on(document, events.ui.tags.loaded, this.selectTag);
+ this.on(document, events.ui.tag.selected, this.pushUrlState);
+ this.on(document, events.ui.tag.select, this.pushUrlState);
+ this.trigger(document, events.tags.want, { caller: this.$node });
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/dispatchers/middle_pane_dispatcher.js b/web-ui/public/js/dispatchers/middle_pane_dispatcher.js
new file mode 100644
index 00000000..12222aec
--- /dev/null
+++ b/web-ui/public/js/dispatchers/middle_pane_dispatcher.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'page/events', 'helpers/triggering', 'mail_view/ui/no_mails_available_pane'], function(defineComponent, events, triggering, NoMailsAvailablePane) {
+ 'use strict';
+
+ return defineComponent(function() {
+ this.defaultAttrs({
+ middlePane: '#middle-pane',
+ noMailsAvailablePane: 'no-mails-available-pane'
+ });
+
+ this.createChildDiv = function (component_id) {
+ var child_div = $('<div>', {id: component_id});
+ this.select('middlePane').append(child_div);
+ return child_div;
+ };
+
+ this.resetChildDiv = function(component_id) {
+ $('#' + component_id).remove();
+ };
+
+ this.refreshMailList = function (ev, data) {
+ this.trigger(document, events.ui.mails.fetchByTag, data);
+ };
+
+ this.cleanSelected = function(ev, data) {
+ this.trigger(document, events.ui.mails.cleanSelected);
+ };
+
+ this.resetScroll = function() {
+ this.select('middlePane').scrollTop(0);
+ };
+
+ this.updateMiddlePaneHeight = function() {
+ var vh = $(window).height();
+ var top = $('#main').outerHeight() + $('#top-pane').outerHeight();
+ this.select('middlePane').css({height: (vh - top) + 'px'});
+ };
+
+ this.onMailsChange = function (ev, data) {
+ this.resetChildDiv(this.attr.noMailsAvailablePane);
+ if (data.mails.length > 0) {
+ NoMailsAvailablePane.teardownAll();
+ } else {
+ var child_div = this.createChildDiv(this.attr.noMailsAvailablePane);
+ NoMailsAvailablePane.attachTo(child_div, {tag: data.tag, forSearch: data.forSearch});
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.middlePane.refreshMailList, this.refreshMailList);
+ this.on(document, events.dispatchers.middlePane.cleanSelected, this.cleanSelected);
+ this.on(document, events.dispatchers.middlePane.resetScroll, this.resetScroll);
+ this.on(document, events.mails.available, this.onMailsChange);
+
+ this.updateMiddlePaneHeight();
+ $(window).on('resize', this.updateMiddlePaneHeight.bind(this));
+ });
+ });
+});
diff --git a/web-ui/public/js/dispatchers/right_pane_dispatcher.js b/web-ui/public/js/dispatchers/right_pane_dispatcher.js
new file mode 100644
index 00000000..870bcd92
--- /dev/null
+++ b/web-ui/public/js/dispatchers/right_pane_dispatcher.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'mail_view/ui/compose_box',
+ 'mail_view/ui/mail_view',
+ 'mail_view/ui/reply_section',
+ 'mail_view/ui/draft_box',
+ 'mail_view/ui/no_message_selected_pane',
+ 'mail_view/ui/feedback_box',
+ 'page/events'
+ ],
+
+ function(defineComponent, ComposeBox, MailView, ReplySection, DraftBox, NoMessageSelectedPane, FeedbackBox, events) {
+ 'use strict';
+
+ return defineComponent(rightPaneDispatcher);
+
+ function rightPaneDispatcher() {
+ this.defaultAttrs({
+ rightPane: '#right-pane',
+ composeBox: 'compose-box',
+ feedbackBox: 'feedback-box',
+ mailView: 'mail-view',
+ noMessageSelectedPane: 'no-message-selected-pane',
+ replySection: 'reply-section',
+ draftBox: 'draft-box',
+ currentTag: ''
+ });
+
+ this.createAndAttach = function(newContainer) {
+ var stage = $('<div>', { id: newContainer });
+ this.select('rightPane').append(stage);
+ return stage;
+ };
+
+ this.reset = function (newContainer) {
+ this.trigger(document, events.dispatchers.rightPane.clear);
+ this.select('rightPane').empty();
+ var stage = this.createAndAttach(newContainer);
+ return stage;
+ };
+
+ this.openComposeBox = function() {
+ var stage = this.reset(this.attr.composeBox);
+ ComposeBox.attachTo(stage, {currentTag: this.attr.currentTag});
+ };
+
+ this.openFeedbackBox = function() {
+ var stage = this.reset(this.attr.feedbackBox);
+ FeedbackBox.attachTo(stage);
+ };
+
+ this.openMail = function(ev, data) {
+ var stage = this.reset(this.attr.mailView);
+ MailView.attachTo(stage, data);
+
+ var replySectionContainer = this.createAndAttach(this.attr.replySection);
+ ReplySection.attachTo(replySectionContainer, { ident: data.ident });
+ };
+
+ this.initializeNoMessageSelectedPane = function () {
+ var stage = this.reset(this.attr.noMessageSelectedPane);
+ NoMessageSelectedPane.attachTo(stage);
+ this.trigger(document, events.dispatchers.middlePane.cleanSelected);
+ };
+
+ this.openNoMessageSelectedPane = function(ev, data) {
+ this.initializeNoMessageSelectedPane();
+
+ this.trigger(document, events.router.pushState, { tag: this.attr.currentTag, isDisplayNoMessageSelected: true });
+ };
+
+ this.openDraft = function (ev, data) {
+ var stage = this.reset(this.attr.draftBox);
+ DraftBox.attachTo(stage, { mailIdent: data.ident, currentTag: this.attr.currentTag });
+ };
+
+ this.selectTag = function(ev, data) {
+ this.trigger(document, events.ui.tags.loaded, {tag: data.tag});
+ };
+
+ this.saveTag = function(ev, data) {
+ this.attr.currentTag = data.tag;
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.rightPane.openComposeBox, this.openComposeBox);
+ this.on(document, events.dispatchers.rightPane.openDraft, this.openDraft);
+ this.on(document, events.ui.mail.open, this.openMail);
+ this.on(document, events.dispatchers.rightPane.openFeedbackBox, this.openFeedbackBox);
+ this.on(document, events.dispatchers.rightPane.openNoMessageSelected, this.openNoMessageSelectedPane);
+ this.on(document, events.dispatchers.rightPane.selectTag, this.selectTag);
+ this.on(document, events.ui.tag.selected, this.saveTag);
+ this.on(document, events.ui.tag.select, this.saveTag);
+ this.on(document, events.dispatchers.rightPane.openNoMessageSelectedWithoutPushState, this.initializeNoMessageSelectedPane);
+ this.initializeNoMessageSelectedPane();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/features/features.js b/web-ui/public/js/features/features.js
new file mode 100644
index 00000000..f71d56ea
--- /dev/null
+++ b/web-ui/public/js/features/features.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['helpers/monitored_ajax'], function(monitoredAjax) {
+ 'use strict';
+ var cachedDisabledFeatures;
+ var cachedMultiUserFeatures;
+
+ function getDisabledFeatures() {
+ cachedDisabledFeatures = cachedDisabledFeatures || fetchFeatures().disabled_features;
+ return cachedDisabledFeatures;
+ }
+
+ function getMultiUserFeatures() {
+ cachedMultiUserFeatures = cachedMultiUserFeatures || fetchFeatures().multi_user;
+ return cachedMultiUserFeatures;
+ }
+
+ function fetchFeatures() {
+ var features;
+ monitoredAjax(this, '/features', {
+ async: false,
+ success: function (results) {
+ features = results;
+ },
+ error: function () {
+ console.error('Could not load feature toggles');
+ }
+ });
+ return features;
+ }
+
+ return {
+ isEnabled: function (featureName) {
+ return ! _.contains(getDisabledFeatures(), featureName);
+ },
+ isAutoRefreshEnabled: function () {
+ return this.isEnabled('autoRefresh');
+ },
+ isLogoutEnabled: function () {
+ return _.has(getMultiUserFeatures(), 'logout');
+ },
+ getLogoutUrl: function () {
+ return getMultiUserFeatures().logout;
+ }
+ };
+});
diff --git a/web-ui/public/js/feedback/feedback_cache.js b/web-ui/public/js/feedback/feedback_cache.js
new file mode 100644
index 00000000..a5d92266
--- /dev/null
+++ b/web-ui/public/js/feedback/feedback_cache.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2016 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define([], function() {
+ 'use strict';
+
+ return (function() {
+ var feedbackCache = '';
+ return {
+ resetCache: function () {
+ feedbackCache = '';
+ },
+ setCache: function(feedback) {
+ feedbackCache = feedback;
+ },
+ getCache: function() {
+ return feedbackCache;
+ }
+ };
+ })();
+});
diff --git a/web-ui/public/js/feedback/feedback_trigger.js b/web-ui/public/js/feedback/feedback_trigger.js
new file mode 100644
index 00000000..598f9060
--- /dev/null
+++ b/web-ui/public/js/feedback/feedback_trigger.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['flight/lib/component', 'views/templates', 'page/events', 'features'],
+ function (defineComponent, templates, events, features) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.render = function () {
+ this.$node.html(templates.feedback.feedback());
+ };
+
+ this.onClick = function() {
+ this.trigger(document, events.dispatchers.rightPane.openFeedbackBox);
+ };
+
+ this.after('initialize', function () {
+ if (features.isEnabled('feedback')) {
+ this.render();
+ this.on('click', this.onClick);
+ }
+ });
+
+ });
+});
diff --git a/web-ui/public/js/foundation/initialize_foundation.js b/web-ui/public/js/foundation/initialize_foundation.js
new file mode 100644
index 00000000..42405dfe
--- /dev/null
+++ b/web-ui/public/js/foundation/initialize_foundation.js
@@ -0,0 +1,5 @@
+
+(function() {
+ 'use strict';
+ $(document).foundation();
+})();
diff --git a/web-ui/public/js/foundation/off_canvas.js b/web-ui/public/js/foundation/off_canvas.js
new file mode 100644
index 00000000..66334470
--- /dev/null
+++ b/web-ui/public/js/foundation/off_canvas.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'page/events'], function (defineComponent, events) {
+ 'use strict';
+ return defineComponent(function() {
+
+ this.closeSlider = function (ev){
+ $('.off-canvas-wrap.content').removeClass('move-right');
+ this.toggleTagsVisibility();
+ };
+
+ this.toggleSlideContent = function (ev) {
+ ev.preventDefault();
+ $('.left-off-canvas-toggle').click();
+ this.toggleTagsVisibility();
+ };
+
+ this.toggleTagsVisibility = function () {
+ if ($('.off-canvas-wrap.content').hasClass('move-right')) {
+ $('#custom-tag-list').addClass('expanded');
+ } else {
+ $('#custom-tag-list').removeClass('expanded');
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on($('#middle-pane-container'), 'click', this.closeSlider);
+ this.on($('#right-pane'), 'click', this.closeSlider);
+ this.on($('.side-nav-toggle'), 'click', this.toggleSlideContent);
+ });
+ });
+});
diff --git a/web-ui/public/js/helpers/browser.js b/web-ui/public/js/helpers/browser.js
new file mode 100644
index 00000000..dacf2263
--- /dev/null
+++ b/web-ui/public/js/helpers/browser.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define([], function () {
+
+ 'use strict';
+
+ function redirect(url) {
+ window.location.replace(url);
+ }
+
+ function getCookie(name) {
+ var value = '; ' + document.cookie;
+ var parts = value.split('; ' + name + '=');
+ if (parts.length === 2) { return parts.pop().split(';').shift(); }
+ }
+
+ return {
+ redirect: redirect,
+ getCookie: getCookie
+ };
+});
diff --git a/web-ui/public/js/helpers/contenttype.js b/web-ui/public/js/helpers/contenttype.js
new file mode 100644
index 00000000..a1e5361a
--- /dev/null
+++ b/web-ui/public/js/helpers/contenttype.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* jshint curly: false */
+define([], function () {
+ 'use strict';
+ var exports = {};
+
+ // Licence: PUBLIC DOMAIN <http://unlicense.org/>
+ // Author: Austin Wright <http://github.com/Acubed>
+
+ function MediaType(s, p){
+ this.type = '';
+ this.params = {};
+ var c, i, n;
+ if(typeof s==='string'){
+ c = splitQuotedString(s);
+ this.type = c.shift();
+ for(i=0; i<c.length; i++){
+ this.parseParameter(c[i]);
+ }
+ }else if(s instanceof MediaType){
+ this.type = s.type;
+ this.q = s.q;
+ for(n in s.params) this.params[n]=s.params[n];
+ }
+ if(typeof p==='string'){
+ c = splitQuotedString(p);
+ for(i=0; i<c.length; i++){
+ this.parseParameter(c[i]);
+ }
+ }else if(typeof p==='object'){
+ for(n in p) this.params[n]=p[n];
+ }
+ }
+ MediaType.prototype.parseParameter = function parseParameter(s){
+ var param = s.split('=',1);
+ var name = param[0].trim();
+ var value = s.substr(param[0].length+1).trim();
+ if(!value || !name) return;
+ if(name==='q' && this.q===undefined){
+ this.q=parseFloat(value);
+ }else{
+ if(value[0]==='"' && value[value.length-1]==='"'){
+ value = value.substr(1, value.length-2);
+ value = value.replace(/\\(.)/g, function(a,b){return b;});
+ }
+ this.params[name]=value;
+ }
+ };
+ MediaType.prototype.toString = function toString(){
+ var str = this.type + ';q='+this.q;
+ for(var n in this.params){
+ str += ';'+n+'=';
+ if(this.params[n].match(/["=;<>\[\]\(\) ,\-]/)){
+ str += '"' + this.params[n].replace(/["\\]/g, function(a){return '\\'+a;}) + '"';
+ }else{
+ str += this.params[n];
+ }
+ }
+ return str;
+ };
+ exports.MediaType = MediaType;
+
+ // Split a string by character, but ignore quoted parts and backslash-escaped characters
+ function splitQuotedString(str, delim, quote){
+ delim = delim || ';';
+ quote = quote || '"';
+ var res = [];
+ var start = 0;
+ var offset = 0;
+ function findNextChar(v, c, i, a){
+ var p = str.indexOf(c, offset+1);
+ return (p<0)?v:Math.min(p,v);
+ }
+ while(offset>=0){
+ offset = [delim,quote].reduce(findNextChar, 1/0);
+ if(offset===1/0) break;
+ switch(str[offset]){
+ case quote:
+ // Skip to end of quoted string
+ while(1){
+ offset=str.indexOf(quote, offset+1);
+ if(offset<0) break;
+ if(str[offset-1]==='\\') continue;
+ break;
+ }
+ continue;
+ case delim:
+ res.push(str.substr(start, offset-start).trim());
+ start = ++offset;
+ break;
+ }
+ }
+ res.push(str.substr(start).trim());
+ return res;
+ }
+ exports.splitQuotedString = splitQuotedString;
+
+ // Split a list of content types found in an Accept header
+ // Maybe use it like: splitContentTypes(request.headers.accept).map(parseMedia)
+ function splitContentTypes(str){
+ return splitQuotedString(str, ',');
+ }
+ exports.splitContentTypes = splitContentTypes;
+
+ function parseMedia(str){
+ var o = new MediaType(str);
+ if(o.q===undefined) o.q=1;
+ return o;
+ }
+ exports.parseMedia = parseMedia;
+
+ // Pick an ideal representation to send given a list of representations to choose from and the client-preferred list
+ function select(reps, accept){
+ var cr = {q:0};
+ var ca = {q:0};
+ var cq = 0;
+ for(var i=0; i<reps.length; i++){
+ var r = reps[i];
+ var rq = r.q || 1;
+ for(var j=0; j<accept.length; j++){
+ var a=accept[j];
+ var aq = a.q || 1;
+ var cmp = mediaCmp(a, r);
+ if(cmp!==null && cmp>=0){
+ if(aq*rq>cq){
+ ca = a;
+ cr = r;
+ cq = ca.q*cr.q;
+ if(cq===1 && cr.type) return cr;
+ }
+ }
+ }
+ }
+ return cr.type&&cr;
+ }
+ exports.select = select;
+
+ // Determine if one media type is a subset of another
+ // If a is a superset of b (b is smaller than a), return 1
+ // If b is a superset of a, return -1
+ // If they are the exact same, return 0
+ // If they are disjoint, return null
+ function mediaCmp(a, b){
+ if(a.type==='*/*' && b.type!=='*/*') return 1;
+ else if(a.type!=='*/*' && b.type==='*/*') return -1;
+ var ac = (a.type||'').split('/');
+ var bc = (b.type||'').split('/');
+ if(ac[0]==='*' && bc[0]!=='*') return 1;
+ if(ac[0]!=='*' && bc[0]==='*') return -1;
+ if(a.type!==b.type) return null;
+ var ap = a.params || {};
+ var bp = b.params || {};
+ var ak = Object.keys(ap);
+ var bk = Object.keys(bp);
+ if(ak.length < bk.length) return 1;
+ if(ak.length > bk.length) return -1;
+ var k = ak.concat(bk).sort();
+ var dir = 0;
+ for(var n in ap){
+ if(ap[n] && !bp[n]){ if(dir<0) return null; else dir=1; }
+ if(!ap[n] && bp[n]){ if(dir>0) return null; else dir=-1; }
+ }
+ return dir;
+ }
+ exports.mediaCmp = mediaCmp;
+
+ return exports;
+});
diff --git a/web-ui/public/js/helpers/iterator.js b/web-ui/public/js/helpers/iterator.js
new file mode 100644
index 00000000..236c7a40
--- /dev/null
+++ b/web-ui/public/js/helpers/iterator.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(function () {
+ 'use strict';
+
+ return Iterator;
+
+ function Iterator(elems, startingIndex) {
+
+ this.index = startingIndex || 0;
+ this.elems = elems;
+
+ this.hasPrevious = function () {
+ return this.index !== 0;
+ };
+
+ this.hasNext = function () {
+ return this.index < this.elems.length - 1;
+ };
+
+ this.previous = function () {
+ return this.elems[--this.index];
+ };
+
+ this.next = function () {
+ return this.elems[++this.index];
+ };
+
+ this.current = function () {
+ return this.elems[this.index];
+ };
+
+ this.hasElements = function () {
+ return this.elems.length > 0;
+ };
+
+ this.removeCurrent = function () {
+ var removed = this.current(),
+ toRemove = this.index;
+
+ if(!this.hasNext()) { this.index--; }
+ this.elems.remove(toRemove);
+ return removed;
+ };
+ }
+});
diff --git a/web-ui/public/js/helpers/monitored_ajax.js b/web-ui/public/js/helpers/monitored_ajax.js
new file mode 100644
index 00000000..bbf85c45
--- /dev/null
+++ b/web-ui/public/js/helpers/monitored_ajax.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['page/events', 'views/i18n', 'helpers/browser'], function (events, i18n, browser) {
+
+ 'use strict';
+
+ var messages = {
+ timeout: 'error.timeout',
+ error: 'error.general',
+ parseerror: 'error.parse'
+ };
+
+ function monitoredAjax(on, url, config) {
+ config = config || {};
+ config.timeout = 60 * 1000;
+
+ var originalBeforeSend = config.beforeSend;
+ config.beforeSend = function () {
+ if (originalBeforeSend) {
+ originalBeforeSend();
+ }
+ };
+
+ config.headers = {'X-XSRF-TOKEN': browser.getCookie('XSRF-TOKEN')};
+
+ var originalComplete = config.complete;
+ config.complete = function () {
+ if (originalComplete) {
+ originalComplete();
+ }
+ };
+
+ return $.ajax(url, config).fail(function (xmlhttprequest, textstatus, message) {
+ if (!config.skipErrorMessage) {
+ var msg = (xmlhttprequest.responseJSON && xmlhttprequest.responseJSON.message) ||
+ messages[textstatus] || messages.error;
+ on.trigger(document, events.ui.userAlerts.displayMessage, {message: i18n.t(msg), class: 'error'});
+ }
+
+ if (xmlhttprequest.status === 302) {
+ var redirectUrl = xmlhttprequest.getResponseHeader('Location');
+ browser.redirect(redirectUrl);
+ } else if (xmlhttprequest.status === 401) {
+ browser.redirect('/');
+ }
+
+ }.bind(this));
+ }
+
+ return monitoredAjax;
+
+});
diff --git a/web-ui/public/js/helpers/sanitizer.js b/web-ui/public/js/helpers/sanitizer.js
new file mode 100644
index 00000000..443e8602
--- /dev/null
+++ b/web-ui/public/js/helpers/sanitizer.js
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2016 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['DOMPurify', 'he'], function (DOMPurify, he) {
+ 'use strict';
+
+ /**
+ * Sanitizes a mail body to safe-to-display HTML
+ */
+ var sanitizer = {};
+
+ sanitizer.whitelist = [{
+ // highlight tag open
+ pre: '&#x3C;&#x65;&#x6D;&#x20;&#x63;&#x6C;&#x61;&#x73;&#x73;&#x3D;&#x22;&#x73;&#x65;&#x61;&#x72;&#x63;&#x68;&#x2D;&#x68;&#x69;&#x67;&#x68;&#x6C;&#x69;&#x67;&#x68;&#x74;&#x22;&#x3E;',
+ post: '<em class="search-highlight">'
+ }, {
+ // highlight tag close
+ pre: '&#x3C;&#x2F;&#x65;&#x6D;&#x3E;',
+ post: '</em>'
+ }];
+
+ /**
+ * Adds html line breaks to a plaintext with line breaks (incl carriage return)
+ *
+ * @param {string} textPlainBody Plaintext input
+ * @returns {string} Plaintext with HTML line breals (<br/>)
+ */
+ sanitizer.addLineBreaks = function (textPlainBody) {
+ return textPlainBody.replace(/(\r)?\n/g, '<br/>').replace(/(&#xD;)?&#xA;/g, '<br/>');
+ };
+
+ /**
+ * Runs a given dirty body through DOMPurify, thereby removing
+ * potentially hazardous XSS attacks. Please be advised that this
+ * will not act as a privacy leak prevention. Contained contents
+ * will still point to remote sources.
+ *
+ * For future reference: Running DOMPurify with these parameters
+ * can help mitigate some of the most widely used privacy leaks.
+ * FORBID_TAGS: ['style', 'svg', 'audio', 'video', 'math'],
+ * FORBID_ATTR: ['src']
+ *
+ * @param {string} dirtyBody The unsanitized string
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.purifyHtml = function (dirtyBody) {
+ return DOMPurify.sanitize(dirtyBody, {
+ SAFE_FOR_JQUERY: true,
+ SAFE_FOR_TEMPLATES: true
+ });
+ };
+
+ /**
+ * Runs a given dirty body through he, thereby encoding everything
+ * as HTML entities.
+ *
+ * @param {string} dirtyBody The unsanitized string
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.purifyText = function (dirtyBody) {
+ var escapedBody = he.encode(dirtyBody, {
+ encodeEverything: true
+ });
+
+ this.whitelist.forEach(function(entry) {
+ while (escapedBody.indexOf(entry.pre) > -1) {
+ escapedBody = escapedBody.replace(entry.pre, entry.post);
+ }
+ });
+
+ return escapedBody;
+ };
+
+ /**
+ * Calls #purify and #addLineBreaks to turn untrusted mail body content
+ * into safe-to-display HTML.
+ *
+ * NB: HTML content is preferred to plaintext content.
+ *
+ * @param {object} mail Pixelated Mail Object
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.sanitize = function (mail) {
+ var body;
+
+ if (mail.htmlBody) {
+ body = this.purifyHtml(mail.htmlBody);
+ } else {
+ body = this.purifyText(mail.textPlainBody);
+ body = this.addLineBreaks(body);
+ }
+
+ return body;
+ };
+
+ /**
+ * Add hooks to DOMPurify for opening links in new windows
+ */
+ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
+ // set all elements owning target to target=_blank
+ if ('target' in node) {
+ node.setAttribute('target', '_blank');
+ }
+
+ // set non-HTML/MathML links to xlink:show=new
+ if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
+ node.setAttribute('xlink:show', 'new');
+ }
+ });
+
+ return sanitizer;
+});
diff --git a/web-ui/public/js/helpers/triggering.js b/web-ui/public/js/helpers/triggering.js
new file mode 100644
index 00000000..d26d9fc6
--- /dev/null
+++ b/web-ui/public/js/helpers/triggering.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define([], function() {
+ 'use strict';
+
+ return function(that, event, data, on) {
+ return function() {
+ if(on) {
+ that.trigger(on, event, data || {});
+ } else {
+ that.trigger(event, data || {});
+ }
+ };
+ };
+});
diff --git a/web-ui/public/js/helpers/view_helper.js b/web-ui/public/js/helpers/view_helper.js
new file mode 100644
index 00000000..ed9e0559
--- /dev/null
+++ b/web-ui/public/js/helpers/view_helper.js
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'helpers/contenttype',
+ 'views/i18n',
+ 'quoted-printable/quoted-printable',
+ 'utf8/utf8',
+ 'helpers/sanitizer'
+ ],
+ function(contentType, i18n, quotedPrintable, utf8, sanitizer) {
+ 'use strict';
+
+ function formatStatusClasses(ss) {
+ return _.map(ss, function(s) {
+ return 'status-' + s;
+ }).join(' ');
+ }
+
+ function formatMailBody(mail) {
+ return sanitizer.sanitize(mail);
+ }
+
+ function moveCaretToEnd(el) {
+ if (typeof el.selectionStart === 'number') {
+ el.selectionStart = el.selectionEnd = el.value.length;
+ } else if (typeof el.createTextRange !== 'undefined') {
+ el.focus();
+ var range = el.createTextRange();
+ range.collapse(false);
+ range.select();
+ }
+ }
+
+ function fixedSizeNumber(num, size) {
+ var res = num.toString();
+ while(res.length < size) {
+ res = '0' + res;
+ }
+ return res;
+ }
+
+ function createTodayDate() {
+ var today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ return today;
+ }
+
+ function moveCaretToEndOfText() {
+ var self = this;
+
+ moveCaretToEnd(self);
+ window.setTimeout(function() {
+ moveCaretToEnd(self);
+ }, 1);
+ }
+
+ function prependFrom(mail) {
+ return i18n.t(
+ 'reply-author-line', {'date': new Date(mail.header.date).toString(), 'from': mail.header.from}
+ );
+ }
+
+ function quoteMail(mail) {
+ return '\n\n' + prependFrom(mail) + mail.textPlainBody.replace(/^/mg, '> ');
+ }
+
+ function formatDate(dateString) {
+ var date = new Date(dateString);
+ var today = createTodayDate();
+ if (date.getTime() > today.getTime()) {
+ return fixedSizeNumber(date.getHours(), 2) + ':' + fixedSizeNumber(date.getMinutes(), 2);
+ } else {
+ return '' + date.getFullYear() + '-' + fixedSizeNumber(date.getMonth() + 1, 2) + '-' + fixedSizeNumber(date.getDate(), 2);
+ }
+ }
+
+ function formatSize(bytes) {
+ var e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'b';
+ }
+
+
+ function formatFingerPrint(fingerprint) {
+ fingerprint = fingerprint || '';
+ return fingerprint.replace(/(.{4})/g, '$1 ').trim();
+ }
+
+ function getSinceDate(sinceDate){
+ var commitDate = new Date(sinceDate);
+ var number = Date.now();
+ var millisecondsSince = number - commitDate;
+
+ var SECONDS = 1000,
+ MIN = 60 * SECONDS,
+ HOUR = MIN * 60,
+ DAY = HOUR * 24,
+ WEEK = DAY * 7,
+ MONTH = WEEK * 4,
+ YEAR = DAY * 365;
+
+ var years = Math.floor(millisecondsSince / YEAR);
+ if (years >= 1){
+ return years + ' year(s)';
+ }
+
+ var months = Math.floor(millisecondsSince / MONTH);
+ if (months >= 1) {
+ return months + ' month(s)';
+ }
+
+ var weeks = Math.floor(millisecondsSince / WEEK);
+ if (weeks >= 1) {
+ return weeks + ' week(s)';
+ }
+
+ var days = Math.floor(millisecondsSince / DAY);
+ if (days >= 1) {
+ return days + ' day(s)';
+ }
+
+ var hours = Math.floor(millisecondsSince / HOUR);
+ if (hours >= 1) {
+ return hours + ' hour(s)';
+ }
+
+ var minutes = Math.floor(millisecondsSince / MIN);
+ return minutes + ' minute(s)';
+ }
+
+ Handlebars.registerHelper('formatDate', formatDate);
+ Handlebars.registerHelper('formatSize', formatSize);
+ Handlebars.registerHelper('formatStatusClasses', formatStatusClasses);
+ Handlebars.registerHelper('formatFingerPrint', formatFingerPrint);
+ Handlebars.registerHelper('sinceDate', getSinceDate);
+
+ return {
+ formatStatusClasses: formatStatusClasses,
+ formatSize: formatSize,
+ formatMailBody: formatMailBody,
+ formatFingerPrint: formatFingerPrint,
+ moveCaretToEndOfText: moveCaretToEndOfText,
+ quoteMail: quoteMail,
+ sinceDate: getSinceDate,
+ i18n: i18n
+ };
+});
diff --git a/web-ui/public/js/lib/highlightRegex.js b/web-ui/public/js/lib/highlightRegex.js
new file mode 100644
index 00000000..17caaa23
--- /dev/null
+++ b/web-ui/public/js/lib/highlightRegex.js
@@ -0,0 +1,127 @@
+/*
+ * jQuery Highlight Regex Plugin v0.1.2
+ *
+ * Based on highlight v3 by Johann Burkard
+ * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
+ *
+ * (c) 2009-13 Jacob Rothstein
+ * MIT license
+ */
+
+;(function( $ ) {
+
+
+
+ var normalize = function( node ) {
+ if ( ! ( node && node.childNodes )) return
+
+ var children = $.makeArray( node.childNodes )
+ , prevTextNode = null
+
+ $.each( children, function( i, child ) {
+ if ( child.nodeType === 3 ) {
+ if ( child.nodeValue === "" ) {
+
+ node.removeChild( child )
+
+ } else if ( prevTextNode !== null ) {
+
+ prevTextNode.nodeValue += child.nodeValue;
+ node.removeChild( child )
+
+ } else {
+
+ prevTextNode = child
+
+ }
+ } else {
+ prevTextNode = null
+
+ if ( child.childNodes ) {
+ normalize( child )
+ }
+ }
+ })
+ }
+
+
+
+
+ $.fn.highlightRegex = function( regex, options ) {
+
+ if ( typeof regex === 'object' && !(regex.constructor.name == 'RegExp' || regex instanceof RegExp ) ) {
+ options = regex
+ regex = undefined
+ }
+
+ if ( typeof options === 'undefined' ) options = {}
+
+ options.className = options.className || 'highlight'
+ options.tagType = options.tagType || 'span'
+ options.attrs = options.attrs || {}
+
+ if ( typeof regex === 'undefined' || regex.source === '' ) {
+
+ $( this ).find( options.tagType + '.' + options.className ).each( function() {
+
+ $( this ).replaceWith( $( this ).text() )
+
+ normalize( $( this ).parent().get( 0 ))
+
+ })
+
+ } else {
+
+ $( this ).each( function() {
+
+ var elt = $( this ).get( 0 )
+
+ normalize( elt )
+
+ $.each( $.makeArray( elt.childNodes ), function( i, searchnode ) {
+
+ var spannode, middlebit, middleclone, pos, match, parent
+
+ normalize( searchnode )
+
+ if ( searchnode.nodeType == 3 ) {
+
+ // don't re-highlight the same node over and over
+ if ( $(searchnode).parent(options.tagType + '.' + options.className).length ) {
+ return;
+ }
+
+ while ( searchnode.data &&
+ ( pos = searchnode.data.search( regex )) >= 0 ) {
+
+ match = searchnode.data.slice( pos ).match( regex )[ 0 ]
+
+ if ( match.length > 0 ) {
+
+ spannode = document.createElement( options.tagType )
+ spannode.className = options.className
+ $(spannode).attr(options.attrs)
+
+ parent = searchnode.parentNode
+ middlebit = searchnode.splitText( pos )
+ searchnode = middlebit.splitText( match.length )
+ middleclone = middlebit.cloneNode( true )
+
+ spannode.appendChild( middleclone )
+ parent.replaceChild( spannode, middlebit )
+
+ } else break
+ }
+
+ } else {
+
+ $( searchnode ).highlightRegex( regex, options )
+
+ }
+ })
+ })
+ }
+
+ return $( this )
+ }
+})( jQuery );
diff --git a/web-ui/public/js/lib/html4-defs.js b/web-ui/public/js/lib/html4-defs.js
new file mode 100644
index 00000000..1ec575da
--- /dev/null
+++ b/web-ui/public/js/lib/html4-defs.js
@@ -0,0 +1,640 @@
+// Copyright Google Inc.
+// Licensed under the Apache Licence Version 2.0
+// Autogenerated at Mon Jul 14 18:51:33 BRT 2014
+// @overrides window
+// @provides html4
+define([], function() {
+var html4 = {};
+html4.atype = {
+ 'NONE': 0,
+ 'URI': 1,
+ 'URI_FRAGMENT': 11,
+ 'SCRIPT': 2,
+ 'STYLE': 3,
+ 'HTML': 12,
+ 'ID': 4,
+ 'IDREF': 5,
+ 'IDREFS': 6,
+ 'GLOBAL_NAME': 7,
+ 'LOCAL_NAME': 8,
+ 'CLASSES': 9,
+ 'FRAME_TARGET': 10,
+ 'MEDIA_QUERY': 13
+};
+html4[ 'atype' ] = html4.atype;
+html4.ATTRIBS = {
+ '*::class': 9,
+ '*::dir': 0,
+ '*::draggable': 0,
+ '*::hidden': 0,
+ '*::id': 4,
+ '*::inert': 0,
+ '*::itemprop': 0,
+ '*::itemref': 6,
+ '*::itemscope': 0,
+ '*::lang': 0,
+ '*::onblur': 2,
+ '*::onchange': 2,
+ '*::onclick': 2,
+ '*::ondblclick': 2,
+ '*::onerror': 2,
+ '*::onfocus': 2,
+ '*::onkeydown': 2,
+ '*::onkeypress': 2,
+ '*::onkeyup': 2,
+ '*::onload': 2,
+ '*::onmousedown': 2,
+ '*::onmousemove': 2,
+ '*::onmouseout': 2,
+ '*::onmouseover': 2,
+ '*::onmouseup': 2,
+ '*::onreset': 2,
+ '*::onscroll': 2,
+ '*::onselect': 2,
+ '*::onsubmit': 2,
+ '*::ontouchcancel': 2,
+ '*::ontouchend': 2,
+ '*::ontouchenter': 2,
+ '*::ontouchleave': 2,
+ '*::ontouchmove': 2,
+ '*::ontouchstart': 2,
+ '*::onunload': 2,
+ '*::spellcheck': 0,
+ '*::style': 3,
+ '*::tabindex': 0,
+ '*::title': 0,
+ '*::translate': 0,
+ 'a::accesskey': 0,
+ 'a::coords': 0,
+ 'a::href': 1,
+ 'a::hreflang': 0,
+ 'a::name': 7,
+ 'a::onblur': 2,
+ 'a::onfocus': 2,
+ 'a::shape': 0,
+ 'a::target': 10,
+ 'a::type': 0,
+ 'area::accesskey': 0,
+ 'area::alt': 0,
+ 'area::coords': 0,
+ 'area::href': 1,
+ 'area::nohref': 0,
+ 'area::onblur': 2,
+ 'area::onfocus': 2,
+ 'area::shape': 0,
+ 'area::target': 10,
+ 'audio::controls': 0,
+ 'audio::loop': 0,
+ 'audio::mediagroup': 5,
+ 'audio::muted': 0,
+ 'audio::preload': 0,
+ 'audio::src': 1,
+ 'bdo::dir': 0,
+ 'blockquote::cite': 1,
+ 'br::clear': 0,
+ 'button::accesskey': 0,
+ 'button::disabled': 0,
+ 'button::name': 8,
+ 'button::onblur': 2,
+ 'button::onfocus': 2,
+ 'button::type': 0,
+ 'button::value': 0,
+ 'canvas::height': 0,
+ 'canvas::width': 0,
+ 'caption::align': 0,
+ 'col::align': 0,
+ 'col::char': 0,
+ 'col::charoff': 0,
+ 'col::span': 0,
+ 'col::valign': 0,
+ 'col::width': 0,
+ 'colgroup::align': 0,
+ 'colgroup::char': 0,
+ 'colgroup::charoff': 0,
+ 'colgroup::span': 0,
+ 'colgroup::valign': 0,
+ 'colgroup::width': 0,
+ 'command::checked': 0,
+ 'command::command': 5,
+ 'command::disabled': 0,
+ 'command::icon': 1,
+ 'command::label': 0,
+ 'command::radiogroup': 0,
+ 'command::type': 0,
+ 'data::value': 0,
+ 'del::cite': 1,
+ 'del::datetime': 0,
+ 'details::open': 0,
+ 'dir::compact': 0,
+ 'div::align': 0,
+ 'dl::compact': 0,
+ 'fieldset::disabled': 0,
+ 'font::color': 0,
+ 'font::face': 0,
+ 'font::size': 0,
+ 'form::accept': 0,
+ 'form::action': 1,
+ 'form::autocomplete': 0,
+ 'form::enctype': 0,
+ 'form::method': 0,
+ 'form::name': 7,
+ 'form::novalidate': 0,
+ 'form::onreset': 2,
+ 'form::onsubmit': 2,
+ 'form::target': 10,
+ 'h1::align': 0,
+ 'h2::align': 0,
+ 'h3::align': 0,
+ 'h4::align': 0,
+ 'h5::align': 0,
+ 'h6::align': 0,
+ 'hr::align': 0,
+ 'hr::noshade': 0,
+ 'hr::size': 0,
+ 'hr::width': 0,
+ 'iframe::align': 0,
+ 'iframe::frameborder': 0,
+ 'iframe::height': 0,
+ 'iframe::marginheight': 0,
+ 'iframe::marginwidth': 0,
+ 'iframe::width': 0,
+ 'img::align': 0,
+ 'img::alt': 0,
+ 'img::border': 0,
+ 'img::height': 0,
+ 'img::hspace': 0,
+ 'img::ismap': 0,
+ 'img::name': 7,
+ 'img::src': 1,
+ 'img::usemap': 11,
+ 'img::vspace': 0,
+ 'img::width': 0,
+ 'input::accept': 0,
+ 'input::accesskey': 0,
+ 'input::align': 0,
+ 'input::alt': 0,
+ 'input::autocomplete': 0,
+ 'input::checked': 0,
+ 'input::disabled': 0,
+ 'input::inputmode': 0,
+ 'input::ismap': 0,
+ 'input::list': 5,
+ 'input::max': 0,
+ 'input::maxlength': 0,
+ 'input::min': 0,
+ 'input::multiple': 0,
+ 'input::name': 8,
+ 'input::onblur': 2,
+ 'input::onchange': 2,
+ 'input::onfocus': 2,
+ 'input::onselect': 2,
+ 'input::pattern': 0,
+ 'input::placeholder': 0,
+ 'input::readonly': 0,
+ 'input::required': 0,
+ 'input::size': 0,
+ 'input::src': 1,
+ 'input::step': 0,
+ 'input::type': 0,
+ 'input::usemap': 11,
+ 'input::value': 0,
+ 'ins::cite': 1,
+ 'ins::datetime': 0,
+ 'label::accesskey': 0,
+ 'label::for': 5,
+ 'label::onblur': 2,
+ 'label::onfocus': 2,
+ 'legend::accesskey': 0,
+ 'legend::align': 0,
+ 'li::type': 0,
+ 'li::value': 0,
+ 'map::name': 7,
+ 'menu::compact': 0,
+ 'menu::label': 0,
+ 'menu::type': 0,
+ 'meter::high': 0,
+ 'meter::low': 0,
+ 'meter::max': 0,
+ 'meter::min': 0,
+ 'meter::value': 0,
+ 'ol::compact': 0,
+ 'ol::reversed': 0,
+ 'ol::start': 0,
+ 'ol::type': 0,
+ 'optgroup::disabled': 0,
+ 'optgroup::label': 0,
+ 'option::disabled': 0,
+ 'option::label': 0,
+ 'option::selected': 0,
+ 'option::value': 0,
+ 'output::for': 6,
+ 'output::name': 8,
+ 'p::align': 0,
+ 'pre::width': 0,
+ 'progress::max': 0,
+ 'progress::min': 0,
+ 'progress::value': 0,
+ 'q::cite': 1,
+ 'select::autocomplete': 0,
+ 'select::disabled': 0,
+ 'select::multiple': 0,
+ 'select::name': 8,
+ 'select::onblur': 2,
+ 'select::onchange': 2,
+ 'select::onfocus': 2,
+ 'select::required': 0,
+ 'select::size': 0,
+ 'source::type': 0,
+ 'table::align': 0,
+ 'table::bgcolor': 0,
+ 'table::border': 0,
+ 'table::cellpadding': 0,
+ 'table::cellspacing': 0,
+ 'table::frame': 0,
+ 'table::rules': 0,
+ 'table::summary': 0,
+ 'table::width': 0,
+ 'tbody::align': 0,
+ 'tbody::char': 0,
+ 'tbody::charoff': 0,
+ 'tbody::valign': 0,
+ 'td::abbr': 0,
+ 'td::align': 0,
+ 'td::axis': 0,
+ 'td::bgcolor': 0,
+ 'td::char': 0,
+ 'td::charoff': 0,
+ 'td::colspan': 0,
+ 'td::headers': 6,
+ 'td::height': 0,
+ 'td::nowrap': 0,
+ 'td::rowspan': 0,
+ 'td::scope': 0,
+ 'td::valign': 0,
+ 'td::width': 0,
+ 'textarea::accesskey': 0,
+ 'textarea::autocomplete': 0,
+ 'textarea::cols': 0,
+ 'textarea::disabled': 0,
+ 'textarea::inputmode': 0,
+ 'textarea::name': 8,
+ 'textarea::onblur': 2,
+ 'textarea::onchange': 2,
+ 'textarea::onfocus': 2,
+ 'textarea::onselect': 2,
+ 'textarea::placeholder': 0,
+ 'textarea::readonly': 0,
+ 'textarea::required': 0,
+ 'textarea::rows': 0,
+ 'textarea::wrap': 0,
+ 'tfoot::align': 0,
+ 'tfoot::char': 0,
+ 'tfoot::charoff': 0,
+ 'tfoot::valign': 0,
+ 'th::abbr': 0,
+ 'th::align': 0,
+ 'th::axis': 0,
+ 'th::bgcolor': 0,
+ 'th::char': 0,
+ 'th::charoff': 0,
+ 'th::colspan': 0,
+ 'th::headers': 6,
+ 'th::height': 0,
+ 'th::nowrap': 0,
+ 'th::rowspan': 0,
+ 'th::scope': 0,
+ 'th::valign': 0,
+ 'th::width': 0,
+ 'thead::align': 0,
+ 'thead::char': 0,
+ 'thead::charoff': 0,
+ 'thead::valign': 0,
+ 'tr::align': 0,
+ 'tr::bgcolor': 0,
+ 'tr::char': 0,
+ 'tr::charoff': 0,
+ 'tr::valign': 0,
+ 'track::default': 0,
+ 'track::kind': 0,
+ 'track::label': 0,
+ 'track::srclang': 0,
+ 'ul::compact': 0,
+ 'ul::type': 0,
+ 'video::controls': 0,
+ 'video::height': 0,
+ 'video::loop': 0,
+ 'video::mediagroup': 5,
+ 'video::muted': 0,
+ 'video::poster': 1,
+ 'video::preload': 0,
+ 'video::src': 1,
+ 'video::width': 0
+};
+html4[ 'ATTRIBS' ] = html4.ATTRIBS;
+html4.eflags = {
+ 'OPTIONAL_ENDTAG': 1,
+ 'EMPTY': 2,
+ 'CDATA': 4,
+ 'RCDATA': 8,
+ 'UNSAFE': 16,
+ 'FOLDABLE': 32,
+ 'SCRIPT': 64,
+ 'STYLE': 128,
+ 'VIRTUALIZED': 256
+};
+html4[ 'eflags' ] = html4.eflags;
+html4.ELEMENTS = {
+ 'a': 0,
+ 'abbr': 0,
+ 'acronym': 0,
+ 'address': 0,
+ 'applet': 272,
+ 'area': 2,
+ 'article': 0,
+ 'aside': 0,
+ 'audio': 0,
+ 'b': 0,
+ 'base': 274,
+ 'basefont': 274,
+ 'bdi': 0,
+ 'bdo': 0,
+ 'big': 0,
+ 'blockquote': 0,
+ 'body': 305,
+ 'br': 2,
+ 'button': 0,
+ 'canvas': 0,
+ 'caption': 0,
+ 'center': 0,
+ 'cite': 0,
+ 'code': 0,
+ 'col': 2,
+ 'colgroup': 1,
+ 'command': 2,
+ 'data': 0,
+ 'datalist': 0,
+ 'dd': 1,
+ 'del': 0,
+ 'details': 0,
+ 'dfn': 0,
+ 'dialog': 272,
+ 'dir': 0,
+ 'div': 0,
+ 'dl': 0,
+ 'dt': 1,
+ 'em': 0,
+ 'fieldset': 0,
+ 'figcaption': 0,
+ 'figure': 0,
+ 'font': 0,
+ 'footer': 0,
+ 'form': 0,
+ 'frame': 274,
+ 'frameset': 272,
+ 'h1': 0,
+ 'h2': 0,
+ 'h3': 0,
+ 'h4': 0,
+ 'h5': 0,
+ 'h6': 0,
+ 'head': 305,
+ 'header': 0,
+ 'hgroup': 0,
+ 'hr': 2,
+ 'html': 305,
+ 'i': 0,
+ 'iframe': 4,
+ 'img': 2,
+ 'input': 2,
+ 'ins': 0,
+ 'isindex': 274,
+ 'kbd': 0,
+ 'keygen': 274,
+ 'label': 0,
+ 'legend': 0,
+ 'li': 1,
+ 'link': 274,
+ 'map': 0,
+ 'mark': 0,
+ 'menu': 0,
+ 'meta': 274,
+ 'meter': 0,
+ 'nav': 0,
+ 'nobr': 0,
+ 'noembed': 276,
+ 'noframes': 276,
+ 'noscript': 276,
+ 'object': 272,
+ 'ol': 0,
+ 'optgroup': 0,
+ 'option': 1,
+ 'output': 0,
+ 'p': 1,
+ 'param': 274,
+ 'pre': 0,
+ 'progress': 0,
+ 'q': 0,
+ 's': 0,
+ 'samp': 0,
+ 'script': 84,
+ 'section': 0,
+ 'select': 0,
+ 'small': 0,
+ 'source': 2,
+ 'span': 0,
+ 'strike': 0,
+ 'strong': 0,
+ 'style': 148,
+ 'sub': 0,
+ 'summary': 0,
+ 'sup': 0,
+ 'table': 0,
+ 'tbody': 1,
+ 'td': 1,
+ 'textarea': 8,
+ 'tfoot': 1,
+ 'th': 1,
+ 'thead': 1,
+ 'time': 0,
+ 'title': 280,
+ 'tr': 1,
+ 'track': 2,
+ 'tt': 0,
+ 'u': 0,
+ 'ul': 0,
+ 'var': 0,
+ 'video': 0,
+ 'wbr': 2
+};
+html4[ 'ELEMENTS' ] = html4.ELEMENTS;
+html4.ELEMENT_DOM_INTERFACES = {
+ 'a': 'HTMLAnchorElement',
+ 'abbr': 'HTMLElement',
+ 'acronym': 'HTMLElement',
+ 'address': 'HTMLElement',
+ 'applet': 'HTMLAppletElement',
+ 'area': 'HTMLAreaElement',
+ 'article': 'HTMLElement',
+ 'aside': 'HTMLElement',
+ 'audio': 'HTMLAudioElement',
+ 'b': 'HTMLElement',
+ 'base': 'HTMLBaseElement',
+ 'basefont': 'HTMLBaseFontElement',
+ 'bdi': 'HTMLElement',
+ 'bdo': 'HTMLElement',
+ 'big': 'HTMLElement',
+ 'blockquote': 'HTMLQuoteElement',
+ 'body': 'HTMLBodyElement',
+ 'br': 'HTMLBRElement',
+ 'button': 'HTMLButtonElement',
+ 'canvas': 'HTMLCanvasElement',
+ 'caption': 'HTMLTableCaptionElement',
+ 'center': 'HTMLElement',
+ 'cite': 'HTMLElement',
+ 'code': 'HTMLElement',
+ 'col': 'HTMLTableColElement',
+ 'colgroup': 'HTMLTableColElement',
+ 'command': 'HTMLCommandElement',
+ 'data': 'HTMLElement',
+ 'datalist': 'HTMLDataListElement',
+ 'dd': 'HTMLElement',
+ 'del': 'HTMLModElement',
+ 'details': 'HTMLDetailsElement',
+ 'dfn': 'HTMLElement',
+ 'dialog': 'HTMLDialogElement',
+ 'dir': 'HTMLDirectoryElement',
+ 'div': 'HTMLDivElement',
+ 'dl': 'HTMLDListElement',
+ 'dt': 'HTMLElement',
+ 'em': 'HTMLElement',
+ 'fieldset': 'HTMLFieldSetElement',
+ 'figcaption': 'HTMLElement',
+ 'figure': 'HTMLElement',
+ 'font': 'HTMLFontElement',
+ 'footer': 'HTMLElement',
+ 'form': 'HTMLFormElement',
+ 'frame': 'HTMLFrameElement',
+ 'frameset': 'HTMLFrameSetElement',
+ 'h1': 'HTMLHeadingElement',
+ 'h2': 'HTMLHeadingElement',
+ 'h3': 'HTMLHeadingElement',
+ 'h4': 'HTMLHeadingElement',
+ 'h5': 'HTMLHeadingElement',
+ 'h6': 'HTMLHeadingElement',
+ 'head': 'HTMLHeadElement',
+ 'header': 'HTMLElement',
+ 'hgroup': 'HTMLElement',
+ 'hr': 'HTMLHRElement',
+ 'html': 'HTMLHtmlElement',
+ 'i': 'HTMLElement',
+ 'iframe': 'HTMLIFrameElement',
+ 'img': 'HTMLImageElement',
+ 'input': 'HTMLInputElement',
+ 'ins': 'HTMLModElement',
+ 'isindex': 'HTMLUnknownElement',
+ 'kbd': 'HTMLElement',
+ 'keygen': 'HTMLKeygenElement',
+ 'label': 'HTMLLabelElement',
+ 'legend': 'HTMLLegendElement',
+ 'li': 'HTMLLIElement',
+ 'link': 'HTMLLinkElement',
+ 'map': 'HTMLMapElement',
+ 'mark': 'HTMLElement',
+ 'menu': 'HTMLMenuElement',
+ 'meta': 'HTMLMetaElement',
+ 'meter': 'HTMLMeterElement',
+ 'nav': 'HTMLElement',
+ 'nobr': 'HTMLElement',
+ 'noembed': 'HTMLElement',
+ 'noframes': 'HTMLElement',
+ 'noscript': 'HTMLElement',
+ 'object': 'HTMLObjectElement',
+ 'ol': 'HTMLOListElement',
+ 'optgroup': 'HTMLOptGroupElement',
+ 'option': 'HTMLOptionElement',
+ 'output': 'HTMLOutputElement',
+ 'p': 'HTMLParagraphElement',
+ 'param': 'HTMLParamElement',
+ 'pre': 'HTMLPreElement',
+ 'progress': 'HTMLProgressElement',
+ 'q': 'HTMLQuoteElement',
+ 's': 'HTMLElement',
+ 'samp': 'HTMLElement',
+ 'script': 'HTMLScriptElement',
+ 'section': 'HTMLElement',
+ 'select': 'HTMLSelectElement',
+ 'small': 'HTMLElement',
+ 'source': 'HTMLSourceElement',
+ 'span': 'HTMLSpanElement',
+ 'strike': 'HTMLElement',
+ 'strong': 'HTMLElement',
+ 'style': 'HTMLStyleElement',
+ 'sub': 'HTMLElement',
+ 'summary': 'HTMLElement',
+ 'sup': 'HTMLElement',
+ 'table': 'HTMLTableElement',
+ 'tbody': 'HTMLTableSectionElement',
+ 'td': 'HTMLTableDataCellElement',
+ 'textarea': 'HTMLTextAreaElement',
+ 'tfoot': 'HTMLTableSectionElement',
+ 'th': 'HTMLTableHeaderCellElement',
+ 'thead': 'HTMLTableSectionElement',
+ 'time': 'HTMLTimeElement',
+ 'title': 'HTMLTitleElement',
+ 'tr': 'HTMLTableRowElement',
+ 'track': 'HTMLTrackElement',
+ 'tt': 'HTMLElement',
+ 'u': 'HTMLElement',
+ 'ul': 'HTMLUListElement',
+ 'var': 'HTMLElement',
+ 'video': 'HTMLVideoElement',
+ 'wbr': 'HTMLElement'
+};
+html4[ 'ELEMENT_DOM_INTERFACES' ] = html4.ELEMENT_DOM_INTERFACES;
+html4.ueffects = {
+ 'NOT_LOADED': 0,
+ 'SAME_DOCUMENT': 1,
+ 'NEW_DOCUMENT': 2
+};
+html4[ 'ueffects' ] = html4.ueffects;
+html4.URIEFFECTS = {
+ 'a::href': 2,
+ 'area::href': 2,
+ 'audio::src': 1,
+ 'blockquote::cite': 0,
+ 'command::icon': 1,
+ 'del::cite': 0,
+ 'form::action': 2,
+ 'img::src': 1,
+ 'input::src': 1,
+ 'ins::cite': 0,
+ 'q::cite': 0,
+ 'video::poster': 1,
+ 'video::src': 1
+};
+html4[ 'URIEFFECTS' ] = html4.URIEFFECTS;
+html4.ltypes = {
+ 'UNSANDBOXED': 2,
+ 'SANDBOXED': 1,
+ 'DATA': 0
+};
+html4[ 'ltypes' ] = html4.ltypes;
+html4.LOADERTYPES = {
+ 'a::href': 2,
+ 'area::href': 2,
+ 'audio::src': 2,
+ 'blockquote::cite': 2,
+ 'command::icon': 1,
+ 'del::cite': 2,
+ 'form::action': 2,
+ 'img::src': 1,
+ 'input::src': 1,
+ 'ins::cite': 2,
+ 'q::cite': 2,
+ 'video::poster': 1,
+ 'video::src': 2
+};
+html4[ 'LOADERTYPES' ] = html4.LOADERTYPES;
+
+return html4
+});
diff --git a/web-ui/public/js/mail_list/domain/refresher.js b/web-ui/public/js/mail_list/domain/refresher.js
new file mode 100644
index 00000000..38c9cde5
--- /dev/null
+++ b/web-ui/public/js/mail_list/domain/refresher.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'page/events', 'features'], function(defineComponent, events, features) {
+ 'use strict';
+
+ return defineComponent(refresher);
+
+ function refresher() {
+ this.defaultAttrs({
+ interval: 20000
+ });
+
+ this.setupRefresher = function() {
+ setTimeout(this.doRefresh.bind(this), this.attr.interval);
+ };
+
+ this.doRefresh = function() {
+ this.trigger(document, events.ui.mails.refresh);
+ this.setupRefresher();
+ };
+
+ this.after('initialize', function () {
+ if (features.isAutoRefreshEnabled()) {
+ this.setupRefresher();
+ }
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list/ui/mail_item_factory.js b/web-ui/public/js/mail_list/ui/mail_item_factory.js
new file mode 100644
index 00000000..7205d35c
--- /dev/null
+++ b/web-ui/public/js/mail_list/ui/mail_item_factory.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'mail_list/ui/mail_items/generic_mail_item',
+ 'mail_list/ui/mail_items/draft_item',
+ 'mail_list/ui/mail_items/sent_item'
+ ],
+ function (GenericMailItem, DraftItem, SentItem) {
+ 'use strict';
+
+ var MAIL_ITEM_TYPE = {
+ 'drafts': DraftItem,
+ 'sent': SentItem,
+ 'trash': GenericMailItem
+ };
+
+ var TEMPLATE_TYPE = {
+ 'drafts': 'draft',
+ 'sent': 'sent',
+ 'trash': 'trash'
+ };
+
+ var createAndAttach = function (nodeToAttachTo, mail, currentMailIdent, currentTag, isChecked) {
+ var mailItemContainer = $('<li>', { id: 'mail-' + mail.ident});
+ nodeToAttachTo.append(mailItemContainer);
+
+ mail.currentTag = currentTag;
+ var mailToCreate = MAIL_ITEM_TYPE[mail.mailbox] || GenericMailItem;
+ mailToCreate.attachTo(mailItemContainer, {
+ mail: mail,
+ selected: mail.ident === currentMailIdent,
+ tag: currentTag,
+ isChecked: isChecked,
+ templateType: TEMPLATE_TYPE[mail.mailbox] || 'single'
+ });
+
+ };
+
+ return {
+ createAndAttach: createAndAttach
+ };
+ }
+);
diff --git a/web-ui/public/js/mail_list/ui/mail_items/draft_item.js b/web-ui/public/js/mail_list/ui/mail_items/draft_item.js
new file mode 100644
index 00000000..57fbafd5
--- /dev/null
+++ b/web-ui/public/js/mail_list/ui/mail_items/draft_item.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'helpers/view_helper',
+ 'mail_list/ui/mail_items/mail_item',
+ 'page/events'
+ ],
+
+ function (defineComponent, viewHelpers, mailItem, events) {
+ 'use strict';
+
+ return defineComponent(draftItem, mailItem);
+
+ function draftItem() {
+ this.triggerOpenMail = function (ev) {
+ if (this.isOpeningOnANewTab(ev)) {
+ return;
+ }
+ this.trigger(document, events.dispatchers.rightPane.openDraft, { ident: this.attr.mail.ident });
+ this.trigger(document, events.ui.mail.updateSelected, { ident: this.attr.mail.ident });
+ this.trigger(document, events.router.pushState, { mailIdent: this.attr.mail.ident });
+ ev.preventDefault(); // don't let the hashchange trigger a popstate
+ };
+
+ this.after('initialize', function () {
+ this.render();
+
+ if (this.attr.isChecked) {
+ this.checkCheckbox();
+ }
+
+ this.on(document, events.ui.composeBox.newMessage, this.doUnselect);
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.mails.teardown, this.teardown);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list/ui/mail_items/generic_mail_item.js b/web-ui/public/js/mail_list/ui/mail_items/generic_mail_item.js
new file mode 100644
index 00000000..939f7e1b
--- /dev/null
+++ b/web-ui/public/js/mail_list/ui/mail_items/generic_mail_item.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'helpers/view_helper',
+ 'mail_list/ui/mail_items/mail_item',
+ 'page/events'
+ ],
+
+ function (defineComponent, viewHelpers, mailItem, events) {
+ 'use strict';
+
+ return defineComponent(genericMailItem, mailItem);
+
+ function genericMailItem() {
+ this.status = {
+ READ: 'read'
+ };
+
+ this.triggerOpenMail = function (ev) {
+ if (this.isOpeningOnANewTab(ev)) {
+ updateMailStatusToRead.call(this);
+ return;
+ }
+ this.trigger(document, events.ui.mail.open, { ident: this.attr.mail.ident });
+ this.trigger(document, events.router.pushState, { mailIdent: this.attr.mail.ident });
+ ev.preventDefault(); // don't let the hashchange trigger a popstate
+ };
+
+ function updateMailStatusToRead() {
+ if (!_.contains(this.attr.mail.status, this.status.READ)) {
+ var mail_read_data = { ident: this.attr.mail.ident, tags: this.attr.mail.tags, mailbox: this.attr.mail.mailbox };
+ this.trigger(document, events.mail.read, mail_read_data);
+ this.attr.mail.status.push(this.status.READ);
+ this.$node.addClass(viewHelpers.formatStatusClasses(this.attr.mail.status));
+ }
+ }
+
+ this.openMail = function (ev, data) {
+ if (data.ident !== this.attr.mail.ident) {
+ return;
+ }
+ updateMailStatusToRead.call(this);
+
+ this.trigger(document, events.ui.mail.updateSelected, { ident: this.attr.mail.ident });
+ };
+
+ this.updateTags = function(ev, data) {
+ if(data.ident === this.attr.mail.ident){
+ this.attr.tags = data.tags;
+ if(!_.contains(this.attr.tags, this.attr.tag)) {
+ this.teardown();
+ } else {
+ this.render();
+ }
+ }
+ };
+
+ this.deleteMail = function(ev, data) {
+ if(data.mail.ident === this.attr.mail.ident){
+ this.teardown();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.render();
+
+ if (this.attr.isChecked) {
+ this.checkCheckbox();
+ }
+
+ this.on(document, events.ui.composeBox.newMessage, this.doUnselect);
+ this.on(document, events.ui.mail.open, this.openMail);
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.mails.teardown, this.teardown);
+ this.on(document, events.mail.tags.update, this.updateTags);
+ this.on(document, events.mail.delete, this.deleteMail);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list/ui/mail_items/mail_item.js b/web-ui/public/js/mail_list/ui/mail_items/mail_item.js
new file mode 100644
index 00000000..be664289
--- /dev/null
+++ b/web-ui/public/js/mail_list/ui/mail_items/mail_item.js
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'helpers/view_helper',
+ 'views/templates',
+ 'page/events'
+ ],
+ function (viewHelper, templates, events) {
+
+ 'use strict';
+ function mailItem() {
+ this.updateSelected = function (ev, data) {
+ if (data.ident === this.attr.mail.ident) { this.doSelect(); }
+ else { this.doUnselect(); }
+ };
+
+ this.isOpeningOnANewTab = function (ev) {
+ return ev.metaKey || ev.ctrlKey || ev.which === 2;
+ };
+
+ this.doSelect = function () {
+ this.$node.addClass('selected');
+ };
+
+ this.doUnselect = function () {
+ this.$node.removeClass('selected');
+ };
+
+ this.doMailChecked = function (ev) {
+ if (ev.target.checked) {
+ this.checkCheckbox();
+ } else {
+ this.uncheckCheckbox();
+ }
+ };
+
+ this.checkboxElement = function () {
+ return this.$node.find('input[type=checkbox]');
+ };
+
+ this.checkCheckbox = function () {
+ this.checkboxElement().prop('checked', true);
+ this.trigger(document, events.ui.mail.checked, { mail: this.attr.mail});
+ };
+
+ this.uncheckCheckbox = function () {
+ this.checkboxElement().prop('checked', false);
+ this.trigger(document, events.ui.mail.unchecked, { mail: this.attr.mail});
+ };
+
+ this.render = function () {
+ this.attr.mail.tagsForListView = _.without(this.attr.mail.tags, this.attr.tag);
+ var mailItemHtml = templates.mails[this.attr.templateType](this.attr.mail);
+ this.$node.html(mailItemHtml);
+ this.$node.addClass("mail-list-entry");
+ this.$node.addClass(viewHelper.formatStatusClasses(this.attr.mail.status));
+ if (this.attr.selected) { this.doSelect(); }
+ this.on(this.$node.find('a'), 'click', this.triggerOpenMail);
+ };
+
+ this.after('initialize', function () {
+ this.on(this.$node.find('input[type=checkbox]'), 'change', this.doMailChecked);
+ this.on(document, events.ui.mails.cleanSelected, this.doUnselect);
+ this.on(document, events.ui.tag.select, this.doUnselect);
+ this.on(document, events.ui.tag.select, this.uncheckCheckbox);
+ this.on(document, events.ui.mails.uncheckAll, this.uncheckCheckbox);
+ this.on(document, events.ui.mails.checkAll, this.checkCheckbox);
+ });
+ }
+
+ return mailItem;
+});
diff --git a/web-ui/public/js/mail_list/ui/mail_items/sent_item.js b/web-ui/public/js/mail_list/ui/mail_items/sent_item.js
new file mode 100644
index 00000000..9e511068
--- /dev/null
+++ b/web-ui/public/js/mail_list/ui/mail_items/sent_item.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'mail_list/ui/mail_items/mail_item',
+ 'page/events'
+ ],
+
+ function (defineComponent, mailItem, events) {
+ 'use strict';
+
+ return defineComponent(sentItem, mailItem);
+
+ function sentItem() {
+ this.triggerOpenMail = function (ev) {
+ if (this.isOpeningOnANewTab(ev)) {
+ return;
+ }
+ this.trigger(document, events.ui.mail.open, { ident: this.attr.mail.ident });
+ this.trigger(document, events.router.pushState, { mailIdent: this.attr.mail.ident });
+ ev.preventDefault(); // don't let the hashchange trigger a popstate
+ };
+
+ this.openMail = function (ev, data) {
+ if (data.ident !== this.attr.mail.ident) {
+ return;
+ }
+ this.trigger(document, events.ui.mail.updateSelected, { ident: this.attr.mail.ident });
+ };
+
+ this.after('initialize', function () {
+ this.render();
+
+ if (this.attr.isChecked) {
+ this.checkCheckbox();
+ }
+
+ this.on(document, events.ui.composeBox.newMessage, this.doUnselect);
+ this.on(document, events.ui.mail.open, this.openMail);
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.mails.teardown, this.teardown);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list/ui/mail_list.js b/web-ui/public/js/mail_list/ui/mail_list.js
new file mode 100644
index 00000000..af4821a8
--- /dev/null
+++ b/web-ui/public/js/mail_list/ui/mail_list.js
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'flight/lib/utils',
+ 'mail_list/ui/mail_item_factory',
+ 'page/router/url_params',
+ 'page/events'
+ ],
+
+ function (defineComponent, utils, MailItemFactory, urlParams, events) {
+ 'use strict';
+
+ return defineComponent(mailList);
+
+ function mailList () {
+ var openMailEventFor = function (tag) {
+ return tag === 'drafts' ? events.dispatchers.rightPane.openDraft : events.ui.mail.open;
+ };
+
+ this.defaultAttrs({
+ mail: '.mail',
+ currentMailIdent: '',
+ urlParams: urlParams,
+ initialized: false,
+ checkedMails: {}
+ });
+
+ this.appendMail = function (mail) {
+ var isChecked = mail.ident in this.attr.checkedMails;
+ MailItemFactory.createAndAttach(this.$node, mail, this.attr.currentMailIdent, this.attr.currentTag, isChecked);
+ };
+
+ this.resetMailList = function () {
+ this.trigger(document, events.mails.teardown);
+ this.$node.empty();
+ };
+
+ this.triggerMailOpenForPopState = function (data) {
+ if (data.mailIdent) {
+ this.trigger(document, openMailEventFor(data.tag), { ident: data.mailIdent });
+ }
+ };
+
+ this.shouldSelectEmailFromUrlMailIdent = function () {
+ return this.attr.urlParams.hasMailIdent();
+ };
+
+ this.selectMailBasedOnUrlMailIdent = function () {
+ var mailIdent = this.attr.urlParams.getMailIdent();
+ this.trigger(document, openMailEventFor(this.attr.currentTag), { ident: mailIdent });
+ this.trigger(document, events.router.pushState, { tag: this.attr.currentTag, mailIdent: mailIdent });
+ };
+
+ this.updateCurrentTagAndMail = function (data) {
+ if (data.ident) {
+ this.attr.currentMailIdent = data.ident;
+ }
+
+ this.attr.currentTag = data.tag || this.attr.currentTag;
+
+ this.updateCheckAllCheckbox();
+ };
+
+ this.renderMails = function (mails) {
+ _.each(mails, this.appendMail, this);
+ this.trigger(document, events.search.highlightResults, {where: '#mail-list'});
+ this.trigger(document, events.search.highlightResults, {where: '.mail-read-view__header'});
+ };
+
+ this.triggerScrollReset = function () {
+ this.trigger(document, events.dispatchers.middlePane.resetScroll);
+ };
+
+ this.showMails = function (event, data) {
+ this.updateCurrentTagAndMail(data);
+ this.refreshMailList(null, data);
+ this.triggerMailOpenForPopState(data);
+ this.openMailFromUrl();
+ };
+
+ this.refreshMailList = function (ev, data) {
+ if (ev) { // triggered by the event, so we need to refresh the tag list
+ this.trigger(document, events.dispatchers.tags.refreshTagList, { skipMailListRefresh: true });
+ }
+ this.resetMailList();
+ this.renderMails(data.mails);
+ };
+
+ this.updateSelected = function (ev, data) {
+ if (data.ident !== this.attr.currentMailIdent) {
+ this.attr.currentMailIdent = data.ident;
+ }
+ };
+
+ this.cleanSelected = function () {
+ this.attr.currentMailIdent = '';
+ this.triggerScrollReset();
+ };
+
+ this.respondWithCheckedMails = function (ev, caller) {
+ this.trigger(caller, events.ui.mail.hereChecked, {checkedMails: this.attr.checkedMails});
+ };
+
+ this.updateCheckAllCheckbox = function () {
+ this.trigger(document, events.ui.mails.hasMailsChecked, _.keys(this.attr.checkedMails).length > 0);
+ };
+
+ this.addToCheckedMails = function (ev, data) {
+ this.attr.checkedMails[data.mail.ident] = data.mail;
+ this.updateCheckAllCheckbox();
+ };
+
+ this.removeFromCheckedMails = function (ev, data) {
+ if (data.mails) {
+ _.each(data.mails, function (mail) {
+ delete this.attr.checkedMails[mail.ident];
+ }, this);
+ } else {
+ delete this.attr.checkedMails[data.mail.ident];
+ }
+ this.updateCheckAllCheckbox();
+ };
+
+ this.refreshWithScroll = function () {
+ this.trigger(document, events.ui.mails.refresh);
+ this.triggerScrollReset();
+ };
+
+ this.refreshAfterSaveDraft = function () {
+ if (this.attr.currentTag === 'drafts') {
+ this.refreshWithScroll();
+ }
+ };
+
+ this.refreshAfterMailSent = function () {
+ if (this.attr.currentTag === 'drafts' || this.attr.currentTag === 'sent') {
+ this.refreshWithScroll();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mails.cleanSelected, this.cleanSelected);
+ this.on(document, events.ui.tag.select, this.cleanSelected);
+
+ this.on(document, events.mails.available, this.showMails);
+ this.on(document, events.mails.availableForRefresh, this.refreshMailList);
+
+ this.on(document, events.mail.draftSaved, this.refreshAfterSaveDraft);
+ this.on(document, events.mail.sent, this.refreshAfterMailSent);
+
+ this.on(document, events.ui.mail.updateSelected, this.updateSelected);
+ this.on(document, events.ui.mail.wantChecked, this.respondWithCheckedMails);
+ this.on(document, events.ui.mail.checked, this.addToCheckedMails);
+ this.on(document, events.ui.mail.unchecked, this.removeFromCheckedMails);
+
+ this.openMailFromUrl = utils.once(function () {
+ if (this.shouldSelectEmailFromUrlMailIdent()) {
+ this.selectMailBasedOnUrlMailIdent();
+ }
+ });
+
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/archive_many_trigger.js b/web-ui/public/js/mail_list_actions/ui/archive_many_trigger.js
new file mode 100644
index 00000000..b148cdce
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/archive_many_trigger.js
@@ -0,0 +1,29 @@
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(definecomponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return definecomponent(archiveManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+ function archiveManyTrigger() {
+
+ this.getMailsToArchive = function() {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.archiveManyEmails = function(event, data) {
+ this.trigger(document, events.mail.archiveMany, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToArchive);
+ this.on(events.ui.mail.hereChecked, this.archiveManyEmails);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/compose_trigger.js b/web-ui/public/js/mail_list_actions/ui/compose_trigger.js
new file mode 100644
index 00000000..ec79cb26
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/compose_trigger.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, events) {
+ 'use strict';
+
+ return defineComponent(composeTrigger);
+
+ function composeTrigger() {
+
+ this.defaultAttrs({});
+
+ this.render = function() {
+ this.$node.html(templates.mailActions.composeTrigger);
+ };
+
+ this.enableComposing = function(event, data) {
+ this.trigger(document, events.dispatchers.rightPane.openComposeBox);
+ };
+
+ this.showEmailSuccess = function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Your message was sent!', class: 'success'});
+ };
+
+ this.showEmailError = function (ev, data) {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Error, message not sent: ' + data.responseJSON.message, class: 'error'});
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on('click', this.enableComposing);
+ this.on(document, events.mail.sent, this.showEmailSuccess);
+ this.on(document, events.mail.send_failed, this.showEmailError);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/delete_many_trigger.js b/web-ui/public/js/mail_list_actions/ui/delete_many_trigger.js
new file mode 100644
index 00000000..dd2f67a5
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/delete_many_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(deleteManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function deleteManyTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToDelete = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.deleteManyEmails = function (event, data) {
+ this.trigger(document, events.ui.mail.deleteMany, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToDelete);
+ this.on(events.ui.mail.hereChecked, this.deleteManyEmails);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/mail_list_actions.js b/web-ui/public/js/mail_list_actions/ui/mail_list_actions.js
new file mode 100644
index 00000000..69e5fde4
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/mail_list_actions.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events',
+ 'page/router/url_params',
+ 'mail_list_actions/ui/compose_trigger',
+ 'mail_list_actions/ui/refresh_trigger',
+ 'mail_list/domain/refresher',
+ 'mail_list_actions/ui/toggle_check_all_trigger',
+ 'mail_list_actions/ui/pagination_trigger',
+ 'mail_list_actions/ui/delete_many_trigger',
+ 'mail_list_actions/ui/recover_many_trigger',
+ 'mail_list_actions/ui/archive_many_trigger',
+ 'mail_list_actions/ui/mark_many_as_read_trigger',
+ 'mail_list_actions/ui/mark_as_unread_trigger'
+ ],
+
+ function (
+ defineComponent,
+ templates,
+ events,
+ urlParams,
+ composeTrigger,
+ refreshTrigger,
+ refresher,
+ toggleCheckAllMailTrigger,
+ paginationTrigger,
+ deleteManyTrigger,
+ recoverManyTrigger,
+ archiveManyTrigger,
+ markManyAsReadTrigger,
+ markAsUnreadTrigger
+ ) {
+ 'use strict';
+ return defineComponent(mailsActions);
+
+ function mailsActions() {
+ this.render = function() {
+ this.$node.html(this.getActionsBoxTemplate());
+ refreshTrigger.attachTo('#refresh-trigger');
+ composeTrigger.attachTo('#compose-trigger');
+ toggleCheckAllMailTrigger.attachTo('#toggle-check-all-emails');
+ paginationTrigger.attachTo('#pagination-trigger');
+ deleteManyTrigger.attachTo('#delete-selected');
+ recoverManyTrigger.attachTo('#recover-selected');
+ archiveManyTrigger.attachTo('#archive-selected');
+ markManyAsReadTrigger.attachTo('#mark-selected-as-read');
+ markAsUnreadTrigger.attachTo('#mark-selected-as-unread');
+ refresher.attachTo(document);
+ };
+
+ this.getCurrentTag = function () {
+ return this.attr.currentTag || urlParams.getTag();
+ };
+
+ this.updateCurrentTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ this.render();
+ };
+
+ this.getActionsBoxTemplate = function () {
+ if(this.getCurrentTag() === 'trash') {
+ return templates.mailActions.trashActionsBox();
+ } else {
+ return templates.mailActions.actionsBox();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.tag.select, this.updateCurrentTag);
+ this.render();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/mark_as_unread_trigger.js b/web-ui/public/js/mail_list_actions/ui/mark_as_unread_trigger.js
new file mode 100644
index 00000000..2584e453
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/mark_as_unread_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(markAsUnreadTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function markAsUnreadTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToMarkAsUnread = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.markManyEmailsAsUnread = function (event, data) {
+ this.trigger(document, events.mail.unread, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToMarkAsUnread);
+ this.on(events.ui.mail.hereChecked, this.markManyEmailsAsUnread);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js b/web-ui/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js
new file mode 100644
index 00000000..c16a2229
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/mark_many_as_read_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(markManyAsReadTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function markManyAsReadTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToMarkAsRead = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.markManyEmailsAsRead = function (event, data) {
+ this.trigger(document, events.mail.read, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToMarkAsRead);
+ this.on(events.ui.mail.hereChecked, this.markManyEmailsAsRead);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/pagination_trigger.js b/web-ui/public/js/mail_list_actions/ui/pagination_trigger.js
new file mode 100644
index 00000000..3bc13d40
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/pagination_trigger.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, events) {
+ 'use strict';
+
+ return defineComponent(paginationTrigger);
+
+ function paginationTrigger() {
+ this.defaultAttrs({
+ previous: '#left-arrow',
+ next: '#right-arrow',
+ currentPage: '#current-page'
+ });
+
+ this.renderWithPageNumber = function(pageNumber) {
+ this.$node.html(templates.mailActions.paginationTrigger({
+ currentPage: pageNumber
+ }));
+ this.on(this.attr.previous, 'click', this.previousPage);
+ this.on(this.attr.next, 'click', this.nextPage);
+ };
+
+ this.render = function() {
+ this.renderWithPageNumber(1);
+ };
+
+ this.updatePageDisplay = function(event, data) {
+ this.renderWithPageNumber(data.currentPage);
+ };
+
+ this.previousPage = function(event) {
+ this.trigger(document, events.ui.page.previous);
+ };
+
+ this.nextPage = function(event) {
+ this.trigger(document, events.ui.page.next);
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on(document, events.ui.page.changed, this.updatePageDisplay);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/recover_many_trigger.js b/web-ui/public/js/mail_list_actions/ui/recover_many_trigger.js
new file mode 100644
index 00000000..e0a32094
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/recover_many_trigger.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_enable_disable_on_event',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withEnableDisableOnEvent, events) {
+ 'use strict';
+
+ return defineComponent(recoverManyTrigger, withEnableDisableOnEvent(events.ui.mails.hasMailsChecked));
+
+ function recoverManyTrigger() {
+ this.defaultAttrs({});
+
+ this.getMailsToRecover = function(event) {
+ this.trigger(document, events.ui.mail.wantChecked, this.$node);
+ };
+
+ this.recoverManyEmails = function (event, data) {
+ this.trigger(document, events.ui.mail.recoverMany, data);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.getMailsToRecover);
+ this.on(events.ui.mail.hereChecked, this.recoverManyEmails);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/refresh_trigger.js b/web-ui/public/js/mail_list_actions/ui/refresh_trigger.js
new file mode 100644
index 00000000..a16270d2
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/refresh_trigger.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, events) {
+ 'use strict';
+
+ return defineComponent(refreshTrigger);
+
+ function refreshTrigger() {
+ this.render = function() {
+ this.$node.html(templates.mailActions.refreshTrigger);
+ };
+
+ this.refresh = function(event) {
+ this.trigger(document, events.ui.mails.refresh);
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on('click', this.refresh);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_list_actions/ui/toggle_check_all_trigger.js b/web-ui/public/js/mail_list_actions/ui/toggle_check_all_trigger.js
new file mode 100644
index 00000000..71c65346
--- /dev/null
+++ b/web-ui/public/js/mail_list_actions/ui/toggle_check_all_trigger.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'page/events'
+ ],
+
+ function(defineComponent, events) {
+ 'use strict';
+
+ return defineComponent(toggleCheckAllEmailsTrigger);
+
+ function toggleCheckAllEmailsTrigger() {
+ this.defaultAttrs({ });
+
+ this.toggleCheckAll = function(event) {
+ if (this.$node.prop('checked')) {
+ this.trigger(document, events.ui.mails.checkAll);
+ } else {
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }
+ };
+
+ this.setCheckbox = function (event, state) {
+ this.$node.prop('checked', state);
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.toggleCheckAll);
+ this.on(document, events.ui.mails.hasMailsChecked, this.setCheckbox);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/data/feedback_sender.js b/web-ui/public/js/mail_view/data/feedback_sender.js
new file mode 100644
index 00000000..2232dbe4
--- /dev/null
+++ b/web-ui/public/js/mail_view/data/feedback_sender.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'helpers/monitored_ajax',
+ 'page/events'
+ ],
+ function (defineComponent, monitoredAjax, events) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ feedbackResource: '/feedback'
+ });
+
+ this.successSubmittingFeedback = function() {
+ this.trigger(document, events.feedback.submitted);
+ };
+
+ this.submitFeedback = function(event, data) {
+ monitoredAjax.call(_, this, this.attr.feedbackResource, {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data)
+ }).done(this.successSubmittingFeedback());
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.feedback.submit, this.submitFeedback);
+ });
+
+ });
+});
diff --git a/web-ui/public/js/mail_view/data/mail_builder.js b/web-ui/public/js/mail_view/data/mail_builder.js
new file mode 100644
index 00000000..7a478dd8
--- /dev/null
+++ b/web-ui/public/js/mail_view/data/mail_builder.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['services/model/mail'], function (mailModel) {
+ 'use strict';
+
+ var mail;
+
+ function recipients(mail, place, v) {
+ if (v !== '' && !_.isUndefined(v)) {
+ if (_.isArray(v)) {
+ mail[place] = v;
+ } else {
+ mail[place] = v.split(' ');
+ }
+ } else {
+ mail[place] = [];
+ }
+ }
+
+ return {
+ newMail: function (ident) {
+ ident = _.isUndefined(ident) ? '' : ident;
+
+ mail = {
+ header: {
+ to: [],
+ cc: [],
+ bcc: [],
+ from: undefined,
+ subject: ''
+ },
+ tags: [],
+ body: '',
+ attachments: [],
+ ident: ident
+ };
+ return this;
+ },
+
+ subject: function (subject) {
+ mail.header.subject = subject;
+ return this;
+ },
+
+ body: function (body) {
+ mail.body = body;
+ return this;
+ },
+
+ to: function (to) {
+ recipients(mail.header, 'to', to);
+ return this;
+ },
+
+ cc: function (cc) {
+ recipients(mail.header, 'cc', cc);
+ return this;
+ },
+
+ bcc: function (bcc) {
+ recipients(mail.header, 'bcc', bcc);
+ return this;
+ },
+
+ header: function (name, value) {
+ mail.header[name] = value;
+ return this;
+ },
+
+ tag: function (tag) {
+ if (_.isUndefined(tag)) {
+ tag = 'drafts';
+ }
+ mail.tags.push(tag);
+ return this;
+ },
+
+ attachment: function (attachmentList) {
+ mail.attachments = attachmentList;
+ return this;
+ },
+
+ build: function () {
+ return mailModel.create(mail);
+ }
+ };
+});
diff --git a/web-ui/public/js/mail_view/data/mail_sender.js b/web-ui/public/js/mail_view/data/mail_sender.js
new file mode 100644
index 00000000..8bb01f70
--- /dev/null
+++ b/web-ui/public/js/mail_view/data/mail_sender.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'mail_view/data/mail_builder',
+ 'page/events',
+ 'helpers/monitored_ajax',
+ 'features'
+ ],
+ function (defineComponent, mailBuilder, events, monitoredAjax, features) {
+ 'use strict';
+
+ return defineComponent(mailSender);
+
+ function mailSender() {
+ function successSendingMail(on){
+ return function(result) {
+ on.trigger(document, events.mail.sent, result);
+ };
+ }
+
+ function failureSendingMail(on) {
+ return function(result) {
+ on.trigger(document, events.mail.send_failed, result);
+ };
+ }
+
+ function successSaveDraft(on){
+ return function(result){
+ on.trigger(document, events.mail.draftSaved, result);
+ };
+ }
+
+ this.defaultAttrs({
+ mailsResource: '/mails'
+ });
+
+ this.sendMail = function(event, data) {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ monitoredAjax.call(_, this, this.attr.mailsResource, {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data)
+ }).done(successSendingMail(this)).fail(failureSendingMail(this));
+
+ };
+
+ this.saveMail = function(mail) {
+ return monitoredAjax.call(_, this, this.attr.mailsResource, {
+ type: 'PUT',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(mail),
+ skipErrorMessage: true
+ });
+ };
+
+ this.saveDraft = function(event, data) {
+ this.saveMail(data)
+ .done(successSaveDraft(this));
+ };
+
+ this.saveMailWithCallback = function(event, data) {
+ this.saveMail(data.mail)
+ .done(function(result) { return data.callback(result); })
+ .fail(function(result) { return data.callback(result); });
+ };
+
+ this.after('initialize', function () {
+ this.on(events.mail.send, this.sendMail);
+ if(features.isEnabled('saveDraft')) {
+ this.on(events.mail.saveDraft, this.saveDraft);
+ }
+ this.on(document, events.mail.save, this.saveMailWithCallback);
+ });
+ }
+ });
diff --git a/web-ui/public/js/mail_view/ui/attachment_icon.js b/web-ui/public/js/mail_view/ui/attachment_icon.js
new file mode 100644
index 00000000..e04fc02a
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/attachment_icon.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'page/events',
+ 'features'
+ ],
+
+ function (defineComponent, events, features) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.render = function () {
+ this.$node.html('<i class="fa fa-paperclip"></i>');
+ };
+
+ this.triggerUploadAttachment = function () {
+ this.trigger(document, events.mail.startUploadAttachment);
+ };
+
+ this.uploadInProgress = function (ev, data) {
+ this.attr.busy = true;
+ this.$node.addClass('busy');
+ };
+
+ this.uploadFinished = function (ev, data) {
+ this.attr.busy = false;
+ this.$node.removeClass('busy');
+ };
+
+ this.after('initialize', function () {
+ if (features.isEnabled('attachment')) {
+ this.render();
+ this.on(document, events.mail.uploadingAttachment, this.uploadInProgress);
+ this.on(document, events.mail.uploadedAttachment, this.uploadFinished);
+ this.on(document, events.mail.failedUploadAttachment, this.uploadFinished);
+ }
+ this.on(this.$node, 'click', function() {
+ if (!this.attr.busy) {
+ this.triggerUploadAttachment();
+ }
+ });
+ });
+ });
+ });
diff --git a/web-ui/public/js/mail_view/ui/attachment_list.js b/web-ui/public/js/mail_view/ui/attachment_list.js
new file mode 100644
index 00000000..4ef64960
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/attachment_list.js
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'views/templates',
+ 'page/events',
+ 'helpers/view_helper',
+ 'helpers/monitored_ajax'
+ ],
+
+ function (templates, events, viewHelper, monitoredAjax) {
+ 'use strict';
+
+ function attachmentList() {
+ this.defaultAttrs({
+ inputFileUpload: '#fileupload',
+ attachmentListItem: '#attachment-list-item',
+ attachmentUploadItem: '#attachment-upload-item',
+ attachmentUploadItemProgress: '#attachment-upload-item-progress',
+ attachmentUploadItemAbort: '#attachment-upload-item-abort',
+ attachmentBaseUrl: '/attachment',
+ attachments: [],
+ closeIcon: '#upload-error-close',
+ uploadError: '#upload-error',
+ dismissButton: '#dismiss-button',
+ uploadFileButton: '#upload-file-button'
+ });
+
+ var ONE_MEGABYTE = 1024*1024;
+ var ATTACHMENT_SIZE_LIMIT = 5*ONE_MEGABYTE;
+
+ this.showAttachment = function (ev, data) {
+ this.trigger(document, events.mail.appendAttachment, data);
+ this.renderAttachmentListView(data);
+ };
+
+ this.addAttachment = function (event, data) {
+ this.attr.attachments.push(data);
+ };
+
+ this.renderAttachmentListView = function (data) {
+ var currentHtml = this.select('attachmentListItem').html();
+ var item = this.buildAttachmentListItem(data);
+ this.select('attachmentListItem').append(item);
+ };
+
+ this.buildAttachmentListItem = function (attachment) {
+ var attachmentData = {ident: attachment.ident,
+ encoding: attachment.encoding,
+ name: attachment.name,
+ size: attachment.size,
+ removable: true};
+
+ var element = $(templates.compose.attachmentItem(attachmentData));
+ var self = this;
+ element.find('i.remove-icon').bind('click', function(event) {
+ var element = $(this);
+ var ident = element.closest('li').attr('data-ident');
+ self.trigger(document, events.mail.removeAttachment, {ident: ident, element: element});
+ event.preventDefault();
+ });
+ return element;
+ };
+
+ this.performPreUploadCheck = function(e, data) {
+ if (data.originalFiles[0].size > ATTACHMENT_SIZE_LIMIT) {
+ return false;
+ }
+
+ return true;
+ };
+
+ this.removeUploadError = function() {
+ var uploadError = this.select('uploadError');
+ if (uploadError) {
+ uploadError.remove();
+ }
+ };
+
+ this.showUploadError = function () {
+ var self = this;
+
+ var html = $(templates.compose.uploadAttachmentFailed());
+ html.insertAfter(self.select('attachmentListItem'));
+
+ self.on(self.select('closeIcon'), 'click', dismissUploadFailed);
+ self.on(self.select('dismissButton'), 'click', dismissUploadFailed);
+ self.on(self.select('uploadFileButton'), 'click', uploadAnotherFile);
+
+ function dismissUploadFailed(event) {
+ event.preventDefault();
+ self.select('uploadError').remove();
+ }
+
+ function uploadAnotherFile(event) {
+ event.preventDefault();
+ self.trigger(document, events.mail.startUploadAttachment);
+ }
+ };
+
+ this.showUploadProgressBar = function(e, data) {
+ var element = $(templates.compose.attachmentUploadItem({
+ name: data.originalFiles[0].name,
+ size: data.originalFiles[0].size
+ }));
+ this.select('attachmentUploadItem').append(element);
+ this.select('attachmentUploadItem').show();
+ };
+
+ this.hideUploadProgressBar = function() {
+ this.select('attachmentUploadItem').hide();
+ this.select('attachmentUploadItem').empty();
+ };
+
+ this.attachUploadAbort = function(e, data) {
+ this.on(this.select('attachmentUploadItemAbort'), 'click', function(e) {
+ data.abort();
+ e.preventDefault();
+ });
+ };
+
+ this.detachUploadAbort = function() {
+ this.off(this.select('attachmentUploadItemAbort'), 'click');
+ };
+
+ this.addJqueryFileUploadConfig = function() {
+ var self = this;
+
+ self.removeUploadError();
+
+ this.select('inputFileUpload').fileupload({
+ add: function(e, data) {
+ if (self.performPreUploadCheck(e, data)) {
+ self.showUploadProgressBar(e, data);
+ self.attachUploadAbort(e, data);
+ data.submit();
+ } else {
+ self.showUploadError();
+ }
+ },
+ url: self.attr.attachmentBaseUrl,
+ dataType: 'json',
+ done: function (e, response) {
+ self.detachUploadAbort();
+ self.hideUploadProgressBar();
+ self.trigger(document, events.mail.uploadedAttachment, response.result);
+ },
+ fail: function(e, data){
+ self.detachUploadAbort();
+ self.hideUploadProgressBar();
+ self.trigger(document, events.mail.failedUploadAttachment);
+ },
+ progressall: function (e, data) {
+ var progressRate = parseInt(data.loaded / data.total * 100, 10);
+ self.select('attachmentUploadItemProgress').css('width', progressRate + '%');
+ }
+ }).bind('fileuploadstart', function (e) {
+ self.trigger(document, events.mail.uploadingAttachment);
+ });
+ };
+
+ this.startUpload = function () {
+ this.addJqueryFileUploadConfig();
+ this.select('inputFileUpload').click();
+ };
+
+ this.removeAttachmentFromList = function(ident) {
+ for (var i = 0; i < this.attr.attachments.length; i++) {
+ if (this.attr.attachments[i].ident === ident) {
+ this.attr.attachments.remove(i);
+ break;
+ }
+ }
+ };
+
+ this.destroyAttachmentElement = function(element) {
+ element.closest('li').remove();
+ };
+
+ this.removeAttachments = function(event, data) {
+ this.removeAttachmentFromList(data.ident);
+ this.destroyAttachmentElement(data.element);
+ };
+
+ this.after('initialize', function () {
+ this.addJqueryFileUploadConfig();
+ this.on(document, events.mail.uploadedAttachment, this.showAttachment);
+ this.on(document, events.mail.startUploadAttachment, this.startUpload);
+ this.on(document, events.mail.appendAttachment, this.addAttachment);
+ this.on(document, events.mail.removeAttachment, this.removeAttachments);
+ });
+ }
+
+ return attachmentList;
+ });
diff --git a/web-ui/public/js/mail_view/ui/compose_box.js b/web-ui/public/js/mail_view/ui/compose_box.js
new file mode 100644
index 00000000..101dc939
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/compose_box.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_mail_edit_base',
+ 'page/events',
+ 'mail_view/data/mail_builder'
+ ],
+
+ function (defineComponent, templates, withMailEditBase, events, mailBuilder) {
+ 'use strict';
+
+ return defineComponent(composeBox, withMailEditBase);
+
+ function composeBox() {
+
+ this.defaultAttrs({
+ 'closeButton': '.close-mail-button'
+ });
+
+ this.showNoMessageSelected = function() {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ };
+
+ this.buildMail = function(tag) {
+ return this.builtMail(tag).build();
+ };
+
+ this.builtMail = function(tag) {
+ return mailBuilder.newMail(this.attr.ident)
+ .subject(this.select('subjectBox').val())
+ .to(this.attr.recipientValues.to)
+ .cc(this.attr.recipientValues.cc)
+ .bcc(this.attr.recipientValues.bcc)
+ .body(this.select('bodyBox').val())
+ .attachment(this.attr.attachments)
+ .tag(tag);
+ };
+
+ this.renderComposeBox = function() {
+ this.render(templates.compose.box, {});
+ this.enableFloatlabel('input.floatlabel');
+ this.enableFloatlabel('textarea.floatlabel');
+ this.select('recipientsFields').show();
+ this.on(this.select('closeButton'), 'click', this.showNoMessageSelected);
+ this.enableAutoSave();
+ };
+
+ this.mailDeleted = function(event, data) {
+ if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ }
+ };
+
+ this.discardDraft = function () {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ };
+
+ this.after('initialize', function () {
+ this.renderComposeBox();
+
+ this.select('toBox').focus();
+ this.on(document, events.mail.deleted, this.mailDeleted);
+ this.on(document, events.mail.sent, this.showNoMessageSelected);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/draft_box.js b/web-ui/public/js/mail_view/ui/draft_box.js
new file mode 100644
index 00000000..afe31914
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/draft_box.js
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_mail_edit_base',
+ 'page/events',
+ 'mail_view/data/mail_builder'
+ ],
+
+ function (defineComponent, templates, withMailEditBase, events, mailBuilder) {
+ 'use strict';
+
+ return defineComponent(draftBox, withMailEditBase);
+
+ function draftBox() {
+ this.defaultAttrs({
+ closeMailButton: '.close-mail-button'
+ });
+
+ this.showNoMessageSelected = function() {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ };
+
+ this.buildMail = function(tag) {
+ return this.builtMail(tag).build();
+ };
+
+ this.builtMail = function(tag) {
+ return mailBuilder.newMail(this.attr.ident)
+ .subject(this.select('subjectBox').val())
+ .to(this.attr.recipientValues.to)
+ .cc(this.attr.recipientValues.cc)
+ .bcc(this.attr.recipientValues.bcc)
+ .body(this.select('bodyBox').val())
+ .attachment(this.attr.attachments)
+ .tag(tag);
+ };
+
+ this.renderDraftBox = function(ev, data) {
+ var mail = data.mail;
+ var body = mail.textPlainBody;
+ this.attr.ident = mail.ident;
+ this.render(templates.compose.box, {
+ recipients: {
+ to: mail.header.to,
+ cc: mail.header.cc,
+ bcc: mail.header.bcc
+ },
+ subject: mail.header.subject,
+ body: body,
+ attachments: this.convertToRemovableAttachments(mail.attachments)
+ });
+
+ var self = this;
+ this.$node.find('i.remove-icon').bind('click', function(event) {
+ var element = $(this);
+ var ident = element.closest('li').attr('data-ident');
+ self.trigger(document, events.mail.removeAttachment, {ident: ident, element: element});
+ event.preventDefault();
+ });
+
+ this.enableFloatlabel('input.floatlabel');
+ this.enableFloatlabel('textarea.floatlabel');
+ this.select('recipientsFields').show();
+ this.select('bodyBox').focus();
+ this.select('tipMsg').hide();
+ this.enableAutoSave();
+ this.bindCollapse();
+ this.on(this.select('closeMailButton'), 'click', this.showNoMessageSelected);
+ };
+
+ this.convertToRemovableAttachments = function(attachments) {
+ return attachments.map(function(attachment) {
+ attachment.removable = true;
+ return attachment;
+ });
+ };
+
+ this.mailDeleted = function(event, data) {
+ if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) {
+ this.trigger(events.dispatchers.rightPane.openNoMessageSelected);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(this, events.mail.here, this.renderDraftBox);
+ this.on(document, events.mail.sent, this.showNoMessageSelected);
+ this.on(document, events.mail.deleted, this.mailDeleted);
+ this.trigger(document, events.mail.want, { mail: this.attr.mailIdent, caller: this });
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/draft_save_status.js b/web-ui/public/js/mail_view/ui/draft_save_status.js
new file mode 100644
index 00000000..47751d91
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/draft_save_status.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'page/events',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, events, i18n) {
+ 'use strict';
+
+ return defineComponent(draftSaveStatus);
+
+ function draftSaveStatus() {
+ this.setMessage = function(msg) {
+ var node = this.$node;
+ return function () { node.text(msg); };
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.mail.saveDraft, this.setMessage(i18n.t('draft-saving')));
+ this.on(document, events.mail.draftSaved, this.setMessage(i18n.t('draft-saved')));
+ this.on(document, events.ui.mail.changedSinceLastSave, this.setMessage(''));
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/feedback_box.js b/web-ui/public/js/mail_view/ui/feedback_box.js
new file mode 100644
index 00000000..4e00ece8
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/feedback_box.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['flight/lib/component', 'views/templates', 'page/events', 'features', 'feedback/feedback_cache'],
+ function (defineComponent, templates, events, features, feedbackCache) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ 'closeButton': '.close-mail-button',
+ 'submitButton': '#send-button',
+ 'textBox': '#text-box',
+ });
+
+ this.render = function () {
+ this.$node.html(templates.compose.feedback());
+ };
+
+ this.startCachingData = function () {
+ this.select('textBox').val(feedbackCache.getCache());
+ this.select('textBox').on('change', this.cacheFeedbackData.bind(this));
+ };
+
+
+ this.cacheFeedbackData = function () {
+ feedbackCache.setCache(this.select('textBox').val());
+ };
+
+ this.showNoMessageSelected = function () {
+ this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected);
+ };
+
+ this.submitFeedback = function () {
+ var feedback = this.select('textBox').val();
+ this.trigger(document, events.feedback.submit, {feedback: feedback});
+ feedbackCache.resetCache();
+ };
+
+ this.showSuccessMessage = function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Thanks for your feedback!'});
+ };
+
+ this.after('initialize', function () {
+ if (features.isEnabled('feedback')) {
+ this.render();
+ this.startCachingData();
+ this.on(document, events.feedback.submitted, this.showNoMessageSelected);
+ this.on(document, events.feedback.submitted, this.showSuccessMessage);
+ this.on(this.select('closeButton'), 'click', this.showNoMessageSelected);
+ this.on(this.select('submitButton'), 'click', this.submitFeedback);
+ }
+ });
+
+ });
+ });
diff --git a/web-ui/public/js/mail_view/ui/forward_box.js b/web-ui/public/js/mail_view/ui/forward_box.js
new file mode 100644
index 00000000..a34bd55d
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/forward_box.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'helpers/view_helper',
+ 'mixins/with_hide_and_show',
+ 'mixins/with_compose_inline',
+ 'page/events',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) {
+ 'use strict';
+
+ return defineComponent(forwardBox, withHideAndShow, withComposeInline);
+
+ function forwardBox() {
+ var fwd = function(v) { return i18n.t('fwd') + ': ' + v; };
+
+ this.fetchTargetMail = function (ev) {
+ this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this });
+ };
+
+ this.setupForwardBox = function() {
+ var mail = this.attr.mail;
+ this.attr.subject = fwd(mail.header.subject);
+ this.attr.attachments = mail.attachments;
+
+ this.renderInlineCompose('forward-box', {
+ subject: this.attr.subject,
+ recipients: { to: [], cc: []},
+ body: viewHelper.quoteMail(mail),
+ attachments: this.convertToRemovableAttachments(mail.attachments)
+ });
+
+ var self = this;
+ this.$node.find('i.remove-icon').bind('click', function(event) {
+ var element = $(this);
+ var ident = element.closest('li').attr('data-ident');
+ self.trigger(document, events.mail.removeAttachment, {ident: ident});
+ event.preventDefault();
+ });
+
+ this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput);
+ this.select('recipientsDisplay').hide();
+ this.select('recipientsFields').show();
+ };
+
+ this.convertToRemovableAttachments = function(attachments) {
+ return attachments.map(function(attachment) {
+ attachment.removable = true;
+ return attachment;
+ });
+ };
+
+ this.showSubjectInput = function() {
+ this.select('subjectDisplay').hide();
+ this.select('subjectInput').show();
+ this.select('subjectInput').focus();
+ };
+
+ this.buildMail = function(tag) {
+ var builder = this.builtMail(tag).subject(this.select('subjectInput').val());
+
+ var headersToFwd = ['bcc', 'cc', 'date', 'from', 'message_id', 'reply_to', 'sender', 'to'];
+ var header = this.attr.mail.header;
+ _.each(headersToFwd, function (h) {
+ if (!_.isUndefined(header[h])) {
+ builder.header('resent_' + h, header[h]);
+ }
+ });
+
+ return builder.build();
+ };
+
+ this.after('initialize', function () {
+ this.setupForwardBox();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/mail_actions.js b/web-ui/public/js/mail_view/ui/mail_actions.js
new file mode 100644
index 00000000..65cd0aaa
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/mail_actions.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events'
+ ],
+
+ function (defineComponent, templates, events) {
+ 'use strict';
+
+ return defineComponent(mailActions);
+
+ function mailActions() {
+
+ this.defaultAttrs({
+ replyButtonTop: '#reply-button-top',
+ viewMoreActions: '#view-more-actions',
+ replyAllButtonTop: '#reply-all-button-top',
+ deleteButtonTop: '#delete-button-top',
+ moreActions: '#more-actions'
+ });
+
+
+ this.displayMailActions = function () {
+
+ this.$node.html(templates.mails.mailActions());
+
+ this.select('moreActions').hide();
+
+ this.on(this.select('replyButtonTop'), 'click', function () {
+ this.trigger(document, events.ui.replyBox.showReply);
+ }.bind(this));
+
+ this.on(this.select('replyAllButtonTop'), 'click', function () {
+ this.trigger(document, events.ui.replyBox.showReplyAll);
+ this.select('moreActions').hide();
+ }.bind(this));
+
+ this.on(this.select('deleteButtonTop'), 'click', function () {
+ this.trigger(document, events.ui.mail.delete, {mail: this.attr.mail});
+ this.select('moreActions').hide();
+ }.bind(this));
+
+ this.on(this.select('viewMoreActions'), 'click', function () {
+ this.select('moreActions').toggle();
+ }.bind(this));
+
+ this.on(this.select('viewMoreActions'), 'blur', function (event) {
+ var replyButtonTopHover = this.select('replyAllButtonTop').is(':hover');
+ var deleteButtonTopHover = this.select('deleteButtonTop').is(':hover');
+
+ if (replyButtonTopHover || deleteButtonTopHover) {
+ event.preventDefault();
+ } else {
+ this.select('moreActions').hide();
+ }
+ }.bind(this));
+
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ this.displayMailActions();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/mail_view.js b/web-ui/public/js/mail_view/ui/mail_view.js
new file mode 100644
index 00000000..3408c8af
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/mail_view.js
@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mail_view/ui/mail_actions',
+ 'helpers/view_helper',
+ 'mixins/with_hide_and_show',
+ 'mixins/with_mail_tagging',
+ 'mixins/with_mail_sandbox',
+ 'page/events',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, templates, mailActions, viewHelpers, withHideAndShow, withMailTagging, withMailSandbox, events, i18n) {
+ 'use strict';
+
+ return defineComponent(mailView, mailActions, withHideAndShow, withMailTagging, withMailSandbox);
+
+ function mailView() {
+ this.defaultAttrs({
+ tags: '.mail-read-view__header-tags-tag',
+ newTagInput: '#new-tag-input',
+ newTagButton: '#new-tag-button',
+ addNew: '.mail-read-view__header-tags-new-button',
+ trashButton: '#trash-button',
+ archiveButton: '#archive-button',
+ closeMailButton: '.close-mail-button'
+ });
+
+ this.displayMail = function (event, data) {
+ this.attr.mail = data.mail;
+
+ var signed, encrypted, attachments;
+
+ data.mail.security_casing = data.mail.security_casing || {};
+ signed = this.checkSigned(data.mail);
+ encrypted = this.checkEncrypted(data.mail);
+ attachments = data.mail.attachments.map(function (attachment) {
+ attachment.received = true;
+ return attachment;
+ });
+
+ if(data.mail.mailbox === 'sent') {
+ encrypted = undefined;
+ signed = undefined;
+ }
+
+ this.$node.html(templates.mails.fullView({
+ header: data.mail.header,
+ body: [],
+ statuses: viewHelpers.formatStatusClasses(data.mail.status),
+ ident: data.mail.ident,
+ tags: data.mail.tags,
+ encryptionStatus: encrypted,
+ signatureStatus: signed,
+ attachments: attachments
+ }));
+
+ this.showMailOnSandbox(this.attr.mail);
+
+ this.attachTagCompletion(this.attr.mail);
+
+ this.select('tags').on('click', function (event) {
+ this.removeTag($(event.target).text());
+ }.bind(this));
+
+ this.addTagLoseFocus();
+ this.on(this.select('newTagButton'), 'click', this.showNewTagInput);
+ this.on(this.select('newTagInput'), 'keydown', this.handleKeyDown);
+ this.on(this.select('newTagInput'), 'blur', this.addTagLoseFocus);
+ this.on(this.select('trashButton'), 'click', this.moveToTrash);
+ this.on(this.select('closeMailButton'), 'click', this.openNoMessageSelectedPane);
+
+ mailActions.attachTo('#mail-actions', data);
+ this.resetScroll();
+ };
+
+ this.resetScroll = function(){
+ $('#right-pane').scrollTop(0);
+ };
+
+ this.checkEncrypted = function(mail) {
+ if(_.isEmpty(mail.security_casing.locks)) {
+ return {
+ cssClass: 'security-status__label--not-encrypted',
+ label: 'not-encrypted'
+ };
+ }
+
+ var statusClass = ['security-status__label--encrypted'];
+ var statusLabel;
+
+ var hasAnyEncryptionInfo = _.any(mail.security_casing.locks, function (lock) {
+ return lock.state === 'valid';
+ });
+
+ if(hasAnyEncryptionInfo) {
+ statusLabel = 'encrypted';
+ } else {
+ statusClass.push('--with-error');
+ statusLabel = 'encryption-error';
+ }
+
+ return {
+ cssClass: statusClass.join(''),
+ label: statusLabel
+ };
+ };
+
+ this.checkSigned = function(mail) {
+ var statusNotSigned = {
+ cssClass: 'security-status__label--not-signed',
+ label: 'not-signed'
+ };
+
+ if(_.isEmpty(mail.security_casing.imprints)) {
+ return statusNotSigned;
+ }
+
+ var hasNoSignatureInformation = _.any(mail.security_casing.imprints, function (imprint) {
+ return imprint.state === 'no_signature_information';
+ });
+
+ if(hasNoSignatureInformation) {
+ return statusNotSigned;
+ }
+
+ var statusClass = ['security-status__label--signed'];
+ var statusLabel = ['signed'];
+
+ if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_revoked'; })) {
+ statusClass.push('--revoked');
+ statusLabel.push('signature-revoked');
+ }
+
+ if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_expired'; })) {
+ statusClass.push('--expired');
+ statusLabel.push('signature-expired');
+ }
+
+ if(this.isNotTrusted(mail)) {
+ statusClass.push('--not-trusted');
+ statusLabel.push('signature-not-trusted');
+ }
+
+ return {
+ cssClass: statusClass.join(''),
+ label: statusLabel.join(' ')
+ };
+ };
+
+ this.isNotTrusted = function(mail){
+ return _.any(mail.security_casing.imprints, function(imprint) {
+ if(_.isNull(imprint.seal)){
+ return true;
+ }
+ var currentTrust = _.isUndefined(imprint.seal.trust) ? imprint.seal.validity : imprint.seal.trust;
+ return currentTrust === 'no_trust';
+ });
+ };
+
+ this.openNoMessageSelectedPane = function(ev, data) {
+ this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected);
+ };
+
+ this.handleKeyDown = function(event) {
+ var ENTER_KEY = 13;
+ var ESC_KEY = 27;
+
+ if (event.which === ENTER_KEY){
+ event.preventDefault();
+ if (this.select('newTagInput').val().trim() !== '') {
+ this.createNewTag();
+ }
+ } else if (event.which === ESC_KEY) {
+ event.preventDefault();
+ this.addTagLoseFocus();
+ }
+ };
+
+ this.addTagLoseFocus = function () {
+ this.select('newTagInput').hide();
+ this.select('newTagInput').typeahead('val', '');
+ this.select('addNew').show();
+ };
+
+ this.showNewTagInput = function () {
+ this.select('newTagInput').show();
+ this.select('newTagInput').focus();
+ this.select('addNew').hide();
+ };
+
+ this.removeTag = function (tag) {
+ tag = tag.toString();
+ var filteredTags = _.without(this.attr.mail.tags, tag);
+ this.updateTags(this.attr.mail, filteredTags);
+ this.trigger(document, events.dispatchers.tags.refreshTagList);
+ };
+
+ this.moveToTrash = function(){
+ this.trigger(document, events.ui.mail.delete, { mail: this.attr.mail });
+ };
+
+ this.tagsUpdated = function(ev, data) {
+ data = data || {};
+ this.attr.mail.tags = data.tags;
+ this.displayMail({}, { mail: this.attr.mail });
+ };
+
+ this.mailDeleted = function(ev, data) {
+ if (_.contains(_.pluck(data.mails, 'ident'), this.attr.mail.ident)) {
+ this.openNoMessageSelectedPane();
+ }
+ };
+
+ this.fetchMailToShow = function () {
+ this.trigger(events.mail.want, {mail: this.attr.ident, caller: this});
+ };
+
+ this.highlightMailContent = function (event, data) {
+ // we can't directly manipulate the iFrame to highlight the content
+ // so we need to take an indirection where we directly manipulate
+ // the mail content to accomodate the highlighting
+ this.trigger(document, events.mail.highlightMailContent, data);
+ };
+
+ this.after('initialize', function () {
+ this.on(this, events.mail.notFound, this.openNoMessageSelectedPane);
+ this.on(this, events.mail.here, this.highlightMailContent);
+ this.on(document, events.mail.display, this.displayMail);
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ this.on(document, events.mail.tags.updated, this.tagsUpdated);
+ this.on(document, events.mail.deleted, this.mailDeleted);
+ this.fetchMailToShow();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/no_mails_available_pane.js b/web-ui/public/js/mail_view/ui/no_mails_available_pane.js
new file mode 100644
index 00000000..c62c6b30
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/no_mails_available_pane.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_hide_and_show',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withHideAndShow, events) {
+ 'use strict';
+
+ //return defineComponent(noMailsAvailablePane, withHideAndShow);
+ return defineComponent(noMailsAvailablePane);
+
+ function noMailsAvailablePane() {
+ this.defaultAttrs({
+ tag: null,
+ forSearch: ''
+ });
+
+ var mailsQueryMatch = /-?in:"?[\w]+"?|tag:"[\w]+"/g;
+
+ this.render = function() {
+ this.attr.tag = 'tags.' + this.attr.tag;
+ this.attr.forSearch = this.attr.forSearch.replace(mailsQueryMatch, '').trim();
+ this.$node.html(templates.noMailsAvailable(this.attr));
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/no_message_selected_pane.js b/web-ui/public/js/mail_view/ui/no_message_selected_pane.js
new file mode 100644
index 00000000..a5fc2393
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/no_message_selected_pane.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_hide_and_show',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withHideAndShow, events) {
+ 'use strict';
+
+ return defineComponent(noMessageSelectedPane, withHideAndShow);
+
+ function noMessageSelectedPane() {
+ this.render = function() {
+ this.$node.html(templates.noMessageSelected());
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/recipients/recipient.js b/web-ui/public/js/mail_view/ui/recipients/recipient.js
new file mode 100644
index 00000000..c13a52b1
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/recipients/recipient.js
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events'
+ ],
+
+ function (defineComponent, templates, events) {
+ 'use strict';
+
+ return defineComponent(recipient);
+
+ function recipient() {
+ this.renderAndPrepend = function (nodeToPrependTo, recipient) {
+ var html = $(templates.compose.fixedRecipient(recipient));
+ html.insertBefore(nodeToPrependTo.children().last());
+ var component = new this.constructor();
+ component.initialize(html, recipient);
+ component.attr.recipient = recipient;
+ return component;
+ };
+
+ this.recipientDelActions = function () {
+ this.on(this.$node.find('.recipient-del'), 'click', function (event) {
+ this.doSelect();
+ this.trigger(events.ui.recipients.deleteRecipient, this);
+ event.preventDefault();
+ });
+
+ this.on(this.$node.find('.recipient-del'), 'mouseover', function () {
+ this.$node.find('.recipient-value').addClass('deleting');
+ this.$node.find('.recipient-del').addClass('deleteTooltip');
+ });
+
+ this.on(this.$node.find('.recipient-del'), 'mouseout', function () {
+ this.$node.find('.recipient-value').removeClass('deleting');
+ this.$node.find('.recipient-del').removeClass('deleteTooltip');
+ });
+ };
+
+ this.destroy = function () {
+ this.$node.remove();
+ this.teardown();
+ };
+
+ this.doSelect = function () {
+ this.$node.find('.recipient-value').addClass('selected');
+ };
+
+ this.doUnselect = function () {
+ this.$node.find('.recipient-value').removeClass('selected');
+ };
+
+ this.isSelected = function () {
+ return this.$node.find('.recipient-value').hasClass('selected');
+ };
+
+ this.sinalizeInvalid = function () {
+ this.$node.find('.recipient-value>span').addClass('invalid-format');
+ };
+
+ this.discoverEncryption = function () {
+ this.$node.addClass('discover-encryption');
+ var p = $.getJSON('/keys?search=' + this.attr.address).promise();
+ p.done(function () {
+ this.$node.find('.recipient-value').addClass('encrypted');
+ this.$node.removeClass('discover-encryption');
+ }.bind(this));
+ p.fail(function () {
+ this.$node.find('.recipient-value').addClass('not-encrypted');
+ this.$node.removeClass('discover-encryption');
+ }.bind(this));
+ };
+
+ this.getMailAddress = function() {
+ return this.$node.find('input[type=hidden]').val();
+ };
+
+ this.triggerEditRecipient = function(event, element) {
+ this.trigger(this.$node.closest('.recipients-area'), events.ui.recipients.clickToEdit, this);
+ };
+
+ this.after('initialize', function () {
+ this.recipientDelActions();
+ this.on('click', this.triggerEditRecipient);
+
+ if (this.attr.invalidAddress){
+ this.sinalizeInvalid();
+ } else {
+ this.discoverEncryption();
+ }
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients.js b/web-ui/public/js/mail_view/ui/recipients/recipients.js
new file mode 100644
index 00000000..2caa8d14
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/recipients/recipients.js
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events',
+ 'helpers/iterator',
+ 'mail_view/ui/recipients/recipients_input',
+ 'mail_view/ui/recipients/recipient',
+ 'mail_view/ui/recipients/recipients_iterator'
+ ],
+ function (defineComponent, templates, events, Iterator, RecipientsInput, Recipient, RecipientsIterator) {
+ 'use strict';
+
+ return defineComponent(recipients);
+
+ function recipients() {
+ this.defaultAttrs({
+ navigationHandler: '.recipients-navigation-handler',
+ recipientsList: '.recipients-list'
+ });
+
+ function getAddresses(recipients) {
+ return _.flatten(_.map(recipients, function (e) { return e.attr.address;}));
+ }
+
+ function moveLeft() { this.attr.iterator.moveLeft(); }
+ function moveRight() { this.attr.iterator.moveRight(); }
+ function deleteCurrentRecipient() {
+ this.attr.iterator.deleteCurrent();
+ this.addressesUpdated();
+ }
+
+ function editCurrentRecipient(event, recipient) {
+ var mailAddr = this.attr.iterator.current().getMailAddress();
+ this.attr.iterator.deleteCurrent();
+ this.attr.input.$node.val(mailAddr).focus();
+ this.unselectAllRecipients();
+ this.addressesUpdated();
+ }
+
+ this.clickToEditRecipient = function(event, recipient) {
+ this.attr.iterator = null;
+ var mailAddr = recipient.getMailAddress();
+
+ var position = this.getRecipientPosition(recipient);
+ this.attr.recipients.splice(position, 1);
+ recipient.destroy();
+
+ this.addressesUpdated();
+ this.unselectAllRecipients();
+ this.attr.input.$node.val(mailAddr).focus();
+ };
+
+ this.getRecipientPosition = function(recipient) {
+ return recipient.$node.closest('.recipients-area').find('.fixed-recipient').index(recipient.$node);
+ };
+
+ this.unselectAllRecipients = function() {
+ this.$node.find('.recipient-value.selected').removeClass('selected');
+ };
+
+ var SPECIAL_KEYS_ACTIONS = {
+ 8: deleteCurrentRecipient,
+ 46: deleteCurrentRecipient,
+ 32: editCurrentRecipient,
+ 13: editCurrentRecipient,
+ 37: moveLeft,
+ 39: moveRight
+ };
+
+ this.addRecipient = function(recipient) {
+ var newRecipient = Recipient.prototype.renderAndPrepend(this.$node.find(this.attr.recipientsList), recipient);
+ this.attr.recipients.push(newRecipient);
+ };
+
+ this.recipientEntered = function (event, recipient) {
+ this.addRecipient(recipient);
+ this.addressesUpdated();
+ };
+
+ this.invalidRecipientEntered = function(event, recipient) {
+ recipient.invalidAddress = true;
+ this.addRecipient(recipient);
+ };
+
+ this.deleteRecipient = function (event, recipient) {
+ this.attr.iterator = null;
+ var position = this.getRecipientPosition(recipient);
+
+ this.attr.recipients.splice(position, 1);
+ recipient.destroy();
+
+ this.addressesUpdated();
+ };
+
+ this.deleteLastRecipient = function () {
+ this.attr.recipients.pop().destroy();
+ this.addressesUpdated();
+ };
+
+ this.enterNavigationMode = function () {
+ this.attr.iterator = new RecipientsIterator({
+ elements: this.attr.recipients,
+ exitInput: this.attr.input.$node
+ });
+
+ this.attr.iterator.current().doSelect();
+ this.attr.input.$node.blur();
+ this.select('navigationHandler').focus();
+ };
+
+ this.leaveNavigationMode = function () {
+ if(this.attr.iterator) { this.attr.iterator.current().unselect(); }
+ this.attr.iterator = null;
+ };
+
+ this.selectLastRecipient = function () {
+ if (this.attr.recipients.length === 0) { return; }
+ this.enterNavigationMode();
+ };
+
+ this.attachInput = function () {
+ this.attr.input = RecipientsInput.prototype.attachAndReturn(this.$node.find('input[type=text]'), this.attr.name);
+ };
+
+ this.processSpecialKey = function (event) {
+ if(SPECIAL_KEYS_ACTIONS.hasOwnProperty(event.which)) { SPECIAL_KEYS_ACTIONS[event.which].apply(this); }
+ };
+
+ this.initializeAddresses = function () {
+ _.each(_.flatten(this.attr.addresses), function (address) {
+ this.addRecipient({ address: address, name: this.attr.name });
+ }.bind(this));
+ };
+
+ this.addressesUpdated = function() {
+ this.trigger(document, events.ui.recipients.updated, {recipientsName: this.attr.name, newRecipients: getAddresses(this.attr.recipients)});
+ };
+
+ this.doCompleteRecipients = function () {
+ var address = this.attr.input.$node.val();
+ if (!_.isEmpty(address)) {
+ var recipient = Recipient.prototype.renderAndPrepend(this.$node, { name: this.attr.name, address: address });
+ this.attr.recipients.push(recipient);
+ this.attr.input.$node.val('');
+ }
+
+ this.trigger(document, events.ui.recipients.updated, {
+ recipientsName: this.attr.name,
+ newRecipients: getAddresses(this.attr.recipients),
+ skipSaveDraft: true
+ });
+
+ };
+
+ this.after('initialize', function () {
+ this.attr.recipients = [];
+ this.attachInput();
+ this.initializeAddresses();
+
+ this.on(events.ui.recipients.deleteRecipient, this.deleteRecipient);
+ this.on(events.ui.recipients.deleteLast, this.deleteLastRecipient);
+ this.on(events.ui.recipients.selectLast, this.selectLastRecipient);
+ this.on(events.ui.recipients.entered, this.recipientEntered);
+ this.on(events.ui.recipients.enteredInvalid, this.invalidRecipientEntered);
+ this.on(events.ui.recipients.clickToEdit, this.clickToEditRecipient);
+
+ this.on(document, events.ui.recipients.doCompleteInput, this.doCompleteRecipients);
+
+ this.on(this.attr.input.$node, 'focus', this.leaveNavigationMode);
+ this.on(this.select('navigationHandler'), 'keydown', this.processSpecialKey);
+
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ });
+ }
+ });
diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_input.js b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js
new file mode 100644
index 00000000..8a9c4eaf
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define([
+ 'flight/lib/component',
+ 'page/events',
+ 'features'
+ ],
+ function (defineComponent, events, features) {
+ 'use strict';
+
+ function recipientsInput() {
+ var EXIT_KEY_CODE_MAP = {
+ 8: 'backspace',
+ 37: 'left'
+ },
+ ENTER_ADDRESS_KEY_CODE_MAP = {
+ 9: 'tab',
+ 186: 'semicolon',
+ 188: 'comma',
+ 13: 'enter',
+ 27: 'esc'
+ },
+ EVENT_FOR = {
+ 8: events.ui.recipients.deleteLast,
+ 37: events.ui.recipients.selectLast
+ },
+ self;
+
+ var simpleAddressMatch = /[^<\w,;]?([^\s<;,]+@[\w-]+\.[^\s>;,]+)/;
+ var canonicalAddressMatch = /([^,;\s][^,;@]+<[^\s;,]+@[\w-]+\.[^\s;,]+>)/;
+ var emailAddressMatch = new RegExp([simpleAddressMatch.source, '|', canonicalAddressMatch.source].join(''), 'g');
+
+ var extractContactNames = function (response) {
+ return _.map(response, function(a) { return { value: a }; });
+ };
+
+ function createEmailCompleter() {
+ var emailCompleter = new Bloodhound({
+ datumTokenizer: function (d) {
+ return [d.value];
+ },
+ queryTokenizer: function (q) {
+ return [q.trim()];
+ },
+ remote: {
+ url: '/contacts?q=%QUERY',
+ filter: extractContactNames
+ }
+ });
+ emailCompleter.initialize();
+ return emailCompleter;
+ }
+
+ function reset(node) {
+ node.typeahead('val', '');
+ }
+
+ function caretIsInTheBeginningOfInput(input) {
+ return input.selectionStart === 0;
+ }
+
+ function isExitKey(keyPressed) {
+ return EXIT_KEY_CODE_MAP.hasOwnProperty(keyPressed);
+ }
+
+ function isEnterAddressKey(keyPressed) {
+ return ENTER_ADDRESS_KEY_CODE_MAP.hasOwnProperty(keyPressed);
+ }
+
+ this.processSpecialKey = function (event) {
+ var keyPressed = event.which;
+
+ if (isExitKey(keyPressed) && caretIsInTheBeginningOfInput(this.$node[0])) {
+ this.trigger(EVENT_FOR[keyPressed]);
+ return;
+ }
+
+ if (!event.shiftKey && isEnterAddressKey(keyPressed)) {
+ this.tokenizeRecipient(event);
+
+ if ((keyPressed !== 9 /* tab */)) {
+ event.preventDefault();
+ }
+ }
+
+ };
+
+ this.tokenizeRecipient = function (event) {
+ if (_.isEmpty(this.$node.val().trim())) {
+ return;
+ }
+
+ this.recipientSelected(null, {value: this.$node.val() });
+ event.preventDefault();
+ };
+
+ this.recipientSelected = function (event, data) {
+ var value = (data && data.value) || this.$node.val();
+
+ var validAddresses = this.extractValidAddresses(value);
+ var invalidAddresses = this.extractInvalidAddresses(value);
+
+ this.triggerEventForEach(validAddresses, events.ui.recipients.entered);
+ this.triggerEventForEach(invalidAddresses, events.ui.recipients.enteredInvalid);
+
+ reset(this.$node);
+ };
+
+ this.triggerEventForEach = function (addresses, event) {
+ var that = this;
+ _.each(addresses, function(address) {
+ if (!_.isEmpty(address.trim())) {
+ that.trigger(that.$node, event, { name: that.attr.name, address: address.trim() });
+ }
+ });
+ };
+
+ this.extractValidAddresses = function(rawAddresses) {
+ return rawAddresses.match(emailAddressMatch);
+ };
+
+ this.extractInvalidAddresses = function(rawAddresses) {
+ return rawAddresses.replace(emailAddressMatch, '').split(/[,;]/);
+ };
+
+ this.init = function () {
+ this.$node.typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 1
+ }, {
+ source: createEmailCompleter().ttAdapter(),
+ templates: {
+ suggestion: function (o) { return _.escape(o.value); }
+ }
+ });
+ };
+
+ this.attachAndReturn = function (node, name) {
+ var input = new this.constructor();
+ input.initialize(node, { name: name});
+ return input;
+ };
+
+ this.warnSendButtonOfInputState = function () {
+ var toTrigger = _.isEmpty(this.$node.val()) ? events.ui.recipients.inputFieldIsEmpty : events.ui.recipients.inputFieldHasCharacters;
+ this.trigger(document, toTrigger, { name: this.attr.name });
+ };
+
+ this.after('initialize', function () {
+ self = this;
+ this.init();
+ this.on('typeahead:selected typeahead:autocompleted', this.recipientSelected);
+ this.on(this.$node, 'focusout', this.tokenizeRecipient);
+ this.on(this.$node, 'keydown', this.processSpecialKey);
+ this.on(this.$node, 'keyup', this.warnSendButtonOfInputState);
+
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ });
+ }
+
+ return defineComponent(recipientsInput);
+
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js
new file mode 100644
index 00000000..624ac4f5
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['helpers/iterator'], function (Iterator) {
+ 'use strict';
+
+ return RecipientsIterator;
+
+ function RecipientsIterator(options) {
+
+ this.iterator = new Iterator(options.elements, options.elements.length - 1);
+ this.input = options.exitInput;
+
+ this.current = function () {
+ return this.iterator.current();
+ };
+
+ this.moveLeft = function () {
+ if (this.iterator.hasPrevious()) {
+ this.iterator.current().doUnselect();
+ this.iterator.previous().doSelect();
+ }
+ };
+
+ this.moveRight = function () {
+ this.iterator.current().doUnselect();
+ if (this.iterator.hasNext()) {
+ this.iterator.next().doSelect();
+ } else {
+ this.input.focus();
+ }
+ };
+
+ this.deleteCurrent = function () {
+ this.iterator.removeCurrent().destroy();
+
+ if (this.iterator.hasElements()) {
+ this.iterator.current().doSelect();
+ } else {
+ this.input.focus();
+ }
+ };
+ }
+
+});
diff --git a/web-ui/public/js/mail_view/ui/reply_box.js b/web-ui/public/js/mail_view/ui/reply_box.js
new file mode 100644
index 00000000..a174d185
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/reply_box.js
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'helpers/view_helper',
+ 'mixins/with_hide_and_show',
+ 'mixins/with_compose_inline',
+ 'page/events',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) {
+ 'use strict';
+
+ return defineComponent(replyBox, withHideAndShow, withComposeInline);
+
+ function replyBox() {
+ this.defaultAttrs({
+ replyType: 'reply',
+ draftReply: false,
+ mail: null,
+ mailBeingRepliedIdent: undefined
+ });
+
+ this.getRecipients = function() {
+ if (this.attr.replyType === 'replyall') {
+ return this.attr.mail.replyToAllAddress();
+ } else {
+ return this.attr.mail.replyToAddress();
+ }
+ };
+
+ var re = function(v) { return i18n.t('re') + ': ' + v; };
+
+ this.setupReplyBox = function() {
+ var recipients, body;
+
+ if (this.attr.draftReply){
+ this.attr.ident = this.attr.mail.ident;
+ this.attr.mailBeingRepliedIdent = this.attr.mail.draft_reply_for;
+
+ recipients = this.attr.mail.recipients();
+ body = this.attr.mail.body;
+ this.attr.subject = this.attr.mail.header.subject;
+ } else {
+ this.attr.mailBeingRepliedIdent = this.attr.mail.ident;
+ recipients = this.getRecipients();
+ body = viewHelper.quoteMail(this.attr.mail);
+ this.attr.subject = re(this.attr.mail.header.subject);
+ }
+
+ this.attr.recipientValues.to = recipients.to;
+ this.attr.recipientValues.cc = recipients.cc;
+
+ this.renderInlineCompose('reply-box', {
+ recipients: recipients,
+ subject: this.attr.subject,
+ body: body
+ });
+
+ this.on(this.select('recipientsDisplay'), 'click keydown', this.showRecipientFields);
+ this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput);
+ };
+
+ this.showRecipientFields = function(ev, data) {
+ if(!ev.keyCode || ev.keyCode === 13){
+ this.select('recipientsDisplay').hide();
+ this.select('recipientsFields').show();
+ $('#recipients-to-area .tt-input').focus();
+ }
+ };
+
+ this.showSubjectInput = function() {
+ this.select('subjectDisplay').hide();
+ this.select('subjectInput').show();
+ this.select('subjectInput').focus();
+ };
+
+ this.buildMail = function(tag) {
+ var builder = this.builtMail(tag).subject(this.select('subjectInput').val());
+ if(!_.isUndefined(this.attr.mail.header.message_id)) {
+ builder.header('in_reply_to', this.attr.mail.header.message_id);
+ }
+
+ if(!_.isUndefined(this.attr.mail.header.list_id)) {
+ builder.header('list_id', this.attr.mail.header.list_id);
+ }
+
+ var mail = builder.build();
+ mail.setDraftReplyFor(this.attr.mailBeingRepliedIdent);
+
+ return mail;
+ };
+
+ this.after('initialize', function () {
+ this.setupReplyBox();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/reply_section.js b/web-ui/public/js/mail_view/ui/reply_section.js
new file mode 100644
index 00000000..cbe64205
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/reply_section.js
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mail_view/ui/reply_box',
+ 'mail_view/ui/forward_box',
+ 'mixins/with_hide_and_show',
+ 'mixins/with_feature_toggle',
+ 'page/events'
+ ],
+
+ function (defineComponent, templates, ReplyBox, ForwardBox, withHideAndShow, withFeatureToggle, events) {
+ 'use strict';
+
+ return defineComponent(replySection, withHideAndShow, withFeatureToggle('replySection'));
+
+ function replySection() {
+ this.defaultAttrs({
+ replyButton: '#reply-button',
+ replyAllButton: '#reply-all-button',
+ forwardButton: '#forward-button',
+ replyBox: '#reply-box',
+ replyType: 'reply',
+ replyContainer: '.reply-container'
+ });
+
+ this.showReply = function() {
+ this.attr.replyType = 'reply';
+ this.fetchEmailToReplyTo();
+ };
+
+ this.showReplyAll = function() {
+ this.attr.replyType = 'replyall';
+ this.fetchEmailToReplyTo();
+ };
+
+ this.showForward = function() {
+ this.attr.replyType = 'forward';
+ this.fetchEmailToReplyTo();
+ };
+
+ this.render = function () {
+ this.$node.html(templates.compose.replySection);
+
+ this.on(this.select('replyButton'), 'click', this.showReply);
+ this.on(this.select('replyAllButton'), 'click', this.showReplyAll);
+ this.on(this.select('forwardButton'), 'click', this.showForward);
+ };
+
+ this.checkForDraftReply = function() {
+ this.render();
+ this.hideContainer();
+
+ this.trigger(document, events.mail.draftReply.want, {ident: this.attr.ident});
+ };
+
+ this.fetchEmailToReplyTo = function (ev) {
+ this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this });
+ };
+
+ this.showDraftReply = function(ev, data) {
+ this.showContainer();
+ this.hideButtons();
+ ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, draftReply: true });
+ };
+
+ this.showReplyComposeBox = function (ev, data) {
+ this.showContainer();
+ this.hideButtons();
+ if(this.attr.replyType === 'forward') {
+ ForwardBox.attachTo(this.select('replyBox'), { mail: data.mail });
+ } else {
+ ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, replyType: this.attr.replyType });
+ }
+ };
+
+ this.hideContainer = function() {
+ this.select('replyContainer').hide();
+ };
+
+ this.showContainer = function() {
+ this.select('replyContainer').show();
+ };
+
+ this.hideButtons = function() {
+ this.select('replyButton').hide();
+ this.select('replyAllButton').hide();
+ this.select('forwardButton').hide();
+ };
+
+ this.showButtons = function () {
+ this.showContainer();
+ this.select('replyBox').empty();
+ this.select('replyButton').show();
+ this.select('replyAllButton').show();
+ this.select('forwardButton').show();
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.replyBox.showReply, this.showReply);
+ this.on(document, events.ui.replyBox.showReplyAll, this.showReplyAll);
+ this.on(document, events.ui.composeBox.trashReply, this.showButtons);
+ this.on(this, events.mail.here, this.showReplyComposeBox);
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+
+ this.on(document, events.ui.replyBox.showReplyContainer, this.showContainer);
+ this.on(document, events.mail.draftReply.here, this.showDraftReply);
+
+ this.checkForDraftReply();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/mail_view/ui/send_button.js b/web-ui/public/js/mail_view/ui/send_button.js
new file mode 100644
index 00000000..66fe1233
--- /dev/null
+++ b/web-ui/public/js/mail_view/ui/send_button.js
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define([
+ 'flight/lib/component',
+ 'flight/lib/utils',
+ 'page/events',
+ 'helpers/view_helper'
+ ],
+ function (defineComponent, utils, events, viewHelper) {
+ 'use strict';
+
+ return defineComponent(sendButton);
+
+ function sendButton() {
+ var RECIPIENTS_BOXES_COUNT = 3;
+
+ this.enableButton = function () {
+ this.$node.prop('disabled', false);
+ };
+
+ this.disableButton = function () {
+ this.$node.prop('disabled', true);
+ };
+
+ this.atLeastOneInputFieldHasRecipients = function () {
+ return _.any(_.values(this.attr.recipients), function (e) { return !_.isEmpty(e); });
+ };
+
+ this.atLeastOneInputFieldHasCharacters = function () {
+ return _.any(_.values(this.attr.inputFieldHasCharacters), function (e) { return e === true; });
+ };
+
+ this.updateButton = function () {
+ if (this.attr.sendingInProgress === false) {
+ if (this.attr.uploading === false && (this.atLeastOneInputFieldHasCharacters() || this.atLeastOneInputFieldHasRecipients())) {
+ this.enableButton();
+ } else {
+ this.disableButton();
+ }
+ }
+ };
+
+ this.inputFieldIsEmpty = function (ev, data) {
+ this.attr.inputFieldHasCharacters[data.name] = false;
+ this.updateButton();
+ };
+
+ this.inputFieldHasCharacters = function (ev, data) {
+ this.attr.inputFieldHasCharacters[data.name] = true;
+ this.updateButton();
+ };
+
+ this.uploadInProgress = function (ev, data) {
+ this.attr.uploading = true;
+ this.updateButton();
+ };
+
+ this.uploadFinished = function (ev, data) {
+ this.attr.uploading = false;
+ this.updateButton();
+ };
+
+ this.updateRecipientsForField = function (ev, data) {
+ this.attr.recipients[data.recipientsName] = data.newRecipients;
+ this.attr.inputFieldHasCharacters[data.recipientsName] = false;
+
+ this.updateButton();
+ };
+
+ this.updateRecipientsAndSendMail = function () {
+
+ this.on(document, events.ui.mail.recipientsUpdated, utils.countThen(RECIPIENTS_BOXES_COUNT, function () {
+ this.trigger(document, events.ui.mail.send);
+ this.off(document, events.ui.mail.recipientsUpdated);
+ }.bind(this)));
+
+ this.disableButton();
+ this.$node.text(viewHelper.i18n.t('sending-mail'));
+
+ this.attr.sendingInProgress = true;
+
+ this.trigger(document, events.ui.recipients.doCompleteInput);
+ };
+
+ this.resetButton = function () {
+ this.attr.sendingInProgress = false;
+ this.attr.uploading = false;
+ this.$node.html(viewHelper.i18n.t('send'));
+ this.enableButton();
+ };
+
+ this.after('initialize', function () {
+ this.attr.recipients = {};
+ this.attr.inputFieldHasCharacters = {};
+ this.resetButton();
+
+ this.on(document, events.ui.recipients.inputFieldHasCharacters, this.inputFieldHasCharacters);
+ this.on(document, events.ui.recipients.inputFieldIsEmpty, this.inputFieldIsEmpty);
+ this.on(document, events.ui.recipients.updated, this.updateRecipientsForField);
+
+ this.on(this.$node, 'click', this.updateRecipientsAndSendMail);
+
+ this.on(document, events.mail.uploadingAttachment, this.uploadInProgress);
+ this.on(document, events.mail.uploadedAttachment, this.uploadFinished);
+ this.on(document, events.mail.failedUploadAttachment, this.uploadFinished);
+
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ this.on(document, events.ui.sendbutton.enable, this.resetButton);
+ this.on(document, events.mail.send_failed, this.resetButton);
+
+ this.disableButton();
+ });
+ }
+
+ }
+);
diff --git a/web-ui/public/js/main.js b/web-ui/public/js/main.js
new file mode 100644
index 00000000..1aeb1a63
--- /dev/null
+++ b/web-ui/public/js/main.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+requirejs.config({
+ baseUrl: '../static/',
+ paths: {
+ 'mail_list': 'js/mail_list',
+ 'page': 'js/page',
+ 'feedback': 'js/feedback',
+ 'flight': 'bower_components/flight',
+ 'DOMPurify': 'bower_components/DOMPurify/dist/purify.min',
+ 'he': 'bower_components/he/he',
+ 'hbs': 'js/generated/hbs',
+ 'helpers': 'js/helpers',
+ 'lib': 'js/lib',
+ 'views': 'js/views',
+ 'tags': 'js/tags',
+ 'mail_list_actions': 'js/mail_list_actions',
+ 'user_alerts': 'js/user_alerts',
+ 'mail_view': 'js/mail_view',
+ 'dispatchers': 'js/dispatchers',
+ 'services': 'js/services',
+ 'mixins': 'js/mixins',
+ 'search': 'js/search',
+ 'foundation': 'js/foundation',
+ 'features': 'js/features/features',
+ 'i18next': 'bower_components/i18next/i18next',
+ 'i18nextXHRBackend': 'bower_components/i18next-xhr-backend/i18nextXHRBackend',
+ 'i18nextBrowserLanguageDetector': 'bower_components/i18next-browser-languagedetector/i18nextBrowserLanguageDetector',
+ 'quoted-printable': 'bower_components/quoted-printable',
+ 'utf8': 'bower_components/utf8',
+ 'user_settings': 'js/user_settings'
+ }
+});
+
+require([
+ 'flight/lib/compose',
+ 'flight/lib/debug'
+], function(compose, debug){
+ 'use strict';
+ debug.enable(true);
+ debug.events.logAll();
+});
+
+require(
+ [
+ 'flight/lib/compose',
+ 'flight/lib/registry',
+ 'flight/lib/advice',
+ 'flight/lib/logger',
+ 'flight/lib/debug',
+ 'page/events',
+ 'page/default',
+ 'js/monkey_patching/all'
+ ],
+
+ function(compose, registry, advice, withLogging, debug, events, initializeDefault, _monkeyPatched) {
+ 'use strict';
+
+ window.Pixelated = window.Pixelated || {};
+ window.Pixelated.events = events;
+
+ compose.mixin(registry, [advice.withAdvice, withLogging]);
+
+ debug.enable(true);
+ debug.events.logAll();
+
+ initializeDefault('');
+ }
+);
diff --git a/web-ui/public/js/mixins/with_auto_refresh.js b/web-ui/public/js/mixins/with_auto_refresh.js
new file mode 100644
index 00000000..c75fda45
--- /dev/null
+++ b/web-ui/public/js/mixins/with_auto_refresh.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['features'],
+ function (features) {
+ 'use strict';
+
+ function withAutoRefresh(refreshMethod) {
+ return function () {
+ this.defaultAttrs({
+ refreshInterval: 15000
+ });
+
+ this.setupRefresher = function () {
+ clearTimeout(this.attr.refreshTimer);
+ this.attr.refreshTimer = setTimeout(function () {
+ this[refreshMethod]();
+ this.setupRefresher();
+ }.bind(this), this.attr.refreshInterval);
+ };
+
+ this.after('initialize', function () {
+ if (features.isAutoRefreshEnabled()) {
+ this.setupRefresher();
+ }
+ });
+ };
+ }
+
+ return withAutoRefresh;
+ }
+);
+
diff --git a/web-ui/public/js/mixins/with_compose_inline.js b/web-ui/public/js/mixins/with_compose_inline.js
new file mode 100644
index 00000000..b8266f28
--- /dev/null
+++ b/web-ui/public/js/mixins/with_compose_inline.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'page/events',
+ 'views/templates',
+ 'mail_view/data/mail_builder',
+ 'mixins/with_mail_edit_base'
+ ],
+ function(events, templates, mailBuilder, withMailEditBase) {
+ 'use strict';
+
+ function withComposeInline() {
+ this.defaultAttrs({
+ subjectDisplay: '#reply-subject',
+ subjectInput: '#subject-container input',
+ forwardBox: '#forward-box',
+ recipientsDisplay: '#all-recipients'
+ });
+
+ this.openMail = function(ev, data) {
+ this.trigger(document, events.ui.mail.open, {ident: this.attr.mail.ident});
+ };
+
+ this.trashReply = function() {
+ this.trigger(document, events.ui.composeBox.trashReply);
+ this.teardown();
+ };
+
+ this.builtMail = function(tag) {
+ return mailBuilder.newMail(this.attr.ident)
+ .subject(this.select('subjectBox').val())
+ .to(this.attr.recipientValues.to)
+ .cc(this.attr.recipientValues.cc)
+ .bcc(this.attr.recipientValues.bcc)
+ .body(this.select('bodyBox').val())
+ .attachment(this.attr.attachments)
+ .tag(tag);
+ };
+
+ this.renderInlineCompose = function(className, viewData) {
+ this.show();
+ this.render(templates.compose.inlineBox, viewData);
+
+ this.$node.addClass(className);
+ this.select('bodyBox').focus();
+
+ this.enableAutoSave();
+ };
+
+ this.updateIdent = function(ev, data) {
+ this.attr.mail.ident = data.ident;
+ };
+
+ this.discardDraft = function() {
+ this.trashReply();
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.mail.sent, this.openMail);
+ this.on(document, events.mail.deleted, this.trashReply);
+ this.on(document, events.mail.draftSaved, this.updateIdent);
+ });
+
+ withMailEditBase.call(this);
+ }
+
+ return withComposeInline;
+ });
diff --git a/web-ui/public/js/mixins/with_enable_disable_on_event.js b/web-ui/public/js/mixins/with_enable_disable_on_event.js
new file mode 100644
index 00000000..5b28a67b
--- /dev/null
+++ b/web-ui/public/js/mixins/with_enable_disable_on_event.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define([],
+ function () {
+ 'use strict';
+
+ function withEnableDisableOnEvent(ev) {
+ return function () {
+ this.disableElement = function () {
+ this.$node.attr('disabled', 'disabled');
+ };
+
+ this.enableElement = function () {
+ this.$node.removeAttr('disabled');
+ };
+
+ this.toggleEnabled = function (ev, enable) {
+ if (enable) {
+ this.enableElement();
+ } else {
+ this.disableElement();
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, ev, this.toggleEnabled);
+ });
+ };
+ }
+
+ return withEnableDisableOnEvent;
+ }
+);
diff --git a/web-ui/public/js/mixins/with_feature_toggle.js b/web-ui/public/js/mixins/with_feature_toggle.js
new file mode 100644
index 00000000..195b08bc
--- /dev/null
+++ b/web-ui/public/js/mixins/with_feature_toggle.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['features'],
+ function(features) {
+ 'use strict';
+
+ function withFeatureToggle(componentName, behaviorForFeatureOff) {
+ return function() {
+
+ this.around('initialize', _.bind(function(basicInitialize, node, attrs) {
+ if(features.isEnabled(componentName)) {
+ return basicInitialize(node, attrs);
+ }
+ else if (behaviorForFeatureOff){
+ behaviorForFeatureOff.call(this);
+
+ return this;
+ }
+ }, this));
+ };
+ }
+
+ return withFeatureToggle;
+
+});
diff --git a/web-ui/public/js/mixins/with_hide_and_show.js b/web-ui/public/js/mixins/with_hide_and_show.js
new file mode 100644
index 00000000..c8902f61
--- /dev/null
+++ b/web-ui/public/js/mixins/with_hide_and_show.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(function(require) {
+ 'use strict';
+
+ function withHideAndShow() {
+ this.hide = function () {
+ this.$node.hide();
+ };
+ this.show = function () {
+ this.$node.show();
+ };
+ }
+
+ return withHideAndShow;
+
+});
diff --git a/web-ui/public/js/mixins/with_mail_edit_base.js b/web-ui/public/js/mixins/with_mail_edit_base.js
new file mode 100644
index 00000000..a088080e
--- /dev/null
+++ b/web-ui/public/js/mixins/with_mail_edit_base.js
@@ -0,0 +1,263 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/compose',
+ 'helpers/view_helper',
+ 'mail_view/ui/recipients/recipients',
+ 'mail_view/ui/draft_save_status',
+ 'page/events',
+ 'views/i18n',
+ 'mail_view/ui/send_button',
+ 'mail_view/ui/attachment_icon',
+ 'mail_view/ui/attachment_list',
+ 'flight/lib/utils'
+ ],
+ function (compose, viewHelper, Recipients, DraftSaveStatus, events, i18n, SendButton, AttachmentIcon, attachmentList, utils) {
+ 'use strict';
+
+ function withMailEditBase() {
+
+ this.defaultAttrs({
+ bodyBox: '#text-box',
+ sendButton: '#send-button',
+ attachmentButton: '#attachment-button',
+ attachmentList: '#attachment-list',
+ cancelButton: '#cancel-button',
+ trashButton: '#trash-button',
+ toArea: '#recipients-to-area',
+ toBox: '#recipients-to-box',
+ ccArea: '#recipients-cc-area',
+ bccArea: '#recipients-bcc-area',
+ ccsTrigger: '#ccs-trigger',
+ bccsTrigger: '#bccs-trigger',
+ toTrigger: '#to-trigger',
+ subjectBox: '#subject',
+ tipMsg: '.tip-msg',
+ draftSaveStatus: '#draft-save-status',
+ recipientsFields: '#recipients-fields',
+ currentTag: '',
+ recipientValues: {to: [], cc: [], bcc: []},
+ saveDraftInterval: 3000
+ });
+
+ this.attachRecipients = function (context) {
+ Recipients.attachTo(this.select('toArea'), {name: 'to', addresses: context.recipients.to});
+ Recipients.attachTo(this.select('ccArea'), {name: 'cc', addresses: context.recipients.cc || []});
+ Recipients.attachTo(this.select('bccArea'), {name: 'bcc', addresses: context.recipients.bcc || []});
+ };
+
+ function thereAreRecipientsToDisplay() {
+
+ var allRecipients = _.chain(this.attr.recipientValues).
+ values().
+ flatten().
+ remove(undefined).
+ value();
+
+ return !_.isEmpty(allRecipients);
+ }
+
+ this.warnSendButtonOfRecipients = function () {
+ if (thereAreRecipientsToDisplay.call(this)) {
+ _.forOwn(this.attr.recipientValues, function (recipients, recipientsType) {
+ if (!_.isUndefined(recipients) && !_.isEmpty(recipients)) {
+ var recipientsUpdatedData = {
+ newRecipients: recipients,
+ recipientsName: recipientsType
+ };
+ this.trigger(document, events.ui.recipients.updated, recipientsUpdatedData);
+ }
+ }.bind(this));
+ }
+ };
+
+ this.render = function (template, context) {
+ this.$node.html(template(context));
+
+ if (!context || _.isEmpty(context)) {
+ context.recipients = {to: [], cc: [], bcc: []};
+ }
+ this.attr.recipientValues = context.recipients;
+ this.attr.attachments = context.attachments || [];
+ this.attachRecipients(context);
+
+ this.on(this.select('trashButton'), 'click', this.discardMail);
+ SendButton.attachTo(this.select('sendButton'));
+ AttachmentIcon.attachTo(this.select('attachmentButton'));
+
+ this.warnSendButtonOfRecipients();
+ };
+
+ this.enableAutoSave = function () {
+ this.select('bodyBox').on('input', this.monitorInput.bind(this));
+ this.select('subjectBox').on('input', this.monitorInput.bind(this));
+ this.on(document, events.mail.appendAttachment, this.monitorInput.bind(this));
+ this.on(document, events.mail.removeAttachment, this.monitorInput.bind(this));
+ DraftSaveStatus.attachTo(this.select('draftSaveStatus'));
+ };
+
+ this.monitorInput = function () {
+ this.trigger(events.ui.mail.changedSinceLastSave);
+ this.cancelPostponedSaveDraft();
+ var mail = this.buildMail();
+ this.postponeSaveDraft(mail);
+ };
+
+ this.discardMail = function () {
+ this.cancelPostponedSaveDraft();
+ if (this.attr.ident) {
+ var mail = this.buildMail();
+ this.trigger(document, events.ui.mail.delete, {mail: mail});
+ } else {
+ this.trigger(document, events.ui.mail.discard);
+ }
+ };
+
+ this.trim_recipient = function (recipients) {
+ return recipients.map(function (recipient) {
+ return recipient.trim();
+ });
+ };
+
+ this.sendMail = function () {
+ this.cancelPostponedSaveDraft();
+ var mail = this.buildMail('sent');
+
+ if (allRecipientsAreEmails(mail)) {
+ mail.header.to = this.trim_recipient(mail.header.to);
+ mail.header.cc = this.trim_recipient(mail.header.cc);
+ mail.header.bcc = this.trim_recipient(mail.header.bcc);
+ this.trigger(events.mail.send, mail);
+ } else {
+ this.trigger(
+ events.ui.userAlerts.displayMessage,
+ {message: i18n.t('recipients-not-valid')}
+ );
+ this.trigger(events.mail.send_failed);
+ }
+ };
+
+ this.buildAndSaveDraft = function () {
+ var mail = this.buildMail();
+ this.saveDraft(mail);
+ };
+
+ this.recipientsUpdated = function (ev, data) {
+ this.attr.recipientValues[data.recipientsName] = data.newRecipients;
+ this.trigger(document, events.ui.mail.recipientsUpdated);
+ if (data.skipSaveDraft) {
+ return;
+ }
+
+ var mail = this.buildMail();
+ this.postponeSaveDraft(mail);
+ };
+
+ this.saveDraft = function (mail) {
+ this.cancelPostponedSaveDraft();
+ this.trigger(document, events.mail.saveDraft, mail);
+ };
+
+ this.cancelPostponedSaveDraft = function () {
+ clearTimeout(this.attr.timeout);
+ };
+
+ this.postponeSaveDraft = function (mail) {
+ this.cancelPostponedSaveDraft();
+
+ this.attr.timeout = window.setTimeout(_.bind(function () {
+ this.saveDraft(mail);
+ }, this), this.attr.saveDraftInterval);
+ };
+
+ this.draftSaved = function (event, data) {
+ this.attr.ident = data.ident;
+ };
+
+ this.validateAnyRecipient = function () {
+ return !_.isEmpty(_.flatten(_.values(this.attr.recipientValues)));
+ };
+
+ function allRecipientsAreEmails(mail) {
+ var allRecipients = mail.header.to.concat(mail.header.cc).concat(mail.header.bcc);
+ return _.isEmpty(allRecipients) ? false : _.all(allRecipients, emailFormatChecker);
+ }
+
+ function emailFormatChecker(email) {
+ var emailFormat = /[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailFormat.test(email);
+ }
+
+ this.saveTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ };
+
+ this.mailSent = function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, {message: 'Your message was sent!'});
+ };
+
+ this.enableFloatlabel = function (element) {
+ var showClass = 'showfloatlabel';
+ $(element).bind('keyup', function () {
+ var label = $(this).prev('label');
+ if (this.value !== '') {
+ label.addClass(showClass);
+ $(this).addClass(showClass);
+ } else {
+ label.removeClass(showClass);
+ $(this).removeClass(showClass);
+ }
+ });
+ };
+
+ this.toggleRecipientsArrows = function () {
+ $('#cc-bcc-collapse').toggleClass('fa-angle-down');
+ $('#cc-bcc-collapse').toggleClass('fa-angle-up');
+ };
+
+ this.before('initialize', function () {
+ if (!this.discardDraft) {
+ this.discardDraft = function () {
+ };
+ }
+ });
+
+ this.bindCollapse = function () {
+ this.on($('#cc-bcc-collapse'), 'click', this.toggleRecipientsArrows);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.dispatchers.rightPane.clear, this.teardown);
+ this.on(document, events.ui.recipients.updated, this.recipientsUpdated);
+ this.on(document, events.mail.draftSaved, this.draftSaved);
+ this.on(document, events.mail.sent, this.mailSent);
+
+ this.on(document, events.ui.mail.send, this.sendMail);
+
+ this.on(document, events.ui.mail.discard, this.discardDraft);
+ this.on(document, events.ui.tag.selected, this.saveTag);
+ this.on(document, events.ui.tag.select, this.saveTag);
+ this.bindCollapse();
+ });
+
+ compose.mixin(this, [attachmentList]);
+ }
+
+ return withMailEditBase;
+ });
diff --git a/web-ui/public/js/mixins/with_mail_sandbox.js b/web-ui/public/js/mixins/with_mail_sandbox.js
new file mode 100644
index 00000000..1a51840d
--- /dev/null
+++ b/web-ui/public/js/mixins/with_mail_sandbox.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ ['helpers/view_helper', 'page/events'],
+ function(viewHelpers, events) {
+ 'use strict';
+
+ function withMailSandbox() {
+ this.showMailOnSandbox = function(mail) {
+ var that = this;
+ var $iframe = $("#read-sandbox");
+ var iframe = $iframe[0];
+ var content = viewHelpers.formatMailBody(mail);
+
+ window.addEventListener('message', function(e) {
+ if (e.origin === 'null' && e.source === iframe.contentWindow) {
+ that.trigger(document, events.ui.replyBox.showReplyContainer);
+ that.trigger(document, events.search.highlightResults, {where: '.mail-read-view__header'});
+ }
+ });
+
+ iframe.onload = function() {
+ if ($iframe.iFrameResize) {
+ // use iframe-resizer to dynamically adapt iframe size to its content
+ var config = {
+ resizedCallback: scaleToFit,
+ checkOrigin: false
+ };
+ $iframe.iFrameResize(config);
+ }
+
+ iframe.contentWindow.postMessage({
+ html: content
+ }, '*');
+
+ // transform scale iframe to fit container width
+ // necessary if iframe is wider than container
+ function scaleToFit() {
+ var parentWidth = $iframe.parent().width();
+ var w = $iframe.width();
+ var scale = 'none';
+
+ // only scale html mails
+ if (mail && mail.htmlBody && (w > parentWidth)) {
+ scale = parentWidth / w;
+ scale = 'scale(' + scale + ',' + scale + ')';
+ }
+
+ $iframe.css({
+ '-webkit-transform-origin': '0 0',
+ '-moz-transform-origin': '0 0',
+ '-ms-transform-origin': '0 0',
+ 'transform-origin': '0 0',
+ '-webkit-transform': scale,
+ '-moz-transform': scale,
+ '-ms-transform': scale,
+ 'transform': scale
+ });
+ }
+ };
+ };
+ }
+
+ return withMailSandbox;
+ }
+);
diff --git a/web-ui/public/js/mixins/with_mail_tagging.js b/web-ui/public/js/mixins/with_mail_tagging.js
new file mode 100644
index 00000000..1fc1c3bd
--- /dev/null
+++ b/web-ui/public/js/mixins/with_mail_tagging.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ ['page/events', 'features'],
+ function (events, features) {
+ 'use strict';
+ function withMailTagging () {
+ this.updateTags = function(mail, tags) {
+ this.trigger(document, events.mail.tags.update, {ident: mail.ident, tags: tags});
+ };
+
+ this.attachTagCompletion = function(mail) {
+ this.tagFilter = function (parsedResult) {
+ var filtered = _.filter(parsedResult, function (tag) {return ! _.contains(mail.tags, tag.name); });
+ return _.map(filtered, function(tag) { return {value: Handlebars.Utils.escapeExpression(tag.name)}; });
+ };
+
+ this.tagCompleter = new Bloodhound({
+ datumTokenizer: function(d) { return [d.value]; },
+ queryTokenizer: function(q) { return [q.trim()]; },
+ remote: {
+ url: '/tags?skipDefaultTags=true&q=%QUERY',
+ filter: this.tagFilter
+ }
+ });
+
+ this.tagCompleter.initialize();
+
+ this.select('newTagInput').typeahead({
+ hint: true,
+ highlight: true,
+ minLength: 1
+ }, {
+ source: this.tagCompleter.ttAdapter()
+ });
+ };
+
+ this.createNewTag = function () {
+ var tagsCopy = this.attr.mail.tags.slice();
+ tagsCopy.push(this.select('newTagInput').val());
+ this.tagCompleter.clear();
+ this.tagCompleter.clearPrefetchCache();
+ this.tagCompleter.clearRemoteCache();
+ this.updateTags(this.attr.mail, _.uniq(tagsCopy));
+ };
+
+ this.after('displayMail', function () {
+ this.on(this.select('newTagInput'), 'typeahead:selected typeahead:autocompleted', this.createNewTag);
+ });
+ }
+
+ return withMailTagging;
+ }
+);
diff --git a/web-ui/public/js/monkey_patching/all.js b/web-ui/public/js/monkey_patching/all.js
new file mode 100644
index 00000000..2c29c9a1
--- /dev/null
+++ b/web-ui/public/js/monkey_patching/all.js
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+require(['js/monkey_patching/array', 'js/monkey_patching/post_message'], function () {});
diff --git a/web-ui/public/js/monkey_patching/array.js b/web-ui/public/js/monkey_patching/array.js
new file mode 100644
index 00000000..d0ccc4b8
--- /dev/null
+++ b/web-ui/public/js/monkey_patching/array.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+(function () {
+ 'use strict';
+
+ // Array Remove - By John Resig (MIT Licensed)
+ Array.prototype.remove = function (from, to) {
+ var rest = this.slice((to || from) + 1 || this.length);
+ this.length = from < 0 ? this.length + from : from;
+ return this.push.apply(this, rest);
+ };
+
+}());
diff --git a/web-ui/public/js/monkey_patching/post_message.js b/web-ui/public/js/monkey_patching/post_message.js
new file mode 100644
index 00000000..363ce581
--- /dev/null
+++ b/web-ui/public/js/monkey_patching/post_message.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+/*
+ * origin window.postMessage fails with non serializable objects, so we fallback to console.log to do the job
+ */
+(function () {
+ 'use strict';
+
+ var originalPostMessage = window.postMessage;
+ window.postMessage = function(a, b) {
+ try {
+ originalPostMessage(a, b);
+ } catch (e) {
+ console.log(a, b);
+ }
+ };
+
+}());
diff --git a/web-ui/public/js/page/default.js b/web-ui/public/js/page/default.js
new file mode 100644
index 00000000..5d52e06c
--- /dev/null
+++ b/web-ui/public/js/page/default.js
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'mail_view/ui/compose_box',
+ 'mail_list_actions/ui/mail_list_actions',
+ 'user_alerts/ui/user_alerts',
+ 'mail_list/ui/mail_list',
+ 'mail_view/ui/no_message_selected_pane',
+ 'mail_view/ui/no_mails_available_pane',
+ 'mail_view/ui/mail_view',
+ 'mail_view/ui/mail_actions',
+ 'mail_view/ui/reply_section',
+ 'mail_view/data/mail_sender',
+ 'services/mail_service',
+ 'services/delete_service',
+ 'services/recover_service',
+ 'tags/ui/tag_list',
+ 'tags/data/tags',
+ 'page/router',
+ 'dispatchers/right_pane_dispatcher',
+ 'dispatchers/middle_pane_dispatcher',
+ 'dispatchers/left_pane_dispatcher',
+ 'search/search_trigger',
+ 'search/results_highlighter',
+ 'foundation/off_canvas',
+ 'page/pane_contract_expand',
+ 'views/i18n',
+ 'views/recipientListFormatter',
+ 'flight/lib/logger',
+ 'user_settings/data/user_settings',
+ 'user_settings/ui/user_settings_icon',
+ 'page/logout',
+ 'page/logout_shortcut',
+ 'feedback/feedback_trigger',
+ 'mail_view/ui/feedback_box',
+ 'mail_view/data/feedback_sender',
+ 'page/version',
+ 'page/unread_count_title',
+ 'page/pix_logo',
+ 'helpers/browser'
+ ],
+
+ function (
+ composeBox,
+ mailListActions,
+ userAlerts,
+ mailList,
+ noMessageSelectedPane,
+ noMailsAvailablePane,
+ mailView,
+ mailViewActions,
+ replyButton,
+ mailSender,
+ mailService,
+ deleteService,
+ recoverService,
+ tagList,
+ tags,
+ router,
+ rightPaneDispatcher,
+ middlePaneDispatcher,
+ leftPaneDispatcher,
+ searchTrigger,
+ resultsHighlighter,
+ offCanvas,
+ paneContractExpand,
+ viewI18n,
+ recipientListFormatter,
+ withLogging,
+ userSettings,
+ userSettingsIcon,
+ logout,
+ logoutShortcut,
+ feedback,
+ feedbackBox,
+ feedbackSender,
+ version,
+ unreadCountTitle,
+ pixLogo,
+ browser) {
+
+ 'use strict';
+ function initialize(path) {
+ viewI18n.init(path + '/static/');
+ viewI18n.loaded(function() {
+ paneContractExpand.attachTo(document);
+
+ userAlerts.attachTo('#user-alerts');
+
+ mailList.attachTo('#mail-list');
+ mailListActions.attachTo('#list-actions');
+
+ searchTrigger.attachTo('#search-trigger');
+ resultsHighlighter.attachTo(document);
+
+ mailSender.attachTo(document);
+
+ mailService.attachTo(document);
+ deleteService.attachTo(document);
+ recoverService.attachTo(document);
+
+ tags.attachTo(document);
+ tagList.attachTo('#tag-list');
+
+ router.attachTo(document);
+
+ rightPaneDispatcher.attachTo(document);
+ middlePaneDispatcher.attachTo(document);
+ leftPaneDispatcher.attachTo(document);
+
+ offCanvas.attachTo(document);
+ userSettings.attachTo(document);
+ userSettingsIcon.attachTo('#user-settings-icon');
+ logout.attachTo('#logout');
+ logoutShortcut.attachTo('#logout-shortcut');
+ version.attachTo('.version');
+
+ feedback.attachTo('#feedback');
+ feedbackSender.attachTo(document);
+
+ unreadCountTitle.attachTo(document);
+
+ pixLogo.attachTo(document);
+
+ $.ajaxSetup({headers: {'X-XSRF-TOKEN': browser.getCookie('XSRF-TOKEN')}});
+ });
+ }
+
+ return initialize;
+ }
+);
diff --git a/web-ui/public/js/page/events.js b/web-ui/public/js/page/events.js
new file mode 100644
index 00000000..68a6aad1
--- /dev/null
+++ b/web-ui/public/js/page/events.js
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(function () {
+ 'use strict';
+
+ var events = {
+ router: {
+ pushState: 'router:pushState'
+ },
+ ui: {
+ sendbutton: {
+ enable: 'ui:sendbutton:enable'
+ },
+ middlePane: {
+ expand: 'ui:middlePane:expand',
+ contract: 'ui:middlePane:contract'
+ },
+ userAlerts: {
+ displayMessage: 'ui:userAlerts:displayMessage'
+ },
+ tag: {
+ selected: 'ui:tagSelected',
+ select: 'ui:tagSelect',
+ },
+ tags: {
+ loaded: 'ui:tagsLoaded'
+ },
+ tagList: {
+ load: 'ui:tagList:load'
+ },
+ mails: {
+ refresh: 'ui:mails:refresh',
+ fetchByTag: 'ui:mails:fetchByTag',
+ cleanSelected: 'ui:mails:cleanSelected',
+ checkAll: 'ui:mails:checkAll',
+ uncheckAll: 'ui:mails:uncheckAll',
+ hasMailsChecked: 'ui:mails:hasMailsChecked'
+ },
+ mail: {
+ open: 'ui:mail:open',
+ updateSelected: 'ui:mail:updateSelected',
+ delete: 'ui:mail:delete',
+ deleteMany: 'ui:mail:deleteMany',
+ recoverMany: 'ui:mail:recoverMany',
+ archiveMany: 'ui:mail:archiveMany',
+ wantChecked: 'ui:mail:wantChecked',
+ hereChecked: 'ui:mail:hereChecked',
+ checked: 'ui:mail:checked',
+ discard: 'ui:mail:discard',
+ unchecked: 'ui:mail:unchecked',
+ changedSinceLastSave: 'ui:mail:changedSinceLastSave',
+ send: 'ui:mail:send',
+ recipientsUpdated: 'ui:mail:recipientsUpdated'
+ },
+ page: {
+ previous: 'ui:page:previous',
+ next: 'ui:page:next',
+ changed: 'ui:page:changed',
+ spinLogo: 'ui:page:spinLogo',
+ stopSpinningLogo: 'ui:page:stopSpinningLogo'
+ },
+ composeBox: {
+ newMessage: 'ui:composeBox:newMessage',
+ newReply: 'ui:composeBox:newReply',
+ trashReply: 'ui:composeBox:trashReply',
+ requestCancelReply: 'ui:composeBox:requestCancelReply'
+ },
+ replyBox: {
+ showReply: 'ui:replyBox:showReply',
+ showReplyAll: 'ui:replyBox:showReplyAll',
+ showReplyContainer: 'ui:replyBox:showReplyContainer',
+ },
+ recipients: {
+ entered: 'ui:recipients:entered',
+ enteredInvalid: 'ui:recipients:enteredInvalid',
+ updated: 'ui:recipients:updated',
+ editRecipient: 'ui:recipients:editRecipient',
+ deleteRecipient: 'ui:recipients:deleteRecipient',
+ deleteLast: 'ui:recipients:deleteLast',
+ selectLast: 'ui:recipients:selectLast',
+ unselectAll: 'ui:recipients:unselectAll',
+ addressesExist: 'ui:recipients:addressesExist',
+ inputFieldHasCharacters: 'ui:recipients:inputFieldHasCharacters',
+ inputFieldIsEmpty: 'ui:recipients:inputFieldIsEmpty',
+ doCompleteInput: 'ui:recipients:doCompleteInput',
+ doCompleteRecipients: 'ui:recipients:doCompleteRecipients',
+ clickToEdit: 'ui:recipients:clickToEdit'
+ },
+ userSettingsBox: {
+ toggle: 'ui:userSettingsBox:toggle'
+ }
+ },
+ search: {
+ perform: 'search:perform',
+ results: 'search:results',
+ empty: 'search:empty',
+ highlightResults: 'search:highlightResults',
+ resetHighlight: 'search:resetHighlight'
+ },
+ feedback: {
+ submit: 'feedback:submit',
+ submitted: 'feedback:submitted'
+ },
+ userSettings: {
+ here: 'userSettings:here',
+ getInfo: 'userSettings:getInfo',
+ destroyPopup: 'userSettings:destroyPopup'
+ },
+ mail: {
+ here: 'mail:here',
+ want: 'mail:want',
+ display: 'mail:display',
+ highlightMailContent: 'mail:highlightMailContent',
+ send: 'mail:send',
+ send_failed: 'mail:send_failed',
+ sent: 'mail:sent',
+ read: 'mail:read',
+ unread: 'mail:unread',
+ delete: 'mail:delete',
+ deleteMany: 'mail:deleteMany',
+ archiveMany: 'mail:archiveMany',
+ recoverMany: 'mail:recoverMany',
+ deleted: 'mail:deleted',
+ saveDraft: 'draft:save',
+ draftSaved: 'draft:saved',
+ draftReply: {
+ want: 'mail:draftReply:want',
+ here: 'mail:draftReply:here',
+ notFound: 'mail:draftReply:notFound'
+ },
+ notFound: 'mail:notFound',
+ save: 'mail:saved',
+ tags: {
+ update: 'mail:tags:update',
+ updated: 'mail:tags:updated'
+ },
+ uploadedAttachment: 'mail:uploaded:attachment',
+ uploadingAttachment: 'mail:uploading:attachment',
+ startUploadAttachment: 'mail:start:upload:attachment',
+ failedUploadAttachment: 'mail:failed:upload:attachment',
+ appendAttachment: 'mail:append:attachment',
+ resetAttachments: 'mail:reset:attachments',
+ removeAttachment: 'mail:remove:attachment'
+ },
+ mails: {
+ available: 'mails:available',
+ availableForRefresh: 'mails:available:refresh',
+ teardown: 'mails:teardown'
+ },
+ tags: {
+ want: 'tags:want',
+ received: 'tags:received',
+ teardown: 'tags:teardown',
+ shortcuts: {
+ teardown: 'tags:shortcuts:teardown'
+ }
+ },
+ route: {
+ toUrl: 'route:toUrl'
+ },
+
+ components: {
+ composeBox: {
+ open: 'components:composeBox:open',
+ close: 'components:composeBox:close'
+ },
+ mailPane: {
+ open: 'components:mailPane:open',
+ close: 'components:mailPane:close'
+ },
+ mailView: {
+ show: 'components:mailView:show',
+ close: 'components:mailView:close'
+ },
+ replySection: {
+ initialize: 'components:replySection:initialize',
+ close: 'components:replySection:close'
+ },
+ noMessageSelectedPane: {
+ open: 'components:noMessageSelectedPane:open',
+ close: 'components:noMessageSelectedPane:close'
+ }
+ },
+
+ dispatchers: {
+ rightPane: {
+ openComposeBox: 'dispatchers:rightPane:openComposeBox',
+ openFeedbackBox: 'dispatchers:rightPane:openFeedbackBox',
+ openNoMessageSelected: 'dispatchers:rightPane:openNoMessageSelected',
+ openNoMessageSelectedWithoutPushState: 'dispatchers:rightPane:openNoMessageSelectedWithoutPushState',
+ refreshMailList: 'dispatchers:rightPane:refreshMailList',
+ openDraft: 'dispatchers:rightPane:openDraft',
+ selectTag: 'dispatchers:rightPane:selectTag',
+ clear: 'dispatchers:rightPane:clear'
+ },
+ middlePane: {
+ refreshMailList: 'dispatchers:middlePane:refreshMailList',
+ cleanSelected: 'dispatchers:middlePane:unselect',
+ resetScroll: 'dispatchers:middlePane:resetScroll'
+ },
+ tags: {
+ refreshTagList: 'dispatchers:tag:refresh'
+ }
+ }
+ };
+
+ return events;
+});
diff --git a/web-ui/public/js/page/logout.js b/web-ui/public/js/page/logout.js
new file mode 100644
index 00000000..81b57db2
--- /dev/null
+++ b/web-ui/public/js/page/logout.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'features', 'views/templates', 'helpers/browser'],
+ function (defineComponent, features, templates, browser) {
+ 'use strict';
+
+ return defineComponent(function () {
+
+ this.defaultAttrs({form: '#logout-form'});
+
+ this.render = function () {
+ var logoutHTML = templates.page.logout({ logout_url: features.getLogoutUrl(),
+ csrf_token: browser.getCookie('XSRF-TOKEN')});
+ this.$node.html(logoutHTML);
+ };
+
+ this.logout = function(){
+ this.select('form').submit();
+ };
+
+ this.after('initialize', function () {
+ if (features.isLogoutEnabled()) {
+ this.render();
+ this.on(this.$node, 'click', this.logout);
+ }
+ });
+
+ });
+});
diff --git a/web-ui/public/js/page/logout_shortcut.js b/web-ui/public/js/page/logout_shortcut.js
new file mode 100644
index 00000000..10a69c7d
--- /dev/null
+++ b/web-ui/public/js/page/logout_shortcut.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'features', 'views/templates'], function (defineComponent, features, templates) {
+ 'use strict';
+
+ return defineComponent(function () {
+
+ this.render = function () {
+ if (features.isLogoutEnabled()) {
+ var logoutShortcutHTML = templates.page.logoutShortcut();
+ this.$node.html(logoutShortcutHTML);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ });
+ });
+});
diff --git a/web-ui/public/js/page/pane_contract_expand.js b/web-ui/public/js/page/pane_contract_expand.js
new file mode 100644
index 00000000..9bb435c4
--- /dev/null
+++ b/web-ui/public/js/page/pane_contract_expand.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['flight/lib/component', 'page/events'], function (describeComponent, events) {
+ 'use strict';
+
+ return describeComponent(paneContractExpand);
+
+ function paneContractExpand() {
+ this.defaultAttrs({
+ RIGHT_PANE_EXPAND_CLASSES: 'small-7 medium-7 large-7 columns',
+ RIGHT_PANE_CONTRACT_CLASSES: 'small-7 medium-4 large-4 columns',
+ MIDDLE_PANE_EXPAND_CLASSES: 'small-5 medium-8 large-8 columns no-padding',
+ MIDDLE_PANE_CONTRACT_CLASSES: 'small-5 medium-5 large-5 columns no-padding'
+ });
+
+ this.expandMiddlePaneContractRightPane = function () {
+ $('#middle-pane-container').attr('class', this.attr.MIDDLE_PANE_EXPAND_CLASSES);
+ $('#right-pane').attr('class', this.attr.RIGHT_PANE_CONTRACT_CLASSES);
+ };
+
+ this.contractMiddlePaneExpandRightPane = function () {
+ $('#middle-pane-container').attr('class', this.attr.MIDDLE_PANE_CONTRACT_CLASSES);
+ $('#right-pane').attr('class', this.attr.RIGHT_PANE_EXPAND_CLASSES);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mail.open, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openComposeBox, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openDraft, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openFeedbackBox, this.contractMiddlePaneExpandRightPane);
+ this.on(document, events.dispatchers.rightPane.openNoMessageSelected, this.expandMiddlePaneContractRightPane);
+ this.expandMiddlePaneContractRightPane();
+ });
+
+ }
+});
diff --git a/web-ui/public/js/page/pix_logo.js b/web-ui/public/js/page/pix_logo.js
new file mode 100644
index 00000000..ad17f3be
--- /dev/null
+++ b/web-ui/public/js/page/pix_logo.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'page/events'
+ ],
+
+ function(defineComponent, events) {
+ 'use strict';
+
+ return defineComponent(pixLogo);
+
+ function pixLogo() {
+ this.turnAnimationOn = function () {
+ $('.logo-part-animation-off').attr('class', 'logo-part-animation-on');
+ };
+
+ this.turnAnimationOff = function () {
+ setTimeout(function(){
+ $('.logo-part-animation-on').attr('class', 'logo-part-animation-off');
+ }, 600);
+ };
+
+ this.triggerSpinLogo = function (ev, data) {
+ this.trigger(document, events.ui.page.spinLogo);
+ };
+
+ this.triggerStopSpinningLogo = function(ev, data) {
+ this.trigger(document, events.ui.page.stopSpinningLogo);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.page.spinLogo, this.turnAnimationOn);
+ this.on(document, events.ui.page.stopSpinningLogo, this.turnAnimationOff);
+
+ this.on(document, events.ui.tag.select, this.triggerSpinLogo);
+ this.on(document, events.mails.available, this.triggerStopSpinningLogo);
+ this.on(document, events.mail.saveDraft, this.triggerSpinLogo);
+ this.on(document, events.mail.draftSaved, this.triggerStopSpinningLogo);
+ this.on(document, events.ui.mail.open, this.triggerSpinLogo);
+ this.on(document, events.dispatchers.rightPane.openDraft, this.triggerSpinLogo);
+ this.on(document, events.search.perform, this.triggerSpinLogo);
+ this.on(document, events.mail.want, this.triggerStopSpinningLogo);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/page/router.js b/web-ui/public/js/page/router.js
new file mode 100644
index 00000000..ce0d7d04
--- /dev/null
+++ b/web-ui/public/js/page/router.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'page/events', 'page/router/url_params'], function (defineComponent, events, urlParams) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ history: window.history
+ });
+
+ function createHash(data) {
+ var hash = '/#/' + data.tag;
+ if (!_.isUndefined(data.mailIdent)) {
+ hash += '/mail/' + data.mailIdent;
+ }
+ return hash;
+ }
+
+ function createState(data, previousState) {
+ return {
+ tag: data.tag || (previousState && previousState.tag) || urlParams.defaultTag(),
+ mailIdent: data.mailIdent,
+ query: data.query,
+ isDisplayNoMessageSelected: !!data.isDisplayNoMessageSelected
+ };
+ }
+
+ this.pushState = function (ev, data) {
+ if (!data.fromPopState) {
+ var nextState = createState(data, this.attr.history.state);
+ this.attr.history.pushState(nextState, '', createHash(nextState));
+ }
+ };
+
+ this.popState = function (ev) {
+ var state = ev.state || {};
+
+ this.trigger(document, events.ui.tag.select, {
+ tag: state.tag || urlParams.getTag(),
+ mailIdent: state.mailIdent,
+ fromPopState: true
+ });
+
+ if (ev.state.isDisplayNoMessageSelected) {
+ this.trigger(document, events.dispatchers.rightPane.openNoMessageSelectedWithoutPushState);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.router.pushState, this.pushState);
+ this.on(document, events.ui.tag.select, this.pushState);
+ this.on(document, events.search.perform, this.pushState);
+ this.on(document, events.search.empty, this.pushState);
+ window.onpopstate = this.popState.bind(this);
+ });
+ });
+});
diff --git a/web-ui/public/js/page/router/url_params.js b/web-ui/public/js/page/router/url_params.js
new file mode 100644
index 00000000..4fa11c6d
--- /dev/null
+++ b/web-ui/public/js/page/router/url_params.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define([], function () {
+ 'use strict';
+
+ function defaultTag() {
+ return 'inbox';
+ }
+
+ function getDocumentHash() {
+ return document.location.hash.replace(/\/$/, '');
+ }
+
+ function hashTag(hash) {
+ if (hasMailIdent(hash)) {
+ return /\/(.+)\/mail\/[-\w]+$/.exec(getDocumentHash())[1];
+ }
+ return hash.substring(2);
+ }
+
+
+ function getTag() {
+ if (document.location.hash !== '') {
+ return hashTag(getDocumentHash());
+ }
+ return defaultTag();
+ }
+
+ function hasMailIdent() {
+ return getDocumentHash().match(/mail\/[-\w]+$/);
+ }
+
+ function getMailIdent() {
+ return /mail\/([-\w]+)$/.exec(getDocumentHash())[1];
+ }
+
+ return {
+ getTag: getTag,
+ hasMailIdent: hasMailIdent,
+ getMailIdent: getMailIdent,
+ defaultTag: defaultTag
+ };
+});
diff --git a/web-ui/public/js/page/unread_count_title.js b/web-ui/public/js/page/unread_count_title.js
new file mode 100644
index 00000000..89dcd47d
--- /dev/null
+++ b/web-ui/public/js/page/unread_count_title.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+define(
+ [
+ 'flight/lib/component',
+ 'page/events',
+ ],
+
+ function (defineComponent, events) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.getTitleText = function () {
+ return document.title;
+ };
+
+ this.updateCount = function (ev, data) {
+ var unread = data.mails.filter(function (mail) {
+ return mail.status.indexOf('read') === -1;
+ }).length;
+
+ var tag = this.toTitleCase(data.tag);
+ var counter = unread > 0 ? ' (' + unread + ') - ' : ' - ';
+ document.title = tag + counter + this.rawTitle;
+ };
+
+ this.toTitleCase = function (str) {
+ return str.replace(/\b\w/g, function (txt) { return txt.toUpperCase(); });
+ };
+
+ this.after('initialize', function () {
+ this.rawTitle = document.title;
+ this.on(document, events.mails.available, this.updateCount);
+ });
+
+ });
+});
diff --git a/web-ui/public/js/page/version.js b/web-ui/public/js/page/version.js
new file mode 100644
index 00000000..9fd5e629
--- /dev/null
+++ b/web-ui/public/js/page/version.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'views/templates', 'helpers/view_helper'], function (defineComponent, templates, viewHelper) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ 'sinceDate': '#version-date'
+ });
+
+ this.render = function () {
+ this.$node.html(templates.page.version());
+ this.renderCommitDate();
+ };
+
+ this.renderCommitDate = function(){
+ var since = this.select('sinceDate').attr('data-since'),
+ commitDate = viewHelper.sinceDate(since);
+ this.select('sinceDate').html(commitDate + ' ago');
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ });
+
+ });
+});
diff --git a/web-ui/public/js/sandbox.js b/web-ui/public/js/sandbox.js
new file mode 100644
index 00000000..33b16ea4
--- /dev/null
+++ b/web-ui/public/js/sandbox.js
@@ -0,0 +1,11 @@
+(function () {
+ 'use strict';
+
+ window.onmessage = function (e) {
+ if (e.data.html) {
+ document.body.innerHTML = e.data.html;
+ var mainWindow = e.source;
+ mainWindow.postMessage('data ok', e.origin);
+ }
+ };
+})();
diff --git a/web-ui/public/js/search/results_highlighter.js b/web-ui/public/js/search/results_highlighter.js
new file mode 100644
index 00000000..831be0cd
--- /dev/null
+++ b/web-ui/public/js/search/results_highlighter.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'page/events'
+ ], function (defineComponent, events) {
+
+ 'use strict';
+
+ return defineComponent(resultsHighlighter);
+
+ function resultsHighlighter(){
+ this.defaultAttrs({
+ keywords: []
+ });
+
+ this.getKeywordsSearch = function (event, data) {
+ this.attr.keywords = data.query.split(' ').map(function(keyword) {
+ return keyword.toLowerCase();
+ });
+ };
+
+ this.highlightResults = function (event, data) {
+ var domIdent = data.where;
+ if(this.attr.keywords) {
+ _.each(this.attr.keywords, function (keyword) {
+ keyword = escapeRegExp(keyword);
+ $(domIdent).highlightRegex(new RegExp(keyword, 'i'), {
+ tagType: 'em',
+ className: 'search-highlight'
+ });
+ });
+ }
+ };
+
+ this.clearHighlights = function (event, data) {
+ this.attr.keywords = [];
+ _.each($('em.search-highlight'), function(highlighted) {
+ var jqueryHighlighted = $(highlighted);
+ var text = jqueryHighlighted.text();
+ jqueryHighlighted.replaceWith(text);
+ });
+ };
+
+ this.highlightString = function (string) {
+ _.each(this.attr.keywords, function (keyword) {
+ keyword = escapeRegExp(keyword);
+ var regex = new RegExp('(' + keyword + ')', 'ig');
+ string = string.replace(regex, '<em class="search-highlight">$1</em>');
+ });
+ return string;
+ };
+
+ /*
+ * Alter data.mail.textPlainBody to highlight each of this.attr.keywords
+ * and pass it back to the mail_view when done
+ */
+ this.highlightMailContent = function(ev, data){
+ var mail = data.mail;
+ mail.textPlainBody = this.highlightString(mail.textPlainBody);
+ this.trigger(document, events.mail.display, data);
+ };
+
+ /*
+ * Escapes the special charaters used regular expressions that
+ * would cause problems with strings in the RegExp constructor
+ */
+ function escapeRegExp(string){
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ }
+
+ this.after('initialize', function () {
+ this.on(document, events.search.perform, this.getKeywordsSearch);
+ this.on(document, events.ui.tag.select, this.clearHighlights);
+ this.on(document, events.search.resetHighlight, this.clearHighlights);
+
+ this.on(document, events.search.highlightResults, this.highlightResults);
+ this.on(document, events.mail.highlightMailContent, this.highlightMailContent);
+ });
+ }
+});
diff --git a/web-ui/public/js/search/search_trigger.js b/web-ui/public/js/search/search_trigger.js
new file mode 100644
index 00000000..2aff027c
--- /dev/null
+++ b/web-ui/public/js/search/search_trigger.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events',
+ 'views/i18n'
+ ], function (defineComponent, templates, events, i18n) {
+
+ 'use strict';
+
+ return defineComponent(searchTrigger);
+
+ function searchTrigger() {
+ this.defaultAttrs({
+ input: 'input[type=search]',
+ form: 'form',
+ searchResultsPrefix: 'search-results-for'
+ });
+
+ this.render = function() {
+ this.$node.html(templates.search.trigger());
+ };
+
+ this.search = function(ev, data) {
+ this.trigger(document, events.search.resetHighlight);
+ ev.preventDefault();
+ var input = this.select('input');
+ var value = input.val();
+ input.blur();
+ if(!_.isEmpty(value)){
+ this.trigger(document, events.search.perform, { query: value });
+ } else {
+ this.trigger(document, events.search.empty);
+ }
+ };
+
+ this.clearInput = function() {
+ this.select('input').val('');
+ };
+
+ this.showOnlySearchTerms = function(event){
+ var value = this.select('input').val();
+ var searchTerms = value.slice((i18n.t(this.attr.searchResultsPrefix) + ': ').length);
+ this.select('input').val(searchTerms);
+ };
+
+ this.showSearchTermsAndPlaceHolder = function(event){
+ var value = this.select('input').val();
+ if (value.length > 0){
+ this.select('input').val(i18n.t(this.attr.searchResultsPrefix) + ': ' + value);
+ }
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on(this.select('form'), 'submit', this.search);
+ this.on(this.select('input'), 'focus', this.showOnlySearchTerms);
+ this.on(this.select('input'), 'blur', this.showSearchTermsAndPlaceHolder);
+ this.on(document, events.ui.tag.selected, this.clearInput);
+ this.on(document, events.ui.tag.select, this.clearInput);
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/services/delete_service.js b/web-ui/public/js/services/delete_service.js
new file mode 100644
index 00000000..0dfc1bdb
--- /dev/null
+++ b/web-ui/public/js/services/delete_service.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['flight/lib/component', 'page/events', 'views/i18n'], function (defineComponent, events, i18n) {
+ 'use strict';
+
+ return defineComponent(function() {
+
+ this.successDeleteMessageFor = function(mail) {
+ return mail.isInTrash() ?
+ i18n.t('delete-single') :
+ i18n.t('trash-single');
+ };
+
+ this.successDeleteManyMessageFor = function(mail) {
+ return mail.isInTrash() ?
+ i18n.t('delete-bulk') :
+ i18n.t('trash-bulk');
+ };
+
+ this.deleteEmail = function (event, data) {
+ this.trigger(document, events.mail.delete, {
+ mail: data.mail,
+ successMessage: this.successDeleteMessageFor(data.mail)
+ });
+ };
+
+ this.deleteManyEmails = function (event, data) {
+ var emails = _.values(data.checkedMails),
+ firstEmail = emails[_.first(_.keys(emails))];
+
+ this.trigger(document, events.mail.deleteMany, {
+ mails: emails,
+ successMessage: this.successDeleteManyMessageFor(firstEmail)
+ });
+
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mail.delete, this.deleteEmail);
+ this.on(document, events.ui.mail.deleteMany, this.deleteManyEmails);
+ });
+
+ });
+});
diff --git a/web-ui/public/js/services/mail_service.js b/web-ui/public/js/services/mail_service.js
new file mode 100644
index 00000000..5e4bd4f3
--- /dev/null
+++ b/web-ui/public/js/services/mail_service.js
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/i18n',
+ 'services/model/mail',
+ 'helpers/monitored_ajax',
+ 'page/events',
+ 'features',
+ 'mixins/with_auto_refresh',
+ 'page/router/url_params'
+ ], function (defineComponent, i18n, Mail, monitoredAjax, events, features, withAutoRefresh, urlParams) {
+
+ 'use strict';
+
+ return defineComponent(mailService, withAutoRefresh('refreshMails'));
+
+ function mailService() {
+ var that;
+
+ this.defaultAttrs({
+ mailsResource: '/mails',
+ singleMailResource: '/mail',
+ currentTag: '',
+ lastQuery: '',
+ currentPage: 1,
+ numPages: 1,
+ pageSize: 25
+ });
+
+ this.errorMessage = function (msg) {
+ return function () {
+ that.trigger(document, events.ui.userAlerts.displayMessage, { message: msg });
+ };
+ };
+
+ this.updateTags = function (ev, data) {
+ var ident = data.ident;
+
+ var success = function (data) {
+ this.refreshMails();
+ $(document).trigger(events.mail.tags.updated, { ident: ident, tags: data.tags });
+ $(document).trigger(events.dispatchers.tags.refreshTagList, { skipMailListRefresh: true });
+ };
+
+ var failure = function (resp) {
+ var msg = i18n.t('failed-change-tags');
+ if (resp.status === 403) {
+ msg = i18n.t('invalid-tag-name');
+ }
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: msg });
+ };
+
+ monitoredAjax(this, '/mail/' + ident + '/tags', {
+ type: 'POST',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({newtags: data.tags})
+ }).done(success.bind(this)).fail(failure.bind(this));
+
+ };
+
+ this.readMail = function (ev, data) {
+ var mailIdents;
+ if (data.checkedMails) {
+ mailIdents = _.map(data.checkedMails, function (mail) {
+ return mail.ident;
+ });
+ } else {
+ mailIdents = [data.ident];
+ }
+ monitoredAjax(this, '/mails/read', {
+ type: 'POST',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerMailsRead(data.checkedMails));
+ };
+
+ this.unreadMail = function (ev, data) {
+ var mailIdents;
+ if (data.checkedMails) {
+ mailIdents = _.map(data.checkedMails, function (mail) {
+ return mail.ident;
+ });
+ } else {
+ mailIdents = [data.ident];
+ }
+ monitoredAjax(this, '/mails/unread', {
+ type: 'POST',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerMailsRead(data.checkedMails));
+ };
+
+ this.triggerMailsRead = function (mails) {
+ return _.bind(function () {
+ this.refreshMails();
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }, this);
+ };
+
+ this.triggerDeleted = function (dataToDelete) {
+ return _.bind(function () {
+ var mails = dataToDelete.mails || [dataToDelete.mail];
+
+ this.refreshMails();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: dataToDelete.successMessage});
+ this.trigger(document, events.ui.mails.uncheckAll);
+ this.trigger(document, events.mail.deleted, { mails: mails });
+ }, this);
+ };
+
+ this.triggerRecovered = function (dataToRecover) {
+ return _.bind(function () {
+ var mails = dataToRecover.mails || [dataToRecover.mail];
+
+ this.refreshMails();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.t(dataToRecover.successMessage)});
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }, this);
+ };
+
+ this.triggerArchived = function (dataToArchive) {
+ return _.bind(function (response) {
+ this.refreshMails();
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.t(response.successMessage)});
+ this.trigger(document, events.ui.mails.uncheckAll);
+ }, this);
+ };
+
+ this.archiveManyMails = function(event, dataToArchive) {
+ var mailIdents = _.map(dataToArchive.checkedMails, function (mail) {
+ return mail.ident;
+ });
+ monitoredAjax(this, '/mails/archive', {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerArchived(dataToArchive))
+ .fail(this.errorMessage(i18n.t('failed-archive')));
+ };
+
+ this.deleteMail = function (ev, data) {
+ monitoredAjax(this, '/mail/' + data.mail.ident,
+ {type: 'DELETE'})
+ .done(this.triggerDeleted(data))
+ .fail(this.errorMessage(i18n.t('failed-delete-single')));
+ };
+
+ this.deleteManyMails = function (ev, data) {
+ var dataToDelete = data;
+ var mailIdents = _.map(data.mails, function (mail) {
+ return mail.ident;
+ });
+
+ monitoredAjax(this, '/mails/delete', {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerDeleted(dataToDelete))
+ .fail(this.errorMessage(i18n.t('failed-delete-bulk')));
+ };
+
+ this.recoverManyMails = function (ev, data) {
+ var dataToRecover = data;
+ var mailIdents = _.map(data.mails, function (mail) {
+ return mail.ident;
+ });
+
+ monitoredAjax(this, '/mails/recover', {
+ type: 'POST',
+ dataType: 'json',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify({idents: mailIdents})
+ }).done(this.triggerRecovered(dataToRecover))
+ .fail(this.errorMessage(i18n.t('Could not move emails to inbox')));
+ };
+
+ function compileQuery(data) {
+ var query = 'tag:"' + that.attr.currentTag + '"';
+
+ if (data.tag === 'all') {
+ query = 'in:all';
+ }
+ return query;
+ }
+
+ this.fetchByTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ this.attr.lastQuery = compileQuery(data);
+ this.updateCurrentPageNumber(1);
+
+ this.refreshMails();
+ };
+
+ this.newSearch = function (ev, data) {
+ this.attr.lastQuery = data.query;
+ this.attr.currentTag = 'all';
+ this.refreshMails();
+ };
+
+ this.mailFromJSON = function (mail) {
+ return Mail.create(mail);
+ };
+
+ this.parseMails = function (data) {
+ data.mails = _.map(data.mails, this.mailFromJSON, this);
+
+ return data;
+ };
+
+ function escaped(s) {
+ return encodeURIComponent(s);
+ }
+
+ this.excludeTrashedEmailsForDraftsAndSent = function (query) {
+ if (query === 'tag:"drafts"' || query === 'tag:"sent"') {
+ return query + ' -in:"trash"';
+ }
+ return query;
+ };
+
+ this.refreshMails = function () {
+ var url = this.attr.mailsResource + '?q=' + escaped(this.attr.lastQuery) + '&p=' + this.attr.currentPage + '&w=' + this.attr.pageSize;
+
+ this.attr.lastQuery = this.excludeTrashedEmailsForDraftsAndSent(this.attr.lastQuery);
+
+ monitoredAjax(this, url, { dataType: 'json' })
+ .done(function (data) {
+ this.attr.numPages = Math.ceil(data.stats.total / this.attr.pageSize);
+ this.trigger(document, events.mails.available, _.merge({tag: this.attr.currentTag, forSearch: this.attr.lastQuery }, this.parseMails(data)));
+ }.bind(this))
+ .fail(function () {
+ this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.t('failed-fetch-messages'), class: 'error' });
+ }.bind(this));
+ };
+
+ function createSingleMailUrl(mailsResource, ident) {
+ return mailsResource + '/' + ident;
+ }
+
+ this.fetchSingle = function (event, data) {
+ var fetchUrl = createSingleMailUrl(this.attr.singleMailResource, data.mail);
+
+ monitoredAjax(this, fetchUrl, { dataType: 'json' })
+ .done(function (mail) {
+ if (_.isNull(mail)) {
+ this.trigger(data.caller, events.mail.notFound);
+ return;
+ }
+
+ this.trigger(data.caller, events.mail.here, { mail: this.mailFromJSON(mail) });
+ }.bind(this));
+ };
+
+ this.previousPage = function () {
+ if (this.attr.currentPage > 1) {
+ this.updateCurrentPageNumber(this.attr.currentPage - 1);
+ this.refreshMails();
+ }
+ };
+
+ this.nextPage = function () {
+ if (this.attr.currentPage < (this.attr.numPages)) {
+ this.updateCurrentPageNumber(this.attr.currentPage + 1);
+ this.refreshMails();
+ }
+ };
+
+ this.updateCurrentPageNumber = function (newCurrentPage) {
+ this.attr.currentPage = newCurrentPage;
+ this.trigger(document, events.ui.page.changed, {
+ currentPage: this.attr.currentPage,
+ numPages: this.attr.numPages
+ });
+ };
+
+ this.wantDraftReplyForMail = function (ev, data) {
+ if (!features.isEnabled('draftReply')) {
+ this.trigger(document, events.mail.draftReply.notFound);
+ return;
+ }
+
+ monitoredAjax(this, '/draft_reply_for/' + data.ident, { dataType: 'json' })
+ .done(function (mail) {
+ if (_.isNull(mail)) {
+ this.trigger(document, events.mail.draftReply.notFound);
+ return;
+ }
+ this.trigger(document, events.mail.draftReply.here, { mail: this.mailFromJSON(mail) });
+ }.bind(this));
+ };
+
+ this.after('initialize', function () {
+ that = this;
+
+ if (features.isEnabled('tags')) {
+ this.on(events.mail.tags.update, this.updateTags);
+ }
+
+ this.on(document, events.mail.draftReply.want, this.wantDraftReplyForMail);
+ this.on(document, events.mail.want, this.fetchSingle);
+ this.on(document, events.mail.read, this.readMail);
+ this.on(document, events.mail.unread, this.unreadMail);
+ this.on(document, events.mail.delete, this.deleteMail);
+ this.on(document, events.mail.deleteMany, this.deleteManyMails);
+ this.on(document, events.mail.recoverMany, this.recoverManyMails);
+ this.on(document, events.mail.archiveMany, this.archiveManyMails);
+ this.on(document, events.search.perform, this.newSearch);
+ this.on(document, events.ui.tag.selected, this.fetchByTag);
+ this.on(document, events.ui.tag.select, this.fetchByTag);
+ this.on(document, events.ui.mails.refresh, this.refreshMails);
+ this.on(document, events.ui.page.previous, this.previousPage);
+ this.on(document, events.ui.page.next, this.nextPage);
+
+ this.fetchByTag(null, {tag: urlParams.getTag()});
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/services/model/mail.js b/web-ui/public/js/services/model/mail.js
new file mode 100644
index 00000000..64a10c1c
--- /dev/null
+++ b/web-ui/public/js/services/model/mail.js
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['helpers/contenttype'], function (contentType) {
+ 'use strict';
+ function isSentMail() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'SENT';
+ }
+
+ function isDraftMail() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'DRAFTS';
+ }
+
+ function isInTrash() {
+ return _.has(this, 'mailbox') && this.mailbox.toUpperCase() === 'TRASH';
+ }
+
+ function setDraftReplyFor(ident) {
+ this.draft_reply_for = ident;
+ }
+
+ function replyToAddress() {
+ return {
+ to: [this.replying.single],
+ cc: []
+ };
+ }
+
+ function replyToAllAddress() {
+ return {
+ to: this.replying.all['to-field'],
+ cc: this.replying.all['cc-field']
+ };
+ }
+
+ function getHeadersFromMailPart (rawBody) {
+ var lines, headerLines, endOfHeaders, headers;
+
+ lines = rawBody.split('\n');
+ endOfHeaders = _.indexOf(lines, '');
+ headerLines = lines.slice(0, endOfHeaders);
+
+ headers = _.map(headerLines, function (headerLine) {
+ return _.map(headerLine.split(':'), function(elem){return elem.trim();});
+ });
+
+ return _.object(headers);
+ }
+
+ function getBodyFromMailPart (rawBody) {
+ var lines, endOfHeaders;
+
+ lines = rawBody.split('\n');
+ endOfHeaders = _.indexOf(lines, '');
+
+ return lines.slice(endOfHeaders + 1).join('\n');
+ }
+
+ function parseWithHeaders(rawBody) {
+ return {headers: getHeadersFromMailPart(rawBody), body: getBodyFromMailPart(rawBody)};
+ }
+
+ function getMailMultiParts () {
+ var mediaType = this.getMailMediaType();
+ var boundary = '--' + mediaType.params.boundary + '\n';
+ var finalBoundary = '--' + mediaType.params.boundary + '--';
+
+ var bodyParts = this.body.split(finalBoundary)[0].split(boundary);
+
+ bodyParts = _.reject(bodyParts, function(bodyPart) { return _.isEmpty(bodyPart.trim()); });
+
+ return _.map(bodyParts, parseWithHeaders);
+ }
+
+ function getMailMediaType () {
+ return new contentType.MediaType(this.header.content_type);
+ }
+
+ function isMailMultipartAlternative () {
+ return this.getMailMediaType().type === 'multipart/alternative';
+ }
+
+ function availableBodyPartsContentType () {
+ var bodyParts = this.getMailMultiParts();
+
+ return _.pluck(_.pluck(bodyParts, 'headers'), 'Content-Type');
+ }
+
+ function getMailPartByContentType (contentType) {
+ var bodyParts = this.getMailMultiParts();
+
+ return _.findWhere(bodyParts, {headers: {'Content-Type': contentType}});
+ }
+
+ return {
+ create: function (mail) {
+ if (!mail) { return; }
+
+ mail.isSentMail = isSentMail;
+ mail.isDraftMail = isDraftMail;
+ mail.isInTrash = isInTrash;
+ mail.setDraftReplyFor = setDraftReplyFor;
+ mail.replyToAddress = replyToAddress;
+ mail.replyToAllAddress = replyToAllAddress;
+ mail.getMailMediaType = getMailMediaType;
+ mail.isMailMultipartAlternative = isMailMultipartAlternative;
+ mail.getMailMultiParts = getMailMultiParts;
+ mail.availableBodyPartsContentType = availableBodyPartsContentType;
+ mail.getMailPartByContentType = getMailPartByContentType;
+ return mail;
+ }
+ };
+});
diff --git a/web-ui/public/js/services/recover_service.js b/web-ui/public/js/services/recover_service.js
new file mode 100644
index 00000000..d7d9cdc9
--- /dev/null
+++ b/web-ui/public/js/services/recover_service.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['flight/lib/component', 'page/events', 'views/i18n'], function (defineComponent, events, i18n) {
+ 'use strict';
+
+ return defineComponent(function() {
+
+ this.recoverManyEmails = function (event, data) {
+ var emails = _.values(data.checkedMails);
+
+ this.trigger(document, events.mail.recoverMany, {
+ mails: emails,
+ successMessage: i18n.t('Your messages were moved to inbox!')
+ });
+
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.mail.recoverMany, this.recoverManyEmails);
+ });
+
+ });
+});
diff --git a/web-ui/public/js/style_guide/main.js b/web-ui/public/js/style_guide/main.js
new file mode 100644
index 00000000..32c213cf
--- /dev/null
+++ b/web-ui/public/js/style_guide/main.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+$(document).ready(function(){
+ 'use strict';
+ $('a[href*=#]').click(function() {
+ if (location.pathname.replace(/^\//,'') === this.pathname.replace(/^\//,'') &&
+ location.hostname === this.hostname) {
+ var $target = $(this.hash);
+ $target = $target.length && $target ||
+ $('[name=' + this.hash.slice(1) +']');
+ if ($target.length) {
+ var targetOffset = $target.offset().top;
+ $('html,body')
+ .animate({scrollTop: targetOffset}, 500);
+ return false;
+ }
+ }
+ });
+});
diff --git a/web-ui/public/js/tags/data/tags.js b/web-ui/public/js/tags/data/tags.js
new file mode 100644
index 00000000..31703b2a
--- /dev/null
+++ b/web-ui/public/js/tags/data/tags.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['flight/lib/component', 'page/events', 'helpers/monitored_ajax', 'mixins/with_feature_toggle', 'mixins/with_auto_refresh'], function (defineComponent, events, monitoredAjax, withFeatureToggle, withAutoRefresh) {
+ 'use strict';
+
+ var DataTags = defineComponent(dataTags, withFeatureToggle('tags', function() {
+ $(document).trigger(events.ui.mails.refresh);
+ }), withAutoRefresh('refreshTags'));
+
+ DataTags.all = {
+ name: 'all',
+ ident: '8752888923742657436',
+ query: 'in:all',
+ default: true,
+ counts:{
+ total:0,
+ read:0,
+ starred:0,
+ replied:0
+ }
+ };
+
+ function dataTags() {
+ function sendTagsBackTo(on) {
+ return function(data) {
+ data.push(DataTags.all);
+ on.trigger(document, events.tags.received, {tags: data});
+ };
+ }
+
+ this.defaultAttrs({
+ tagsResource: '/tags'
+ });
+
+ this.fetchTags = function(event, params) {
+ monitoredAjax(this, this.attr.tagsResource)
+ .done(sendTagsBackTo(this));
+ };
+
+ this.refreshTags = function() {
+ var notTriggeredByEvent = null;
+ this.fetchTags(notTriggeredByEvent);
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.tags.want, this.fetchTags);
+ this.on(document, events.mail.sent, this.fetchTags);
+ });
+ }
+
+ return DataTags;
+});
diff --git a/web-ui/public/js/tags/ui/tag.js b/web-ui/public/js/tags/ui/tag.js
new file mode 100644
index 00000000..37814cfc
--- /dev/null
+++ b/web-ui/public/js/tags/ui/tag.js
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events',
+ 'views/i18n'
+ ],
+
+ function (defineComponent, templates, events, i18n) {
+ 'use strict';
+
+ var Tag = defineComponent(tag);
+
+ Tag.appendedTo = function (parent, data) {
+ var res = new this();
+ res.renderAndAttach(parent, data);
+ return res;
+ };
+
+ return Tag;
+
+ function tag() {
+
+ var ALWAYS_HIDE_BADGE_FOR = ['sent', 'trash', 'all'];
+ var TOTAL_BADGE = ['drafts'];
+
+ this.displayBadge = function(tag) {
+ if(_.include(ALWAYS_HIDE_BADGE_FOR, tag.name)) { return false; }
+ if(this.badgeType(tag) === 'total') {
+ return tag.counts.total > 0;
+ } else {
+ return (tag.counts.total - tag.counts.read) > 0;
+ }
+ };
+
+ this.badgeType = function(tag) {
+ return _.include(TOTAL_BADGE, tag.name) ? 'total' : 'unread';
+ };
+
+ this.doUnselect = function () {
+ this.$node.removeClass('selected');
+ };
+
+ this.doSelect = function () {
+ this.$node.addClass('selected');
+ };
+
+ this.selectTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ if (data.tag === this.attr.tag.name) {
+ this.doSelect();
+ }
+ else {
+ this.doUnselect();
+ }
+ };
+
+ this.selectTagAll = function () {
+ this.selectTag(null, {tag: 'all'});
+ };
+
+ this.viewFor = function (tag, template, currentTag) {
+ return template({
+ tagName: tag.default ? i18n.t('tags.' + tag.name) : tag.name,
+ ident: this.hashIdent(tag.ident),
+ count: this.badgeType(tag) === 'total' ? tag.counts.total : (tag.counts.total - tag.counts.read),
+ displayBadge: this.displayBadge(tag),
+ badgeType: this.badgeType(tag),
+ icon: tag.icon,
+ selected: tag.name === currentTag ? 'selected' : ''
+ });
+ };
+
+ this.decreaseReadCountIfMatchingTag = function (ev, data) {
+ var mailbox_and_tags = _.flatten([data.tags, data.mailbox]);
+ if (_.contains(mailbox_and_tags, this.attr.tag.name)) {
+ this.attr.tag.counts.read++;
+ this.$node.html(this.viewFor(this.attr.tag, templates.tags.tagInner, this.attr.currentTag));
+ if (!_.isUndefined(this.attr.shortcut)) {
+ this.attr.shortcut.reRender();
+ }
+ }
+ };
+
+ this.triggerSelect = function () {
+ this.trigger(document, events.ui.tag.select, { tag: this.attr.tag.name });
+
+ this.removeSearchingClass();
+ };
+
+ this.addSearchingClass = function() {
+ if (this.attr.tag.name === 'all'){
+ this.$node.addClass('searching');
+ }
+ };
+
+ this.hashIdent = function(ident) {
+ if (typeof ident === 'undefined') {
+ return '';
+ }
+ if (typeof ident === 'number') {
+ return ident;
+ }
+ if (ident.match(/^[a-zA-Z0-9]+$/)) {
+ return ident;
+ }
+
+ /*jslint bitwise: true */
+ return Math.abs(String(ident).split('').reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a;},0));
+ };
+
+ this.removeSearchingClass = function() {
+ if (this.attr.tag.name === 'all'){
+ this.$node.removeClass('searching');
+ }
+ };
+
+ this.after('initialize', function () {
+ this.on('click', this.triggerSelect);
+ this.on(document, events.mail.read, this.decreaseReadCountIfMatchingTag);
+ this.on(document, events.search.perform, this.addSearchingClass);
+ this.on(document, events.search.empty, this.removeSearchingClass);
+
+ this.on(document, events.ui.tag.select, this.selectTag);
+ this.on(document, events.search.perform, this.selectTagAll);
+ this.on(document, events.search.empty, this.selectTagAll);
+ });
+
+ this.renderAndAttach = function (parent, data) {
+ var rendered = this.viewFor(data.tag, templates.tags.tag, data.currentTag);
+ parent.append(rendered);
+ this.initialize('#tag-' + this.hashIdent(data.tag.ident), data);
+ this.on(parent, events.tags.teardown, this.teardown);
+ };
+ }
+ }
+);
diff --git a/web-ui/public/js/tags/ui/tag_base.js b/web-ui/public/js/tags/ui/tag_base.js
new file mode 100644
index 00000000..9dc1ccbb
--- /dev/null
+++ b/web-ui/public/js/tags/ui/tag_base.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['views/i18n', 'page/events'], function(i18n, events) {
+ 'use strict';
+
+ function tagBase() {
+ var ALWAYS_HIDE_BADGE_FOR = ['sent', 'trash', 'all'];
+ var TOTAL_BADGE = ['drafts'];
+
+ this.displayBadge = function(tag) {
+ if(_.include(ALWAYS_HIDE_BADGE_FOR, tag.name)) { return false; }
+ if(this.badgeType(tag) === 'total') {
+ return tag.counts.total > 0;
+ } else {
+ return (tag.counts.total - tag.counts.read) > 0;
+ }
+ };
+
+ this.badgeType = function(tag) {
+ return _.include(TOTAL_BADGE, tag.name) ? 'total' : 'unread';
+ };
+
+ this.doUnselect = function () {
+ this.$node.removeClass('selected');
+ };
+
+ this.doSelect = function () {
+ this.$node.addClass('selected');
+ };
+
+ this.selectTag = function (ev, data) {
+ this.attr.currentTag = data.tag;
+ if (data.tag === this.attr.tag.name) {
+ this.doSelect();
+ }
+ else {
+ this.doUnselect();
+ }
+ };
+
+ this.selectTagAll = function () {
+ this.selectTag(null, {tag: 'all'});
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.ui.tag.select, this.selectTag);
+ this.on(document, events.search.perform, this.selectTagAll);
+ this.on(document, events.search.empty, this.selectTagAll);
+ });
+ }
+
+ return tagBase;
+
+});
diff --git a/web-ui/public/js/tags/ui/tag_list.js b/web-ui/public/js/tags/ui/tag_list.js
new file mode 100644
index 00000000..a2172c6d
--- /dev/null
+++ b/web-ui/public/js/tags/ui/tag_list.js
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'tags/ui/tag',
+ 'views/templates',
+ 'page/events',
+ 'page/router/url_params'
+ ],
+
+ function(defineComponent, Tag, templates, events, urlParams) {
+ 'use strict';
+
+ var ICON_FOR = {
+ 'inbox': 'inbox',
+ 'sent': 'send',
+ 'drafts': 'pencil',
+ 'trash': 'trash-o',
+ 'all': 'archive'
+ };
+
+ var ORDER = {
+ 'inbox': '0',
+ 'sent': '1',
+ 'drafts': '2',
+ 'trash': '3',
+ 'all': '4'
+ };
+
+ return defineComponent(tagList);
+
+ function tagOrder(nm) {
+ return ORDER[nm.name] || '999' + nm.name;
+ }
+
+ function tagList() {
+ this.defaultAttrs({
+ defaultTagList: '#default-tag-list',
+ customTagList: '#custom-tag-list'
+ });
+
+ function renderTag(tag, defaultList, customList) {
+ var list = tag.default ? defaultList : customList;
+
+ var tagComponent = Tag.appendedTo(list, {tag: tag, currentTag: this.getCurrentTag()});
+ }
+
+ function resetTagList(lists) {
+ _.each(lists, function (list) {
+ this.trigger(list, events.tags.teardown);
+ list.empty();
+ }.bind(this));
+
+ }
+
+ this.renderTagList = function(tags) {
+ var defaultList = this.select('defaultTagList');
+ var customList = this.select('customTagList');
+
+ resetTagList.call(this, [defaultList, customList]);
+
+ tags.forEach(function (tag) {
+ renderTag.call(this, tag, defaultList, customList);
+ }.bind(this));
+ };
+
+ this.displayTags = function(ev, data) {
+ this.renderTagList(_.sortBy(data.tags, tagOrder));
+ };
+
+ this.getCurrentTag = function () {
+ return this.attr.currentTag || urlParams.getTag();
+ };
+
+ this.updateCurrentTag = function(ev, data) {
+ this.attr.currentTag = data.tag;
+ };
+
+ this.renderTagListTemplate = function () {
+ this.$node.html(templates.tags.tagList());
+ };
+
+ this.after('initialize', function() {
+ this.on(document, events.tags.received, this.displayTags);
+ this.on(document, events.ui.tag.select, this.updateCurrentTag);
+ this.renderTagListTemplate();
+ });
+ }
+ }
+);
diff --git a/web-ui/public/js/user_alerts/ui/user_alerts.js b/web-ui/public/js/user_alerts/ui/user_alerts.js
new file mode 100644
index 00000000..e944a7a5
--- /dev/null
+++ b/web-ui/public/js/user_alerts/ui/user_alerts.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'mixins/with_hide_and_show',
+ 'page/events'
+ ],
+
+ function(defineComponent, templates, withHideAndShow, events) {
+ 'use strict';
+
+ return defineComponent(userAlerts, withHideAndShow);
+
+ function userAlerts() {
+ this.defaultAttrs({
+ dismissTimeout: 3000
+ });
+
+ this.render = function(message) {
+ this.$node.html(templates.userAlerts.message(message));
+ this.show();
+ setTimeout(this.hide.bind(this), this.attr.dismissTimeout);
+ };
+
+
+ this.displayMessage = function(ev, data) {
+ this.render({
+ message: {
+ content: data.message,
+ class: 'message-panel__growl--' + (data.class || 'success')
+ }
+ });
+ };
+
+ this.after('initialize', function() {
+ this.on(document, events.ui.userAlerts.displayMessage, this.displayMessage);
+ });
+ }
+ }
+);
+
diff --git a/web-ui/public/js/user_settings/data/user_settings.js b/web-ui/public/js/user_settings/data/user_settings.js
new file mode 100644
index 00000000..dac29cec
--- /dev/null
+++ b/web-ui/public/js/user_settings/data/user_settings.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2015 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'helpers/monitored_ajax',
+ 'page/events'
+ ],
+ function (defineComponent, monitoredAjax, events) {
+ 'use strict';
+
+ return defineComponent(function() {
+ this.defaultAttrs({
+ userSettingsResource: '/user-settings',
+ userSettings: {}
+ });
+
+ this.sendInfo = function() {
+ this.trigger(document, events.userSettings.here, this.attr.userSettings);
+ };
+
+ this.getUserSettings = function() {
+ var getUserSettingsSuccess = function (userSettings) {
+ this.attr.userSettings = userSettings;
+ };
+
+ monitoredAjax(this, this.attr.userSettingsResource, {
+ type: 'GET',
+ contentType: 'application/json; charset=utf-8'
+ }).done(getUserSettingsSuccess.bind(this));
+ };
+
+ this.after('initialize', function() {
+ this.getUserSettings();
+ this.on(document, events.userSettings.getInfo, this.sendInfo);
+ });
+ });
+});
diff --git a/web-ui/public/js/user_settings/ui/user_settings_box.js b/web-ui/public/js/user_settings/ui/user_settings_box.js
new file mode 100644
index 00000000..d3de23ed
--- /dev/null
+++ b/web-ui/public/js/user_settings/ui/user_settings_box.js
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'features',
+ 'views/templates',
+ 'page/events',
+ 'helpers/monitored_ajax'
+ ], function (defineComponent, features, templates, events, monitoredAjax) {
+
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ close: '#user-settings-close',
+ userSettingsBoxContainer: '#user-settings-box'
+ });
+
+ this.render = function (event, userSettings) {
+ if (features.isLogoutEnabled()) {
+ this.$node.addClass('extra-bottom-space');
+ }
+
+ this.$node.addClass('arrow-box');
+ this.$node.html(templates.page.userSettingsBox(userSettings));
+
+ this.on(this.attr.close, 'click', function() {
+ this.trigger(document, events.userSettings.destroyPopup);
+ });
+
+ this.on(document, 'click', function(e) {
+ var userSettingsBoxContainer = $(this.attr.userSettingsBoxContainer).get(0);
+ var target = e.target || e.srcElement;
+
+ if (target !== userSettingsBoxContainer && !isChildOf(target, userSettingsBoxContainer)) {
+ this.destroy();
+ }
+ });
+
+ function isChildOf(child, parent) {
+ if (child.parentNode === parent) {
+ return true;
+ } else if (child.parentNode === null) {
+ return false;
+ } else {
+ return isChildOf(child.parentNode, parent);
+ }
+ }
+ };
+
+ this.destroy = function () {
+ this.$node.remove();
+ this.teardown();
+ };
+
+ this.after('initialize', function () {
+ this.on(document, events.userSettings.here, this.render);
+ this.on(document, events.userSettings.destroyPopup, this.destroy);
+ this.trigger(document, events.userSettings.getInfo);
+ });
+ });
+});
diff --git a/web-ui/public/js/user_settings/ui/user_settings_icon.js b/web-ui/public/js/user_settings/ui/user_settings_icon.js
new file mode 100644
index 00000000..a6385dc1
--- /dev/null
+++ b/web-ui/public/js/user_settings/ui/user_settings_icon.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(
+ [
+ 'flight/lib/component',
+ 'views/templates',
+ 'page/events',
+ 'user_settings/ui/user_settings_box'
+ ], function (defineComponent, templates, events, userSettingsBox) {
+ 'use strict';
+
+ return defineComponent(function () {
+ this.defaultAttrs({
+ userSettingsBox: $('#user-settings-box')
+ });
+
+ this.render = function () {
+ this.$node.html(templates.page.userSettingsIcon());
+ };
+
+ this.toggleUserSettingsBox = function() {
+ if(this.attr.userSettingsBox.children().length === 0) {
+ var div = $('<div>');
+ $(this.attr.userSettingsBox).append(div);
+ userSettingsBox.attachTo(div);
+ this.attr.userSettingsInfo = userSettingsBox;
+ } else {
+ this.trigger(document, events.userSettings.destroyPopup);
+ }
+ };
+
+ this.triggerToggleUserSettingsBox = function(e) {
+ this.trigger(document, events.ui.userSettingsBox.toggle);
+ e.stopPropagation();
+ };
+
+ this.after('initialize', function () {
+ this.render();
+ this.on('click', this.triggerToggleUserSettingsBox);
+ this.on(document, events.ui.userSettingsBox.toggle, this.toggleUserSettingsBox);
+ });
+ });
+});
diff --git a/web-ui/public/js/views/i18n.js b/web-ui/public/js/views/i18n.js
new file mode 100644
index 00000000..29a1beca
--- /dev/null
+++ b/web-ui/public/js/views/i18n.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+define(['i18next',
+ 'i18nextXHRBackend',
+ 'i18nextBrowserLanguageDetector'],
+function(i18n, i18n_backend, I18n_detector) {
+ 'use strict';
+
+ var detector = new I18n_detector();
+ var detect = detector.detect.bind(detector);
+
+ detector.detect = function(detectionOrder) {
+ var result = detect(detectionOrder);
+ return result.replace('-', '_');
+ };
+
+ function t(i18n_key, options) {
+ var result = i18n.t(i18n_key, options);
+ var safe_string = new Handlebars.SafeString(result);
+ return safe_string.string;
+ }
+
+ function loaded(callback) {
+ i18n.on('loaded', function(loaded) {
+ callback();
+ });
+ }
+
+ function init(path) {
+ i18n
+ .use(i18n_backend)
+ .use(detector)
+ .init({
+ fallbackLng: 'en_US',
+ backend: {
+ loadPath: path + 'locales/{{lng}}/{{ns}}.json'
+ }
+ });
+ // Handlebars.registerHelper('t', self.bind(self));
+ Handlebars.registerHelper('t', t);
+ }
+
+ return {
+ t: t,
+ init: init,
+ loaded: loaded
+ };
+});
diff --git a/web-ui/public/js/views/recipientListFormatter.js b/web-ui/public/js/views/recipientListFormatter.js
new file mode 100644
index 00000000..0b887142
--- /dev/null
+++ b/web-ui/public/js/views/recipientListFormatter.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(function() {
+ 'use strict';
+ Handlebars.registerHelper('formatRecipients', function (header) {
+ function wrapWith(begin, end) {
+ return function (x) {
+ return begin + Handlebars.Utils.escapeExpression(x) + end;
+ };
+ }
+
+ var to = _.map(header.to, wrapWith('<span class="to">', '</span>'));
+ var cc = _.map(header.cc, wrapWith('<span class="cc">cc: ', '</span>'));
+ var bcc = _.map(header.bcc, wrapWith('<span class="bcc">bcc: ', '</span>'));
+
+ return new Handlebars.SafeString(to.concat(cc, bcc).join(', '));
+ });
+});
diff --git a/web-ui/public/js/views/templates.js b/web-ui/public/js/views/templates.js
new file mode 100644
index 00000000..8792f8cb
--- /dev/null
+++ b/web-ui/public/js/views/templates.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2014 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['hbs/templates'], function (templates) {
+ 'use strict';
+
+ var Templates = {
+ compose: {
+ box: window.Pixelated['public/templates/compose/compose_box.hbs'],
+ inlineBox: window.Pixelated['public/templates/compose/inline_box.hbs'],
+ replySection: window.Pixelated['public/templates/compose/reply_section.hbs'],
+ recipientInput: window.Pixelated['public/templates/compose/recipient_input.hbs'],
+ fixedRecipient: window.Pixelated['public/templates/compose/fixed_recipient.hbs'],
+ recipients: window.Pixelated['public/templates/compose/recipients.hbs'],
+ feedback: window.Pixelated['public/templates/compose/feedback_box.hbs'],
+ attachmentsList: window.Pixelated['public/templates/compose/attachments_list.hbs'],
+ attachmentItem: window.Pixelated['public/templates/compose/attachment_item.hbs'],
+ attachmentUploadItem: window.Pixelated['public/templates/compose/attachment_upload_item.hbs'],
+ uploadAttachmentFailed: window.Pixelated['public/templates/compose/upload_attachment_failed.hbs']
+ },
+ tags: {
+ tagList: window.Pixelated['public/templates/tags/tag_list.hbs'],
+ tag: window.Pixelated['public/templates/tags/tag.hbs'],
+ tagInner: window.Pixelated['public/templates/tags/tag_inner.hbs'],
+ shortcut: window.Pixelated['public/templates/tags/shortcut.hbs']
+ },
+ userAlerts: {
+ message: window.Pixelated['public/templates/user_alerts/message.hbs']
+ },
+ mails: {
+ single: window.Pixelated['public/templates/mails/single.hbs'],
+ fullView: window.Pixelated['public/templates/mails/full_view.hbs'],
+ mailActions: window.Pixelated['public/templates/mails/mail_actions.hbs'],
+ draft: window.Pixelated['public/templates/mails/draft.hbs'],
+ sent: window.Pixelated['public/templates/mails/sent.hbs'],
+ trash: window.Pixelated['public/templates/mails/trash.hbs']
+ },
+ mailActions: {
+ actionsBox: window.Pixelated['public/templates/mail_actions/actions_box.hbs'],
+ trashActionsBox: window.Pixelated['public/templates/mail_actions/trash_actions_box.hbs'],
+ composeTrigger: window.Pixelated['public/templates/mail_actions/compose_trigger.hbs'],
+ refreshTrigger: window.Pixelated['public/templates/mail_actions/refresh_trigger.hbs'],
+ paginationTrigger: window.Pixelated['public/templates/mail_actions/pagination_trigger.hbs']
+ },
+ noMessageSelected: window.Pixelated['public/templates/compose/no_message_selected.hbs'],
+ noMailsAvailable: window.Pixelated['public/templates/compose/no_mails_available.hbs'],
+ search: {
+ trigger: window.Pixelated['public/templates/search/search_trigger.hbs']
+ },
+ page: {
+ userSettingsIcon: window.Pixelated['public/templates/page/user_settings_icon.hbs'],
+ userSettingsBox: window.Pixelated['public/templates/page/user_settings_box.hbs'],
+ logout: window.Pixelated['public/templates/page/logout.hbs'],
+ logoutShortcut: window.Pixelated['public/templates/page/logout_shortcut.hbs'],
+ version: window.Pixelated['public/templates/page/version.hbs']
+ },
+ feedback: {
+ feedback: window.Pixelated['public/templates/feedback/feedback_trigger.hbs']
+ }
+ };
+
+ Handlebars.registerPartial('tag_inner', Templates.tags.tagInner);
+ Handlebars.registerPartial('recipients', Templates.compose.recipients);
+ Handlebars.registerPartial('attachments_list', Templates.compose.attachmentsList);
+ Handlebars.registerPartial('attachments_upload', Templates.compose.attachmentsList);
+ Handlebars.registerPartial('attachment_item', Templates.compose.attachmentItem);
+ Handlebars.registerPartial('attachment_upload_item', Templates.compose.attachmentUploadItem);
+ Handlebars.registerPartial('uploadAttachmentFailed', Templates.compose.uploadAttachmentFailed);
+
+ return Templates;
+});
diff --git a/web-ui/public/locales/en_US/translation.json b/web-ui/public/locales/en_US/translation.json
new file mode 100644
index 00000000..3e006156
--- /dev/null
+++ b/web-ui/public/locales/en_US/translation.json
@@ -0,0 +1,72 @@
+{
+ "compose": "Compose",
+ "re": "Re",
+ "fwd": "Fwd",
+ "trash-single": "Your message was moved to trash!",
+ "trash-bulk": "Your messages were moved to trash!",
+ "your-message-was-archived": "Your message was archived",
+ "delete-single": "Your message was permanently deleted!",
+ "delete-bulk": "Your messages were permanently deleted!",
+ "draft-saving": "Saving to Drafts...",
+ "draft-saved": "Draft saved",
+ "recipients-not-valid": "One or more of the recipients are not valid emails",
+ "failed-change-tags": "Could not change mail tags",
+ "invalid-tag-name": "Invalid tag name",
+ "failed-delete-single": "Could not delete email",
+ "failed-delete-bulk": "Could not delete emails",
+ "failed-fetch-messages": "Could not fetch messages",
+ "failed-archive": "Could not archive emails",
+ "to": "to",
+ "cc": "CC",
+ "bcc": "BCC",
+ "body": "Body",
+ "subject": "Subject",
+ "send": "Send",
+ "reply": "Reply",
+ "reply-to-all": "Reply to all",
+ "delete-this-message": "Delete this message",
+ "mark-as-read": "Mark as read",
+ "mark-as-unread": "Mark as unread",
+ "delete": "Delete",
+ "archive": "Archive",
+ "nothing-selected": "Nothing selected",
+ "add-tag-placeholder": "Press Enter to add tag",
+ "no-subject": "<No subject>",
+ "no-recipient": "<No recipients>",
+ "you": "you",
+ "encrypted": "Encrypted",
+ "not-encrypted": "Not encrypted",
+ "signed": "Verified sender",
+ "not-signed": "Not signed",
+ "sending-mail": "Sending...",
+ "trash-button": "Delete it",
+ "search-placeholder" : "Search...",
+ "search-results-for": "Search results for",
+ "forward": "Forward",
+ "feedback-placeholder": "Tell us what you liked, didn't like, what is missing and generally what you think about Pixelated.",
+ "user-account": "My Account",
+ "email-address": "Email address",
+ "public-key-fingerprint": "Public key fingerprint",
+ "version": "version",
+ "logout": "Logout",
+ "delete-permanently": "Delete Permanently",
+ "move-to-inbox": "Move to Inbox",
+ "reply-author-line": "On {{date}}, <{{from}}> wrote:\n",
+ "refresh": "refresh",
+ "click-to-remove": "Click to remove",
+ "no-results-for": "No results for",
+ "no-emails-in": "No emails in",
+ "error": {
+ "timeout": "A timeout occurred",
+ "general": "Problems talking to server",
+ "parse": "Got invalid response from server"
+ },
+ "tags": {
+ "inbox": "Inbox",
+ "sent": "Sent",
+ "drafts": "Drafts",
+ "trash": "Trash",
+ "all": "All",
+ "tags": "Tags"
+ }
+}
diff --git a/web-ui/public/locales/pt_BR/translation.json b/web-ui/public/locales/pt_BR/translation.json
new file mode 100644
index 00000000..ff766a2b
--- /dev/null
+++ b/web-ui/public/locales/pt_BR/translation.json
@@ -0,0 +1,72 @@
+{
+ "compose": "Escrever",
+ "re": "Res",
+ "fwd": "Enc",
+ "trash-single": "Sua mensagem foi movida para a lixeira!",
+ "trash-bulk": "Suas mensagens foram movidas para a lixeira!",
+ "your-message-was-archived": "Sua mensagem foi arquivada",
+ "delete-single": "Sua mensagem foi permanentemente deletada!",
+ "delete-bulk": "Suas mensagens foram permanentemente deletadas!",
+ "draft-saving": "Salvando rascunho...",
+ "draft-saved": "Rascunho salvo",
+ "recipients-not-valid": "Um ou mais destinatários não são emails válidos",
+ "failed-change-tags": "Não pode atualizar as tags do email",
+ "invalid-tag-name": "Nome inválido para tag",
+ "failed-delete-single": "Não pode deletar o email",
+ "failed-delete-bulk": "Não foi possível remover os emails",
+ "failed-fetch-messages": "Não pode receber as mensagens",
+ "failed-archive": "Não foi possível arquivar os emails",
+ "to": "para",
+ "cc": "CC",
+ "bcc": "CCO",
+ "body": "Mensagem",
+ "subject": "Assunto",
+ "send": "Enviar",
+ "reply": "Responder",
+ "reply-to-all": "Responder para todos",
+ "delete-this-message": "Deletar essa mensagem",
+ "mark-as-read": "Marcar como lida",
+ "mark-as-unread": "Marcar como não lida",
+ "delete": "Deletar",
+ "archive": "Arquivar",
+ "nothing-selected": "Nada selecionado",
+ "add-tag-placeholder": "Aperte enter para adicionar a tag",
+ "no-subject": "<Sem assunto>",
+ "no-recipient": "<Sem destinatários>",
+ "you": "você",
+ "encrypted": "Criptografado",
+ "not-encrypted": "Não criptografado",
+ "signed": "Rementente verificado",
+ "not-signed": "Não assinado",
+ "sending-mail": "Enviando...",
+ "trash-button": "Deletar",
+ "search-placeholder" : "Pesquisar...",
+ "search-results-for": "Resultado da pesquisa por",
+ "forward": "Encaminhar",
+ "feedback-placeholder": "Nos diga o que gosta, não gosta, o que está faltando e o que pensa sobre o Pixelated.",
+ "user-account": "Opções de usuário",
+ "email-address": "Endereço de email",
+ "public-key-fingerprint": "Identificação da chave pública",
+ "version": "versão",
+ "logout": "Sair",
+ "delete-permanently": "Excluir permanentemente",
+ "move-to-inbox": "Mover para Caixa de Entrada",
+ "reply-author-line": "Em {{date}}, <{{from}}> escreveu:\n",
+ "refresh": "atualizar",
+ "click-to-remove": "Pressione para remover",
+ "no-results-for": "Sem resultados para",
+ "no-emails-in": "Nenhum email em",
+ "error": {
+ "timeout": "A operação excedeu o limite de tempo",
+ "general": "Problemas ao se comunicar com o servidor",
+ "parse": "Obteve uma resposta inválida do servidor"
+ },
+ "tags": {
+ "inbox": "Caixa de Entrada",
+ "sent": "Enviadas",
+ "drafts": "Rascunhos",
+ "trash": "Lixeira",
+ "all": "Todas",
+ "tags": "Etiquetas"
+ }
+}
diff --git a/web-ui/public/locales/sv_SE/translation.json b/web-ui/public/locales/sv_SE/translation.json
new file mode 100644
index 00000000..d4da0711
--- /dev/null
+++ b/web-ui/public/locales/sv_SE/translation.json
@@ -0,0 +1,42 @@
+{
+ "compose": "Skriv nytt",
+ "re": "Sv",
+ "fwd": "VB",
+ "trash-single": "Ditt meddelande har flyttats till papperskorgen!",
+ "trash-bulk": "Ditt meddelande har arkiverats!",
+ "recipients-not-valid": "En eller flera mottagare är inte giltiga epost-adresser",
+ "failed-change-tags": "Kan inte ändra taggar",
+ "invalid-tag-name": "Ogiltigt taggnamn",
+ "failed-delete-single": "Kan inte ta bort meddelande",
+ "failed-fetch-messages": "Kan inte hämta meddelanden",
+ "to": "till",
+ "cc": "CC",
+ "bcc": "BCC",
+ "body": "Innehåll",
+ "subject": "Titel",
+ "send": "Skicka",
+ "reply": "Svara",
+ "reply-to-all": "Svara Alla",
+ "mark-as-read": "Markera som läst",
+ "delete": "Ta bort",
+ "archive": "Arkivera",
+ "nothing-selected": "INGET VALT",
+ "add-tag-placeholder": "Tryck retur för att skapa",
+ "no-subject": "<Ingen titel>",
+ "no-recipient": "<Inga mottagare>",
+ "you": "du",
+ "encrypted": "krypterad",
+ "not-encrypted": "Meddelandet var läsbart medans det var på väg.",
+ "signed": "Certifierad avsändare.",
+ "not-signed": "Avsändaren kunde inte säkert identifieras.",
+ "search-placeholder" : "Sök...",
+ "search-results-for": "Sökresultat för",
+ "forward": "Vidarebefodra",
+ "tags": {
+ "inbox": "Inlåda",
+ "sent": "Skickat",
+ "drafts": "Utkast",
+ "trash": "Skräp",
+ "all": "Alla"
+ }
+}
diff --git a/web-ui/public/normalize.min.css b/web-ui/public/normalize.min.css
new file mode 100644
index 00000000..d3c7f4d5
--- /dev/null
+++ b/web-ui/public/normalize.min.css
@@ -0,0 +1 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} \ No newline at end of file
diff --git a/web-ui/public/opensans.css b/web-ui/public/opensans.css
new file mode 100644
index 00000000..a42f346c
--- /dev/null
+++ b/web-ui/public/opensans.css
@@ -0,0 +1,69 @@
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ src: local("Open Sans Light"), local("OpenSans-Light"), url("/fonts/OpenSans-Light.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local("Open Sans"), local("OpenSans"), url("/fonts/OpenSans.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("/fonts/OpenSans-Semibold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local("Open Sans Bold"), local("OpenSans-Bold"), url("/fonts/OpenSans-Bold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 800;
+ src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("/fonts/OpenSans-Extrabold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("/fonts/OpenSansLight-Italic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ src: local("Open Sans Italic"), local("OpenSans-Italic"), url("/fonts/OpenSans-Italic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("/fonts/OpenSans-SemiboldItalic.woff ") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 700;
+ src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("/fonts/OpenSans-BoldItalic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 800;
+ src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("/fonts/OpenSans-ExtraboldItalic.woff") format("woff");
+}
diff --git a/web-ui/public/pixelated-logo-orange.svg b/web-ui/public/pixelated-logo-orange.svg
new file mode 100644
index 00000000..7e0ef43d
--- /dev/null
+++ b/web-ui/public/pixelated-logo-orange.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="509.707px" height="142.439px" viewBox="0 0 509.707 142.439" enable-background="new 0 0 509.707 142.439"
+ xml:space="preserve">
+<g>
+ <path fill="#F9A731" d="M0,35.469v71.365l62.837,35.605l62.833-35.605V35.469L62.813,0L0,35.469z M60.262,116.617L23.735,96.332
+ V52.46l36.586,20.999L60.262,116.617z M101.936,96.332l-36.148,20.285l0.067-43.123l36.081-21.034V96.332z M101.936,46.44
+ L62.951,69.553L23.733,46.44l39.218-21.131L101.936,46.44z"/>
+ <path fill="#F9A731" d="M169.505,42.332h-19.968v59.328h13.52V79.655h6.448c11.579,0,20.279-6.832,20.279-19.056
+ C189.784,48.302,181.084,42.332,169.505,42.332z M166.866,68.868h-3.809v-15.75h3.809c5.323,0,10.357,1.798,10.357,7.91
+ C177.224,67.07,172.189,68.868,166.866,68.868z"/>
+ <rect x="194.309" y="42.332" fill="#F9A731" width="13.52" height="59.328"/>
+ <polygon fill="#F9A731" points="266.516,42.332 249.689,42.332 238.759,58.514 227.827,42.332 211.721,42.332 230.417,69.73
+ 210.228,101.66 226.982,101.66 238.759,81.453 250.534,101.66 268.01,101.66 247.099,69.73 "/>
+ <polygon fill="#F9A731" points="270.128,101.66 304.069,101.66 304.069,89.795 283.647,89.795 283.647,77.857 303.207,77.857
+ 303.207,65.991 283.647,65.991 283.647,54.199 304.069,54.199 304.069,42.332 270.128,42.332 "/>
+ <path fill="#F9A731" d="M354.807,42.332l-19.156,47.463H322.33V42.332h-13.52v59.328h22.053h11.888h2.636l4.386-11.865h22.578
+ l4.391,11.865h14.524l-23.944-59.328H354.807z M354.377,77.928l6.614-17.257h0.145l6.615,17.257H354.377z"/>
+ <polygon fill="#F9A731" points="379.939,54.199 394.073,54.199 394.073,101.66 407.592,101.66 407.592,54.199 421.687,54.199
+ 421.687,42.332 379.939,42.332 "/>
+ <polygon fill="#F9A731" points="426.265,101.66 460.207,101.66 460.207,89.795 439.785,89.795 439.785,77.857 459.344,77.857
+ 459.344,65.991 439.785,65.991 439.785,54.199 460.207,54.199 460.207,42.332 426.265,42.332 "/>
+ <path fill="#F9A731" d="M479.792,42.332h-14.94v59.328h14.94c16.324,0,29.914-12.37,29.914-29.699
+ C509.707,54.701,496.044,42.332,479.792,42.332z M480.457,89.577h-2.084V54.414h2.084c10.067,0,16.9,7.695,16.9,17.619
+ C497.285,81.955,490.455,89.577,480.457,89.577z"/>
+</g>
+</svg>
diff --git a/web-ui/public/pixelated.css b/web-ui/public/pixelated.css
new file mode 100644
index 00000000..b3e1d16e
--- /dev/null
+++ b/web-ui/public/pixelated.css
@@ -0,0 +1,128 @@
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ background-color: #EAEAEA;
+ height: 100vh;
+ color: #3E3A37;
+
+ background-image: url("hive-bg.png");
+ background-repeat: repeat;
+}
+
+.content {
+ height: 100vh;
+ width: 100%;
+}
+
+.error {
+ color: #D72A25;
+ margin: 10px 0 0 0;
+}
+
+.message-panel {
+ width: 100%;
+ margin: 10px auto;
+ z-index: 10000;
+ text-align: center;
+ }
+
+.message-panel span{
+ background: #F7E8AF;
+ padding: 5px 60px;
+ border: 1px solid #f2db81;
+ color: #987b0f;
+ box-shadow: 1px 1px 3px #69560b;
+}
+
+.message-panel.message-panel-small span{
+ padding: 5px 0px;
+ display: inline-block;
+ width: 100%;
+}
+
+.login {
+ display: block;
+ width: 240px;
+ margin: auto;
+ padding: 45px 40px 35px 40px;
+ background-color: #FFF;
+ margin-top: 2%;
+ margin-bottom: 2%;
+}
+
+form#login_form {
+ padding: 10px 0;
+}
+
+.disclaimer {
+ display: block;
+ margin-top: 10%;
+ width: 50%;
+ margin: auto;
+ background-color: #2BA6CB;
+ color: #FFFFFF;
+ font-weight: 300;
+ font-size: 0.8rem;
+ padding: 1em;
+ margin-bottom: 20px;
+}
+
+.disclaimer li {
+ margin-top: 1em;
+}
+
+.logo {
+ width: 100%;
+ height: auto;
+}
+
+input {
+ display: block;
+ margin: 10px 0;
+ padding-left: 5px;
+}
+
+input.text-field {
+ width: 97%;
+}
+
+button, .button, input[type=button] {
+ cursor: pointer;
+ margin: 0 0 1.25rem;
+ border: none;
+ position: relative;
+ text-decoration: none;
+ text-align: center;
+ -webkit-appearance: none;
+ display: inline-block;
+ padding: 0.4rem 1.1rem;
+ font-size: 0.9rem;
+ background-color: #2ba6cb;
+ border-color: #2285a2;
+ color: white;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ -ms-border-radius: 2px;
+ -o-border-radius: 2px;
+ border-radius: 2px;
+}
+
+button:hover, button:focus, .button:hover, .button:focus, input[type=button]:hover, input[type=button]:focus {
+ background-color: #2285a2;
+ outline: none;
+ color: white;
+}
+
+ul.accounts {
+ margin-bottom: 5%;
+}
+
+ul.accounts li {
+ display: inline-block;
+ list-style: none;
+ margin-right: 35px;
+ margin-top: 0px;
+}
+
+ul.accounts li span {
+ font-weight: bold;
+}
diff --git a/web-ui/public/robots.txt b/web-ui/public/robots.txt
new file mode 100644
index 00000000..6b0157e2
--- /dev/null
+++ b/web-ui/public/robots.txt
@@ -0,0 +1,3 @@
+# robotstxt.org
+
+User-agent: * \ No newline at end of file
diff --git a/web-ui/public/sandbox.html b/web-ui/public/sandbox.html
new file mode 100644
index 00000000..d24a8452
--- /dev/null
+++ b/web-ui/public/sandbox.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <link href="css/sandbox.css" rel="stylesheet" type="text/css">
+
+ <script src="sandbox.min.js" type="text/javascript"></script>
+</head>
+
+<body></body>
+
+</html>
diff --git a/web-ui/public/scss/_mixins.scss b/web-ui/public/scss/_mixins.scss
new file mode 100644
index 00000000..d3aa0220
--- /dev/null
+++ b/web-ui/public/scss/_mixins.scss
@@ -0,0 +1,71 @@
+// SHARED MIXINS
+@mixin btn-transition {
+ @include transition-property(background-color);
+ @include transition-duration(300ms);
+ @include transition-timing-function(ease-out);
+}
+
+@mixin tooltip($top: 8px, $left: 40px) {
+ background: rgba(0, 0, 0, 0.7);
+ color: $white;
+ position: absolute;
+ z-index: 2;
+ left: $left;
+ top: $top;
+ font-size: 0.8rem;
+ padding: 2px 10px;
+ white-space: nowrap;
+ @include border-radius(2px);
+}
+
+// FORM MIXINS
+@mixin check-box {
+ background-color: $white;
+ border: 1px solid $light_gray;
+ padding: 7px;
+ margin: 3px 0;
+ cursor: pointer;
+ display: inline-block;
+ position: relative;
+ @include border-radius(2px);
+ @include appearance(none);
+
+ &:focus {
+ outline: none;
+ border-color: $medium_dark_grey;
+ }
+
+ &:active, &:checked:active {
+ }
+
+ &:checked {
+ background-color: $contrast;
+ border: 1px solid darken($lighter_gray, 10%);
+ color: $dark_grey;
+ }
+
+ &:checked:after {
+ content: '\2714';
+ font-size: 1em;
+ position: absolute;
+ bottom: -2px;
+ left: 1px;
+ color: $navigation_background;
+ }
+}
+
+
+@mixin searching($top, $left, $color, $size){
+ &.searching {
+ &:after {
+ font-family: FontAwesome;
+ content: "\f002";
+ font-size: $size;
+ top: $top;
+ left: $left;
+ position: absolute;
+ color: $color;
+ text-shadow: -1px 0 $contrast, 0 1px $contrast, 1px 0 $contrast, 0 -1px $contrast;
+ }
+ }
+}
diff --git a/web-ui/public/scss/_others.scss b/web-ui/public/scss/_others.scss
new file mode 100644
index 00000000..039d94bd
--- /dev/null
+++ b/web-ui/public/scss/_others.scss
@@ -0,0 +1,72 @@
+.hidden {
+ display: none;
+}
+
+.no-padding {
+ padding: 0;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.search-highlight {
+ background-color: $search-highlight;
+}
+
+button {
+ border: 1px solid transparent;
+
+ i {
+ margin-left: 5px;
+ }
+
+ &#trash-button {
+ background: $white;
+ border: 1px solid $medium_light_grey;
+ color: $medium_light_grey;
+ float: right;
+ margin-left: 5px;
+
+ &:hover, &:focus {
+ background: $contrast;
+ }
+ }
+
+ &.no-style {
+ background: transparent;
+ color: $medium_light_grey;
+ padding: 0;
+ margin: 0;
+
+ i {
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+ }
+ }
+}
+
+section {
+ display: inline-block;
+ vertical-align: top;
+ height: 100vh;
+ overflow-y: scroll;
+
+ &#left-pane {
+ background-color: $navigation_background;
+ color: white;
+ }
+
+ &#middle-pane {
+ background: $white;
+ }
+
+ &#right-pane {
+ padding: 0 10px 60px 0px;
+ background: $white;
+ box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.12);
+ z-index: 2;
+ overflow-y: auto;
+ }
+}
diff --git a/web-ui/public/scss/base/_colors.scss b/web-ui/public/scss/base/_colors.scss
new file mode 100644
index 00000000..17333ff9
--- /dev/null
+++ b/web-ui/public/scss/base/_colors.scss
@@ -0,0 +1,64 @@
+/* Pixelated Color Palette - don't change these! */
+$dark_slate_gray: #3E3A37;
+$light_gray: #C2C2C2;
+$lighter_blue: #91C2D1;
+$light_blue: #3DABC4;
+$dark_blue: #178CA6;
+$bullet-blue: #5cacde;
+$light_orange: #FF9C00;
+$dark_orange: #FF7902;
+
+
+/* Side nav background color */
+$navigation_background: $dark_slate_gray;
+
+/* Action buttons and links */
+$action_buttons: $light_blue;
+
+/* Primary Highlight*/
+$primary_highlight: $light_orange;
+
+/* Logo color*/
+$logo_color: $light_orange;
+
+/* Unread count dialog bubble background color */
+$secondary_callout: darken($primary_highlight, 5);
+
+/* Grayscale */
+$contrast: #EEE;
+$white: #FFF;
+$dark_white: #FAFAFA;
+$lighter_gray: #DDD;
+$medium_light_grey: #999;
+$medium_grey: #777;
+$medium_dark_grey: #666;
+$dark_grey: #333;
+$black: #000;
+$top_pane: $contrast;
+$total_count_bg: #C0B9B9;
+$background_dropdown_grey: #f0f0f0;
+
+$background_light_grey: #F5F5F5;
+$border_light_grey: #D9D9D9;
+
+/* Feedback to Users */
+$warning: #F7E8AF;
+$search-highlight: #FFEF29;
+
+/* Light gray indicator icons */
+$indicator_icon_color: $light_gray;
+
+$error: #D93C38;
+$attention: #F6A41C;
+$success: #50BA5B;
+
+$will_be_encrypted: $success;
+$wont_be_encrypted: $attention;
+$recipients_font_color: #828282;
+
+/* Attachments */
+$attachment_text: #555;
+$attachment_icon: lighten($attachment_text, 30);
+$attachment_size: lighten($attachment_text, 30);
+$attachment_area_background: #F5F5F5;
+$attachment_area_border: #D9D9D9;
diff --git a/web-ui/public/scss/base/_fonts.scss b/web-ui/public/scss/base/_fonts.scss
new file mode 100644
index 00000000..a8f2d7da
--- /dev/null
+++ b/web-ui/public/scss/base/_fonts.scss
@@ -0,0 +1,68 @@
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ src: local('Open Sans Light'), local('OpenSans-Light'), url('/static/fonts/OpenSans-Light.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Open Sans'), local('OpenSans'), url('/static/fonts/OpenSans.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url('/static/fonts/OpenSans-Semibold.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Open Sans Bold'), local('OpenSans-Bold'), url('/static/fonts/OpenSans-Bold.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 800;
+ src: local('Open Sans Extrabold'), local('OpenSans-Extrabold'), url('/static/fonts/OpenSans-Extrabold.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url('/static/fonts/OpenSansLight-Italic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ src: local('Open Sans Italic'), local('OpenSans-Italic'), url('/static/fonts/OpenSans-Italic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ src: local('Open Sans Semibold Italic'), local('OpenSans-SemiboldItalic'), url('/static/fonts/OpenSans-SemiboldItalic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 700;
+ src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url('/static/fonts/OpenSans-BoldItalic.woff') format('woff');
+}
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 800;
+ src: local('Open Sans Extrabold Italic'), local('OpenSans-ExtraboldItalic'), url('/static/fonts/OpenSans-ExtraboldItalic.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'icomoon';
+ font-style: normal;
+ font-weight: 400;
+ src: url('/static/fonts/icomoon.woff') format('woff'), url('/static/fonts/icomoon.ttf') format('truetype'), ;
+}
+
diff --git a/web-ui/public/scss/base/_scaffolding.scss b/web-ui/public/scss/base/_scaffolding.scss
new file mode 100644
index 00000000..b8b5fa3b
--- /dev/null
+++ b/web-ui/public/scss/base/_scaffolding.scss
@@ -0,0 +1,10 @@
+html {
+ height: 100% ;
+}
+
+body {
+ min-height: 100% ;
+ overflow: hidden;
+ background: $white;
+}
+
diff --git a/web-ui/public/scss/mixins/_position-helpers.scss b/web-ui/public/scss/mixins/_position-helpers.scss
new file mode 100644
index 00000000..254bfc6c
--- /dev/null
+++ b/web-ui/public/scss/mixins/_position-helpers.scss
@@ -0,0 +1,9 @@
+@mixin absolute-center-unknown-height-width() {
+ margin: auto;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ -ms-transform: translate(-50%, -50%);
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+}
diff --git a/web-ui/public/scss/mixins/_tags.scss b/web-ui/public/scss/mixins/_tags.scss
new file mode 100644
index 00000000..9bb287ea
--- /dev/null
+++ b/web-ui/public/scss/mixins/_tags.scss
@@ -0,0 +1,110 @@
+$tags-font-size: 0.6rem;
+
+@mixin tags {
+ & > * {
+ display: inline;
+ }
+
+ &-tag {
+ font-size: $tags-font-size;
+ font-weight: 700;
+ background-color: $dark_blue;
+ color: white;
+ padding: 2px 4px;
+ margin: 0 1px;
+ border-radius: 2px;
+ }
+}
+
+@mixin tags-editable {
+ @include tags;
+
+ &-tag:hover {
+ text-decoration: line-through;
+ cursor: pointer;
+ position: relative;
+
+ &:before {
+ @include tooltip(130%, 25%);
+
+ content: "click to remove";
+ text-transform: lowercase;
+ }
+ }
+
+ &-label {
+ vertical-align: bottom;
+ color: $light_gray;
+ }
+
+ &-new-button {
+ font-size: $tags-font-size;
+ padding: 0;
+ background: transparent;
+ border-radius: 2px;
+ padding: 2px;
+
+ &:hover {
+ opacity: 1;
+ background: $lighter_gray;
+ }
+ }
+
+ &-name-input {
+ opacity: 0.6;
+ transition: background-color 150ms ease-out;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ // twitter typeahead classes. those are set via JS, with relatively high specificity,
+ // hence box-model-related properties are repeated
+ // https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#class-names
+
+ $suggestion-border: 1px solid darken($contrast, 5%);
+ $input-field-padding: 1px 5px;
+ $input-field-margin: 2px;
+
+ & * .tt-input {
+ border-radius: $input-field-margin;
+ padding: $input-field-padding;
+ margin-top: 2px;
+ font-size: $tags-font-size;
+ }
+
+ & * .tt-hint {
+ color: $medium_light_grey;
+ padding: $input-field-padding;
+ margin-top: $input-field-margin;
+ font-size: $tags-font-size;
+ background: transparent;
+ }
+
+ & * .tt-dropdown-menu {
+ min-width: 250px;
+ padding: 0;
+ font-size: $tags-font-size;
+ background-color: $contrast;
+ border: $suggestion-border;
+ }
+
+ & * .tt-suggestion {
+ padding: 5px 10px;
+ font-size: $tags-font-size;
+ border-bottom: $suggestion-border;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ p {
+ margin: 0;
+ }
+ }
+
+ & * .tt-cursor {
+ background-color: $white;
+ }
+ }
+}
diff --git a/web-ui/public/scss/sandbox.scss b/web-ui/public/scss/sandbox.scss
new file mode 100644
index 00000000..3c1be358
--- /dev/null
+++ b/web-ui/public/scss/sandbox.scss
@@ -0,0 +1,27 @@
+$search-highlight: #FFEF29;
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Open Sans'), local('OpenSans'), url('/sandbox/fonts/OpenSans.woff') format('woff');
+}
+
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ font-size: 13px;
+ line-height: 1.2em;
+ background: white;
+ color: #333;
+ padding: 0;
+ margin: 0;
+ font-weight: normal;
+ -webkit-font-smoothing: antialiased;
+ font-style: normal;
+ box-sizing: border-box;
+ word-wrap: break-word;
+}
+
+.search-highlight {
+ background-color: $search-highlight;
+}
diff --git a/web-ui/public/scss/style.scss b/web-ui/public/scss/style.scss
new file mode 100644
index 00000000..e99ab194
--- /dev/null
+++ b/web-ui/public/scss/style.scss
@@ -0,0 +1,39 @@
+// vendor stylesheets and resets
+@import "vendor/reset";
+@import "vendor/scut";
+@import "compass/css3";
+@import "vendor/foundation";
+@import "vendor/customfont";
+
+// basic configuration
+@import "base/fonts";
+@import "base/colors";
+@import "base/scaffolding";
+
+// mixins
+@import "mixins/position-helpers";
+@import "mixins/tags";
+
+// TODO
+@import "mixins";
+
+// templates
+@import "templates/no-content-placeholder";
+@import "templates/unread-count";
+
+// views
+@import "views/message-panel";
+@import "views/close-button";
+@import "views/no-message-selected";
+@import "views/no-mails-available";
+@import "views/read-view";
+@import "views/security-labels";
+@import "views/compose-view";
+@import "views/compose-button";
+@import "views/mail-list";
+@import "views/_action-bar.scss";
+@import "views/_navigation.scss";
+
+// misc stuff
+@import "others";
+
diff --git a/web-ui/public/scss/templates/_no-content-placeholder.scss b/web-ui/public/scss/templates/_no-content-placeholder.scss
new file mode 100644
index 00000000..c6807011
--- /dev/null
+++ b/web-ui/public/scss/templates/_no-content-placeholder.scss
@@ -0,0 +1,5 @@
+.no-content-placeholder {
+ @include absolute-center-unknown-height-width;
+
+ color: $medium_dark_grey;
+}
diff --git a/web-ui/public/scss/templates/_unread-count.scss b/web-ui/public/scss/templates/_unread-count.scss
new file mode 100644
index 00000000..f7852227
--- /dev/null
+++ b/web-ui/public/scss/templates/_unread-count.scss
@@ -0,0 +1,14 @@
+.mail-count {
+ background: $white;
+ border-radius: 50%;
+ border: 1px solid $white;
+ color: $white;
+ font-size: 0.7em;
+ font-weight: 700;
+ left: 0;
+ margin-left: 5px;
+ opacity: 0.95;
+ padding: 0px 5px 0;
+ position: absolute;
+ top: 1px;
+}
diff --git a/web-ui/public/scss/vendor/_customfont.scss b/web-ui/public/scss/vendor/_customfont.scss
new file mode 100644
index 00000000..d72cca0f
--- /dev/null
+++ b/web-ui/public/scss/vendor/_customfont.scss
@@ -0,0 +1,9 @@
+[class^="icon-"], [class*=" icon-"] {
+ /* use !important to prevent issues with browser extensions that change fonts */
+ font-family: 'icomoon' !important;
+ line-height: 1;
+}
+
+.icon-px-sent:before {
+ content: "\e900";
+}
diff --git a/web-ui/public/scss/vendor/_foundation.scss b/web-ui/public/scss/vendor/_foundation.scss
new file mode 100644
index 00000000..7918cf26
--- /dev/null
+++ b/web-ui/public/scss/vendor/_foundation.scss
@@ -0,0 +1,2066 @@
+@import 'compass/css3';
+
+meta {
+ &.foundation-version {
+ font-family: "/5.2.3/";
+ }
+ &.foundation-mq-small {
+ font-family: "/only screen/";
+ width: 0em;
+ }
+ &.foundation-mq-medium {
+ font-family: "/only screen and (min-width:40.063em)/";
+ width: 40.063em;
+ }
+ &.foundation-mq-large {
+ font-family: "/only screen and (min-width:64.063em)/";
+ width: 64.063em;
+ }
+ &.foundation-mq-xlarge {
+ font-family: "/only screen and (min-width:90.063em)/";
+ width: 90.063em;
+ }
+ &.foundation-mq-xxlarge {
+ font-family: "/only screen and (min-width:120.063em)/";
+ width: 120.063em;
+ }
+ &.foundation-data-attribute-namespace {
+ font-family: false;
+ }
+}
+
+html, body {
+ height: 100%;
+}
+
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ &:before, &:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+}
+
+html {
+ font-size: 100%;
+}
+
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ font-size: 13px;
+ line-height: 1.2em;
+ background: white;
+ color: #333;
+ padding: 0;
+ margin: 0;
+ font-weight: normal;
+ -webkit-font-smoothing: antialiased;
+ font-style: normal;
+ position: relative;
+ cursor: default;
+}
+
+a:hover {
+ cursor: pointer;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+ -ms-interpolation-mode: bicubic;
+}
+
+#map_canvas {
+ img, embed, object {
+ max-width: none !important;
+ }
+}
+
+.map_canvas {
+ img, embed, object {
+ max-width: none !important;
+ }
+}
+
+.left {
+ float: left !important;
+}
+
+.right {
+ float: right !important;
+}
+
+.clearfix {
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+}
+
+.hide {
+ display: none;
+}
+
+.antialiased {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+img {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+textarea {
+ height: auto;
+ min-height: 50px;
+ &:focus {
+ outline: none;
+ }
+}
+
+select {
+ width: 100%;
+}
+
+.row {
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ margin-top: 0;
+ margin-bottom: 0;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+ &.collapse {
+ > {
+ .column, .columns {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
+ .row {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+ .row {
+ width: auto;
+ margin-left: -0.9375em;
+ margin-right: -0.9375em;
+ margin-top: 0;
+ margin-bottom: 0;
+ max-width: none;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+ &.collapse {
+ width: auto;
+ margin: 0;
+ max-width: none;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+ }
+ }
+}
+
+.column, .columns {
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ width: 100%;
+ float: left;
+}
+
+@media only screen {
+ .small-push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .small-pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .small-push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .small-pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .small-push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .small-pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .small-push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .small-pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .small-push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .small-pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .small-push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .small-pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .small-push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .small-pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .small-push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .small-pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .small-push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .small-pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .small-push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .small-pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .small-push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .small-pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .small-push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .small-pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+ .column, .columns {
+ position: relative;
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ float: left;
+ }
+ .small-1 {
+ width: 8.33333%;
+ }
+ .small-2 {
+ width: 16.66667%;
+ }
+ .small-3 {
+ width: 25%;
+ }
+ .small-4 {
+ width: 33.33333%;
+ }
+ .small-5 {
+ width: 41.66667%;
+ }
+ .small-6 {
+ width: 50%;
+ }
+ .small-7 {
+ width: 58.33333%;
+ }
+ .small-8 {
+ width: 66.66667%;
+ }
+ .small-9 {
+ width: 75%;
+ }
+ .small-10 {
+ width: 83.33333%;
+ }
+ .small-11 {
+ width: 91.66667%;
+ }
+ .small-12 {
+ width: 100%;
+ }
+ [class*="column"] + [class*="column"] {
+ &:last-child {
+ float: right;
+ }
+ &.end {
+ float: left;
+ }
+ }
+ .small-offset-0 {
+ margin-left: 0% !important;
+ }
+ .small-offset-1 {
+ margin-left: 8.33333% !important;
+ }
+ .small-offset-2 {
+ margin-left: 16.66667% !important;
+ }
+ .small-offset-3 {
+ margin-left: 25% !important;
+ }
+ .small-offset-4 {
+ margin-left: 33.33333% !important;
+ }
+ .small-offset-5 {
+ margin-left: 41.66667% !important;
+ }
+ .small-offset-6 {
+ margin-left: 50% !important;
+ }
+ .small-offset-7 {
+ margin-left: 58.33333% !important;
+ }
+ .small-offset-8 {
+ margin-left: 66.66667% !important;
+ }
+ .small-offset-9 {
+ margin-left: 75% !important;
+ }
+ .small-offset-10 {
+ margin-left: 83.33333% !important;
+ }
+ .small-offset-11 {
+ margin-left: 91.66667% !important;
+ }
+ .small-reset-order {
+ margin-left: 0;
+ margin-right: 0;
+ left: auto;
+ right: auto;
+ float: left;
+ }
+ .column.small-centered, .columns.small-centered {
+ margin-left: auto;
+ margin-right: auto;
+ float: none !important;
+ }
+ .column.small-uncentered, .columns.small-uncentered {
+ margin-left: 0;
+ margin-right: 0;
+ float: left !important;
+ }
+ .column.small-uncentered.opposite, .columns.small-uncentered.opposite {
+ float: right;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ .medium-push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .medium-pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .medium-push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .medium-pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .medium-push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .medium-pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .medium-push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .medium-pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .medium-push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .medium-pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .medium-push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .medium-pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .medium-push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .medium-pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .medium-push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .medium-pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .medium-push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .medium-pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .medium-push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .medium-pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .medium-push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .medium-pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .medium-push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .medium-pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+ .column, .columns {
+ position: relative;
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ float: left;
+ }
+ .medium-1 {
+ width: 8.33333%;
+ }
+ .medium-2 {
+ width: 16.66667%;
+ }
+ .medium-3 {
+ width: 25%;
+ }
+ .medium-4 {
+ width: 33.33333%;
+ }
+ .medium-5 {
+ width: 41.66667%;
+ }
+ .medium-6 {
+ width: 50%;
+ }
+ .medium-7 {
+ width: 58.33333%;
+ }
+ .medium-8 {
+ width: 66.66667%;
+ }
+ .medium-9 {
+ width: 75%;
+ }
+ .medium-10 {
+ width: 83.33333%;
+ }
+ .medium-11 {
+ width: 91.66667%;
+ }
+ .medium-12 {
+ width: 100%;
+ }
+ [class*="column"] + [class*="column"] {
+ &:last-child {
+ float: right;
+ }
+ &.end {
+ float: left;
+ }
+ }
+ .medium-offset-0 {
+ margin-left: 0% !important;
+ }
+ .medium-offset-1 {
+ margin-left: 8.33333% !important;
+ }
+ .medium-offset-2 {
+ margin-left: 16.66667% !important;
+ }
+ .medium-offset-3 {
+ margin-left: 25% !important;
+ }
+ .medium-offset-4 {
+ margin-left: 33.33333% !important;
+ }
+ .medium-offset-5 {
+ margin-left: 41.66667% !important;
+ }
+ .medium-offset-6 {
+ margin-left: 50% !important;
+ }
+ .medium-offset-7 {
+ margin-left: 58.33333% !important;
+ }
+ .medium-offset-8 {
+ margin-left: 66.66667% !important;
+ }
+ .medium-offset-9 {
+ margin-left: 75% !important;
+ }
+ .medium-offset-10 {
+ margin-left: 83.33333% !important;
+ }
+ .medium-offset-11 {
+ margin-left: 91.66667% !important;
+ }
+ .medium-reset-order {
+ margin-left: 0;
+ margin-right: 0;
+ left: auto;
+ right: auto;
+ float: left;
+ }
+ .column.medium-centered, .columns.medium-centered {
+ margin-left: auto;
+ margin-right: auto;
+ float: none !important;
+ }
+ .column.medium-uncentered, .columns.medium-uncentered {
+ margin-left: 0;
+ margin-right: 0;
+ float: left !important;
+ }
+ .column.medium-uncentered.opposite, .columns.medium-uncentered.opposite {
+ float: right;
+ }
+ .push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+}
+
+@media only screen and (min-width: 64.063em) {
+ .large-push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .large-pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .large-push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .large-pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .large-push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .large-pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .large-push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .large-pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .large-push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .large-pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .large-push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .large-pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .large-push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .large-pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .large-push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .large-pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .large-push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .large-pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .large-push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .large-pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .large-push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .large-pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .large-push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .large-pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+ .column, .columns {
+ position: relative;
+ padding-left: 0.9375em;
+ padding-right: 0.9375em;
+ float: left;
+ }
+ .large-1 {
+ width: 8.33333%;
+ }
+ .large-2 {
+ width: 16.66667%;
+ }
+ .large-3 {
+ width: 25%;
+ }
+ .large-4 {
+ width: 33.33333%;
+ }
+ .large-5 {
+ width: 41.66667%;
+ }
+ .large-6 {
+ width: 50%;
+ }
+ .large-7 {
+ width: 58.33333%;
+ }
+ .large-8 {
+ width: 66.66667%;
+ }
+ .large-9 {
+ width: 75%;
+ }
+ .large-10 {
+ width: 83.33333%;
+ }
+ .large-11 {
+ width: 91.66667%;
+ }
+ .large-12 {
+ width: 100%;
+ }
+ [class*="column"] + [class*="column"] {
+ &:last-child {
+ float: right;
+ }
+ &.end {
+ float: left;
+ }
+ }
+ .large-offset-0 {
+ margin-left: 0% !important;
+ }
+ .large-offset-1 {
+ margin-left: 8.33333% !important;
+ }
+ .large-offset-2 {
+ margin-left: 16.66667% !important;
+ }
+ .large-offset-3 {
+ margin-left: 25% !important;
+ }
+ .large-offset-4 {
+ margin-left: 33.33333% !important;
+ }
+ .large-offset-5 {
+ margin-left: 41.66667% !important;
+ }
+ .large-offset-6 {
+ margin-left: 50% !important;
+ }
+ .large-offset-7 {
+ margin-left: 58.33333% !important;
+ }
+ .large-offset-8 {
+ margin-left: 66.66667% !important;
+ }
+ .large-offset-9 {
+ margin-left: 75% !important;
+ }
+ .large-offset-10 {
+ margin-left: 83.33333% !important;
+ }
+ .large-offset-11 {
+ margin-left: 91.66667% !important;
+ }
+ .large-reset-order {
+ margin-left: 0;
+ margin-right: 0;
+ left: auto;
+ right: auto;
+ float: left;
+ }
+ .column.large-centered, .columns.large-centered {
+ margin-left: auto;
+ margin-right: auto;
+ float: none !important;
+ }
+ .column.large-uncentered, .columns.large-uncentered {
+ margin-left: 0;
+ margin-right: 0;
+ float: left !important;
+ }
+ .column.large-uncentered.opposite, .columns.large-uncentered.opposite {
+ float: right;
+ }
+ .push-0 {
+ position: relative;
+ left: 0%;
+ right: auto;
+ }
+ .pull-0 {
+ position: relative;
+ right: 0%;
+ left: auto;
+ }
+ .push-1 {
+ position: relative;
+ left: 8.33333%;
+ right: auto;
+ }
+ .pull-1 {
+ position: relative;
+ right: 8.33333%;
+ left: auto;
+ }
+ .push-2 {
+ position: relative;
+ left: 16.66667%;
+ right: auto;
+ }
+ .pull-2 {
+ position: relative;
+ right: 16.66667%;
+ left: auto;
+ }
+ .push-3 {
+ position: relative;
+ left: 25%;
+ right: auto;
+ }
+ .pull-3 {
+ position: relative;
+ right: 25%;
+ left: auto;
+ }
+ .push-4 {
+ position: relative;
+ left: 33.33333%;
+ right: auto;
+ }
+ .pull-4 {
+ position: relative;
+ right: 33.33333%;
+ left: auto;
+ }
+ .push-5 {
+ position: relative;
+ left: 41.66667%;
+ right: auto;
+ }
+ .pull-5 {
+ position: relative;
+ right: 41.66667%;
+ left: auto;
+ }
+ .push-6 {
+ position: relative;
+ left: 50%;
+ right: auto;
+ }
+ .pull-6 {
+ position: relative;
+ right: 50%;
+ left: auto;
+ }
+ .push-7 {
+ position: relative;
+ left: 58.33333%;
+ right: auto;
+ }
+ .pull-7 {
+ position: relative;
+ right: 58.33333%;
+ left: auto;
+ }
+ .push-8 {
+ position: relative;
+ left: 66.66667%;
+ right: auto;
+ }
+ .pull-8 {
+ position: relative;
+ right: 66.66667%;
+ left: auto;
+ }
+ .push-9 {
+ position: relative;
+ left: 75%;
+ right: auto;
+ }
+ .pull-9 {
+ position: relative;
+ right: 75%;
+ left: auto;
+ }
+ .push-10 {
+ position: relative;
+ left: 83.33333%;
+ right: auto;
+ }
+ .pull-10 {
+ position: relative;
+ right: 83.33333%;
+ left: auto;
+ }
+ .push-11 {
+ position: relative;
+ left: 91.66667%;
+ right: auto;
+ }
+ .pull-11 {
+ position: relative;
+ right: 91.66667%;
+ left: auto;
+ }
+}
+
+.inline-list {
+ margin: 0 auto 1.0625rem auto;
+ margin-left: -1.375rem;
+ margin-right: 0;
+ padding: 0;
+ list-style: none;
+ overflow: hidden;
+ > li {
+ list-style: none;
+ float: left;
+ margin-left: 1.375rem;
+ display: block;
+ > * {
+ display: block;
+ }
+ }
+}
+
+.text-left {
+ text-align: left !important;
+}
+
+.text-right {
+ text-align: right !important;
+}
+
+.text-center {
+ text-align: center !important;
+}
+
+.text-justify {
+ text-align: justify !important;
+}
+
+@media only screen and (max-width: 40em) {
+ .small-only-text-left {
+ text-align: left !important;
+ }
+ .small-only-text-right {
+ text-align: right !important;
+ }
+ .small-only-text-center {
+ text-align: center !important;
+ }
+ .small-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen {
+ .small-text-left {
+ text-align: left !important;
+ }
+ .small-text-right {
+ text-align: right !important;
+ }
+ .small-text-center {
+ text-align: center !important;
+ }
+ .small-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (max-width: 64em) {
+ .medium-only-text-left {
+ text-align: left !important;
+ }
+ .medium-only-text-right {
+ text-align: right !important;
+ }
+ .medium-only-text-center {
+ text-align: center !important;
+ }
+ .medium-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ .medium-text-left {
+ text-align: left !important;
+ }
+ .medium-text-right {
+ text-align: right !important;
+ }
+ .medium-text-center {
+ text-align: center !important;
+ }
+ .medium-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 64.063em) and (max-width: 90em) {
+ .large-only-text-left {
+ text-align: left !important;
+ }
+ .large-only-text-right {
+ text-align: right !important;
+ }
+ .large-only-text-center {
+ text-align: center !important;
+ }
+ .large-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 64.063em) {
+ .large-text-left {
+ text-align: left !important;
+ }
+ .large-text-right {
+ text-align: right !important;
+ }
+ .large-text-center {
+ text-align: center !important;
+ }
+ .large-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 90.063em) and (max-width: 120em) {
+ .xlarge-only-text-left {
+ text-align: left !important;
+ }
+ .xlarge-only-text-right {
+ text-align: right !important;
+ }
+ .xlarge-only-text-center {
+ text-align: center !important;
+ }
+ .xlarge-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 90.063em) {
+ .xlarge-text-left {
+ text-align: left !important;
+ }
+ .xlarge-text-right {
+ text-align: right !important;
+ }
+ .xlarge-text-center {
+ text-align: center !important;
+ }
+ .xlarge-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 120.063em) and (max-width: 99999999em) {
+ .xxlarge-only-text-left {
+ text-align: left !important;
+ }
+ .xxlarge-only-text-right {
+ text-align: right !important;
+ }
+ .xxlarge-only-text-center {
+ text-align: center !important;
+ }
+ .xxlarge-only-text-justify {
+ text-align: justify !important;
+ }
+}
+
+@media only screen and (min-width: 120.063em) {
+ .xxlarge-text-left {
+ text-align: left !important;
+ }
+ .xxlarge-text-right {
+ text-align: right !important;
+ }
+ .xxlarge-text-center {
+ text-align: center !important;
+ }
+ .xxlarge-text-justify {
+ text-align: justify !important;
+ }
+}
+
+/* Typography resets */
+
+div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, p, blockquote, th, td {
+ margin: 0;
+ padding: 0;
+}
+
+/* Default Link Styles */
+
+a {
+ color: #2ba6cb;
+ text-decoration: none;
+ line-height: inherit;
+ &:hover, &:focus {
+ color: #258faf;
+ outline: none;
+ }
+ img {
+ border: none;
+ }
+}
+
+/* Default paragraph styles */
+
+p {
+ font-family: inherit;
+ font-weight: normal;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ margin-bottom: 1.25rem;
+ text-rendering: optimizeLegibility;
+ &.lead {
+ font-size: 1.21875rem;
+ line-height: 1.4;
+ }
+ aside {
+ font-size: 0.875rem;
+ line-height: 1.35;
+ font-style: italic;
+ }
+}
+
+/* Default header styles */
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: normal;
+ font-style: normal;
+ color: #222;
+ text-rendering: optimizeLegibility;
+ margin-top: 0.2rem;
+ margin-bottom: 0.5rem;
+ line-height: 1.2;
+}
+
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small {
+ font-size: 60%;
+ color: #6f6f6f;
+ line-height: 0;
+}
+
+h1 {
+ font-size: 2.125rem;
+}
+
+h2 {
+ font-size: 1.6875rem;
+}
+
+h3 {
+ font-size: 1.375rem;
+}
+
+h4, h5 {
+ font-size: 1.125rem;
+}
+
+h6 {
+ font-size: 1rem;
+}
+
+.subheader {
+ line-height: 1.4;
+ color: #6f6f6f;
+ font-weight: normal;
+ margin-top: 0.2rem;
+ margin-bottom: 0.5rem;
+}
+
+hr {
+ border: solid #dddddd;
+ border-width: 1px 0 0;
+ clear: both;
+ margin: 1.25rem 0 1.1875rem;
+ height: 0;
+}
+
+/* Helpful Typography Defaults */
+
+em, i {
+ font-style: italic;
+ line-height: inherit;
+}
+
+strong, b {
+ font-weight: bold;
+ line-height: inherit;
+}
+
+small {
+ font-size: 60%;
+ line-height: inherit;
+}
+
+code {
+ font-family: Consolas, "Liberation Mono", Courier, monospace;
+ font-weight: bold;
+ color: #910b0e;
+}
+
+/* Lists */
+
+ul, ol, dl {
+ font-size: 0.9rem;
+ line-height: 1.6;
+ margin-bottom: 1.25rem;
+ list-style-position: outside;
+ font-family: inherit;
+}
+
+ul {
+ margin-left: 0;
+ &.bullets {
+ margin-left: 1.1rem;
+ li {
+ margin-left: 1.25rem;
+ margin-bottom: 0;
+ list-style: circle;
+ }
+ }
+ li {
+ margin-bottom: 0;
+ list-style: none;
+ }
+}
+
+/* Abbreviations */
+
+abbr, acronym {
+ text-transform: uppercase;
+ font-size: 90%;
+ color: #222222;
+ border-bottom: 1px dotted #dddddd;
+ cursor: help;
+}
+
+abbr {
+ text-transform: none;
+}
+
+/* Blockquotes */
+
+blockquote {
+ margin: 0 0 1.25rem;
+ padding: 0.5625rem 1.25rem 0 1.1875rem;
+ border-left: 1px solid #dddddd;
+ cite {
+ display: block;
+ font-size: 0.8125rem;
+ color: #555555;
+ &:before {
+ content: "\2014 \0020";
+ }
+ a {
+ color: #555555;
+ &:visited {
+ color: #555555;
+ }
+ }
+ }
+ line-height: 1.6;
+ color: #6f6f6f;
+ p {
+ line-height: 1.6;
+ color: #6f6f6f;
+ }
+}
+
+/* Microformats */
+
+.vcard {
+ display: inline-block;
+ margin: 0 0 1.25rem 0;
+ border: 1px solid #dddddd;
+ padding: 0.625rem 0.75rem;
+ li {
+ margin: 0;
+ display: block;
+ }
+ .fn {
+ font-weight: bold;
+ font-size: 0.9375rem;
+ }
+}
+
+.vevent {
+ .summary {
+ font-weight: bold;
+ }
+ abbr {
+ cursor: default;
+ text-decoration: none;
+ font-weight: bold;
+ border: none;
+ padding: 0 0.0625rem;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ h1, h2, h3, h4, h5, h6 {
+ line-height: 1.2;
+ }
+ h1 {
+ font-size: 2.55rem;
+ }
+ h2 {
+ font-size: 2.3125rem;
+ }
+ h3 {
+ font-size: 1.4875rem;
+ }
+ h4 {
+ font-size: 1.1375rem;
+ }
+}
+
+/*
+ * Print styles.
+ *
+ * Inlined to avoid required HTTP connection: www.phpied.com/delay-loading-your-print-css/
+ * Credit to Paul Irish and HTML5 Boilerplate (html5boilerplate.com)
+*/
+
+.print-only {
+ display: none !important;
+}
+
+@media print {
+ * {
+ background: transparent !important;
+ color: black !important;
+ /* Black prints faster: h5bp.com/s */
+ box-shadow: none !important;
+ text-shadow: none !important;
+ }
+ a {
+ text-decoration: underline;
+ &:visited {
+ text-decoration: underline;
+ }
+ &[href]:after {
+ content: " (" attr(href) ")";
+ }
+ }
+ abbr[title]:after {
+ content: " (" attr(title) ")";
+ }
+ .ir a:after {
+ content: "";
+ }
+ a {
+ &[href^="javascript:"]:after, &[href^="#"]:after {
+ content: "";
+ }
+ }
+ pre, blockquote {
+ border: 1px solid #999999;
+ page-break-inside: avoid;
+ }
+ thead {
+ display: table-header-group;
+ /* h5bp.com/t */
+ }
+ tr {
+ page-break-inside: avoid;
+ }
+ img {
+ page-break-inside: avoid;
+ max-width: 100% !important;
+ }
+ @page {
+ margin: 0.5cm;
+ }
+
+ p, h2, h3 {
+ orphans: 3;
+ widows: 3;
+ }
+ h2, h3 {
+ page-break-after: avoid;
+ }
+ .hide-on-print {
+ display: none !important;
+ }
+ .print-only {
+ display: block !important;
+ }
+ .hide-for-print {
+ display: none !important;
+ }
+ .show-for-print {
+ display: inherit !important;
+ }
+}
+
+.reveal-modal-bg {
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ background: black;
+ background: rgba(0, 0, 0, 0.45);
+ z-index: 99;
+ display: none;
+ top: 0;
+ left: 0;
+}
+
+dialog, .reveal-modal {
+ visibility: hidden;
+ display: none;
+ position: absolute;
+ z-index: 100;
+ width: 100vw;
+ top: 0;
+ left: 0;
+ background-color: white;
+ padding: 1.25rem;
+ border: solid 1px #666666;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
+}
+
+@media only screen and (max-width: 40em) {
+ dialog, .reveal-modal {
+ min-height: 100vh;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ left: 50%;
+ }
+}
+
+dialog {
+ .column, .columns {
+ min-width: 0;
+ }
+}
+
+.reveal-modal {
+ .column, .columns {
+ min-width: 0;
+ }
+}
+
+dialog > :first-child, .reveal-modal > :first-child {
+ margin-top: 0;
+}
+
+dialog > :last-child, .reveal-modal > :last-child {
+ margin-bottom: 0;
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ margin-left: -26%;
+ width: 50%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ top: 6.25rem;
+ }
+}
+
+dialog .close-reveal-modal, .reveal-modal .close-reveal-modal {
+ font-size: 2.5rem;
+ line-height: 1;
+ position: absolute;
+ top: 0.5rem;
+ right: 0.6875rem;
+ color: #aaaaaa;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+dialog[open] {
+ display: block;
+ visibility: visible;
+}
+
+@media only screen and (min-width: 40.063em) {
+ dialog, .reveal-modal {
+ padding: 1.875rem;
+ }
+ dialog.radius, .reveal-modal.radius {
+ border-radius: 3px;
+ }
+ dialog.round, .reveal-modal.round {
+ border-radius: 1000px;
+ }
+ dialog.collapse, .reveal-modal.collapse {
+ padding: 0;
+ }
+ dialog.full, .reveal-modal.full {
+ top: 0;
+ left: 0;
+ height: 100vh;
+ min-height: 100vh;
+ margin-left: 0 !important;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.tiny, .reveal-modal.tiny {
+ margin-left: -15%;
+ width: 30%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.small, .reveal-modal.small {
+ margin-left: -20%;
+ width: 40%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.medium, .reveal-modal.medium {
+ margin-left: -30%;
+ width: 60%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.large, .reveal-modal.large {
+ margin-left: -35%;
+ width: 70%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.xlarge, .reveal-modal.xlarge {
+ margin-left: -47.5%;
+ width: 95%;
+ }
+}
+
+@media only screen and (min-width: 40.063em) and (min-width: 40.063em) {
+ dialog.full, .reveal-modal.full {
+ margin-left: -50vw;
+ width: 100vw;
+ }
+}
+
+@media print {
+ dialog, .reveal-modal {
+ background: white !important;
+ }
+}
+
+.label {
+ font-weight: normal;
+ text-align: center;
+ text-decoration: none;
+ line-height: 1;
+ white-space: nowrap;
+ display: inline-block;
+ position: relative;
+ margin-bottom: inherit;
+ padding: 0.25rem 0.5rem 0.375rem;
+ font-size: 0.6875rem;
+ background-color: #2ba6cb;
+ color: white;
+ &.radius {
+ border-radius: 3px;
+ }
+ &.round {
+ border-radius: 1000px;
+ }
+ &.alert {
+ background-color: #c60f13;
+ color: white;
+ }
+ &.success {
+ background-color: #5da423;
+ color: white;
+ }
+ &.secondary {
+ background-color: #e9e9e9;
+ color: #333333;
+ }
+}
+
+button, .button, input[type=button] {
+ cursor: pointer;
+ margin: 0 0 1.25rem;
+ border: none;
+ position: relative;
+ text-decoration: none;
+ text-align: center;
+ -webkit-appearance: none;
+ display: inline-block;
+ padding: 0.4rem 1.1rem;
+ font-size: 0.9rem;
+ background-color: #2ba6cb;
+ border-color: #2285a2;
+ color: white;
+ transition: background-color 150ms ease-out;
+ @include border-radius(2px);
+ &:hover, &:focus {
+ background-color: #2285a2;
+ outline: none;
+ color: white;
+ }
+ &.large {
+ padding-top: 1.125rem;
+ padding-right: 2.25rem;
+ padding-bottom: 1.1875rem;
+ padding-left: 2.25rem;
+ font-size: 1.25rem;
+ }
+
+ &.small {
+ padding-top: 0.875rem;
+ padding-right: 1.75rem;
+ padding-bottom: 0.9375rem;
+ padding-left: 1.75rem;
+ font-size: 0.8125rem;
+ }
+
+ &.tiny {
+ padding-top: 0.625rem;
+ padding-right: 1.25rem;
+ padding-bottom: 0.6875rem;
+ padding-left: 1.25rem;
+ font-size: 0.6875rem;
+ }
+
+ &.expand {
+ padding-right: 0;
+ padding-left: 0;
+ width: 100%;
+ }
+
+ &.left-align {
+ text-align: left;
+ text-indent: 0.75rem;
+ }
+
+ &.right-align {
+ text-align: right;
+ padding-right: 0.75rem;
+ }
+
+ &.round {
+ border-radius: 1000px;
+ }
+
+ &.disabled, &[disabled] {
+ background-color: #2285a2;
+ border-color: #2285a2;
+ color: white;
+ cursor: default;
+ opacity: 0.5;
+ box-shadow: none;
+ &:hover, &:focus {
+ background-color: #2285a2;
+ opacity: 0.5;
+ }
+ }
+}
+
+
+@media only screen and (min-width: 40.063em) {
+ button, .button {
+ display: inline-block;
+ }
+}
+
+.keystroke, kbd {
+ background-color: #ededed;
+ border-color: #dddddd;
+ color: #222222;
+ border-style: solid;
+ border-width: 1px;
+ margin: 0;
+ font-family: "Consolas", "Menlo", "Courier", monospace;
+ font-size: inherit;
+ padding: 0.125rem 0.25rem 0;
+ border-radius: 3px;
+}
+
+
+
+/* We use this to get basic styling on all basic form elements */
+input[type="text"],
+input[type="password"],
+input[type="date"],
+input[type="datetime"],
+input[type="datetime-local"],
+input[type="month"],
+input[type="week"],
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="tel"],
+input[type="time"],
+input[type="url"],
+textarea {
+ -webkit-appearance: none;
+ background-color: white;
+ font-family: inherit;
+ border: 1px solid #cccccc;
+ color: rgba(0, 0, 0, 0.75);
+ display: block;
+ font-size: 0.875rem;
+ margin: 0 0 1rem 0;
+ padding: 0.4rem;
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+ input[type="text"]:focus,
+ input[type="password"]:focus,
+ input[type="date"]:focus,
+ input[type="datetime"]:focus,
+ input[type="datetime-local"]:focus,
+ input[type="month"]:focus,
+ input[type="week"]:focus,
+ input[type="email"]:focus,
+ input[type="number"]:focus,
+ input[type="search"]:focus,
+ input[type="tel"]:focus,
+ input[type="time"]:focus,
+ input[type="url"]:focus,
+ textarea:focus {}
+ input[type="text"]:focus,
+ input[type="password"]:focus,
+ input[type="date"]:focus,
+ input[type="datetime"]:focus,
+ input[type="datetime-local"]:focus,
+ input[type="month"]:focus,
+ input[type="week"]:focus,
+ input[type="email"]:focus,
+ input[type="number"]:focus,
+ input[type="search"]:focus,
+ input[type="tel"]:focus,
+ input[type="time"]:focus,
+ input[type="url"]:focus,
+ textarea:focus {
+ background: #fafafa;
+ border-color: #999999;
+ outline: none; }
+ input[type="text"][disabled], fieldset[disabled] input[type="text"],
+ input[type="password"][disabled], fieldset[disabled]
+ input[type="password"],
+ input[type="date"][disabled], fieldset[disabled]
+ input[type="date"],
+ input[type="datetime"][disabled], fieldset[disabled]
+ input[type="datetime"],
+ input[type="datetime-local"][disabled], fieldset[disabled]
+ input[type="datetime-local"],
+ input[type="month"][disabled], fieldset[disabled]
+ input[type="month"],
+ input[type="week"][disabled], fieldset[disabled]
+ input[type="week"],
+ input[type="email"][disabled], fieldset[disabled]
+ input[type="email"],
+ input[type="number"][disabled], fieldset[disabled]
+ input[type="number"],
+ input[type="search"][disabled], fieldset[disabled]
+ input[type="search"],
+ input[type="tel"][disabled], fieldset[disabled]
+ input[type="tel"],
+ input[type="time"][disabled], fieldset[disabled]
+ input[type="time"],
+ input[type="url"][disabled], fieldset[disabled]
+ input[type="url"],
+ textarea[disabled], fieldset[disabled]
+ textarea {
+ background-color: #dddddd; }
+ input[type="text"].radius,
+ input[type="password"].radius,
+ input[type="date"].radius,
+ input[type="datetime"].radius,
+ input[type="datetime-local"].radius,
+ input[type="month"].radius,
+ input[type="week"].radius,
+ input[type="email"].radius,
+ input[type="number"].radius,
+ input[type="search"].radius,
+ input[type="tel"].radius,
+ input[type="time"].radius,
+ input[type="url"].radius,
+ textarea.radius {
+ border-radius: 3px; }
+
+input[type="submit"] {
+ -webkit-appearance: none; }
+
+/* Respect enforced amount of rows for textarea */
+textarea[rows] {
+ height: auto; }
+
+/* Add height value for select elements to match text input height */
+select {
+ -webkit-appearance: none !important;
+ background-color: #fafafa;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iM3B4IiB2aWV3Qm94PSIwIDAgNiAzIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA2IDMiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwb2x5Z29uIHBvaW50cz0iNS45OTIsMCAyLjk5MiwzIC0wLjAwOCwwICIvPjwvc3ZnPg==");
+ background-repeat: no-repeat;
+ background-position: 97% center;
+ border: 1px solid #cccccc;
+ padding: 0.5rem;
+ font-size: 0.875rem;
+ color: rgba(0, 0, 0, 0.75);
+ line-height: normal;
+ border-radius: 0;
+}
+ select.radius {
+ border-radius: 3px; }
+ select:hover {
+ background-color: #f3f3f3;
+ border-color: #999999; }
+
+/* Adjust margin for form elements below */
+input[type="file"],
+input[type="checkbox"],
+input[type="radio"],
+select {
+ margin: 0 0 1rem 0; }
+
+input[type="checkbox"] + label,
+input[type="radio"] + label {
+ display: inline-block;
+
+ margin-left: 0.5rem;
+ margin-right: 1rem;
+ margin-bottom: 0;
+ vertical-align: baseline; }
+
+/* Normalize file input width */
+input[type="file"] {
+ width: 100%; }
diff --git a/web-ui/public/scss/vendor/_reset.scss b/web-ui/public/scss/vendor/_reset.scss
new file mode 100644
index 00000000..55f8d054
--- /dev/null
+++ b/web-ui/public/scss/vendor/_reset.scss
@@ -0,0 +1,421 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif;
+ /* 1 */
+ -ms-text-size-adjust: 100%;
+ /* 2 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio, canvas, progress, video {
+ display: inline-block;
+ /* 1 */
+ vertical-align: baseline;
+ /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden], template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background: transparent;
+ &:active, &:hover {
+ outline: 0;
+ }
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b, strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code, kbd, pre, samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button, input, optgroup, select, textarea {
+ color: inherit;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+ margin: 0;
+ /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+ text-transform: none;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button, html input[type="button"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */
+}
+
+input {
+ &[type="reset"], &[type="submit"] {
+ -webkit-appearance: button;
+ /* 2 */
+ cursor: pointer;
+ /* 3 */
+ }
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled], html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+input {
+ &::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+ }
+ line-height: normal;
+ &[type="checkbox"], &[type="radio"] {
+ box-sizing: border-box;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+ }
+ &[type="number"] {
+ &::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
+ height: auto;
+ }
+ }
+ &[type="search"] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ /* 2 */
+ box-sizing: content-box;
+ &::-webkit-search-cancel-button, &::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ }
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0;
+ /* 1 */
+ padding: 0;
+ /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td, th {
+ padding: 0;
+}
diff --git a/web-ui/public/scss/vendor/_scut.scss b/web-ui/public/scss/vendor/_scut.scss
new file mode 100644
index 00000000..3e16fa65
--- /dev/null
+++ b/web-ui/public/scss/vendor/_scut.scss
@@ -0,0 +1,1518 @@
+/*
+* Scut, a collection of Sass utilities
+* to ease and improve our implementations of common style-code patterns.
+* v1.3.0
+* Docs at http://davidtheclark.github.io/scut
+*/
+
+@mixin scut-clearfix {
+
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+
+}
+
+%scut-clearfix {
+ @include scut-clearfix;
+}
+@mixin scut-list-unstyled(
+ $no-margin: true
+) {
+
+ list-style-type: none;
+ padding-left: 0;
+
+ @if $no-margin {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+}
+
+%scut-list-unstyled {
+ @include scut-list-unstyled();
+}
+// Depends on `list-unstyled` and `clearfix`.
+
+@mixin scut-list-floated (
+ $space: false,
+ $dir: left,
+ $no-margin: true
+) {
+
+ @include scut-list-unstyled($no-margin);
+ @include scut-clearfix;
+
+ & > li {
+ float: $dir;
+ }
+
+ @if $space {
+ & > li + li {
+ margin-#{$dir}: $space;
+ }
+ }
+
+}
+
+%scut-list-floated {
+ @include scut-list-floated;
+}
+
+@function scut-autoOrValue ($val) {
+ @if $val == a or $val == auto {
+ @return auto;
+ }
+ @else {
+ @return $val;
+ }
+}
+
+@mixin scut-coords (
+ $coordinates: n n n n
+) {
+
+ $top: nth($coordinates, 1);
+ $right: nth($coordinates, 2);
+ $bottom: nth($coordinates, 3);
+ $left: nth($coordinates, 4);
+
+ @if $top != n {
+ top: scut-autoOrValue($top);
+ }
+ @if $right != n {
+ right: scut-autoOrValue($right);
+ }
+ @if $bottom != n {
+ bottom: scut-autoOrValue($bottom);
+ }
+ @if $left != n {
+ left: scut-autoOrValue($left);
+ }
+
+}
+@function scut-strip-unit (
+ $num
+) {
+
+ @return $num / ($num * 0 + 1);
+
+}
+// Depends on `scut-strip-unit`.
+
+$scut-em-base: 16 !default;
+
+@function scut-em (
+ $pixels,
+ $base: $scut-em-base
+) {
+
+ // $base could be in em or px (no unit = px).
+ // Adjust accordingly to create a $divisor that
+ // serves as context for $pixels.
+ $multiplier: if(unit($base) == em, 16, 1);
+ $divisor: scut-strip-unit($base) * $multiplier;
+
+ $em-vals: ();
+ @each $val in $pixels {
+ $val-in-ems: (scut-strip-unit($val) / $divisor) * 1em;
+ $em-vals: append($em-vals, $val-in-ems);
+ }
+
+ @if length($em-vals) == 1 {
+ // return a single value instead of a list,
+ // so it can be used in calculations
+ @return nth($em-vals, 1);
+ }
+ @else {
+ @return $em-vals;
+ }
+
+}
+// Depends on `scut-strip-unit`.
+
+$scut-rem-base: 16 !default;
+
+@function scut-rem (
+ $pixels
+) {
+
+ $rem-vals: ();
+ @each $val in $pixels {
+ $val-in-rems: scut-strip-unit($val) / $scut-rem-base * 1rem;
+ $rem-vals: append($rem-vals, $val-in-rems);
+ }
+
+ @if length($rem-vals) == 1 {
+ // return a single value instead of a list,
+ // so it can be used in calculations
+ @return nth($rem-vals, 1);
+ }
+ @else {
+ @return $rem-vals;
+ }
+
+}
+@mixin scut-border (
+ $style,
+ $sides: n y
+) {
+
+ @if length($sides) == 2 {
+ @if nth($sides, 1) != n {
+ border-top: $style;
+ border-bottom: $style;
+ }
+ @if nth($sides, 2) != n {
+ border-left: $style;
+ border-right: $style;
+ }
+ }
+
+ @else if length($sides) == 4 {
+ @if nth($sides, 1) != n {
+ border-top: $style;
+ }
+ @if nth($sides, 2) != n {
+ border-right: $style;
+ }
+ @if nth($sides, 3) != n {
+ border-bottom: $style;
+ }
+ @if nth($sides, 4) != n {
+ border-left: $style;
+ }
+ }
+
+ @else {
+ @warn "Scut-border requires a $sides argument of 2 or 4 values."
+ }
+
+}
+@mixin scut-circle (
+ $size,
+ $color: inherit
+) {
+
+ border-radius: 50%;
+ display: inline-block;
+
+ @if $color == inherit {
+ // If user wants to inherit the color,
+ // take advantage of the fact that border
+ // color defaults to the text color of the element.
+ border-width: $size / 2;
+ border-style: solid;
+ height: 0;
+ width: 0;
+ }
+ @else {
+ // Otherwise, just use background-color.
+ background-color: $color;
+ height: $size;
+ width: $size;
+ }
+
+}
+@mixin scut-color-swap (
+ $off,
+ $on,
+ $duration: 0,
+ $bg: false
+) {
+
+ $transition-properties: null;
+ $off-is-list: type-of($off) == list;
+ $on-is-list: type-of($on) == list;
+
+ // If $off IS a list,
+ // assign color and background-color.
+ @if $off-is-list {
+ color: nth($off, 1);
+ background-color: nth($off, 2);
+ $transition-properties: background-color, color;
+ }
+
+ // If $off IS NOT a list and $bg is TRUE,
+ // assign background-color.
+ @else if $bg and not($off-is-list) {
+ background-color: $off;
+ $transition-properties: background-color;
+ }
+
+ // If $off IS NOT a list and $bg is FALSE,
+ // assign color.
+ @else {
+ color: $off;
+ $transition-properties: color;
+ }
+
+ // Only set-up transition if $duration != 0.
+ @if $duration != 0 {
+ transition-property: $transition-properties;
+ transition-duration: $duration;
+ }
+
+ &:hover,
+ &:focus {
+
+ // $on is treated the same as $off, above.
+ @if $on-is-list {
+ color: nth($on, 1);
+ background-color: nth($on, 2);
+ }
+
+ @else if $bg and not($on-is-list) {
+ background-color: $on;
+ }
+
+ @else {
+ color: $on;
+ }
+ }
+
+}
+@mixin scut-hd-bp (
+ $ratio: 1.3
+) {
+
+ @media (-o-min-device-pixel-ratio: ($ratio / 1)),
+ (-webkit-min-device-pixel-ratio: $ratio),
+ (min-resolution: (round(96 * $ratio) * 1dpi)) {
+ @content;
+ }
+
+}
+
+@mixin scut-hide-visually {
+
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+
+}
+
+%scut-hide-visually {
+ @include scut-hide-visually;
+}
+@mixin scut-image-replace {
+
+ text-indent: 102%;
+ white-space: nowrap;
+ overflow: hidden;
+ padding: 0;
+
+}
+
+%scut-image-replace {
+ @include scut-image-replace;
+}
+
+// Depends on scut-rem and scut-strip-unit
+
+@mixin scut-rem-fallback (
+ $pixels,
+ $property: font-size
+) {
+
+ $px-vals: null;
+ @each $val in $pixels {
+ $val-in-px: scut-strip-unit($val) * 1px;
+ $px-vals: append($px-vals, $val-in-px);
+ }
+ $rem-vals: scut-rem($pixels);
+
+ #{$property}: $px-vals;
+ #{$property}: $rem-vals;
+
+}
+@mixin scut-reset-border-box {
+ // Make everything a border-box, because why not?
+ html {
+ box-sizing: border-box;
+ }
+ *, *:before, *:after {
+ box-sizing: inherit;
+ }
+}
+
+@mixin scut-reset-antialias {
+ // Antialias!
+ body {
+ -webkit-font-smoothing: antialiased;
+ }
+ *, *:before, *:after {
+ -webkit-font-smoothing: inherit;
+ }
+}
+
+@mixin scut-reset-semanticize {
+ // Make headers and <b> semantic, not presentational.
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-size: 1em;
+ font-weight: normal;
+ margin: 0;
+ }
+ b {
+ font-weight: normal;
+ }
+}
+
+@mixin scut-reset-pointer {
+ // Clickable form elements should have a pointer.
+ label,
+ select,
+ option,
+ button {
+ cursor: pointer;
+ }
+}
+
+@mixin scut-reset-form {
+ fieldset {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ }
+ textarea {
+ resize: vertical;
+ }
+}
+
+@mixin scut-reset-button {
+ // Reset default button styles, which are never used.
+ button,
+ input[type="button"],
+ input[type="submit"],
+ input[type="reset"] {
+ background: transparent;
+ border: 0;
+ color: inherit;
+ font: inherit;
+ margin: 0;
+ padding: 0;
+ width: auto;
+ -webkit-appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ &::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ }
+}
+
+@mixin scut-reset-paragraph {
+ // Some paragraph margins just get in the way.
+ p:first-of-type {
+ margin-top: 0;
+ }
+ p:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+@mixin scut-reset-media {
+ // You want these elements fluid, probably.
+ img,
+ video {
+ max-width: 100%;
+ height: auto;
+ }
+}
+
+@mixin scut-reset-figure {
+ // Remove default margins.
+ figure {
+ margin: 0;
+ }
+}
+
+// Call them all, minus exclusions!
+@mixin scut-reset ($exclude: false) {
+ @if not(index($exclude, border-box)) {
+ @include scut-reset-border-box;
+ }
+ @if not(index($exclude, antialias)) {
+ @include scut-reset-antialias;
+ }
+ @if not(index($exclude, semanticize)) {
+ @include scut-reset-semanticize;
+ }
+ @if not(index($exclude, pointer)) {
+ @include scut-reset-pointer;
+ }
+ @if not(index($exclude, form)) {
+ @include scut-reset-form;
+ }
+ @if not(index($exclude, button)) {
+ @include scut-reset-button;
+ }
+ @if not(index($exclude, paragraph)) {
+ @include scut-reset-paragraph;
+ }
+ @if not(index($exclude, media)) {
+ @include scut-reset-media;
+ }
+ @if not(index($exclude, figure)) {
+ @include scut-reset-figure;
+ }
+}
+
+@mixin scut-selected (
+ $active: false
+) {
+
+ @if $active {
+ &:hover,
+ &:focus,
+ &:active {
+ @content;
+ }
+ }
+ @else {
+ &:hover,
+ &:focus {
+ @content;
+ }
+ }
+
+}
+@mixin scut-triangle (
+ $direction: right,
+ $size: 0.75em,
+ $color: inherit
+) {
+
+ display: inline-block;
+ height: 0;
+ width: 0;
+ // For improved appearance in some Webkit browsers
+ -webkit-transform: rotate(360deg);
+
+ // Set up some variables
+ $width: null;
+ $height: null;
+ $border-widths: null;
+
+ @if type-of($size) == list {
+ $width: nth($size, 1);
+ $height: nth($size, 2);
+ }
+ @else {
+ $width: $size;
+ $height: $size;
+ }
+
+ @if ($direction == up) or ($direction == down) {
+ // For up and down, width gets two borders but height only one,
+ // so divide second border-width value by 2
+ $border-widths: $height ($width / 2);
+ }
+ @else if ($direction == right) or ($direction == left) {
+ // For right and left, height gets two borders but width only one,
+ // so divide first border-width value by 2
+ $border-widths: ($height / 2) $width;
+ }
+ @else {
+ // For right triangles (the rest), both sides get two borders,
+ // so divide both by 2
+ $border-widths: ($height / 2) ($width / 2);
+ }
+
+ border-width: $border-widths;
+ border-style: solid;
+
+
+ // STANDARD TRIANGLES
+
+ @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) {
+ border-color: transparent;
+ @if $direction == up {
+ border-bottom-color: $color;
+ border-top-width: 0;
+ }
+ @else if $direction == right {
+ border-left-color: $color;
+ border-right-width: 0;
+ }
+ @else if $direction == down {
+ border-top-color: $color;
+ border-bottom-width: 0;
+ }
+ @else if $direction == left {
+ border-right-color: $color;
+ border-left-width: 0;
+ }
+ }
+
+
+ // CORNER TRIANGLES
+
+ @else if ($direction == top-right) or ($direction == top-left) {
+ border-top-color: $color;
+ border-bottom-color: transparent;
+ @if $direction == top-right {
+ border-left-color: transparent;
+ border-right-color: $color;
+ }
+ @else if $direction == top-left {
+ border-left-color: $color;
+ border-right-color: transparent;
+ }
+ }
+
+ @else if ($direction == bottom-right) or ($direction == bottom-left) {
+ border-top-color: transparent;
+ border-bottom-color: $color;
+ @if $direction == bottom-right {
+ border-left-color: transparent;
+ border-right-color: $color;
+ }
+ @else if $direction == bottom-left {
+ border-left-color: $color;
+ border-right-color: transparent;
+ }
+ }
+
+}
+
+%scut-triangle {
+ @include scut-triangle;
+}
+@mixin scut-center-absolutely (
+ $dimensions
+) {
+
+ $width: nth($dimensions, 1);
+ $height: nth($dimensions, 2);
+
+ position: absolute;
+
+ @if $width != n {
+ width: $width;
+ left: 50%;
+ margin-left: (-$width / 2);
+ }
+
+ @if $height != n {
+ height: $height;
+ top: 50%;
+ margin-top: (-$height / 2);
+ }
+
+}
+@mixin scut-center-block (
+ $max-width: false
+) {
+
+ margin-left: auto;
+ margin-right: auto;
+ @if $max-width {
+ max-width: $max-width;
+ }
+
+}
+
+%scut-center-block {
+ @include scut-center-block;
+}
+
+@mixin scut-center-transform (
+ $axis: false // or x or y
+) {
+
+ position: absolute;
+
+ @if $axis != x {
+ top: 50%;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+
+ @if $axis != y {
+ left: 50%;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ $translate-val: null;
+
+ @if not($axis) {
+ $translate-val: translate(-50%, -50%);
+ }
+ @else if $axis != x {
+ $translate-val: translateY(-50%);
+ }
+ @else if $axis != y {
+ $translate-val: translateX(-50%);
+ }
+
+ -webkit-transform: $translate-val;
+ -ms-transform: $translate-val;
+ transform: $translate-val;
+}
+
+%scut-center-transform {
+ @include scut-center-transform;
+}
+
+%scut-center-transform-x {
+ @include scut-center-transform(x);
+}
+
+%scut-center-transform-y {
+ @include scut-center-transform(y);
+}
+
+@mixin scut-fill (
+ $width-height: false
+) {
+
+ position: absolute;
+ left: 0;
+ top: 0;
+ @if $width-height {
+ width: 100%;
+ height: 100%;
+ }
+ @else {
+ right: 0;
+ bottom: 0;
+ }
+
+}
+
+%scut-fill {
+ @include scut-fill;
+}
+@mixin scut-list-custom (
+ $content: "\2022",
+ $marker-width: 0.75em,
+ $pad: 0,
+ $no-margin: false
+) {
+
+ $content-val: null;
+ $counter: index($content, count);
+ @if $counter {
+ @if length($content) == 3 {
+ $content-val: counter(scutlistcounter, nth($content, 3))nth($content,2);
+ }
+ @else if length($content) == 2 {
+ $content-val: counter(scutlistcounter)nth($content,2);
+ }
+ @else {
+ $content-val: counter(scutlistcounter);
+ }
+ }
+ @else {
+ $content-val: $content;
+ }
+
+ padding-left: $marker-width + $pad;
+ list-style-type: none;
+
+ @if $no-margin {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ & > li {
+ position: relative;
+ @if $counter {
+ counter-increment: scutlistcounter;
+ }
+ &:before {
+ content: $content-val;
+ display: block;
+ position: absolute;
+ top: 0;
+ left: -$marker-width;
+ width: $marker-width;
+ @content;
+ }
+ }
+
+}
+// Depends on `list-floated`, which depends in turn on `list-unstyled` and `clearfix`.
+
+@mixin scut-list-divided (
+ $divider: "|",
+ $space: 0.5em,
+ $dir: left,
+ $height: false,
+ $no-margin: true
+) {
+
+ @include scut-list-floated($dir: $dir, $no-margin: $no-margin);
+
+ $pseudo: if($dir == left, 'before', 'after');
+
+ // If an explicit height is passed,
+ // things are different: All <li>s
+ // need the pseudo-element (to force height),
+ // but the first's must be hidden.
+
+ @if $height {
+ & > li {
+ height: $height;
+ }
+ & > li:#{$pseudo} {
+ height: $height;
+ content: $divider;
+ display: inline-block;
+ vertical-align: middle;
+ @content;
+ }
+ & > li:first-child:#{$pseudo} {
+ width: 0;
+ overflow: hidden;
+ }
+ }
+
+ & > li + li:#{$pseudo} {
+ @if not($height) {
+ content: $divider;
+ display: inline-block;
+ @content;
+ }
+ margin-left: $space;
+ margin-right: $space;
+ }
+
+}
+
+%scut-list-bar {
+ @include scut-list-divided;
+}
+
+%scut-list-breadcrumb {
+ @include scut-list-divided("/");
+}
+// Depends on `list-unstyled`.
+
+@mixin scut-list-inline (
+ $space: false,
+ $no-margin: true
+) {
+
+ @include scut-list-unstyled($no-margin);
+
+ & > li {
+ display: inline-block;
+ }
+
+ @if $space {
+ & > li + li {
+ margin-left: $space;
+ }
+ }
+
+}
+
+%scut-list-inline {
+ @include scut-list-inline;
+}
+// Depends on `list-unstyled`.
+
+@mixin scut-list-punctuated (
+ $divider: ", ",
+ $display: inline,
+ $no-margin: true
+) {
+
+ @include scut-list-unstyled($no-margin);
+
+ & > li {
+ display: $display;
+ &:not(:last-child):after {
+ content: $divider;
+ }
+ }
+
+}
+
+%scut-list-comma {
+ @include scut-list-punctuated;
+}
+@mixin scut-margin (
+ $margin
+) {
+
+ @if length($margin) == 1 and $margin != n {
+ margin-top: $margin;
+ margin-right: $margin;
+ margin-bottom: $margin;
+ margin-left: $margin;
+ }
+
+ @if length($margin) == 2 {
+ $margin-y: nth($margin, 1);
+ $margin-x: nth($margin, 2);
+ @if $margin-y != n {
+ margin-top: $margin-y;
+ margin-bottom: $margin-y;
+ }
+ @if $margin-x != n {
+ margin-left: $margin-x;
+ margin-right: $margin-x;
+ }
+ }
+
+ @if length($margin) == 3 {
+ $margin-y-top: nth($margin, 1);
+ $margin-x: nth($margin, 2);
+ $margin-y-bottom: nth($margin, 3);
+ @if $margin-y-top != n {
+ margin-top: $margin-y-top;
+ }
+ @if $margin-x != n {
+ margin-right: $margin-x;
+ margin-left: $margin-x;
+ }
+ @if $margin-y-bottom != n {
+ margin-bottom: $margin-y-bottom;
+ }
+ }
+
+ @if length($margin) == 4 {
+ $margin-top: nth($margin, 1);
+ $margin-right: nth($margin, 2);
+ $margin-bottom: nth($margin, 3);
+ $margin-left: nth($margin, 4);
+ @if $margin-top != n {
+ margin-top: $margin-top;
+ }
+ @if $margin-right != n {
+ margin-right: $margin-right;
+ }
+ @if $margin-bottom != n {
+ margin-bottom: $margin-bottom;
+ }
+ @if $margin-left != n {
+ margin-left: $margin-left;
+ }
+ }
+
+}
+@mixin scut-padding (
+ $padding
+) {
+
+ @if length($padding) == 1 and $padding != n {
+ padding-top: $padding;
+ padding-right: $padding;
+ padding-bottom: $padding;
+ padding-left: $padding;
+ }
+
+ @if length($padding) == 2 {
+ $padding-y: nth($padding, 1);
+ $padding-x: nth($padding, 2);
+ @if $padding-y != n {
+ padding-top: $padding-y;
+ padding-bottom: $padding-y;
+ }
+ @if $padding-x != n {
+ padding-left: $padding-x;
+ padding-right: $padding-x;
+ }
+ }
+
+ @if length($padding) == 3 {
+ $padding-y-top: nth($padding, 1);
+ $padding-x: nth($padding, 2);
+ $padding-y-bottom: nth($padding, 3);
+ @if $padding-y-top != n {
+ padding-top: $padding-y-top;
+ }
+ @if $padding-x != n {
+ padding-right: $padding-x;
+ padding-left: $padding-x;
+ }
+ @if $padding-y-bottom != n {
+ padding-bottom: $padding-y-bottom;
+ }
+ }
+
+ @if length($padding) == 4 {
+ $padding-top: nth($padding, 1);
+ $padding-right: nth($padding, 2);
+ $padding-bottom: nth($padding, 3);
+ $padding-left: nth($padding, 4);
+ @if $padding-top != n {
+ padding-top: $padding-top;
+ }
+ @if $padding-right != n {
+ padding-right: $padding-right;
+ }
+ @if $padding-bottom != n {
+ padding-bottom: $padding-bottom;
+ }
+ @if $padding-left != n {
+ padding-left: $padding-left;
+ }
+ }
+}
+// Depends on `positioning-coordinates`.
+
+@mixin scut-absolute (
+ $coordinates: 0 n n 0
+) {
+
+ position: absolute;
+ @include scut-coords($coordinates);
+
+}
+
+%scut-absolute {
+ @include scut-absolute;
+}
+// Depends on `positioning-coordinates`.
+
+@mixin scut-fixed (
+ $coordinates: 0 n n 0
+) {
+
+ position: fixed;
+ @include scut-coords($coordinates);
+
+}
+
+%scut-fixed {
+ @include scut-fixed;
+}
+// Depends on `positioning-coordinates`.
+
+@mixin scut-relative (
+ $coordinates: n n n n
+) {
+
+ position: relative;
+ @include scut-coords($coordinates);
+
+}
+@mixin scut-ratio-box (
+ $ratio: 1/1
+) {
+
+ overflow: hidden;
+ position: relative;
+
+ // The container's height, as a percentage of the
+ // container's width, is set by assigning
+ // padding-top to a pseudo-element.
+ &:before {
+ content: "";
+ display: block;
+ height: 0;
+ padding-top: (1 / $ratio) * 100%;
+ }
+
+}
+
+%scut-ratio-box {
+ @include scut-ratio-box;
+}
+@mixin scut-size(
+ $size
+) {
+
+ @if length($size) == 1 {
+ width: $size;
+ height: $size;
+ }
+ @else if length($size) == 2 {
+ width: nth($size, 1);
+ height: nth($size, 2);
+ }
+
+}
+@mixin scut-sticky-footer-fixed (
+ $height,
+ $wrapper: ".wrapper",
+ $footer: ".scut-sticky"
+) {
+
+ html,
+ body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ }
+
+ #{$wrapper} {
+ min-height: 100%;
+ margin-bottom: -$height;
+ &:after {
+ content: "";
+ display: block;
+ }
+ }
+
+ #{$wrapper}:after,
+ #{$footer} {
+ height: $height;
+ }
+
+}
+
+// deprecated
+@mixin scut-sticky-footer (
+ $height,
+ $wrapper: ".wrapper",
+ $footer: ".scut-sticky"
+){
+ @include scut-sticky-footer-fixed($height, $wrapper, $footer);
+}
+@mixin scut-sticky-footer-fluid (
+ $wrapper: ".wrapper",
+ $footer: ".scut-sticky"
+) {
+
+ html,
+ body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ }
+
+ #{$wrapper} {
+ display: table;
+ height: 100%;
+ width: 100%;
+ }
+
+ #{$footer} {
+ display: table-row;
+ height: 1px;
+ }
+
+}
+@mixin scut-vcenter-ib (
+ $inner...
+) {
+
+ // The inner element is vertically centered
+ // by middle-aligning it with an inline pseudo-element
+ // whose height is 100%.
+
+ &:before {
+ content: "";
+ height: 100%;
+ display: inline-block;
+ vertical-align: middle;
+ // A small negative right margin is set
+ // to account for the default
+ // word-spacing of inline-block.
+ margin-right: -0.25em;
+ }
+
+ $inner: if(length($inner) == 0, ".scut-inner", $inner);
+ @each $cell-selector in $inner {
+ $cell-selector: unquote($cell-selector);
+ & > #{$cell-selector} {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+
+}
+
+%scut-vcenter-ib {
+ @include scut-vcenter-ib;
+}
+
+@mixin scut-vcenter-lh (
+ $height
+) {
+
+ height: $height;
+ line-height: $height;
+
+}
+@mixin scut-vcenter-td (
+ $inner...
+) {
+
+ display: table;
+
+ $inner: if(length($inner) == 0, ".scut-inner", $inner);
+ @each $cell-selector in $inner {
+ $cell-selector: unquote($cell-selector);
+ & > #{$cell-selector} {
+ display: table-cell;
+ vertical-align: middle;
+ }
+ }
+
+}
+
+
+%scut-vcenter-td {
+ @include scut-vcenter-td;
+}
+
+// Depends on scut-center-transform
+
+@mixin scut-vcenter-tt () {
+ @include scut-center-transform(y);
+}
+
+%scut-vcenter-tt {
+ @include scut-vcenter-tt;
+}
+// space
+$scut-space: "\0020";
+// non-breaking space
+$scut-nbsp: "\00a0";
+
+// quotation mark
+$scut-quot: "\0022";
+// left single curly quote
+$scut-lsquo: "\2018";
+// right single curly quote
+$scut-rsquo: "\2019";
+// left double curly quote
+$scut-ldquo: "\201C";
+// right double curly quote
+$scut-rdquo: "\201D";
+// left single angle quote (guillemet)
+$scut-lsaquo: "\2039";
+// right single angle quote (guillemet)
+$scut-rsaquo: "\203A";
+// left double angle quote (guillemet)
+$scut-laquo: "\00ab";
+// right double angle quote (guillemet)
+$scut-raquo: "\00bb";
+
+// em dash (mutton)
+$scut-mdash: "\2014";
+// en dash (nut)
+$scut-ndash: "\2013";
+// hyphen
+$scut-hyphen: "\2010";
+
+// ampersand
+$scut-amp: "\0026";
+// greater than
+$scut-gt: "\003e";
+// less than
+$scut-lt: "\003c";
+// times
+$scut-times: "\00D7";
+// big times
+$scut-bigtimes: "\2715";
+// checkmark
+$scut-checkmark: "\2713";
+
+// section sign (double S, hurricane, sectional symbol, the legal doughnut, signum sectionis)
+$scut-sect: "\00a7";
+// paragraph symbol (pilcrow)
+$scut-para: "\00b6";
+
+// middot (interpunct, interpoint)
+$scut-middot: "\00b7";
+// o-slash (slashed o)
+$scut-oslash: "\00f8";
+// bullet
+$scut-bull: "\2022";
+// white bullet
+$scut-whibull: "\25E6";
+// horizontal ellipsis
+$scut-hellip: "\2026";
+// vertical ellipsis
+$scut-vellip: "\22EE";
+// midline horizontal ellipsis
+$scut-midhellip: "\22EF";
+
+// up-pointing triangle
+$scut-utri: "\25b2";
+// down-pointing triangle
+$scut-dtri: "\25bc";
+// left-pointing triangle
+$scut-ltri: "\25c0";
+// right-pointing triangle
+$scut-rtri: "\25b6";
+// up-pointing small triangle
+$scut-ustri: "\25b4";
+// down-pointing small triangle
+$scut-dstri: "\25be";
+// left-pointing small triangle
+$scut-lstri: "\25c2";
+// right-pointing small triangle
+$scut-rstri: "\25b8";
+// diamond
+$scut-diamond: "\25c6";
+// fisheye
+$scut-fisheye: "\25c9";
+// bullseye
+$scut-bullseye: "\25ce";
+// circle
+$scut-circle: "\25cf";
+// white circle
+$scut-whitecircle: "\25cb";
+// square
+$scut-square: "\25a0";
+// white square
+$scut-whitesquare: "\25a1";
+// small square
+$scut-ssquare: "\25aa";
+// small white square
+$scut-swhitesquare: "\25ab";
+@function main-src($formats, $file-path, $font-family) {
+ // Return the list of `src` values, in order, that
+ // a good `@font-face` will need, including only
+ // those formats specified in the list `$formats`.
+ $result: ();
+ @if index($formats, eot) {
+ $eot-val: url('#{$file-path}.eot?#iefix') format('embedded-opentype');
+ $result: append($result, $eot-val, comma);
+ }
+ @if index($formats, woff2) {
+ $woff2-val: url('#{$file-path}.woff2') format('woff2');
+ $result: append($result, $woff2-val, comma);
+ }
+ @if index($formats, woff) {
+ $woff-val: url('#{$file-path}.woff') format('woff');
+ $result: append($result, $woff-val, comma);
+ }
+ @if index($formats, ttf) {
+ $ttf-val: url('#{$file-path}.ttf') format('truetype');
+ $result: append($result, $ttf-val, comma);
+ }
+ @if index($formats, svg) {
+ $svg-val: url('#{$file-path}.svg##{$font-family}') format('svg');
+ $result: append($result, $svg-val, comma);
+ }
+ @return $result;
+}
+
+@mixin scut-font-face (
+ $font-family,
+ $file-path,
+ $weight: normal,
+ $style: normal,
+ $formats: eot woff2 woff ttf svg
+) {
+
+ @if index('italic' 'oblique', $weight) {
+ $style: $weight;
+ $weight: normal;
+ }
+
+ @font-face {
+ font-family: $font-family;
+ font-weight: $weight;
+ font-style: $style;
+
+ @if index($formats, eot) {
+ src: url('#{$file-path}.eot');
+ }
+ src: main-src($formats, $file-path, $font-family);
+ }
+
+}
+
+@mixin scut-hanging-indent (
+ $indent: 1em
+) {
+
+ // padding-left creates the indent,
+ // while text-indent pulls the first line
+ // back to the edge.
+
+ padding-left: $indent;
+ text-indent: -$indent;
+
+}
+
+%scut-hanging-indent {
+ @include scut-hanging-indent;
+}
+@mixin scut-indented-ps (
+ $indent: 1.5em,
+ $no-first-indent: true
+) {
+
+ p {
+ margin: 0;
+ text-indent: $indent;
+ }
+
+ @if $no-first-indent {
+ p:first-of-type {
+ text-indent: 0;
+ }
+ }
+
+}
+
+%scut-indented-ps {
+ @include scut-indented-ps;
+}
+@mixin scut-key-val (
+ $divider: ":",
+ $pad: 0.25em,
+ $indent: 1em,
+ $spacing: 0,
+ $pad-left: 0
+) {
+
+ & > dt {
+ clear: both;
+ float: left;
+ &:after {
+ content: $divider;
+ margin-right: $pad;
+ @if $pad-left != 0 {
+ margin-left: $pad-left;
+ }
+ }
+ }
+
+ & > dd {
+ margin-left: $indent;
+ @if $spacing != 0 {
+ margin-bottom: $spacing;
+ }
+ }
+
+}
+
+%scut-key-val {
+ @include scut-key-val;
+}
+@mixin scut-link-bb (
+ $color: inherit,
+ $style: solid,
+ $width: 1px
+) {
+
+ text-decoration: none;
+
+ border-bottom-width: $width;
+ border-bottom-style: $style;
+ @if $color != inherit {
+ border-bottom-color: $color;
+ }
+
+}
+
+%scut-link-bb {
+ @include scut-link-bb;
+}
+// SCUT LINK UNSTYLED
+// http://davidtheclark.github.io/scut/#link-unstyled
+
+@mixin scut-link-unstyled() {
+
+ text-decoration: none;
+ color: inherit;
+
+}
+
+%scut-link-unstyled {
+ @include scut-link-unstyled();
+}
+
+@mixin scut-reverse-italics (
+ $elements: null
+) {
+
+ $element-list: em, cite, i;
+ font-style: italic;
+ #{join($element-list, $elements)} {
+ font-style: normal;
+ }
+
+}
+
+%scut-reverse-italics {
+ @include scut-reverse-italics;
+}
+
+@mixin scut-side-lined (
+ $height: 1px,
+ $space: 0.5em,
+ $color: inherit,
+ $style: solid,
+ $v-adjust: false,
+ $double: false
+) {
+
+ display: block;
+ overflow: hidden;
+ text-align: center;
+
+ &:before,
+ &:after {
+ content: "";
+ display: inline-block;
+ vertical-align: middle;
+ position: relative;
+ width: 50%;
+
+ border-top-style: $style;
+ border-top-width: $height;
+
+ @if $color != inherit {
+ border-top-color: $color;
+ }
+
+ @if $v-adjust != false {
+ bottom: $v-adjust;
+ }
+
+ @if $double != false {
+ height: $double;
+ border-bottom-style: $style;
+ border-bottom-width: $height;
+ @if $color != inherit {
+ border-bottom-color: $color;
+ }
+ }
+ }
+
+ &:before {
+ right: $space;
+ margin-left: -50%;
+ }
+ &:after {
+ left: $space;
+ margin-right: -50%;
+ }
+
+}
+
+%scut-side-lined {
+ @include scut-side-lined;
+}
+@mixin scut-truncate {
+
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+}
+
+%scut-truncate {
+ @include scut-truncate;
+} \ No newline at end of file
diff --git a/web-ui/public/scss/views/_action-bar.scss b/web-ui/public/scss/views/_action-bar.scss
new file mode 100644
index 00000000..40e677b0
--- /dev/null
+++ b/web-ui/public/scss/views/_action-bar.scss
@@ -0,0 +1,159 @@
+#top-pane {
+ height: auto;
+ overflow: hidden;
+ background: $top_pane;
+ border-top: 1px solid $top_pane;
+
+ #list-actions {
+ width: 100%;
+ height: 34px;
+ margin: 0;
+ border-top: 1px solid $white;
+ border-bottom: 2px solid lighten($top_pane, 30%);
+ background: $white;
+ clear: both;
+ overflow: hidden;
+ padding-left: 10px;
+
+ li {
+ display: inline-block;
+ margin: 1px -3px;
+ vertical-align: top;
+
+ input[type=checkbox] {
+ @include check-box;
+
+ margin: 7px 13px 7px;
+ }
+
+ select {
+ padding: 1px 3px;
+ margin: 0;
+ }
+
+ input[type=button] {
+ margin: 2px;
+ padding: 4px 10px;
+ background: $background_light_grey;
+ color: $dark_grey;
+ text-transform: uppercase;
+ font-weight: 400;
+ font-size: 0.8em;
+ opacity: 0.7;
+ border: 1px solid darken($contrast, 10%);
+
+ @include border-radius(1px);
+
+ @include btn-transition;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &[disabled=disabled] {
+ opacity: 0.5;
+ cursor: default;
+ }
+ }
+ }
+
+ #pagination-trigger {
+ cursor: pointer;
+ margin: 4px 12px 0 5px;
+
+ span {
+ padding-left: 5px;
+ }
+ }
+ }
+
+ #compose-search-trigger {
+ padding: 4px;
+ }
+
+ #actions {
+ ul {
+ margin: 0;
+
+ li {
+ display: inline-block;
+ margin-right: -5px;
+
+ a {
+ transition: background-color 150ms ease-out;
+ background: $top_pane;
+ color: $white;
+ font-size: 1.5em;
+ display: block;
+ padding: 14px 20px;
+ margin: 0 1px 0px;
+ opacity: 0.35;
+
+ &.selected {
+ background: $top_pane;
+ opacity: 1;
+ cursor: default;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+ }
+ }
+
+ #search-trigger {
+ padding: 5px;
+ padding-left: 0;
+
+ input {
+ margin: 0;
+ padding: 8px 30px;
+ color: $navigation_background;
+ background: white;
+ border: none;
+ transition: background-color 150ms ease-out;
+
+ &:hover {
+ background: darken(white, 2%);
+ }
+
+ &:focus {
+ background: darken(white, 5%);
+ }
+ }
+
+ form:before {
+ font-family: "FontAwesome";
+ content: "\f002";
+ position: absolute;
+ padding: 0 10px;
+ top: 15px;
+ color: $medium_light_grey;
+ }
+ }
+}
+
+#refresh-mails-trigger {
+ i {
+ margin-top: 3px;
+ cursor: pointer;
+ opacity: 0.9;
+ padding: 4px;
+
+ &:hover {
+ opacity: 1;
+
+ &:after {
+ content: "\f021";
+ }
+
+ &:before {
+ content: attr(data-label);
+ font-size: 0.8em;
+ padding-right: 5px;
+ }
+ }
+ }
+}
diff --git a/web-ui/public/scss/views/_close-button.scss b/web-ui/public/scss/views/_close-button.scss
new file mode 100644
index 00000000..37171c18
--- /dev/null
+++ b/web-ui/public/scss/views/_close-button.scss
@@ -0,0 +1,22 @@
+.close-mail-button {
+ $button-size: 27px;
+
+ margin-right: 3px;
+ float: left;
+ background: $lighter_gray;
+ color: $medium_light_grey;
+ width: $button-size;
+ height: $button-size;
+ padding: 0;
+ border-radius: 0;
+
+ &:hover, &:focus, &:active {
+ background-color: darken($lighter_gray, 2);
+ color: darken($medium_light_grey, 10);
+ }
+
+ i {
+ padding: 0;
+ margin: 0;
+ }
+}
diff --git a/web-ui/public/scss/views/_compose-button.scss b/web-ui/public/scss/views/_compose-button.scss
new file mode 100644
index 00000000..81e0bb33
--- /dev/null
+++ b/web-ui/public/scss/views/_compose-button.scss
@@ -0,0 +1,27 @@
+// COMPOSE BUTTON
+#compose {
+ margin-bottom: 5px;
+ padding-right: 4px;
+ #compose-trigger {
+ width: 100%;
+ display: inline-block;
+ padding: 5px;
+ #compose-mails-trigger {
+ background: $action_buttons;
+ color: $white;
+ padding: 10px 30px;
+ text-align: center;
+ font-weight: 400;
+ font-size: 1.2em;
+ width: 100%;
+ height: 100%;
+ margin-bottom: 0px;
+ @include btn-transition;
+ &:hover {
+ background: lighten($action_buttons, 10%);
+ cursor: pointer;
+ }
+ }
+ }
+}
+
diff --git a/web-ui/public/scss/views/_compose-view.scss b/web-ui/public/scss/views/_compose-view.scss
new file mode 100644
index 00000000..9e120357
--- /dev/null
+++ b/web-ui/public/scss/views/_compose-view.scss
@@ -0,0 +1,451 @@
+.compose-view {
+ overflow: auto;
+
+ &__buttons {
+ &-attachment {
+ cursor: pointer;
+ margin-left: 18px;
+ padding-top: 0px;
+ display: inline;
+ border: 1px $contrast solid;
+ background: $background_light_grey;
+ padding: 7px 4px;
+ font-size: 0.8em;
+
+ span {
+ -ms-transform: rotate(224deg);
+ -webkit-transform: rotate(224deg);
+ transform: rotate(224deg);
+ outline: 0;
+ }
+
+ i.fa-paperclip {
+ font-size: 1.7em;
+ }
+
+ &--busy {
+ color: lighten($recipients_font_color, 10%);
+ cursor: progress;
+ }
+ }
+ }
+
+ &__attachments {
+ &-wrapper {
+ padding: 0;
+ margin-top: 30px;
+ }
+
+ &-list {
+ &-item {
+ display: block;
+ position: relative;
+ margin-bottom: 8px;
+ padding: 5px;
+ border: 1px solid $border_light_grey;
+ border-radius: 2px;
+ background-color: $contrast;
+
+ &-label {
+ color: $attachment_text;
+ text-decoration: none;
+
+ &:hover, &:focus {
+ color: $attachment_icon;
+ outline: none;
+ }
+ }
+
+ &-icon {
+ color: #a2a2a2;
+ float: right;
+ margin-top: 7px;
+ cursor: pointer;
+ }
+
+ &-progress {
+ width: 0%;
+ position: absolute;
+ right: 0;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ min-height: 100%;
+
+ &-bar {
+ height: 100%;
+ background-color: rgba($light_blue, 0.3);
+ }
+ }
+
+ }
+
+ &--upload {
+ display: none;
+ }
+
+ }
+
+ &-error {
+ background-color: $background_light_grey;
+ border-radius: 2px;
+ border: 1px solid $error;
+ display: block;
+ font-size: 0.9rem;
+ margin-bottom: 20px;
+ padding: 5px;
+ width: 100%;
+
+ &-close {
+ float: left;
+ margin: 5px 5px 0 0;
+ }
+
+ & > * {
+ color: $error;
+ }
+
+ & > a {
+ display: inline-block;
+ text-decoration: underline;
+ padding: 5px;
+ }
+ }
+ }
+
+}
+
+// COMPOSE PANE
+#compose-box, #draft-box, #reply-box, #feedback-box {
+ div.floatlabel {
+ position: relative;
+ }
+
+ .input-container {
+ padding: 1px;
+ }
+
+ label, span {
+ color: $recipients_font_color;
+ padding: 0.5rem;
+ display: inline-block;
+ }
+
+ label {
+ padding: 13px 10px;
+ }
+
+ span {
+ padding: 3px;
+
+ &.attachment-size {
+ color: $attachment_size;
+ cursor: pointer;
+ }
+ }
+
+ label.floatlabel {
+ padding: 0.4rem !important;
+ position: absolute;
+ font-size: 0.6rem;
+ transition: all 0.1s linear;
+ opacity: 0;
+ font-weight: bold;
+ }
+
+ label.showfloatlabel {
+ color: $light_blue !important;
+ top: -0.3rem;
+ opacity: 1;
+ }
+
+ input, textarea {
+ margin: 0;
+ border: none;
+ transition: all 0.1s linear;
+ }
+
+ input.showfloatlabel, textarea.showfloatlabel {
+ padding-top: 1rem !important;
+ }
+
+ input#subject, #feedback-subject {
+ font-size: 1.6875rem;
+ line-height: 1.4;
+ border-top: 1px solid $lighter_gray;
+ }
+
+ #feedback-subject {
+ color: $dark_grey;
+ }
+
+ textarea {
+ border-bottom: 2px solid $lighter_gray;
+ min-height: 400px;
+ font-family: inherit;
+ font-weight: normal;
+ font-size: 1rem;
+ line-height: 1.6;
+ text-rendering: optimizeLegibility;
+ }
+
+ &.reply-box, &.forward-box {
+ margin: 0;
+
+ h4 {
+ font-size: 0.9em;
+ font-style: italic;
+ color: $medium_grey;
+ margin: 2px 0;
+ clear: both;
+ cursor: pointer;
+
+ &:hover {
+ background: $contrast;
+ }
+ }
+
+ textarea {
+ min-height: 200px;
+ margin: 10px 0;
+ }
+
+ p {
+ padding: 5px;
+ margin: 10px 0;
+ font-style: italic;
+ cursor: pointer;
+
+ &:hover {
+ background: $contrast;
+ }
+ }
+ }
+
+ button.close-mail-button {
+ margin: 1px;
+ }
+
+ .buttons-group {
+ margin-top: 0px;
+ }
+
+ .recipients-area {
+ -webkit-appearance: none;
+ background-color: white;
+ font-family: inherit;
+ display: flex;
+ flex-wrap: wrap;
+ font-size: 0.898em;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: relative;
+
+ .compose-column-label {
+ width: 5%;
+ display: inline-block;
+ }
+
+ .compose-column-recipients {
+ width: 95%;
+ display: inline-block;
+ }
+
+ .recipients-label {
+ width: 100%;
+ height: 100%;
+ }
+
+ .recipients-navigation-handler {
+ z-index: -1;
+ position: absolute;
+ top: -200px;
+ }
+
+ .twitter-typeahead {
+ flex: 1 1 50px;
+
+ .tt-dropdown-menu {
+ background: $dark_white;
+
+ div div {
+ padding: 8px;
+
+ &:hover {
+ background: $background_dropdown_grey;
+ }
+ }
+ }
+ }
+
+ .invalid-format {
+ border-bottom: 1px dotted $error;
+ }
+
+ input[type=text] {
+ vertical-align: top;
+ height: 35px;
+ margin-left: 1px;
+ font-size: 0.9em;
+ width: 100%;
+ }
+
+ .fixed-recipient {
+ display: inline-block;
+ margin-right: -3px;
+ flex: none;
+ position: relative;
+
+ .recipient-value {
+ &.selected {
+ border: 1px solid $medium_dark_grey;
+ }
+
+ &:before {
+ font-family: FontAwesome;
+ padding-right: 6px;
+ font-size: 1.4em;
+ }
+
+ &.encrypted {
+ border-bottom-color: $will_be_encrypted;
+
+ &:before {
+ color: $will_be_encrypted;
+ content: "\f023 ";
+ }
+ }
+
+ &.not-encrypted {
+ border-bottom-color: $wont_be_encrypted;
+
+ &:before {
+ color: $wont_be_encrypted;
+ content: "\f09c";
+ }
+ }
+
+ &.deleting span {
+ text-decoration: line-through;
+ }
+
+ & span {
+ margin: 0px;
+ padding: 0px 0px 0px 0px;
+ vertical-align: top;
+ cursor: pointer;
+ }
+
+ margin: 3px;
+ padding: 5px;
+ background-color: $background_light_grey;
+ border: 1px solid $border_light_grey;
+ border-radius: 2px;
+ }
+
+ .recipient-del {
+ position: relative;
+ color: $recipients_font_color;
+
+ &:hover, &:focus {
+ color: $recipients_font_color;
+ }
+
+ &:before {
+ margin-left: 0.4em;
+ font-weight: bold;
+ content: "x";
+ }
+
+ &.deleteTooltip:hover:after {
+ position: absolute;
+ content: attr(data-label);
+ font-size: 0.5rem;
+
+ @include tooltip(25px, 0px);
+ }
+ }
+ }
+
+ input.recipients-input:focus {
+ background-color: $dark_white !important;
+ border-color: $medium_light_grey;
+ outline: none;
+ width: 270px;
+ }
+ }
+
+ .collapse {
+ display: block;
+ position: absolute;
+ right: 10px;
+ padding-right: 15px;
+ padding-left: 15px;
+ font-family: 'FontAwesome';
+ font-weight: bolder;
+ font-size: larger;
+ cursor: pointer;
+ }
+
+ .collapse + input, .collapse + input + * {
+ display: none;
+ }
+
+ .collapse + input:checked + * {
+ display: block;
+ }
+}
+
+#reply-section {
+ padding-left: 30px;
+
+ .reply-container {
+ margin: 10px 0;
+ padding: 10px;
+ border: 1px dashed darken($contrast, 10%);
+
+ @include btn-transition;
+ }
+
+ button {
+ margin: 0;
+ }
+
+ #all-recipients {
+ color: $black;
+ }
+
+ #all-recipients:focus {
+ background-color: darken($contrast, 10%);
+ }
+
+ #reply-button, #reply-all-button, #forward-button {
+ text-align: center;
+ font-weight: 100;
+ font-size: 1.1em;
+ background: $white;
+ color: $medium_light_grey;
+ padding: 25px;
+ margin: 0;
+
+ @include border-radius(0);
+
+ &:hover {
+ background: darken($contrast, 5%);
+ cursor: pointer;
+ }
+ }
+}
+
+.buttons-group {
+ clear: both;
+ margin: 20px 0 0;
+ padding: 0;
+}
+
+#draft-save-status {
+ float: right;
+ padding: 0.4rem 1.1rem;
+ color: $lighter_blue;
+}
diff --git a/web-ui/public/scss/views/_mail-list.scss b/web-ui/public/scss/views/_mail-list.scss
new file mode 100644
index 00000000..f5c4c60f
--- /dev/null
+++ b/web-ui/public/scss/views/_mail-list.scss
@@ -0,0 +1,124 @@
+.mail-list-entry {
+ @include scut-clearfix;
+
+ border-bottom: 1px solid white;
+ transition: background-color 150ms ease-out;
+ font-weight: bold;
+ height: 80px;
+ position: relative;
+
+ // Workaround:
+ // Foundation is of the opinion that a 1.6 line height for all lists
+ // is a totally good idea. Please remove when Foundation is gone
+ line-height: normal;
+
+ &.status-read {
+ font-weight: normal;
+ color: $attachment_text;
+
+ .mail-list-entry__checkbox::after {
+ display: none;
+ }
+ }
+
+
+ &.selected {
+ background: $light_blue;
+ z-index: 10; // overlay the box-shadow of the right page (z-index: 2)
+
+ &:hover {
+ background: $light_blue;
+ }
+
+ a {
+ color: $white;
+ }
+ }
+
+ &:hover {
+ background: darken($contrast, 5%);
+ }
+
+ &__checkbox {
+ margin-right: 5px;
+ display: block;
+ float: left;
+ margin: {
+ top: 8px;
+ left: 20px;
+ }
+
+ &::after {
+ content: '';
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ -moz-border-radius: 15px;
+ -webkit-border-radius: 15px;
+ border-radius: 15px;
+ background-color: $bullet-blue;
+ position: absolute;
+ left: 48px;
+ top: 13px;
+ }
+
+ & > input[type=checkbox] {
+ @include check-box;
+ }
+ }
+
+ &__item {
+ display: block;
+ color: $dark_grey;
+ padding: 8px 10px 10px 67px;
+ height: 100%;
+
+ &-from {
+ white-space: nowrap;
+ font-size: 0.8em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ }
+
+ &-date {
+ font-size: 0.7em;
+ float: right;
+ display: inline-block;
+ }
+
+ &-subject {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 85%;
+
+ &-icon {
+ color: $light_gray;
+ }
+ }
+
+ &-attachment {
+ width: 14px;
+ text-align: right;
+ display: inline-block;
+ float: right;
+ color: $light_gray;
+ }
+
+ &-tags {
+ @include tags;
+
+ // Workaround:
+ // Foundation is of the opinion that a 1.6 line height and a 0.6 rem margin-bottom
+ // for all lists is a totally good idea. Please remove when Foundation is gone
+ line-height: normal;
+ margin-bottom: 0;
+ }
+
+ &:hover, &:focus, &:active {
+ color: $dark_grey;
+ }
+ }
+}
diff --git a/web-ui/public/scss/views/_message-panel.scss b/web-ui/public/scss/views/_message-panel.scss
new file mode 100644
index 00000000..4a0a7a6b
--- /dev/null
+++ b/web-ui/public/scss/views/_message-panel.scss
@@ -0,0 +1,26 @@
+.message-panel {
+ width: 100%;
+ margin: 10px auto;
+ position: fixed;
+ z-index: 10000;
+ text-align: center;
+
+ &__growl {
+ padding: 5px 60px;
+
+ &--success {
+ background: $warning;
+ color: darken($warning, 50%);
+ border: 1px solid darken($warning, 10%);
+ @include box-shadow(1px 1px 3px darken($warning, 60%));
+ }
+
+ &--error {
+ font-weight: bold;
+ color: white;
+ background: $error;
+ border: 1px solid darken($error, 10%);
+ @include box-shadow(1px 1px 3px darken($error, 60%));
+ }
+ }
+}
diff --git a/web-ui/public/scss/views/_navigation.scss b/web-ui/public/scss/views/_navigation.scss
new file mode 100644
index 00000000..2c33a791
--- /dev/null
+++ b/web-ui/public/scss/views/_navigation.scss
@@ -0,0 +1,589 @@
+#logo {
+ color: $white;
+}
+
+#logout {
+ color: $white;
+ cursor: pointer;
+}
+
+#user-settings-box {
+ position: fixed;
+ z-index: 10;
+
+ & > div {
+ position: fixed;
+ left: 70px;
+ bottom: 0px;
+ z-index: 1;
+ padding: 10px 16px 10px 18px;
+ background-color: rgba($dark_slate_gray, 0.9);
+ min-width: 230px;
+
+ &.extra-bottom-space {
+ bottom: 33px;
+ }
+
+ header {
+ border-bottom: 1px solid white;
+ margin-bottom: 10px;
+ }
+
+ #user-settings-close {
+ float: right;
+ }
+
+ h1, i {
+ font-size: 1.2em;
+ color: white;
+ line-height: 1.2em;
+ }
+
+ h2 {
+ font-size: 1.1em;
+ color: white;
+ line-height: 1.1em;
+ display: inline;
+ margin-left: 5px;
+ }
+
+ i.fa-user {
+ margin-right: 10px;
+ float: left;
+ }
+
+ i.fa-close {
+ margin-left: 10px;
+ float: right;
+ cursor: pointer;
+ }
+
+ p {
+ font-size: 1.1em;
+ color: $light_orange;
+ }
+ }
+}
+
+@keyframes hideshow {
+ 0% {
+ fill: lighten($logo_color, 30);
+ }
+
+ 25% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+.logo-part-animation-off {
+ animation: none;
+}
+
+.logo-part-animation-on {
+ animation: hideshow 0.6s ease infinite;
+ opacity: 1;
+
+ &:nth-child(2) {
+ opacity: 0;
+ animation-delay: 0.1s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(4) {
+ animation-delay: 0.3s;
+ }
+
+ &:nth-child(5) {
+ animation-delay: 0.4s;
+ }
+
+ &:nth-child(6) {
+ animation-delay: 0.5s;
+ }
+}
+
+.arrow-box:before {
+ right: 100%;
+ top: 65%;
+ border: 20px solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border-right-color: rgba($dark_slate_gray, 0.9);
+ margin-top: -20px;
+}
+
+.side-nav-toggle, .side-nav-toggle-icon {
+ color: white;
+ cursor: pointer;
+
+ &:hover, &:focus {
+ color: white;
+ }
+
+ background: $navigation_background;
+
+ &.logout {
+ color: $action_buttons;
+ }
+}
+
+.side-nav-toggle-icon {
+ padding: 6px 0px 8px 19px;
+ display: block;
+ left: 0;
+ top: 0;
+ position: relative;
+
+ .fa-navicon {
+ font-size: 24px;
+ &:before {
+ margin-left: -5px;
+ }
+ }
+}
+
+.left-off-canvas-logo {
+ svg {
+ width: 162px;
+ height: 56px;
+ padding-left: 6px;
+ padding-top: 2px;
+
+ path, polygon, rect {
+ fill: $logo_color;
+ }
+ }
+}
+
+.collapsed-nav {
+ width: 50px;
+ position: absolute;
+ height: 100vh;
+ background: $navigation_background;
+
+ ul.shortcuts {
+ li {
+ position: relative;
+ margin-bottom: 5px;
+ opacity: 0.8;
+
+ &.selected {
+ background: $contrast;
+ opacity: 1;
+ cursor: default;
+
+ a {
+ color: $navigation_background;
+ }
+ }
+
+ @include searching(6px, 26px, $medium_dark_grey, 0.9em);
+
+ a {
+ display: block;
+ position: relative;
+ font-size: 1.4em;
+ padding: 5px;
+ color: white;
+ text-align: center;
+
+ &:hover {
+ background: darken($contrast, 10%);
+ color: $navigation_background;
+
+ @include btn-transition;
+
+ &.logout {
+ color: $black;
+ background: $action_buttons;
+ }
+ }
+
+ &[title]:hover:after {
+ content: attr(title);
+
+ @include tooltip;
+ }
+ }
+ }
+ }
+
+ #custom-tags-shortcuts {
+ li {
+ border-top: 1px solid $lighter_gray;
+ }
+ }
+
+ div.shortcut-label {
+ font-size: xx-small;
+ text-transform: uppercase;
+ text-align: center;
+ }
+}
+
+.move-right {
+ ul.shortcuts {
+ li {
+ display: none;
+ }
+ }
+}
+
+.left-off-canvas-menu {
+ width: 222px;
+ -webkit-backface-visibility: hidden;
+ box-sizing: content-box;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ position: absolute;
+ overflow-y: auto;
+}
+
+.left-off-canvas-menu * {
+ -webkit-backface-visibility: hidden;
+}
+
+.off-canvas-wrap {
+ -webkit-backface-visibility: hidden;
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.off-canvas-wrap.move-right, .off-canvas-wrap.move-left {
+ min-height: 100%;
+ -webkit-overflow-scrolling: touch;
+}
+
+.inner-wrap {
+ -webkit-backface-visibility: hidden;
+ width: 100%;
+}
+
+.inner-wrap:before, .inner-wrap:after {
+ content: " ";
+ display: table;
+}
+
+.inner-wrap:after {
+ clear: both;
+}
+
+.off-canvas-wrap.content {
+ -webkit-ransition: -webkit-transform 500ms ease;
+ -moz-transition: -moz-transform 500ms ease;
+ -ms-transition: -ms-transform 500ms ease;
+ -o-transition: -o-transform 500ms ease;
+ transition: transform 500ms ease;
+
+ &.move-right {
+ -webkit-transform: translate3d(10rem, 0, 0);
+ -moz-transform: translate3d(10rem, 0, 0);
+ -ms-transform: translate3d(10rem, 0, 0);
+ -o-transform: translate3d(10rem, 0, 0);
+ transform: translate3d(10rem, 0, 0);
+
+ #user-settings-box > div {
+ left: 20px;
+ }
+ }
+}
+
+.move-right .exit-off-canvas {
+ -webkit-backface-visibility: hidden;
+ transition: background 300ms ease;
+ cursor: pointer;
+ display: block;
+ position: absolute;
+ background: rgba(255, 255, 255, 0.2);
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+@media only screen and (min-width: 40.063em) {
+ .move-right .exit-off-canvas:hover {
+ background: rgba(255, 255, 255, 0.05);
+ }
+}
+
+.off-canvas-wrap.move-right.menu {
+ position: absolute;
+}
+
+.off-canvas-wrap.content {
+ left: 50px;
+ padding-right: 50px;
+}
+
+.offcanvas-overlap .left-off-canvas-menu, .offcanvas-overlap .right-off-canvas-menu {
+ -ms-transform: none;
+ -webkit-transform: none;
+ -moz-transform: none;
+ -o-transform: none;
+ transform: none;
+}
+
+.offcanvas-overlap .exit-offcanvas-menu {
+ -webkit-backface-visibility: hidden;
+ transition: background 300ms ease;
+ cursor: pointer;
+ display: block;
+ position: absolute;
+ background: rgba(255, 255, 255, 0.2);
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+div.side-nav-bottom {
+ width: 100%;
+ position: fixed;
+ bottom: 20px;
+ background-color: $navigation_background;
+
+ .version {
+ padding-left: 55px;
+ padding-bottom: 3px;
+ }
+}
+
+#left-pane nav {
+ border-right: 1px solid lighten($navigation_background, 10%);
+
+ ul#default-tag-list, #custom-tag-list {
+ li {
+ transition: background-color 150ms ease-out;
+ padding: 2px 10px;
+ cursor: pointer;
+
+ &:hover {
+ background: $light_gray;
+ color: $navigation_background;
+ }
+
+ &.selected {
+ font-weight: bold;
+ background: $contrast;
+ color: $navigation_background;
+ }
+ }
+ }
+
+ ul#default-tag-list {
+
+ span.tag-label {
+ padding-left: 2px;
+ }
+
+ li {
+ padding: 5px 10px 5px 18px;
+ position: relative;
+
+ @include searching(4px, 19px, $dark_grey, 0.7em);
+
+ &:before {
+ font-size: 1.5em;
+ font-family: "FontAwesome";
+ margin-right: 16px;
+ font-weight: normal;
+ position: relative;
+ top: 2px;
+ margin-left: -3px;
+ }
+
+ &:after {
+ padding-left: 10px;
+ }
+
+ &:nth-child(1) {
+ &:before {
+ content: "\f01c";
+ }
+ }
+
+ &:nth-child(2) {
+ &:before {
+ font-family: "icomoon";
+ content: "\e900";
+ margin-left: -5px;
+ }
+ }
+
+ &:nth-child(3) {
+ &:before {
+ content: "\f040";
+ }
+ }
+
+ &:nth-child(4) {
+ &:before {
+ content: "\f014";
+ }
+ }
+
+ &:nth-child(5) {
+ &:before {
+ content: "\f187";
+ margin-left: -5px;
+ }
+ }
+ }
+ }
+
+ ul#custom-tag-list {
+ visibility: hidden;
+ opacity: 0;
+ transition-duration: 500ms;
+ height: 100%;
+ max-height: 220px;
+ overflow: auto;
+ background-color: lighten($navigation_background, 1);
+
+ li {
+ white-space: nowrap;
+ overflow: hidden;
+ font-size: 0.8em;
+ padding: 5px 10px 5px 15px;
+
+ &.custom-tag {
+ text-overflow: ellipsis;
+ }
+
+ span.tag-label {
+ padding: 5px 20px 5px 38px;
+ }
+ }
+
+ .unread-count, .total-count {
+ padding: 1px 4px;
+ position: relative;
+ }
+
+ }
+
+ ul#custom-tag-list.expanded {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ div.tags-icon {
+ border-top: 1px solid white;
+ padding-top: 25px;
+ margin-bottom: 20px;
+
+ i {
+ font-size: 1.5em;
+ font-family: "FontAwesome";
+ margin-right: 13px;
+ font-weight: normal;
+ position: relative;
+ top: 2px;
+ left: 16px;
+ }
+
+ span.tag-label {
+ font-size: 0.9rem;
+ padding-left: 16px;
+ margin-bottom: 10px;
+ }
+ }
+
+ ul#logout, ul#feedback, ul#user-settings-icon {
+ margin-bottom: 0;
+
+ li {
+ background-color: $navigation_background;
+ padding: 5px 10px;
+ position: relative;
+
+ @include searching(4px, 19px, $dark_grey, 0.7em);
+
+ &:hover {
+ color: $navigation_background;
+ }
+
+ div {
+ padding-left: 7px;
+
+ &:before {
+ font-size: 1.5em;
+ font-family: "FontAwesome";
+ margin-right: 13px;
+ font-weight: normal;
+ position: relative;
+ top: 2px;
+ }
+ }
+ }
+ }
+
+ ul {
+ &#logout li {
+ color: $action_buttons;
+
+ &:hover {
+ background-color: $action_buttons;
+ }
+ }
+
+ &#user-settings-icon {
+ li {
+ color: white;
+
+ &:hover {
+ background-color: white;
+ }
+ }
+ }
+
+ &#feedback {
+ margin-bottom: 0;
+
+ li {
+ color: $light_orange;
+
+ &:hover {
+ background-color: $light_orange;
+ }
+ }
+ }
+ }
+
+ h3 {
+ color: white;
+ text-transform: uppercase;
+ font-size: 0.6em;
+ padding: 5px;
+ font-weight: 600;
+ margin: 0 10px;
+ border-bottom: 1px dotted lighten($navigation_background, 10%);
+ }
+}
+
+.unread-count {
+ @extend .mail-count;
+
+ background: $secondary_callout;
+}
+
+.total-count {
+ @extend .mail-count;
+
+ background: $medium_light_grey;
+}
diff --git a/web-ui/public/scss/views/_no-mails-available.scss b/web-ui/public/scss/views/_no-mails-available.scss
new file mode 100644
index 00000000..bf5d256a
--- /dev/null
+++ b/web-ui/public/scss/views/_no-mails-available.scss
@@ -0,0 +1,3 @@
+.no-mails-available-pane {
+ @extend .no-content-placeholder;
+}
diff --git a/web-ui/public/scss/views/_no-message-selected.scss b/web-ui/public/scss/views/_no-message-selected.scss
new file mode 100644
index 00000000..0e367bf2
--- /dev/null
+++ b/web-ui/public/scss/views/_no-message-selected.scss
@@ -0,0 +1,14 @@
+.no-message-selected-pane {
+ background: $contrast;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ &__text {
+ @extend .no-content-placeholder;
+
+ margin-bottom: 40px; // aligns label with "no results for XYZ"
+ }
+}
diff --git a/web-ui/public/scss/views/_read-view.scss b/web-ui/public/scss/views/_read-view.scss
new file mode 100644
index 00000000..f69d51a5
--- /dev/null
+++ b/web-ui/public/scss/views/_read-view.scss
@@ -0,0 +1,165 @@
+.mail-read-view {
+ $component-vertical-spacing: 10px;
+ $view-top-spacing: 3px;
+
+ // NB! Setting overflow: hidden on an element causes
+ // a new float context to be created, so elements that
+ // are floated inside an element that has overflow: hidden
+ // applied are cleared.
+ overflow: hidden;
+
+ hr {
+ margin: 0;
+ }
+
+ &__header {
+ @include scut-clearfix;
+
+ font-size: 0.9em;
+ margin: 0;
+ margin: $view-top-spacing 0 $component-vertical-spacing 0;
+
+ &-recipients {
+ display: inline;
+ margin-bottom: 5px;
+ line-height: 1.5em;
+
+ &-separator {
+ margin: 0 10px;
+ }
+
+ &--highlight-sender {
+ font-weight: bold;
+ }
+ }
+
+ &-date {
+ display: inline;
+ float: right;
+ }
+
+ &-subject {
+ display: inline;
+ float: left;
+ max-width: 80%;
+ }
+
+ &-actions {
+ display: inline;
+ float: right;
+ max-width: 20%;
+ background: $white;
+ white-space: nowrap;
+ margin-top: $component-vertical-spacing;
+
+ &-button {
+ color: $medium_light_grey;
+ background-color: inherit;
+ display: inline;
+ border: 1px solid $lighter_gray;
+ line-height: 2em;
+
+ margin-bottom: 0;
+
+ i {
+ // workaround: remove padding and margin inserted by font-awesome
+ margin: 0;
+ padding: 0;
+ }
+
+ &:hover, &:active, &:focus {
+ @include btn-transition;
+
+ background: darken($contrast, 5%);
+ color: inherit;
+ }
+
+ &--reply {
+ padding: 0 20px;
+ margin-right: -4px; // force buttons together
+
+ }
+
+ &--more {
+ padding: 0 5px;
+ }
+ }
+
+ &-dropdown {
+ $container-right-padding: 10px;
+
+ background: inherit;
+ position: absolute;
+ border: 1px solid $lighter_gray;
+ right: $container-right-padding;
+
+ &-entry {
+ box-sizing: border-box;
+ background: inherit;
+ padding: 5px 10px;
+ display: block;
+ border-bottom: 1px solid $lighter_gray;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ cursor: pointer;
+ background: $contrast;
+ }
+ }
+ }
+ }
+
+ &-tags {
+ @include tags-editable;
+
+ clear: both;
+ margin: 0 0 10px;
+ }
+ }
+
+ &__body {
+ margin: $component-vertical-spacing 0;
+ width: 100%;
+ border: none;
+ }
+
+ &__attachments {
+ margin: $component-vertical-spacing 0;
+
+ &-header {
+ font-weight: bold;
+ }
+
+ &-item {
+ display: block;
+ margin-bottom: 8px;
+ padding: 5px;
+ border: 1px solid $border_light_grey;
+ border-radius: 2px;
+ background-color: $background_light_grey;
+
+ &-label {
+ color: $attachment_text;
+ text-decoration: none;
+
+ &:hover, &:focus {
+ i.download-icon {
+ color: lighten($attachment_icon, 15);
+ }
+
+ color: $attachment_icon;
+ outline: none;
+ }
+ }
+
+ &-download {
+ color: #a2a2a2;
+ float: right;
+ margin-top: 5px;
+ }
+ }
+ }
+}
diff --git a/web-ui/public/scss/views/_security-labels.scss b/web-ui/public/scss/views/_security-labels.scss
new file mode 100644
index 00000000..ac966ded
--- /dev/null
+++ b/web-ui/public/scss/views/_security-labels.scss
@@ -0,0 +1,67 @@
+.security-status {
+ margin: 0 0 5px;
+
+ &__label {
+ display: inline-block;
+ padding: 2px 6px;
+ white-space: nowrap;
+ background: $success;
+ color: $white;
+ border-radius: 12px;
+
+ &:before {
+ font-family: FontAwesome;
+ }
+
+ &--encrypted {
+ &:before {
+ content: "\f023";
+ }
+
+ &--with-error {
+ background: $attention;
+ &:before {
+ content: "\f023 \f057";
+ }
+ }
+ }
+
+ &--not-encrypted {
+ background: $attention;
+
+ &:before {
+ content: "\f09c";
+ }
+ }
+
+ &--signed {
+ &:before {
+ content: "\f00c";
+ }
+
+ &--revoked, &--expired {
+ background: $attention;
+
+ &:before {
+ content: "\f05e";
+ }
+ }
+
+ &--not-trusted {
+ background: $error;
+
+ &:before {
+ content: "\f05e";
+ }
+ }
+ }
+
+ &--not-signed {
+ background: $attention;
+
+ &:before {
+ content: "\f05e";
+ }
+ }
+ }
+}
diff --git a/web-ui/public/signup.css b/web-ui/public/signup.css
new file mode 100644
index 00000000..61ac8587
--- /dev/null
+++ b/web-ui/public/signup.css
@@ -0,0 +1,174 @@
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+}
+
+.field-group {
+ position:relative;
+ margin-bottom: 35px;
+}
+
+label {
+ font-size: 0.9em;
+ margin-bottom: 10px;
+ display: inline-block;
+}
+
+input {
+ display: block;
+ border: solid 1px #4da3b6;
+ width: 100%;
+ height: auto;
+ padding: 10px 5px;
+ margin-bottom: 20px;
+}
+
+.animated-label {
+ color:#999;
+ position:absolute;
+ pointer-events:none;
+ left: 6px;
+ top:10px;
+ transition:0.2s ease all;
+ -moz-transition:0.2s ease all;
+ -webkit-transition:0.2s ease all;
+}
+
+input:focus {
+ outline:none;
+}
+
+input:focus ~ .animated-label, input:valid ~ .animated-label{
+ top:-20px;
+ left: 0;
+ font-size:0.8em;
+ color:#4da3b6;
+}
+
+.blue-button {
+ background: #178ca6;
+ color: white;
+ display: block;
+ text-decoration: none;
+ text-align: center;
+ padding: 10px 0 10px 0;
+ width: 104%;
+ margin: 0 auto;
+}
+
+.blue-button:hover {
+ background: #4da3b6;
+}
+
+a {
+ text-decoration: none;
+ color: #4da3b6;
+}
+
+h1 {
+ font-size: 1.5em;
+ text-align: center;
+}
+
+header {
+ width: 18%;
+ margin: 0 auto;
+}
+
+.link-message {
+ text-align: center;
+ font-size: 0.8em;
+}
+
+.logo {
+ width: 100%;
+ height: auto;
+ padding-top: 20%;
+ margin-bottom: 30px;
+}
+
+.message h1 {
+ margin-bottom: 35px;
+}
+
+.message p {
+ padding-left: 5%;
+ padding-right: 5%;
+ width: 40%;
+ margin: 0 auto;
+ text-align: center;
+ line-height: 1.8em;
+ font-size: 0.9em;
+}
+
+.form-container {
+ width: 20%;
+ margin: 0 auto;
+ padding-top: 40px;
+}
+
+.domain-label {
+ position: relative;
+ top: 26px;
+ padding-left: 20px;
+ left: 100%;
+}
+
+.sent-email-icon {
+ width: 60px;
+}
+
+.disabled {
+ pointer-events: none;
+ background: #d4d4d4;
+}
+
+.link-message .disabled {
+ pointer-events: none;
+ color: #d4d4d4;
+ background: none;
+}
+
+/* Medium Devices, Desktops */
+@media only screen and (max-width : 992px) {
+ header {
+ width: 20%;
+ }
+
+ .form-container {
+ width: 30%;
+ }
+
+ .message p {
+ width: 70%
+ }
+}
+
+/* Small Devices, Tablets */
+@media only screen and (max-width : 768px) {
+ header {
+ width: 30%;
+ }
+
+ .form-container {
+ width: 50%;
+ }
+
+ .message p {
+ width: 80%
+ }
+}
+
+/* Extra Small Devices, Phones */
+@media only screen and (max-width : 480px) {
+ header {
+ width: 60%;
+ }
+
+ .form-container {
+ width: 80%;
+ }
+
+ .message p {
+ width: 85%
+ }
+}
diff --git a/web-ui/public/signup.html b/web-ui/public/signup.html
new file mode 100644
index 00000000..9814f585
--- /dev/null
+++ b/web-ui/public/signup.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title>Pixelated Mail</title>
+ <meta name="description" content="">
+ <meta name="viewport" content="width=device-width">
+ <link rel="stylesheet" type="text/css" href="/static/normalize.min.css" />
+ <link rel="stylesheet" type="text/css" href="/static/signup.css" />
+ </head>
+ <body>
+ <header><img src="/static/images/pixelated-logo-orange.svg" alt="Pixelated" class="logo"/></header>
+ <div class="message">
+ <div id="app"></div>
+ <script src="/static/signup.js"></script>
+ </div>
+ </body>
+</html>
diff --git a/web-ui/public/snap.svg-min.js b/web-ui/public/snap.svg-min.js
new file mode 100644
index 00000000..ca9601ab
--- /dev/null
+++ b/web-ui/public/snap.svg-min.js
@@ -0,0 +1,20 @@
+// Snap.svg 0.3.0
+//
+// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// build: 2014-06-03
+!function(a){var b,c,d="0.4.2",e="hasOwnProperty",f=/[\.\/]/,g=/\s*,\s*/,h="*",i=function(a,b){return a-b},j={n:{}},k=function(){for(var a=0,b=this.length;b>a;a++)if("undefined"!=typeof this[a])return this[a]},l=function(){for(var a=this.length;--a;)if("undefined"!=typeof this[a])return this[a]},m=function(a,d){a=String(a);var e,f=c,g=Array.prototype.slice.call(arguments,2),h=m.listeners(a),j=0,n=[],o={},p=[],q=b;p.firstDefined=k,p.lastDefined=l,b=a,c=0;for(var r=0,s=h.length;s>r;r++)"zIndex"in h[r]&&(n.push(h[r].zIndex),h[r].zIndex<0&&(o[h[r].zIndex]=h[r]));for(n.sort(i);n[j]<0;)if(e=o[n[j++]],p.push(e.apply(d,g)),c)return c=f,p;for(r=0;s>r;r++)if(e=h[r],"zIndex"in e)if(e.zIndex==n[j]){if(p.push(e.apply(d,g)),c)break;do if(j++,e=o[n[j]],e&&p.push(e.apply(d,g)),c)break;while(e)}else o[e.zIndex]=e;else if(p.push(e.apply(d,g)),c)break;return c=f,b=q,p};m._events=j,m.listeners=function(a){var b,c,d,e,g,i,k,l,m=a.split(f),n=j,o=[n],p=[];for(e=0,g=m.length;g>e;e++){for(l=[],i=0,k=o.length;k>i;i++)for(n=o[i].n,c=[n[m[e]],n[h]],d=2;d--;)b=c[d],b&&(l.push(b),p=p.concat(b.f||[]));o=l}return p},m.on=function(a,b){if(a=String(a),"function"!=typeof b)return function(){};for(var c=a.split(g),d=0,e=c.length;e>d;d++)!function(a){for(var c,d=a.split(f),e=j,g=0,h=d.length;h>g;g++)e=e.n,e=e.hasOwnProperty(d[g])&&e[d[g]]||(e[d[g]]={n:{}});for(e.f=e.f||[],g=0,h=e.f.length;h>g;g++)if(e.f[g]==b){c=!0;break}!c&&e.f.push(b)}(c[d]);return function(a){+a==+a&&(b.zIndex=+a)}},m.f=function(a){var b=[].slice.call(arguments,1);return function(){m.apply(null,[a,null].concat(b).concat([].slice.call(arguments,0)))}},m.stop=function(){c=1},m.nt=function(a){return a?new RegExp("(?:\\.|\\/|^)"+a+"(?:\\.|\\/|$)").test(b):b},m.nts=function(){return b.split(f)},m.off=m.unbind=function(a,b){if(!a)return void(m._events=j={n:{}});var c=a.split(g);if(c.length>1)for(var d=0,i=c.length;i>d;d++)m.off(c[d],b);else{c=a.split(f);var k,l,n,d,i,o,p,q=[j];for(d=0,i=c.length;i>d;d++)for(o=0;o<q.length;o+=n.length-2){if(n=[o,1],k=q[o].n,c[d]!=h)k[c[d]]&&n.push(k[c[d]]);else for(l in k)k[e](l)&&n.push(k[l]);q.splice.apply(q,n)}for(d=0,i=q.length;i>d;d++)for(k=q[d];k.n;){if(b){if(k.f){for(o=0,p=k.f.length;p>o;o++)if(k.f[o]==b){k.f.splice(o,1);break}!k.f.length&&delete k.f}for(l in k.n)if(k.n[e](l)&&k.n[l].f){var r=k.n[l].f;for(o=0,p=r.length;p>o;o++)if(r[o]==b){r.splice(o,1);break}!r.length&&delete k.n[l].f}}else{delete k.f;for(l in k.n)k.n[e](l)&&k.n[l].f&&delete k.n[l].f}k=k.n}}},m.once=function(a,b){var c=function(){return m.unbind(a,c),b.apply(this,arguments)};return m.on(a,c)},m.version=d,m.toString=function(){return"You are running Eve "+d},"undefined"!=typeof module&&module.exports?module.exports=m:"function"==typeof define&&define.amd?define("eve",[],function(){return m}):a.eve=m}(this),function(a,b){"function"==typeof define&&define.amd?define(["eve"],function(c){return b(a,c)}):b(a,a.eve)}(this,function(a,b){var c=function(b){var c={},d=a.requestAnimationFrame||a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame||a.msRequestAnimationFrame||function(a){setTimeout(a,16)},e=Array.isArray||function(a){return a instanceof Array||"[object Array]"==Object.prototype.toString.call(a)},f=0,g="M"+(+new Date).toString(36),h=function(){return g+(f++).toString(36)},i=Date.now||function(){return+new Date},j=function(a){var b=this;if(null==a)return b.s;var c=b.s-a;b.b+=b.dur*c,b.B+=b.dur*c,b.s=a},k=function(a){var b=this;return null==a?b.spd:void(b.spd=a)},l=function(a){var b=this;return null==a?b.dur:(b.s=b.s*a/b.dur,void(b.dur=a))},m=function(){var a=this;delete c[a.id],a.update(),b("mina.stop."+a.id,a)},n=function(){var a=this;a.pdif||(delete c[a.id],a.update(),a.pdif=a.get()-a.b)},o=function(){var a=this;a.pdif&&(a.b=a.get()-a.pdif,delete a.pdif,c[a.id]=a)},p=function(){var a,b=this;if(e(b.start)){a=[];for(var c=0,d=b.start.length;d>c;c++)a[c]=+b.start[c]+(b.end[c]-b.start[c])*b.easing(b.s)}else a=+b.start+(b.end-b.start)*b.easing(b.s);b.set(a)},q=function(){var a=0;for(var e in c)if(c.hasOwnProperty(e)){var f=c[e],g=f.get();a++,f.s=(g-f.b)/(f.dur/f.spd),f.s>=1&&(delete c[e],f.s=1,a--,function(a){setTimeout(function(){b("mina.finish."+a.id,a)})}(f)),f.update()}a&&d(q)},r=function(a,b,e,f,g,i,s){var t={id:h(),start:a,end:b,b:e,s:0,dur:f-e,spd:1,get:g,set:i,easing:s||r.linear,status:j,speed:k,duration:l,stop:m,pause:n,resume:o,update:p};c[t.id]=t;var u,v=0;for(u in c)if(c.hasOwnProperty(u)&&(v++,2==v))break;return 1==v&&d(q),t};return r.time=i,r.getById=function(a){return c[a]||null},r.linear=function(a){return a},r.easeout=function(a){return Math.pow(a,1.7)},r.easein=function(a){return Math.pow(a,.48)},r.easeinout=function(a){if(1==a)return 1;if(0==a)return 0;var b=.48-a/1.04,c=Math.sqrt(.1734+b*b),d=c-b,e=Math.pow(Math.abs(d),1/3)*(0>d?-1:1),f=-c-b,g=Math.pow(Math.abs(f),1/3)*(0>f?-1:1),h=e+g+.5;return 3*(1-h)*h*h+h*h*h},r.backin=function(a){if(1==a)return 1;var b=1.70158;return a*a*((b+1)*a-b)},r.backout=function(a){if(0==a)return 0;a-=1;var b=1.70158;return a*a*((b+1)*a+b)+1},r.elastic=function(a){return a==!!a?a:Math.pow(2,-10*a)*Math.sin(2*(a-.075)*Math.PI/.3)+1},r.bounce=function(a){var b,c=7.5625,d=2.75;return 1/d>a?b=c*a*a:2/d>a?(a-=1.5/d,b=c*a*a+.75):2.5/d>a?(a-=2.25/d,b=c*a*a+.9375):(a-=2.625/d,b=c*a*a+.984375),b},a.mina=r,r}("undefined"==typeof b?function(){}:b),d=function(){function d(a,b){if(a){if(a.tagName)return y(a);if(f(a,"array")&&d.set)return d.set.apply(d,a);if(a instanceof u)return a;if(null==b)return a=z.doc.querySelector(a),y(a)}return a=null==a?"100%":a,b=null==b?"100%":b,new x(a,b)}function e(a,b){if(b){if("#text"==a&&(a=z.doc.createTextNode(b.text||"")),"string"==typeof a&&(a=e(a)),"string"==typeof b)return"xlink:"==b.substring(0,6)?a.getAttributeNS(W,b.substring(6)):"xml:"==b.substring(0,4)?a.getAttributeNS(X,b.substring(4)):a.getAttribute(b);for(var c in b)if(b[A](c)){var d=B(b[c]);d?"xlink:"==c.substring(0,6)?a.setAttributeNS(W,c.substring(6),d):"xml:"==c.substring(0,4)?a.setAttributeNS(X,c.substring(4),d):a.setAttribute(c,d):a.removeAttribute(c)}}else a=z.doc.createElementNS(X,a);return a}function f(a,b){return b=B.prototype.toLowerCase.call(b),"finite"==b?isFinite(a):"array"==b&&(a instanceof Array||Array.isArray&&Array.isArray(a))?!0:"null"==b&&null===a||b==typeof a&&null!==a||"object"==b&&a===Object(a)||L.call(a).slice(8,-1).toLowerCase()==b}function h(a){if("function"==typeof a||Object(a)!==a)return a;var b=new a.constructor;for(var c in a)a[A](c)&&(b[c]=h(a[c]));return b}function i(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return a.push(a.splice(c,1)[0])}function j(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),g=d.cache=d.cache||{},h=d.count=d.count||[];return g[A](f)?(i(h,f),c?c(g[f]):g[f]):(h.length>=1e3&&delete g[h.shift()],h.push(f),g[f]=a.apply(b,e),c?c(g[f]):g[f])}return d}function k(a,b,c,d,e,f){if(null==e){var g=a-c,h=b-d;return g||h?(180+180*E.atan2(-h,-g)/I+360)%360:0}return k(a,b,e,f)-k(c,d,e,f)}function l(a){return a%360*I/180}function m(a){return 180*a/I%360}function n(a){var b=[];return a=a.replace(/(?:^|\s)(\w+)\(([^)]+)\)/g,function(a,c,d){return d=d.split(/\s*,\s*|\s+/),"rotate"==c&&1==d.length&&d.push(0,0),"scale"==c&&(d.length>2?d=d.slice(0,2):2==d.length&&d.push(0,0),1==d.length&&d.push(d[0],0,0)),b.push("skewX"==c?["m",1,0,E.tan(l(d[0])),1,0,0]:"skewY"==c?["m",1,E.tan(l(d[0])),0,1,0,0]:[c.charAt(0)].concat(d)),a}),b}function o(a,b){var c=eb(a),e=new d.Matrix;if(c)for(var f=0,g=c.length;g>f;f++){var h,i,j,k,l,m=c[f],n=m.length,o=B(m[0]).toLowerCase(),p=m[0]!=o,q=p?e.invert():0;"t"==o&&2==n?e.translate(m[1],0):"t"==o&&3==n?p?(h=q.x(0,0),i=q.y(0,0),j=q.x(m[1],m[2]),k=q.y(m[1],m[2]),e.translate(j-h,k-i)):e.translate(m[1],m[2]):"r"==o?2==n?(l=l||b,e.rotate(m[1],l.x+l.width/2,l.y+l.height/2)):4==n&&(p?(j=q.x(m[2],m[3]),k=q.y(m[2],m[3]),e.rotate(m[1],j,k)):e.rotate(m[1],m[2],m[3])):"s"==o?2==n||3==n?(l=l||b,e.scale(m[1],m[n-1],l.x+l.width/2,l.y+l.height/2)):4==n?p?(j=q.x(m[2],m[3]),k=q.y(m[2],m[3]),e.scale(m[1],m[1],j,k)):e.scale(m[1],m[1],m[2],m[3]):5==n&&(p?(j=q.x(m[3],m[4]),k=q.y(m[3],m[4]),e.scale(m[1],m[2],j,k)):e.scale(m[1],m[2],m[3],m[4])):"m"==o&&7==n&&e.add(m[1],m[2],m[3],m[4],m[5],m[6])}return e}function p(a,b){if(null==b){var c=!0;if(b=a.node.getAttribute("linearGradient"==a.type||"radialGradient"==a.type?"gradientTransform":"pattern"==a.type?"patternTransform":"transform"),!b)return new d.Matrix;b=n(b)}else b=d._.rgTransform.test(b)?B(b).replace(/\.{3}|\u2026/g,a._.transform||J):n(b),f(b,"array")&&(b=d.path?d.path.toString.call(b):B(b)),a._.transform=b;var e=o(b,a.getBBox(1));return c?e:void(a.matrix=e)}function q(a){var b=a.node.ownerSVGElement&&y(a.node.ownerSVGElement)||a.node.parentNode&&y(a.node.parentNode)||d.select("svg")||d(0,0),c=b.select("defs"),e=null==c?!1:c.node;return e||(e=w("defs",b.node).node),e}function r(a){return a.node.ownerSVGElement&&y(a.node.ownerSVGElement)||d.select("svg")}function s(a,b,c){function d(a){if(null==a)return J;if(a==+a)return a;e(j,{width:a});try{return j.getBBox().width}catch(b){return 0}}function f(a){if(null==a)return J;if(a==+a)return a;e(j,{height:a});try{return j.getBBox().height}catch(b){return 0}}function g(d,e){null==b?i[d]=e(a.attr(d)||0):d==b&&(i=e(null==c?a.attr(d)||0:c))}var h=r(a).node,i={},j=h.querySelector(".svg---mgr");switch(j||(j=e("rect"),e(j,{x:-9e9,y:-9e9,width:10,height:10,"class":"svg---mgr",fill:"none"}),h.appendChild(j)),a.type){case"rect":g("rx",d),g("ry",f);case"image":g("width",d),g("height",f);case"text":g("x",d),g("y",f);break;case"circle":g("cx",d),g("cy",f),g("r",d);break;case"ellipse":g("cx",d),g("cy",f),g("rx",d),g("ry",f);break;case"line":g("x1",d),g("x2",d),g("y1",f),g("y2",f);break;case"marker":g("refX",d),g("markerWidth",d),g("refY",f),g("markerHeight",f);break;case"radialGradient":g("fx",d),g("fy",f);break;case"tspan":g("dx",d),g("dy",f);break;default:g(b,d)}return h.removeChild(j),i}function t(a){f(a,"array")||(a=Array.prototype.slice.call(arguments,0));for(var b=0,c=0,d=this.node;this[b];)delete this[b++];for(b=0;b<a.length;b++)"set"==a[b].type?a[b].forEach(function(a){d.appendChild(a.node)}):d.appendChild(a[b].node);var e=d.childNodes;for(b=0;b<e.length;b++)this[c++]=y(e[b]);return this}function u(a){if(a.snap in Y)return Y[a.snap];var b,c=this.id=V();try{b=a.ownerSVGElement}catch(d){}if(this.node=a,b&&(this.paper=new x(b)),this.type=a.tagName,this.anims={},this._={transform:[]},a.snap=c,Y[c]=this,"g"==this.type&&(this.add=t),this.type in{g:1,mask:1,pattern:1})for(var e in x.prototype)x.prototype[A](e)&&(this[e]=x.prototype[e])}function v(a){this.node=a}function w(a,b){var c=e(a);b.appendChild(c);var d=y(c);return d}function x(a,b){var c,d,f,g=x.prototype;if(a&&"svg"==a.tagName){if(a.snap in Y)return Y[a.snap];var h=a.ownerDocument;c=new u(a),d=a.getElementsByTagName("desc")[0],f=a.getElementsByTagName("defs")[0],d||(d=e("desc"),d.appendChild(h.createTextNode("Created with Snap")),c.node.appendChild(d)),f||(f=e("defs"),c.node.appendChild(f)),c.defs=f;for(var i in g)g[A](i)&&(c[i]=g[i]);c.paper=c.root=c}else c=w("svg",z.doc.body),e(c.node,{height:b,version:1.1,width:a,xmlns:X});return c}function y(a){return a?a instanceof u||a instanceof v?a:a.tagName&&"svg"==a.tagName.toLowerCase()?new x(a):a.tagName&&"object"==a.tagName.toLowerCase()&&"image/svg+xml"==a.type?new x(a.contentDocument.getElementsByTagName("svg")[0]):new u(a):a}d.version="0.3.0",d.toString=function(){return"Snap v"+this.version},d._={};var z={win:a,doc:a.document};d._.glob=z;var A="hasOwnProperty",B=String,C=parseFloat,D=parseInt,E=Math,F=E.max,G=E.min,H=E.abs,I=(E.pow,E.PI),J=(E.round,""),K=" ",L=Object.prototype.toString,M=/^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\))\s*$/i,N=" \n \f\r   ᠎              \u2028\u2029",O=(d._.separator=new RegExp("[,"+N+"]+"),new RegExp("["+N+"]","g"),new RegExp("["+N+"]*,["+N+"]*")),P={hs:1,rg:1},Q=new RegExp("([a-z])["+N+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+N+"]*,?["+N+"]*)+)","ig"),R=new RegExp("([rstm])["+N+",]*((-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?["+N+"]*,?["+N+"]*)+)","ig"),S=new RegExp("(-?\\d*\\.?\\d*(?:e[\\-+]?\\d+)?)["+N+"]*,?["+N+"]*","ig"),T=0,U="S"+(+new Date).toString(36),V=function(){return U+(T++).toString(36)},W="http://www.w3.org/1999/xlink",X="http://www.w3.org/2000/svg",Y={},Z=d.url=function(a){return"url('#"+a+"')"};d._.$=e,d._.id=V,d.format=function(){var a=/\{([^\}]+)\}/g,b=/(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g,c=function(a,c,d){var e=d;return c.replace(b,function(a,b,c,d,f){b=b||d,e&&(b in e&&(e=e[b]),"function"==typeof e&&f&&(e=e()))}),e=(null==e||e==d?a:e)+""};return function(b,d){return B(b).replace(a,function(a,b){return c(a,b,d)})}}(),d._.clone=h,d._.cacher=j,d.rad=l,d.deg=m,d.angle=k,d.is=f,d.snapTo=function(a,b,c){if(c=f(c,"finite")?c:10,f(a,"array")){for(var d=a.length;d--;)if(H(a[d]-b)<=c)return a[d]}else{a=+a;var e=b%a;if(c>e)return b-e;if(e>a-c)return b-e+a}return b},d.getRGB=j(function(a){if(!a||(a=B(a)).indexOf("-")+1)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bb};if("none"==a)return{r:-1,g:-1,b:-1,hex:"none",toString:bb};if(!(P[A](a.toLowerCase().substring(0,2))||"#"==a.charAt())&&(a=$(a)),!a)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bb};var b,c,e,g,h,i,j=a.match(M);return j?(j[2]&&(e=D(j[2].substring(5),16),c=D(j[2].substring(3,5),16),b=D(j[2].substring(1,3),16)),j[3]&&(e=D((h=j[3].charAt(3))+h,16),c=D((h=j[3].charAt(2))+h,16),b=D((h=j[3].charAt(1))+h,16)),j[4]&&(i=j[4].split(O),b=C(i[0]),"%"==i[0].slice(-1)&&(b*=2.55),c=C(i[1]),"%"==i[1].slice(-1)&&(c*=2.55),e=C(i[2]),"%"==i[2].slice(-1)&&(e*=2.55),"rgba"==j[1].toLowerCase().slice(0,4)&&(g=C(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100)),j[5]?(i=j[5].split(O),b=C(i[0]),"%"==i[0].slice(-1)&&(b/=100),c=C(i[1]),"%"==i[1].slice(-1)&&(c/=100),e=C(i[2]),"%"==i[2].slice(-1)&&(e/=100),("deg"==i[0].slice(-3)||"°"==i[0].slice(-1))&&(b/=360),"hsba"==j[1].toLowerCase().slice(0,4)&&(g=C(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100),d.hsb2rgb(b,c,e,g)):j[6]?(i=j[6].split(O),b=C(i[0]),"%"==i[0].slice(-1)&&(b/=100),c=C(i[1]),"%"==i[1].slice(-1)&&(c/=100),e=C(i[2]),"%"==i[2].slice(-1)&&(e/=100),("deg"==i[0].slice(-3)||"°"==i[0].slice(-1))&&(b/=360),"hsla"==j[1].toLowerCase().slice(0,4)&&(g=C(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100),d.hsl2rgb(b,c,e,g)):(b=G(E.round(b),255),c=G(E.round(c),255),e=G(E.round(e),255),g=G(F(g,0),1),j={r:b,g:c,b:e,toString:bb},j.hex="#"+(16777216|e|c<<8|b<<16).toString(16).slice(1),j.opacity=f(g,"finite")?g:1,j)):{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bb}},d),d.hsb=j(function(a,b,c){return d.hsb2rgb(a,b,c).hex}),d.hsl=j(function(a,b,c){return d.hsl2rgb(a,b,c).hex}),d.rgb=j(function(a,b,c,d){if(f(d,"finite")){var e=E.round;return"rgba("+[e(a),e(b),e(c),+d.toFixed(2)]+")"}return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)});var $=function(a){var b=z.doc.getElementsByTagName("head")[0]||z.doc.getElementsByTagName("svg")[0],c="rgb(255, 0, 0)";return($=j(function(a){if("red"==a.toLowerCase())return c;b.style.color=c,b.style.color=a;var d=z.doc.defaultView.getComputedStyle(b,J).getPropertyValue("color");return d==c?null:d}))(a)},_=function(){return"hsb("+[this.h,this.s,this.b]+")"},ab=function(){return"hsl("+[this.h,this.s,this.l]+")"},bb=function(){return 1==this.opacity||null==this.opacity?this.hex:"rgba("+[this.r,this.g,this.b,this.opacity]+")"},cb=function(a,b,c){if(null==b&&f(a,"object")&&"r"in a&&"g"in a&&"b"in a&&(c=a.b,b=a.g,a=a.r),null==b&&f(a,string)){var e=d.getRGB(a);a=e.r,b=e.g,c=e.b}return(a>1||b>1||c>1)&&(a/=255,b/=255,c/=255),[a,b,c]},db=function(a,b,c,e){a=E.round(255*a),b=E.round(255*b),c=E.round(255*c);var g={r:a,g:b,b:c,opacity:f(e,"finite")?e:1,hex:d.rgb(a,b,c),toString:bb};return f(e,"finite")&&(g.opacity=e),g};d.color=function(a){var b;return f(a,"object")&&"h"in a&&"s"in a&&"b"in a?(b=d.hsb2rgb(a),a.r=b.r,a.g=b.g,a.b=b.b,a.opacity=1,a.hex=b.hex):f(a,"object")&&"h"in a&&"s"in a&&"l"in a?(b=d.hsl2rgb(a),a.r=b.r,a.g=b.g,a.b=b.b,a.opacity=1,a.hex=b.hex):(f(a,"string")&&(a=d.getRGB(a)),f(a,"object")&&"r"in a&&"g"in a&&"b"in a&&!("error"in a)?(b=d.rgb2hsl(a),a.h=b.h,a.s=b.s,a.l=b.l,b=d.rgb2hsb(a),a.v=b.b):(a={hex:"none"},a.r=a.g=a.b=a.h=a.s=a.v=a.l=-1,a.error=1)),a.toString=bb,a},d.hsb2rgb=function(a,b,c,d){f(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,a=a.h,d=a.o),a*=360;var e,g,h,i,j;return a=a%360/60,j=c*b,i=j*(1-H(a%2-1)),e=g=h=c-j,a=~~a,e+=[j,i,0,0,i,j][a],g+=[i,j,j,i,0,0][a],h+=[0,0,i,j,j,i][a],db(e,g,h,d)},d.hsl2rgb=function(a,b,c,d){f(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h),(a>1||b>1||c>1)&&(a/=360,b/=100,c/=100),a*=360;var e,g,h,i,j;return a=a%360/60,j=2*b*(.5>c?c:1-c),i=j*(1-H(a%2-1)),e=g=h=c-j/2,a=~~a,e+=[j,i,0,0,i,j][a],g+=[i,j,j,i,0,0][a],h+=[0,0,i,j,j,i][a],db(e,g,h,d)},d.rgb2hsb=function(a,b,c){c=cb(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;return f=F(a,b,c),g=f-G(a,b,c),d=0==g?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=0==g?0:g/f,{h:d,s:e,b:f,toString:_}},d.rgb2hsl=function(a,b,c){c=cb(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;return g=F(a,b,c),h=G(a,b,c),i=g-h,d=0==i?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=0==i?0:.5>f?i/(2*f):i/(2-2*f),{h:d,s:e,l:f,toString:ab}},d.parsePathString=function(a){if(!a)return null;var b=d.path(a);if(b.arr)return d.path.clone(b.arr);var c={a:7,c:6,o:2,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,u:3,z:0},e=[];return f(a,"array")&&f(a[0],"array")&&(e=d.path.clone(a)),e.length||B(a).replace(Q,function(a,b,d){var f=[],g=b.toLowerCase();if(d.replace(S,function(a,b){b&&f.push(+b)}),"m"==g&&f.length>2&&(e.push([b].concat(f.splice(0,2))),g="l",b="m"==b?"l":"L"),"o"==g&&1==f.length&&e.push([b,f[0]]),"r"==g)e.push([b].concat(f));else for(;f.length>=c[g]&&(e.push([b].concat(f.splice(0,c[g]))),c[g]););}),e.toString=d.path.toString,b.arr=d.path.clone(e),e};var eb=d.parseTransformString=function(a){if(!a)return null;var b=[];return f(a,"array")&&f(a[0],"array")&&(b=d.path.clone(a)),b.length||B(a).replace(R,function(a,c,d){{var e=[];c.toLowerCase()}d.replace(S,function(a,b){b&&e.push(+b)}),b.push([c].concat(e))}),b.toString=d.path.toString,b};d._.svgTransform2string=n,d._.rgTransform=new RegExp("^[a-z]["+N+"]*-?\\.?\\d","i"),d._.transform2matrix=o,d._unit2px=s;z.doc.contains||z.doc.compareDocumentPosition?function(a,b){var c=9==a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a==d||!(!d||1!=d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)for(;b;)if(b=b.parentNode,b==a)return!0;return!1};d._.getSomeDefs=q,d._.getSomeSVG=r,d.select=function(a){return y(z.doc.querySelector(a))},d.selectAll=function(a){for(var b=z.doc.querySelectorAll(a),c=(d.set||Array)(),e=0;e<b.length;e++)c.push(y(b[e]));return c},setInterval(function(){for(var a in Y)if(Y[A](a)){var b=Y[a],c=b.node;("svg"!=b.type&&!c.ownerSVGElement||"svg"==b.type&&(!c.parentNode||"ownerSVGElement"in c.parentNode&&!c.ownerSVGElement))&&delete Y[a]}},1e4),function(a){function g(a){function b(a,b){var c=e(a.node,b);c=c&&c.match(g),c=c&&c[2],c&&"#"==c.charAt()&&(c=c.substring(1),c&&(i[c]=(i[c]||[]).concat(function(c){var d={};d[b]=Z(c),e(a.node,d)})))}function c(a){var b=e(a.node,"xlink:href");b&&"#"==b.charAt()&&(b=b.substring(1),b&&(i[b]=(i[b]||[]).concat(function(b){a.attr("xlink:href","#"+b)})))}for(var d,f=a.selectAll("*"),g=/^\s*url\(("|'|)(.*)\1\)\s*$/,h=[],i={},j=0,k=f.length;k>j;j++){d=f[j],b(d,"fill"),b(d,"stroke"),b(d,"filter"),b(d,"mask"),b(d,"clip-path"),c(d);var l=e(d.node,"id");l&&(e(d.node,{id:d.id}),h.push({old:l,id:d.id}))}for(j=0,k=h.length;k>j;j++){var m=i[h[j].old];if(m)for(var n=0,o=m.length;o>n;n++)m[n](h[j].id)}}function h(a,b,c){return function(d){var e=d.slice(a,b);return 1==e.length&&(e=e[0]),c?c(e):e}}function i(a){return function(){var b=a?"<"+this.type:"",c=this.node.attributes,d=this.node.childNodes;if(a)for(var e=0,f=c.length;f>e;e++)b+=" "+c[e].name+'="'+c[e].value.replace(/"/g,'\\"')+'"';if(d.length){for(a&&(b+=">"),e=0,f=d.length;f>e;e++)3==d[e].nodeType?b+=d[e].nodeValue:1==d[e].nodeType&&(b+=y(d[e]).toString());a&&(b+="</"+this.type+">")}else a&&(b+="/>");return b}}a.attr=function(a,c){{var d=this;d.node}if(!a)return d;if(f(a,"string")){if(!(arguments.length>1))return b("snap.util.getattr."+a,d).firstDefined();var e={};e[a]=c,a=e}for(var g in a)a[A](g)&&b("snap.util.attr."+g,d,a[g]);return d},a.getBBox=function(a){if(!d.Matrix||!d.path)return this.node.getBBox();var b=this,c=new d.Matrix;if(b.removed)return d._.box();for(;"use"==b.type;)if(a||(c=c.add(b.transform().localMatrix.translate(b.attr("x")||0,b.attr("y")||0))),b.original)b=b.original;else{var e=b.attr("xlink:href");b=b.original=b.node.ownerDocument.getElementById(e.substring(e.indexOf("#")+1))}var f=b._,g=d.path.get[b.type]||d.path.get.deflt;try{return a?(f.bboxwt=g?d.path.getBBox(b.realPath=g(b)):d._.box(b.node.getBBox()),d._.box(f.bboxwt)):(b.realPath=g(b),b.matrix=b.transform().localMatrix,f.bbox=d.path.getBBox(d.path.map(b.realPath,c.add(b.matrix))),d._.box(f.bbox))}catch(h){return d._.box()}};var j=function(){return this.string};a.transform=function(a){var b=this._;if(null==a){for(var c,f=this,g=new d.Matrix(this.node.getCTM()),h=p(this),i=[h],k=new d.Matrix,l=h.toTransformString(),m=B(h)==B(this.matrix)?B(b.transform):l;"svg"!=f.type&&(f=f.parent());)i.push(p(f));for(c=i.length;c--;)k.add(i[c]);return{string:m,globalMatrix:g,totalMatrix:k,localMatrix:h,diffMatrix:g.clone().add(h.invert()),global:g.toTransformString(),total:k.toTransformString(),local:l,toString:j}}return a instanceof d.Matrix?this.matrix=a:p(this,a),this.node&&("linearGradient"==this.type||"radialGradient"==this.type?e(this.node,{gradientTransform:this.matrix}):"pattern"==this.type?e(this.node,{patternTransform:this.matrix}):e(this.node,{transform:this.matrix})),this},a.parent=function(){return y(this.node.parentNode)},a.append=a.add=function(a){if(a){if("set"==a.type){var b=this;return a.forEach(function(a){b.add(a)}),this}a=y(a),this.node.appendChild(a.node),a.paper=this.paper}return this},a.appendTo=function(a){return a&&(a=y(a),a.append(this)),this},a.prepend=function(a){if(a){if("set"==a.type){var b,c=this;return a.forEach(function(a){b?b.after(a):c.prepend(a),b=a}),this}a=y(a);var d=a.parent();this.node.insertBefore(a.node,this.node.firstChild),this.add&&this.add(),a.paper=this.paper,this.parent()&&this.parent().add(),d&&d.add()}return this},a.prependTo=function(a){return a=y(a),a.prepend(this),this},a.before=function(a){if("set"==a.type){var b=this;return a.forEach(function(a){var c=a.parent();b.node.parentNode.insertBefore(a.node,b.node),c&&c.add()}),this.parent().add(),this}a=y(a);var c=a.parent();return this.node.parentNode.insertBefore(a.node,this.node),this.parent()&&this.parent().add(),c&&c.add(),a.paper=this.paper,this},a.after=function(a){a=y(a);var b=a.parent();return this.node.nextSibling?this.node.parentNode.insertBefore(a.node,this.node.nextSibling):this.node.parentNode.appendChild(a.node),this.parent()&&this.parent().add(),b&&b.add(),a.paper=this.paper,this},a.insertBefore=function(a){a=y(a);var b=this.parent();return a.node.parentNode.insertBefore(this.node,a.node),this.paper=a.paper,b&&b.add(),a.parent()&&a.parent().add(),this},a.insertAfter=function(a){a=y(a);var b=this.parent();return a.node.parentNode.insertBefore(this.node,a.node.nextSibling),this.paper=a.paper,b&&b.add(),a.parent()&&a.parent().add(),this},a.remove=function(){var a=this.parent();return this.node.parentNode&&this.node.parentNode.removeChild(this.node),delete this.paper,this.removed=!0,a&&a.add(),this},a.select=function(a){return y(this.node.querySelector(a))},a.selectAll=function(a){for(var b=this.node.querySelectorAll(a),c=(d.set||Array)(),e=0;e<b.length;e++)c.push(y(b[e]));return c},a.asPX=function(a,b){return null==b&&(b=this.attr(a)),+s(this,a,b)},a.use=function(){var a,b=this.node.id;return b||(b=this.id,e(this.node,{id:b})),a="linearGradient"==this.type||"radialGradient"==this.type||"pattern"==this.type?w(this.type,this.node.parentNode):w("use",this.node.parentNode),e(a.node,{"xlink:href":"#"+b}),a.original=this,a};var k=/\S+/g;a.addClass=function(a){var b,c,d,e,f=(a||"").match(k)||[],g=this.node,h=g.className.baseVal,i=h.match(k)||[];if(f.length){for(b=0;d=f[b++];)c=i.indexOf(d),~c||i.push(d);e=i.join(" "),h!=e&&(g.className.baseVal=e)}return this},a.removeClass=function(a){var b,c,d,e,f=(a||"").match(k)||[],g=this.node,h=g.className.baseVal,i=h.match(k)||[];if(i.length){for(b=0;d=f[b++];)c=i.indexOf(d),~c&&i.splice(c,1);e=i.join(" "),h!=e&&(g.className.baseVal=e)}return this},a.hasClass=function(a){var b=this.node,c=b.className.baseVal,d=c.match(k)||[];return!!~d.indexOf(a)},a.toggleClass=function(a,b){if(null!=b)return b?this.addClass(a):this.removeClass(a);var c,d,e,f,g=(a||"").match(k)||[],h=this.node,i=h.className.baseVal,j=i.match(k)||[];for(c=0;e=g[c++];)d=j.indexOf(e),~d?j.splice(d,1):j.push(e);return f=j.join(" "),i!=f&&(h.className.baseVal=f),this},a.clone=function(){var a=y(this.node.cloneNode(!0));return e(a.node,"id")&&e(a.node,{id:a.id}),g(a),a.insertAfter(this),a},a.toDefs=function(){var a=q(this);return a.appendChild(this.node),this},a.pattern=a.toPattern=function(a,b,c,d){var g=w("pattern",q(this));return null==a&&(a=this.getBBox()),f(a,"object")&&"x"in a&&(b=a.y,c=a.width,d=a.height,a=a.x),e(g.node,{x:a,y:b,width:c,height:d,patternUnits:"userSpaceOnUse",id:g.id,viewBox:[a,b,c,d].join(" ")}),g.node.appendChild(this.node),g},a.marker=function(a,b,c,d,g,h){var i=w("marker",q(this));return null==a&&(a=this.getBBox()),f(a,"object")&&"x"in a&&(b=a.y,c=a.width,d=a.height,g=a.refX||a.cx,h=a.refY||a.cy,a=a.x),e(i.node,{viewBox:[a,b,c,d].join(K),markerWidth:c,markerHeight:d,orient:"auto",refX:g||0,refY:h||0,id:i.id}),i.node.appendChild(this.node),i};var l=function(a,b,d,e){"function"!=typeof d||d.length||(e=d,d=c.linear),this.attr=a,this.dur=b,d&&(this.easing=d),e&&(this.callback=e)};d._.Animation=l,d.animation=function(a,b,c,d){return new l(a,b,c,d)},a.inAnim=function(){var a=this,b=[];for(var c in a.anims)a.anims[A](c)&&!function(a){b.push({anim:new l(a._attrs,a.dur,a.easing,a._callback),mina:a,curStatus:a.status(),status:function(b){return a.status(b)},stop:function(){a.stop()}})}(a.anims[c]);return b},d.animate=function(a,d,e,f,g,h){"function"!=typeof g||g.length||(h=g,g=c.linear);var i=c.time(),j=c(a,d,i,i+f,c.time,e,g);return h&&b.once("mina.finish."+j.id,h),j},a.stop=function(){for(var a=this.inAnim(),b=0,c=a.length;c>b;b++)a[b].stop();return this},a.animate=function(a,d,e,g){"function"!=typeof e||e.length||(g=e,e=c.linear),a instanceof l&&(g=a.callback,e=a.easing,d=e.dur,a=a.attr);var i,j,k,m,n=[],o=[],p={},q=this;for(var r in a)if(a[A](r)){q.equal?(m=q.equal(r,B(a[r])),i=m.from,j=m.to,k=m.f):(i=+q.attr(r),j=+a[r]);var s=f(i,"array")?i.length:1;p[r]=h(n.length,n.length+s,k),n=n.concat(i),o=o.concat(j)}var t=c.time(),u=c(n,o,t,t+d,c.time,function(a){var b={};for(var c in p)p[A](c)&&(b[c]=p[c](a));q.attr(b)},e);return q.anims[u.id]=u,u._attrs=a,u._callback=g,b("snap.animcreated."+q.id,u),b.once("mina.finish."+u.id,function(){delete q.anims[u.id],g&&g.call(q)}),b.once("mina.stop."+u.id,function(){delete q.anims[u.id]}),q};var m={};a.data=function(a,c){var e=m[this.id]=m[this.id]||{};if(0==arguments.length)return b("snap.data.get."+this.id,this,e,null),e;if(1==arguments.length){if(d.is(a,"object")){for(var f in a)a[A](f)&&this.data(f,a[f]);return this}return b("snap.data.get."+this.id,this,e[a],a),e[a]}return e[a]=c,b("snap.data.set."+this.id,this,c,a),this},a.removeData=function(a){return null==a?m[this.id]={}:m[this.id]&&delete m[this.id][a],this},a.outerSVG=a.toString=i(1),a.innerSVG=i()}(u.prototype),d.parse=function(a){var b=z.doc.createDocumentFragment(),c=!0,d=z.doc.createElement("div");if(a=B(a),a.match(/^\s*<\s*svg(?:\s|>)/)||(a="<svg>"+a+"</svg>",c=!1),d.innerHTML=a,a=d.getElementsByTagName("svg")[0])if(c)b=a;else for(;a.firstChild;)b.appendChild(a.firstChild);return d.innerHTML=J,new v(b)},v.prototype.select=u.prototype.select,v.prototype.selectAll=u.prototype.selectAll,d.fragment=function(){for(var a=Array.prototype.slice.call(arguments,0),b=z.doc.createDocumentFragment(),c=0,e=a.length;e>c;c++){var f=a[c];f.node&&f.node.nodeType&&b.appendChild(f.node),f.nodeType&&b.appendChild(f),"string"==typeof f&&b.appendChild(d.parse(f).node)}return new v(b)},d._.make=w,d._.wrap=y,x.prototype.el=function(a,b){var c=w(a,this.node);return b&&c.attr(b),c},b.on("snap.util.getattr",function(){var a=b.nt();a=a.substring(a.lastIndexOf(".")+1);var c=a.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()});return fb[A](c)?this.node.ownerDocument.defaultView.getComputedStyle(this.node,null).getPropertyValue(c):e(this.node,a)});var fb={"alignment-baseline":0,"baseline-shift":0,clip:0,"clip-path":0,"clip-rule":0,color:0,"color-interpolation":0,"color-interpolation-filters":0,"color-profile":0,"color-rendering":0,cursor:0,direction:0,display:0,"dominant-baseline":0,"enable-background":0,fill:0,"fill-opacity":0,"fill-rule":0,filter:0,"flood-color":0,"flood-opacity":0,font:0,"font-family":0,"font-size":0,"font-size-adjust":0,"font-stretch":0,"font-style":0,"font-variant":0,"font-weight":0,"glyph-orientation-horizontal":0,"glyph-orientation-vertical":0,"image-rendering":0,kerning:0,"letter-spacing":0,"lighting-color":0,marker:0,"marker-end":0,"marker-mid":0,"marker-start":0,mask:0,opacity:0,overflow:0,"pointer-events":0,"shape-rendering":0,"stop-color":0,"stop-opacity":0,stroke:0,"stroke-dasharray":0,"stroke-dashoffset":0,"stroke-linecap":0,"stroke-linejoin":0,"stroke-miterlimit":0,"stroke-opacity":0,"stroke-width":0,"text-anchor":0,"text-decoration":0,"text-rendering":0,"unicode-bidi":0,visibility:0,"word-spacing":0,"writing-mode":0};b.on("snap.util.attr",function(a){var c=b.nt(),d={};c=c.substring(c.lastIndexOf(".")+1),d[c]=a;var f=c.replace(/-(\w)/gi,function(a,b){return b.toUpperCase()}),g=c.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()});fb[A](g)?this.node.style[f]=null==a?J:a:e(this.node,d)}),function(){}(x.prototype),d.ajax=function(a,c,d,e){var g=new XMLHttpRequest,h=V();if(g){if(f(c,"function"))e=d,d=c,c=null;else if(f(c,"object")){var i=[];for(var j in c)c.hasOwnProperty(j)&&i.push(encodeURIComponent(j)+"="+encodeURIComponent(c[j]));c=i.join("&")}return g.open(c?"POST":"GET",a,!0),c&&(g.setRequestHeader("X-Requested-With","XMLHttpRequest"),g.setRequestHeader("Content-type","application/x-www-form-urlencoded")),d&&(b.once("snap.ajax."+h+".0",d),b.once("snap.ajax."+h+".200",d),b.once("snap.ajax."+h+".304",d)),g.onreadystatechange=function(){4==g.readyState&&b("snap.ajax."+h+"."+g.status,e,g)},4==g.readyState?g:(g.send(c),g)}},d.load=function(a,b,c){d.ajax(a,function(a){var e=d.parse(a.responseText);c?b.call(c,e):b(e)})};var gb=function(a){var b=a.getBoundingClientRect(),c=a.ownerDocument,d=c.body,e=c.documentElement,f=e.clientTop||d.clientTop||0,h=e.clientLeft||d.clientLeft||0,i=b.top+(g.win.pageYOffset||e.scrollTop||d.scrollTop)-f,j=b.left+(g.win.pageXOffset||e.scrollLeft||d.scrollLeft)-h;return{y:i,x:j}};return d.getElementByPoint=function(a,b){var c=this,d=(c.canvas,z.doc.elementFromPoint(a,b));if(z.win.opera&&"svg"==d.tagName){var e=gb(d),f=d.createSVGRect();f.x=a-e.x,f.y=b-e.y,f.width=f.height=1;var g=d.getIntersectionList(f,null);g.length&&(d=g[g.length-1])}return d?y(d):null},d.plugin=function(a){a(d,u,x,z,v)},z.win.Snap=d,d}();return d.plugin(function(a){function b(a,b,d,e,f,g){return null==b&&"[object SVGMatrix]"==c.call(a)?(this.a=a.a,this.b=a.b,this.c=a.c,this.d=a.d,this.e=a.e,void(this.f=a.f)):void(null!=a?(this.a=+a,this.b=+b,this.c=+d,this.d=+e,this.e=+f,this.f=+g):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0))}var c=Object.prototype.toString,d=String,e=Math,f="";!function(c){function g(a){return a[0]*a[0]+a[1]*a[1]
+}function h(a){var b=e.sqrt(g(a));a[0]&&(a[0]/=b),a[1]&&(a[1]/=b)}c.add=function(a,c,d,e,f,g){var h,i,j,k,l=[[],[],[]],m=[[this.a,this.c,this.e],[this.b,this.d,this.f],[0,0,1]],n=[[a,d,f],[c,e,g],[0,0,1]];for(a&&a instanceof b&&(n=[[a.a,a.c,a.e],[a.b,a.d,a.f],[0,0,1]]),h=0;3>h;h++)for(i=0;3>i;i++){for(k=0,j=0;3>j;j++)k+=m[h][j]*n[j][i];l[h][i]=k}return this.a=l[0][0],this.b=l[1][0],this.c=l[0][1],this.d=l[1][1],this.e=l[0][2],this.f=l[1][2],this},c.invert=function(){var a=this,c=a.a*a.d-a.b*a.c;return new b(a.d/c,-a.b/c,-a.c/c,a.a/c,(a.c*a.f-a.d*a.e)/c,(a.b*a.e-a.a*a.f)/c)},c.clone=function(){return new b(this.a,this.b,this.c,this.d,this.e,this.f)},c.translate=function(a,b){return this.add(1,0,0,1,a,b)},c.scale=function(a,b,c,d){return null==b&&(b=a),(c||d)&&this.add(1,0,0,1,c,d),this.add(a,0,0,b,0,0),(c||d)&&this.add(1,0,0,1,-c,-d),this},c.rotate=function(b,c,d){b=a.rad(b),c=c||0,d=d||0;var f=+e.cos(b).toFixed(9),g=+e.sin(b).toFixed(9);return this.add(f,g,-g,f,c,d),this.add(1,0,0,1,-c,-d)},c.x=function(a,b){return a*this.a+b*this.c+this.e},c.y=function(a,b){return a*this.b+b*this.d+this.f},c.get=function(a){return+this[d.fromCharCode(97+a)].toFixed(4)},c.toString=function(){return"matrix("+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)].join()+")"},c.offset=function(){return[this.e.toFixed(4),this.f.toFixed(4)]},c.determinant=function(){return this.a*this.d-this.b*this.c},c.split=function(){var b={};b.dx=this.e,b.dy=this.f;var c=[[this.a,this.c],[this.b,this.d]];b.scalex=e.sqrt(g(c[0])),h(c[0]),b.shear=c[0][0]*c[1][0]+c[0][1]*c[1][1],c[1]=[c[1][0]-c[0][0]*b.shear,c[1][1]-c[0][1]*b.shear],b.scaley=e.sqrt(g(c[1])),h(c[1]),b.shear/=b.scaley,this.determinant()<0&&(b.scalex=-b.scalex);var d=-c[0][1],f=c[1][1];return 0>f?(b.rotate=a.deg(e.acos(f)),0>d&&(b.rotate=360-b.rotate)):b.rotate=a.deg(e.asin(d)),b.isSimple=!(+b.shear.toFixed(9)||b.scalex.toFixed(9)!=b.scaley.toFixed(9)&&b.rotate),b.isSuperSimple=!+b.shear.toFixed(9)&&b.scalex.toFixed(9)==b.scaley.toFixed(9)&&!b.rotate,b.noRotation=!+b.shear.toFixed(9)&&!b.rotate,b},c.toTransformString=function(a){var b=a||this.split();return+b.shear.toFixed(9)?"m"+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)]:(b.scalex=+b.scalex.toFixed(4),b.scaley=+b.scaley.toFixed(4),b.rotate=+b.rotate.toFixed(4),(b.dx||b.dy?"t"+[+b.dx.toFixed(4),+b.dy.toFixed(4)]:f)+(1!=b.scalex||1!=b.scaley?"s"+[b.scalex,b.scaley,0,0]:f)+(b.rotate?"r"+[+b.rotate.toFixed(4),0,0]:f))}}(b.prototype),a.Matrix=b,a.matrix=function(a,c,d,e,f,g){return new b(a,c,d,e,f,g)}}),d.plugin(function(a,c,d,e,f){function g(d){return function(e){if(b.stop(),e instanceof f&&1==e.node.childNodes.length&&("radialGradient"==e.node.firstChild.tagName||"linearGradient"==e.node.firstChild.tagName||"pattern"==e.node.firstChild.tagName)&&(e=e.node.firstChild,n(this).appendChild(e),e=l(e)),e instanceof c)if("radialGradient"==e.type||"linearGradient"==e.type||"pattern"==e.type){e.node.id||p(e.node,{id:e.id});var g=q(e.node.id)}else g=e.attr(d);else if(g=a.color(e),g.error){var h=a(n(this).ownerSVGElement).gradient(e);h?(h.node.id||p(h.node,{id:h.id}),g=q(h.node.id)):g=e}else g=r(g);var i={};i[d]=g,p(this.node,i),this.node.style[d]=t}}function h(a){b.stop(),a==+a&&(a+="px"),this.node.style.fontSize=a}function i(a){for(var b=[],c=a.childNodes,d=0,e=c.length;e>d;d++){var f=c[d];3==f.nodeType&&b.push(f.nodeValue),"tspan"==f.tagName&&b.push(1==f.childNodes.length&&3==f.firstChild.nodeType?f.firstChild.nodeValue:i(f))}return b}function j(){return b.stop(),this.node.style.fontSize}var k=a._.make,l=a._.wrap,m=a.is,n=a._.getSomeDefs,o=/^url\(#?([^)]+)\)$/,p=a._.$,q=a.url,r=String,s=a._.separator,t="";b.on("snap.util.attr.mask",function(a){if(a instanceof c||a instanceof f){if(b.stop(),a instanceof f&&1==a.node.childNodes.length&&(a=a.node.firstChild,n(this).appendChild(a),a=l(a)),"mask"==a.type)var d=a;else d=k("mask",n(this)),d.node.appendChild(a.node);!d.node.id&&p(d.node,{id:d.id}),p(this.node,{mask:q(d.id)})}}),function(a){b.on("snap.util.attr.clip",a),b.on("snap.util.attr.clip-path",a),b.on("snap.util.attr.clipPath",a)}(function(a){if(a instanceof c||a instanceof f){if(b.stop(),"clipPath"==a.type)var d=a;else d=k("clipPath",n(this)),d.node.appendChild(a.node),!d.node.id&&p(d.node,{id:d.id});p(this.node,{"clip-path":q(d.id)})}}),b.on("snap.util.attr.fill",g("fill")),b.on("snap.util.attr.stroke",g("stroke"));var u=/^([lr])(?:\(([^)]*)\))?(.*)$/i;b.on("snap.util.grad.parse",function(a){a=r(a);var b=a.match(u);if(!b)return null;var c=b[1],d=b[2],e=b[3];return d=d.split(/\s*,\s*/).map(function(a){return+a==a?+a:a}),1==d.length&&0==d[0]&&(d=[]),e=e.split("-"),e=e.map(function(a){a=a.split(":");var b={color:a[0]};return a[1]&&(b.offset=parseFloat(a[1])),b}),{type:c,params:d,stops:e}}),b.on("snap.util.attr.d",function(c){b.stop(),m(c,"array")&&m(c[0],"array")&&(c=a.path.toString.call(c)),c=r(c),c.match(/[ruo]/i)&&(c=a.path.toAbsolute(c)),p(this.node,{d:c})})(-1),b.on("snap.util.attr.#text",function(a){b.stop(),a=r(a);for(var c=e.doc.createTextNode(a);this.node.firstChild;)this.node.removeChild(this.node.firstChild);this.node.appendChild(c)})(-1),b.on("snap.util.attr.path",function(a){b.stop(),this.attr({d:a})})(-1),b.on("snap.util.attr.class",function(a){b.stop(),this.node.className.baseVal=a})(-1),b.on("snap.util.attr.viewBox",function(a){var c;c=m(a,"object")&&"x"in a?[a.x,a.y,a.width,a.height].join(" "):m(a,"array")?a.join(" "):a,p(this.node,{viewBox:c}),b.stop()})(-1),b.on("snap.util.attr.transform",function(a){this.transform(a),b.stop()})(-1),b.on("snap.util.attr.r",function(a){"rect"==this.type&&(b.stop(),p(this.node,{rx:a,ry:a}))})(-1),b.on("snap.util.attr.textpath",function(a){if(b.stop(),"text"==this.type){var d,e,f;if(!a&&this.textPath){for(e=this.textPath;e.node.firstChild;)this.node.appendChild(e.node.firstChild);return e.remove(),void delete this.textPath}if(m(a,"string")){var g=n(this),h=l(g.parentNode).path(a);g.appendChild(h.node),d=h.id,h.attr({id:d})}else a=l(a),a instanceof c&&(d=a.attr("id"),d||(d=a.id,a.attr({id:d})));if(d)if(e=this.textPath,f=this.node,e)e.attr({"xlink:href":"#"+d});else{for(e=p("textPath",{"xlink:href":"#"+d});f.firstChild;)e.appendChild(f.firstChild);f.appendChild(e),this.textPath=l(e)}}})(-1),b.on("snap.util.attr.text",function(a){if("text"==this.type){for(var c=this.node,d=function(a){var b=p("tspan");if(m(a,"array"))for(var c=0;c<a.length;c++)b.appendChild(d(a[c]));else b.appendChild(e.doc.createTextNode(a));return b.normalize&&b.normalize(),b};c.firstChild;)c.removeChild(c.firstChild);for(var f=d(a);f.firstChild;)c.appendChild(f.firstChild)}b.stop()})(-1),b.on("snap.util.attr.fontSize",h)(-1),b.on("snap.util.attr.font-size",h)(-1),b.on("snap.util.getattr.transform",function(){return b.stop(),this.transform()})(-1),b.on("snap.util.getattr.textpath",function(){return b.stop(),this.textPath})(-1),function(){function c(c){return function(){b.stop();var d=e.doc.defaultView.getComputedStyle(this.node,null).getPropertyValue("marker-"+c);return"none"==d?d:a(e.doc.getElementById(d.match(o)[1]))}}function d(a){return function(c){b.stop();var d="marker"+a.charAt(0).toUpperCase()+a.substring(1);if(""==c||!c)return void(this.node.style[d]="none");if("marker"==c.type){var e=c.node.id;return e||p(c.node,{id:c.id}),void(this.node.style[d]=q(e))}}}b.on("snap.util.getattr.marker-end",c("end"))(-1),b.on("snap.util.getattr.markerEnd",c("end"))(-1),b.on("snap.util.getattr.marker-start",c("start"))(-1),b.on("snap.util.getattr.markerStart",c("start"))(-1),b.on("snap.util.getattr.marker-mid",c("mid"))(-1),b.on("snap.util.getattr.markerMid",c("mid"))(-1),b.on("snap.util.attr.marker-end",d("end"))(-1),b.on("snap.util.attr.markerEnd",d("end"))(-1),b.on("snap.util.attr.marker-start",d("start"))(-1),b.on("snap.util.attr.markerStart",d("start"))(-1),b.on("snap.util.attr.marker-mid",d("mid"))(-1),b.on("snap.util.attr.markerMid",d("mid"))(-1)}(),b.on("snap.util.getattr.r",function(){return"rect"==this.type&&p(this.node,"rx")==p(this.node,"ry")?(b.stop(),p(this.node,"rx")):void 0})(-1),b.on("snap.util.getattr.text",function(){if("text"==this.type||"tspan"==this.type){b.stop();var a=i(this.node);return 1==a.length?a[0]:a}})(-1),b.on("snap.util.getattr.#text",function(){return this.node.textContent})(-1),b.on("snap.util.getattr.viewBox",function(){b.stop();var c=p(this.node,"viewBox");return c?(c=c.split(s),a._.box(+c[0],+c[1],+c[2],+c[3])):void 0})(-1),b.on("snap.util.getattr.points",function(){var a=p(this.node,"points");return b.stop(),a?a.split(s):void 0})(-1),b.on("snap.util.getattr.path",function(){var a=p(this.node,"d");return b.stop(),a})(-1),b.on("snap.util.getattr.class",function(){return this.node.className.baseVal})(-1),b.on("snap.util.getattr.fontSize",j)(-1),b.on("snap.util.getattr.font-size",j)(-1)}),d.plugin(function(){function a(a){return a}function c(a){return function(b){return+b.toFixed(3)+a}}var d={"+":function(a,b){return a+b},"-":function(a,b){return a-b},"/":function(a,b){return a/b},"*":function(a,b){return a*b}},e=String,f=/[a-z]+$/i,g=/^\s*([+\-\/*])\s*=\s*([\d.eE+\-]+)\s*([^\d\s]+)?\s*$/;b.on("snap.util.attr",function(a){var c=e(a).match(g);if(c){var h=b.nt(),i=h.substring(h.lastIndexOf(".")+1),j=this.attr(i),k={};b.stop();var l=c[3]||"",m=j.match(f),n=d[c[1]];if(m&&m==l?a=n(parseFloat(j),+c[2]):(j=this.asPX(i),a=n(this.asPX(i),this.asPX(i,c[2]+l))),isNaN(j)||isNaN(a))return;k[i]=a,this.attr(k)}})(-10),b.on("snap.util.equal",function(h,i){var j=e(this.attr(h)||""),k=e(i).match(g);if(k){b.stop();var l=k[3]||"",m=j.match(f),n=d[k[1]];return m&&m==l?{from:parseFloat(j),to:n(parseFloat(j),+k[2]),f:c(m)}:(j=this.asPX(h),{from:j,to:n(j,this.asPX(h,k[2]+l)),f:a})}})(-10)}),d.plugin(function(a,c,d,e){var f=d.prototype,g=a.is;f.rect=function(a,b,c,d,e,f){var h;return null==f&&(f=e),g(a,"object")&&"[object Object]"==a?h=a:null!=a&&(h={x:a,y:b,width:c,height:d},null!=e&&(h.rx=e,h.ry=f)),this.el("rect",h)},f.circle=function(a,b,c){var d;return g(a,"object")&&"[object Object]"==a?d=a:null!=a&&(d={cx:a,cy:b,r:c}),this.el("circle",d)};var h=function(){function a(){this.parentNode.removeChild(this)}return function(b,c){var d=e.doc.createElement("img"),f=e.doc.body;d.style.cssText="position:absolute;left:-9999em;top:-9999em",d.onload=function(){c.call(d),d.onload=d.onerror=null,f.removeChild(d)},d.onerror=a,f.appendChild(d),d.src=b}}();f.image=function(b,c,d,e,f){var i=this.el("image");if(g(b,"object")&&"src"in b)i.attr(b);else if(null!=b){var j={"xlink:href":b,preserveAspectRatio:"none"};null!=c&&null!=d&&(j.x=c,j.y=d),null!=e&&null!=f?(j.width=e,j.height=f):h(b,function(){a._.$(i.node,{width:this.offsetWidth,height:this.offsetHeight})}),a._.$(i.node,j)}return i},f.ellipse=function(a,b,c,d){var e;return g(a,"object")&&"[object Object]"==a?e=a:null!=a&&(e={cx:a,cy:b,rx:c,ry:d}),this.el("ellipse",e)},f.path=function(a){var b;return g(a,"object")&&!g(a,"array")?b=a:a&&(b={d:a}),this.el("path",b)},f.group=f.g=function(a){var b=this.el("g");return 1==arguments.length&&a&&!a.type?b.attr(a):arguments.length&&b.add(Array.prototype.slice.call(arguments,0)),b},f.svg=function(a,b,c,d,e,f,h,i){var j={};return g(a,"object")&&null==b?j=a:(null!=a&&(j.x=a),null!=b&&(j.y=b),null!=c&&(j.width=c),null!=d&&(j.height=d),null!=e&&null!=f&&null!=h&&null!=i&&(j.viewBox=[e,f,h,i])),this.el("svg",j)},f.mask=function(a){var b=this.el("mask");return 1==arguments.length&&a&&!a.type?b.attr(a):arguments.length&&b.add(Array.prototype.slice.call(arguments,0)),b},f.ptrn=function(a,b,c,d,e,f,h,i){if(g(a,"object"))var j=a;else arguments.length?(j={},null!=a&&(j.x=a),null!=b&&(j.y=b),null!=c&&(j.width=c),null!=d&&(j.height=d),null!=e&&null!=f&&null!=h&&null!=i&&(j.viewBox=[e,f,h,i])):j={patternUnits:"userSpaceOnUse"};return this.el("pattern",j)},f.use=function(a){if(null!=a){{make("use",this.node)}return a instanceof c&&(a.attr("id")||a.attr({id:ID()}),a=a.attr("id")),this.el("use",{"xlink:href":a})}return c.prototype.use.call(this)},f.text=function(a,b,c){var d={};return g(a,"object")?d=a:null!=a&&(d={x:a,y:b,text:c||""}),this.el("text",d)},f.line=function(a,b,c,d){var e={};return g(a,"object")?e=a:null!=a&&(e={x1:a,x2:c,y1:b,y2:d}),this.el("line",e)},f.polyline=function(a){arguments.length>1&&(a=Array.prototype.slice.call(arguments,0));var b={};return g(a,"object")&&!g(a,"array")?b=a:null!=a&&(b={points:a}),this.el("polyline",b)},f.polygon=function(a){arguments.length>1&&(a=Array.prototype.slice.call(arguments,0));var b={};return g(a,"object")&&!g(a,"array")?b=a:null!=a&&(b={points:a}),this.el("polygon",b)},function(){function c(){return this.selectAll("stop")}function d(b,c){var d=j("stop"),e={offset:+c+"%"};return b=a.color(b),e["stop-color"]=b.hex,b.opacity<1&&(e["stop-opacity"]=b.opacity),j(d,e),this.node.appendChild(d),this}function e(){if("linearGradient"==this.type){var b=j(this.node,"x1")||0,c=j(this.node,"x2")||1,d=j(this.node,"y1")||0,e=j(this.node,"y2")||0;return a._.box(b,d,math.abs(c-b),math.abs(e-d))}var f=this.node.cx||.5,g=this.node.cy||.5,h=this.node.r||0;return a._.box(f-h,g-h,2*h,2*h)}function g(a,c){function d(a,b){for(var c=(b-l)/(a-m),d=m;a>d;d++)g[d].offset=+(+l+c*(d-m)).toFixed(2);m=a,l=b}var e,f=b("snap.util.grad.parse",null,c).firstDefined();if(!f)return null;f.params.unshift(a),e="l"==f.type.toLowerCase()?h.apply(0,f.params):i.apply(0,f.params),f.type!=f.type.toLowerCase()&&j(e.node,{gradientUnits:"userSpaceOnUse"});var g=f.stops,k=g.length,l=0,m=0;k--;for(var n=0;k>n;n++)"offset"in g[n]&&d(n,g[n].offset);for(g[k].offset=g[k].offset||100,d(k,g[k].offset),n=0;k>=n;n++){var o=g[n];e.addStop(o.color,o.offset)}return e}function h(b,f,g,h,i){var k=a._.make("linearGradient",b);return k.stops=c,k.addStop=d,k.getBBox=e,null!=f&&j(k.node,{x1:f,y1:g,x2:h,y2:i}),k}function i(b,f,g,h,i,k){var l=a._.make("radialGradient",b);return l.stops=c,l.addStop=d,l.getBBox=e,null!=f&&j(l.node,{cx:f,cy:g,r:h}),null!=i&&null!=k&&j(l.node,{fx:i,fy:k}),l}var j=a._.$;f.gradient=function(a){return g(this.defs,a)},f.gradientLinear=function(a,b,c,d){return h(this.defs,a,b,c,d)},f.gradientRadial=function(a,b,c,d,e){return i(this.defs,a,b,c,d,e)},f.toString=function(){var b,c=this.node.ownerDocument,d=c.createDocumentFragment(),e=c.createElement("div"),f=this.node.cloneNode(!0);return d.appendChild(e),e.appendChild(f),a._.$(f,{xmlns:"http://www.w3.org/2000/svg"}),b=e.innerHTML,d.removeChild(d.firstChild),b},f.clear=function(){for(var a,b=this.node.firstChild;b;)a=b.nextSibling,"defs"!=b.tagName?b.parentNode.removeChild(b):f.clear.call({node:b}),b=a}}()}),d.plugin(function(a,b){function c(a){var b=c.ps=c.ps||{};return b[a]?b[a].sleep=100:b[a]={sleep:100},setTimeout(function(){for(var c in b)b[K](c)&&c!=a&&(b[c].sleep--,!b[c].sleep&&delete b[c])}),b[a]}function d(a,b,c,d){return null==a&&(a=b=c=d=0),null==b&&(b=a.y,c=a.width,d=a.height,a=a.x),{x:a,y:b,width:c,w:c,height:d,h:d,x2:a+c,y2:b+d,cx:a+c/2,cy:b+d/2,r1:N.min(c,d)/2,r2:N.max(c,d)/2,r0:N.sqrt(c*c+d*d)/2,path:w(a,b,c,d),vb:[a,b,c,d].join(" ")}}function e(){return this.join(",").replace(L,"$1")}function f(a){var b=J(a);return b.toString=e,b}function g(a,b,c,d,e,f,g,h,j){return null==j?n(a,b,c,d,e,f,g,h):i(a,b,c,d,e,f,g,h,o(a,b,c,d,e,f,g,h,j))}function h(c,d){function e(a){return+(+a).toFixed(3)}return a._.cacher(function(a,f,h){a instanceof b&&(a=a.attr("d")),a=E(a);for(var j,k,l,m,n,o="",p={},q=0,r=0,s=a.length;s>r;r++){if(l=a[r],"M"==l[0])j=+l[1],k=+l[2];else{if(m=g(j,k,l[1],l[2],l[3],l[4],l[5],l[6]),q+m>f){if(d&&!p.start){if(n=g(j,k,l[1],l[2],l[3],l[4],l[5],l[6],f-q),o+=["C"+e(n.start.x),e(n.start.y),e(n.m.x),e(n.m.y),e(n.x),e(n.y)],h)return o;p.start=o,o=["M"+e(n.x),e(n.y)+"C"+e(n.n.x),e(n.n.y),e(n.end.x),e(n.end.y),e(l[5]),e(l[6])].join(),q+=m,j=+l[5],k=+l[6];continue}if(!c&&!d)return n=g(j,k,l[1],l[2],l[3],l[4],l[5],l[6],f-q)}q+=m,j=+l[5],k=+l[6]}o+=l.shift()+l}return p.end=o,n=c?q:d?p:i(j,k,l[0],l[1],l[2],l[3],l[4],l[5],1)},null,a._.clone)}function i(a,b,c,d,e,f,g,h,i){var j=1-i,k=R(j,3),l=R(j,2),m=i*i,n=m*i,o=k*a+3*l*i*c+3*j*i*i*e+n*g,p=k*b+3*l*i*d+3*j*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,w=j*e+i*g,x=j*f+i*h,y=90-180*N.atan2(q-s,r-t)/O;return{x:o,y:p,m:{x:q,y:r},n:{x:s,y:t},start:{x:u,y:v},end:{x:w,y:x},alpha:y}}function j(b,c,e,f,g,h,i,j){a.is(b,"array")||(b=[b,c,e,f,g,h,i,j]);var k=D.apply(null,b);return d(k.min.x,k.min.y,k.max.x-k.min.x,k.max.y-k.min.y)}function k(a,b,c){return b>=a.x&&b<=a.x+a.width&&c>=a.y&&c<=a.y+a.height}function l(a,b){return a=d(a),b=d(b),k(b,a.x,a.y)||k(b,a.x2,a.y)||k(b,a.x,a.y2)||k(b,a.x2,a.y2)||k(a,b.x,b.y)||k(a,b.x2,b.y)||k(a,b.x,b.y2)||k(a,b.x2,b.y2)||(a.x<b.x2&&a.x>b.x||b.x<a.x2&&b.x>a.x)&&(a.y<b.y2&&a.y>b.y||b.y<a.y2&&b.y>a.y)}function m(a,b,c,d,e){var f=-3*b+9*c-9*d+3*e,g=a*f+6*b-12*c+6*d;return a*g-3*b+3*c}function n(a,b,c,d,e,f,g,h,i){null==i&&(i=1),i=i>1?1:0>i?0:i;for(var j=i/2,k=12,l=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],n=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],o=0,p=0;k>p;p++){var q=j*l[p]+j,r=m(q,a,c,e,g),s=m(q,b,d,f,h),t=r*r+s*s;o+=n[p]*N.sqrt(t)}return j*o}function o(a,b,c,d,e,f,g,h,i){if(!(0>i||n(a,b,c,d,e,f,g,h)<i)){var j,k=1,l=k/2,m=k-l,o=.01;for(j=n(a,b,c,d,e,f,g,h,m);S(j-i)>o;)l/=2,m+=(i>j?1:-1)*l,j=n(a,b,c,d,e,f,g,h,m);return m}}function p(a,b,c,d,e,f,g,h){if(!(Q(a,c)<P(e,g)||P(a,c)>Q(e,g)||Q(b,d)<P(f,h)||P(b,d)>Q(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(k){var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(!(n<+P(a,c).toFixed(2)||n>+Q(a,c).toFixed(2)||n<+P(e,g).toFixed(2)||n>+Q(e,g).toFixed(2)||o<+P(b,d).toFixed(2)||o>+Q(b,d).toFixed(2)||o<+P(f,h).toFixed(2)||o>+Q(f,h).toFixed(2)))return{x:l,y:m}}}}function q(a,b,c){var d=j(a),e=j(b);if(!l(d,e))return c?0:[];for(var f=n.apply(0,a),g=n.apply(0,b),h=~~(f/8),k=~~(g/8),m=[],o=[],q={},r=c?0:[],s=0;h+1>s;s++){var t=i.apply(0,a.concat(s/h));m.push({x:t.x,y:t.y,t:s/h})}for(s=0;k+1>s;s++)t=i.apply(0,b.concat(s/k)),o.push({x:t.x,y:t.y,t:s/k});for(s=0;h>s;s++)for(var u=0;k>u;u++){var v=m[s],w=m[s+1],x=o[u],y=o[u+1],z=S(w.x-v.x)<.001?"y":"x",A=S(y.x-x.x)<.001?"y":"x",B=p(v.x,v.y,w.x,w.y,x.x,x.y,y.x,y.y);if(B){if(q[B.x.toFixed(4)]==B.y.toFixed(4))continue;q[B.x.toFixed(4)]=B.y.toFixed(4);var C=v.t+S((B[z]-v[z])/(w[z]-v[z]))*(w.t-v.t),D=x.t+S((B[A]-x[A])/(y[A]-x[A]))*(y.t-x.t);C>=0&&1>=C&&D>=0&&1>=D&&(c?r++:r.push({x:B.x,y:B.y,t1:C,t2:D}))}}return r}function r(a,b){return t(a,b)}function s(a,b){return t(a,b,1)}function t(a,b,c){a=E(a),b=E(b);for(var d,e,f,g,h,i,j,k,l,m,n=c?0:[],o=0,p=a.length;p>o;o++){var r=a[o];if("M"==r[0])d=h=r[1],e=i=r[2];else{"C"==r[0]?(l=[d,e].concat(r.slice(1)),d=l[6],e=l[7]):(l=[d,e,d,e,h,i,h,i],d=h,e=i);for(var s=0,t=b.length;t>s;s++){var u=b[s];if("M"==u[0])f=j=u[1],g=k=u[2];else{"C"==u[0]?(m=[f,g].concat(u.slice(1)),f=m[6],g=m[7]):(m=[f,g,f,g,j,k,j,k],f=j,g=k);var v=q(l,m,c);if(c)n+=v;else{for(var w=0,x=v.length;x>w;w++)v[w].segment1=o,v[w].segment2=s,v[w].bez1=l,v[w].bez2=m;n=n.concat(v)}}}}}return n}function u(a,b,c){var d=v(a);return k(d,b,c)&&t(a,[["M",b,c],["H",d.x2+10]],1)%2==1}function v(a){var b=c(a);if(b.bbox)return J(b.bbox);if(!a)return d();a=E(a);for(var e,f=0,g=0,h=[],i=[],j=0,k=a.length;k>j;j++)if(e=a[j],"M"==e[0])f=e[1],g=e[2],h.push(f),i.push(g);else{var l=D(f,g,e[1],e[2],e[3],e[4],e[5],e[6]);h=h.concat(l.min.x,l.max.x),i=i.concat(l.min.y,l.max.y),f=e[5],g=e[6]}var m=P.apply(0,h),n=P.apply(0,i),o=Q.apply(0,h),p=Q.apply(0,i),q=d(m,n,o-m,p-n);return b.bbox=J(q),q}function w(a,b,c,d,f){if(f)return[["M",+a+ +f,b],["l",c-2*f,0],["a",f,f,0,0,1,f,f],["l",0,d-2*f],["a",f,f,0,0,1,-f,f],["l",2*f-c,0],["a",f,f,0,0,1,-f,-f],["l",0,2*f-d],["a",f,f,0,0,1,f,-f],["z"]];var g=[["M",a,b],["l",c,0],["l",0,d],["l",-c,0],["z"]];return g.toString=e,g}function x(a,b,c,d,f){if(null==f&&null==d&&(d=c),a=+a,b=+b,c=+c,d=+d,null!=f)var g=Math.PI/180,h=a+c*Math.cos(-d*g),i=a+c*Math.cos(-f*g),j=b+c*Math.sin(-d*g),k=b+c*Math.sin(-f*g),l=[["M",h,j],["A",c,c,0,+(f-d>180),0,i,k]];else l=[["M",a,b],["m",0,-d],["a",c,d,0,1,1,0,2*d],["a",c,d,0,1,1,0,-2*d],["z"]];return l.toString=e,l}function y(b){var d=c(b),g=String.prototype.toLowerCase;if(d.rel)return f(d.rel);a.is(b,"array")&&a.is(b&&b[0],"array")||(b=a.parsePathString(b));var h=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=b[0][1],j=b[0][2],k=i,l=j,m++,h.push(["M",i,j]));for(var n=m,o=b.length;o>n;n++){var p=h[n]=[],q=b[n];if(q[0]!=g.call(q[0]))switch(p[0]=g.call(q[0]),p[0]){case"a":p[1]=q[1],p[2]=q[2],p[3]=q[3],p[4]=q[4],p[5]=q[5],p[6]=+(q[6]-i).toFixed(3),p[7]=+(q[7]-j).toFixed(3);break;case"v":p[1]=+(q[1]-j).toFixed(3);break;case"m":k=q[1],l=q[2];default:for(var r=1,s=q.length;s>r;r++)p[r]=+(q[r]-(r%2?i:j)).toFixed(3)}else{p=h[n]=[],"m"==q[0]&&(k=q[1]+i,l=q[2]+j);for(var t=0,u=q.length;u>t;t++)h[n][t]=q[t]}var v=h[n].length;switch(h[n][0]){case"z":i=k,j=l;break;case"h":i+=+h[n][v-1];break;case"v":j+=+h[n][v-1];break;default:i+=+h[n][v-2],j+=+h[n][v-1]}}return h.toString=e,d.rel=f(h),h}function z(b){var d=c(b);if(d.abs)return f(d.abs);if(I(b,"array")&&I(b&&b[0],"array")||(b=a.parsePathString(b)),!b||!b.length)return[["M",0,0]];var g,h=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=+b[0][1],j=+b[0][2],k=i,l=j,m++,h[0]=["M",i,j]);for(var n,o,p=3==b.length&&"M"==b[0][0]&&"R"==b[1][0].toUpperCase()&&"Z"==b[2][0].toUpperCase(),q=m,r=b.length;r>q;q++){if(h.push(n=[]),o=b[q],g=o[0],g!=g.toUpperCase())switch(n[0]=g.toUpperCase(),n[0]){case"A":n[1]=o[1],n[2]=o[2],n[3]=o[3],n[4]=o[4],n[5]=o[5],n[6]=+o[6]+i,n[7]=+o[7]+j;break;case"V":n[1]=+o[1]+j;break;case"H":n[1]=+o[1]+i;break;case"R":for(var s=[i,j].concat(o.slice(1)),t=2,u=s.length;u>t;t++)s[t]=+s[t]+i,s[++t]=+s[t]+j;h.pop(),h=h.concat(G(s,p));break;case"O":h.pop(),s=x(i,j,o[1],o[2]),s.push(s[0]),h=h.concat(s);break;case"U":h.pop(),h=h.concat(x(i,j,o[1],o[2],o[3])),n=["U"].concat(h[h.length-1].slice(-2));break;case"M":k=+o[1]+i,l=+o[2]+j;default:for(t=1,u=o.length;u>t;t++)n[t]=+o[t]+(t%2?i:j)}else if("R"==g)s=[i,j].concat(o.slice(1)),h.pop(),h=h.concat(G(s,p)),n=["R"].concat(o.slice(-2));else if("O"==g)h.pop(),s=x(i,j,o[1],o[2]),s.push(s[0]),h=h.concat(s);else if("U"==g)h.pop(),h=h.concat(x(i,j,o[1],o[2],o[3])),n=["U"].concat(h[h.length-1].slice(-2));else for(var v=0,w=o.length;w>v;v++)n[v]=o[v];if(g=g.toUpperCase(),"O"!=g)switch(n[0]){case"Z":i=+k,j=+l;break;case"H":i=n[1];break;case"V":j=n[1];break;case"M":k=n[n.length-2],l=n[n.length-1];default:i=n[n.length-2],j=n[n.length-1]}}return h.toString=e,d.abs=f(h),h}function A(a,b,c,d){return[a,b,c,d,c,d]}function B(a,b,c,d,e,f){var g=1/3,h=2/3;return[g*a+h*c,g*b+h*d,g*e+h*c,g*f+h*d,e,f]}function C(b,c,d,e,f,g,h,i,j,k){var l,m=120*O/180,n=O/180*(+f||0),o=[],p=a._.cacher(function(a,b,c){var d=a*N.cos(c)-b*N.sin(c),e=a*N.sin(c)+b*N.cos(c);return{x:d,y:e}});if(k)y=k[0],z=k[1],w=k[2],x=k[3];else{l=p(b,c,-n),b=l.x,c=l.y,l=p(i,j,-n),i=l.x,j=l.y;var q=(N.cos(O/180*f),N.sin(O/180*f),(b-i)/2),r=(c-j)/2,s=q*q/(d*d)+r*r/(e*e);s>1&&(s=N.sqrt(s),d=s*d,e=s*e);var t=d*d,u=e*e,v=(g==h?-1:1)*N.sqrt(S((t*u-t*r*r-u*q*q)/(t*r*r+u*q*q))),w=v*d*r/e+(b+i)/2,x=v*-e*q/d+(c+j)/2,y=N.asin(((c-x)/e).toFixed(9)),z=N.asin(((j-x)/e).toFixed(9));y=w>b?O-y:y,z=w>i?O-z:z,0>y&&(y=2*O+y),0>z&&(z=2*O+z),h&&y>z&&(y-=2*O),!h&&z>y&&(z-=2*O)}var A=z-y;if(S(A)>m){var B=z,D=i,E=j;z=y+m*(h&&z>y?1:-1),i=w+d*N.cos(z),j=x+e*N.sin(z),o=C(i,j,d,e,f,0,h,D,E,[z,B,w,x])}A=z-y;var F=N.cos(y),G=N.sin(y),H=N.cos(z),I=N.sin(z),J=N.tan(A/4),K=4/3*d*J,L=4/3*e*J,M=[b,c],P=[b+K*G,c-L*F],Q=[i+K*I,j-L*H],R=[i,j];if(P[0]=2*M[0]-P[0],P[1]=2*M[1]-P[1],k)return[P,Q,R].concat(o);o=[P,Q,R].concat(o).join().split(",");for(var T=[],U=0,V=o.length;V>U;U++)T[U]=U%2?p(o[U-1],o[U],n).y:p(o[U],o[U+1],n).x;return T}function D(a,b,c,d,e,f,g,h){for(var i,j,k,l,m,n,o,p,q=[],r=[[],[]],s=0;2>s;++s)if(0==s?(j=6*a-12*c+6*e,i=-3*a+9*c-9*e+3*g,k=3*c-3*a):(j=6*b-12*d+6*f,i=-3*b+9*d-9*f+3*h,k=3*d-3*b),S(i)<1e-12){if(S(j)<1e-12)continue;l=-k/j,l>0&&1>l&&q.push(l)}else o=j*j-4*k*i,p=N.sqrt(o),0>o||(m=(-j+p)/(2*i),m>0&&1>m&&q.push(m),n=(-j-p)/(2*i),n>0&&1>n&&q.push(n));for(var t,u=q.length,v=u;u--;)l=q[u],t=1-l,r[0][u]=t*t*t*a+3*t*t*l*c+3*t*l*l*e+l*l*l*g,r[1][u]=t*t*t*b+3*t*t*l*d+3*t*l*l*f+l*l*l*h;return r[0][v]=a,r[1][v]=b,r[0][v+1]=g,r[1][v+1]=h,r[0].length=r[1].length=v+2,{min:{x:P.apply(0,r[0]),y:P.apply(0,r[1])},max:{x:Q.apply(0,r[0]),y:Q.apply(0,r[1])}}}function E(a,b){var d=!b&&c(a);if(!b&&d.curve)return f(d.curve);for(var e=z(a),g=b&&z(b),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},j=(function(a,b,c){var d,e;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];switch(!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null),a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"].concat(C.apply(0,[b.x,b.y].concat(a.slice(1))));break;case"S":"C"==c||"S"==c?(d=2*b.x-b.bx,e=2*b.y-b.by):(d=b.x,e=b.y),a=["C",d,e].concat(a.slice(1));break;case"T":"Q"==c||"T"==c?(b.qx=2*b.x-b.qx,b.qy=2*b.y-b.qy):(b.qx=b.x,b.qy=b.y),a=["C"].concat(B(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"].concat(B(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"].concat(A(b.x,b.y,a[1],a[2]));break;case"H":a=["C"].concat(A(b.x,b.y,a[1],b.y));break;case"V":a=["C"].concat(A(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"].concat(A(b.x,b.y,b.X,b.Y))}return a}),k=function(a,b){if(a[b].length>7){a[b].shift();for(var c=a[b];c.length;)m[b]="A",g&&(n[b]="A"),a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),r=Q(e.length,g&&g.length||0)}},l=function(a,b,c,d,f){a&&b&&"M"==a[f][0]&&"M"!=b[f][0]&&(b.splice(f,0,["M",d.x,d.y]),c.bx=0,c.by=0,c.x=a[f][1],c.y=a[f][2],r=Q(e.length,g&&g.length||0))},m=[],n=[],o="",p="",q=0,r=Q(e.length,g&&g.length||0);r>q;q++){e[q]&&(o=e[q][0]),"C"!=o&&(m[q]=o,q&&(p=m[q-1])),e[q]=j(e[q],h,p),"A"!=m[q]&&"C"==o&&(m[q]="C"),k(e,q),g&&(g[q]&&(o=g[q][0]),"C"!=o&&(n[q]=o,q&&(p=n[q-1])),g[q]=j(g[q],i,p),"A"!=n[q]&&"C"==o&&(n[q]="C"),k(g,q)),l(e,g,h,i,q),l(g,e,i,h,q);var s=e[q],t=g&&g[q],u=s.length,v=g&&t.length;h.x=s[u-2],h.y=s[u-1],h.bx=M(s[u-4])||h.x,h.by=M(s[u-3])||h.y,i.bx=g&&(M(t[v-4])||i.x),i.by=g&&(M(t[v-3])||i.y),i.x=g&&t[v-2],i.y=g&&t[v-1]}return g||(d.curve=f(e)),g?[e,g]:e}function F(a,b){if(!b)return a;var c,d,e,f,g,h,i;for(a=E(a),e=0,g=a.length;g>e;e++)for(i=a[e],f=1,h=i.length;h>f;f+=2)c=b.x(i[f],i[f+1]),d=b.y(i[f],i[f+1]),i[f]=c,i[f+1]=d;return a}function G(a,b){for(var c=[],d=0,e=a.length;e-2*!b>d;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}var H=b.prototype,I=a.is,J=a._.clone,K="hasOwnProperty",L=/,?([a-z]),?/gi,M=parseFloat,N=Math,O=N.PI,P=N.min,Q=N.max,R=N.pow,S=N.abs,T=h(1),U=h(),V=h(0,1),W=a._unit2px,X={path:function(a){return a.attr("path")},circle:function(a){var b=W(a);return x(b.cx,b.cy,b.r)},ellipse:function(a){var b=W(a);return x(b.cx||0,b.cy||0,b.rx,b.ry)},rect:function(a){var b=W(a);return w(b.x||0,b.y||0,b.width,b.height,b.rx,b.ry)},image:function(a){var b=W(a);return w(b.x||0,b.y||0,b.width,b.height)},line:function(a){return"M"+[a.attr("x1")||0,a.attr("y1")||0,a.attr("x2"),a.attr("y2")]},polyline:function(a){return"M"+a.attr("points")},polygon:function(a){return"M"+a.attr("points")+"z"},deflt:function(a){var b=a.node.getBBox();return w(b.x,b.y,b.width,b.height)}};a.path=c,a.path.getTotalLength=T,a.path.getPointAtLength=U,a.path.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return V(a,b).end;var d=V(a,c,1);return b?V(d,b).end:d},H.getTotalLength=function(){return this.node.getTotalLength?this.node.getTotalLength():void 0},H.getPointAtLength=function(a){return U(this.attr("d"),a)},H.getSubpath=function(b,c){return a.path.getSubpath(this.attr("d"),b,c)},a._.box=d,a.path.findDotsAtSegment=i,a.path.bezierBBox=j,a.path.isPointInsideBBox=k,a.path.isBBoxIntersect=l,a.path.intersection=r,a.path.intersectionNumber=s,a.path.isPointInside=u,a.path.getBBox=v,a.path.get=X,a.path.toRelative=y,a.path.toAbsolute=z,a.path.toCubic=E,a.path.map=F,a.path.toString=e,a.path.clone=f}),d.plugin(function(a){var d=Math.max,e=Math.min,f=function(a){if(this.items=[],this.bindings={},this.length=0,this.type="set",a)for(var b=0,c=a.length;c>b;b++)a[b]&&(this[this.items.length]=this.items[this.items.length]=a[b],this.length++)},g=f.prototype;g.push=function(){for(var a,b,c=0,d=arguments.length;d>c;c++)a=arguments[c],a&&(b=this.items.length,this[b]=this.items[b]=a,this.length++);return this},g.pop=function(){return this.length&&delete this[this.length--],this.items.pop()},g.forEach=function(a,b){for(var c=0,d=this.items.length;d>c;c++)if(a.call(b,this.items[c],c)===!1)return this;return this},g.animate=function(d,e,f,g){"function"!=typeof f||f.length||(g=f,f=c.linear),d instanceof a._.Animation&&(g=d.callback,f=d.easing,e=f.dur,d=d.attr);var h=arguments;if(a.is(d,"array")&&a.is(h[h.length-1],"array"))var i=!0;var j,k=function(){j?this.b=j:j=this.b},l=0,m=g&&function(){l++==this.length&&g.call(this)};return this.forEach(function(a,c){b.once("snap.animcreated."+a.id,k),i?h[c]&&a.animate.apply(a,h[c]):a.animate(d,e,f,m)})},g.remove=function(){for(;this.length;)this.pop().remove();return this},g.bind=function(a,b,c){var d={};if("function"==typeof b)this.bindings[a]=b;else{var e=c||a;this.bindings[a]=function(a){d[e]=a,b.attr(d)}}return this},g.attr=function(a){var b={};for(var c in a)this.bindings[c]?this.bindings[c](a[c]):b[c]=a[c];for(var d=0,e=this.items.length;e>d;d++)this.items[d].attr(b);return this},g.clear=function(){for(;this.length;)this.pop()},g.splice=function(a,b){a=0>a?d(this.length+a,0):a,b=d(0,e(this.length-a,b));var c,g=[],h=[],i=[];for(c=2;c<arguments.length;c++)i.push(arguments[c]);for(c=0;b>c;c++)h.push(this[a+c]);for(;c<this.length-a;c++)g.push(this[a+c]);var j=i.length;for(c=0;c<j+g.length;c++)this.items[a+c]=this[a+c]=j>c?i[c]:g[c-j];for(c=this.items.length=this.length-=b-j;this[c];)delete this[c++];return new f(h)},g.exclude=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]==a)return this.splice(b,1),!0;return!1},g.insertAfter=function(a){for(var b=this.items.length;b--;)this.items[b].insertAfter(a);return this},g.getBBox=function(){for(var a=[],b=[],c=[],f=[],g=this.items.length;g--;)if(!this.items[g].removed){var h=this.items[g].getBBox();a.push(h.x),b.push(h.y),c.push(h.x+h.width),f.push(h.y+h.height)}return a=e.apply(0,a),b=e.apply(0,b),c=d.apply(0,c),f=d.apply(0,f),{x:a,y:b,x2:c,y2:f,width:c-a,height:f-b,cx:a+(c-a)/2,cy:b+(f-b)/2}},g.clone=function(a){a=new f;for(var b=0,c=this.items.length;c>b;b++)a.push(this.items[b].clone());return a},g.toString=function(){return"Snap‘s set"},g.type="set",a.set=function(){var a=new f;return arguments.length&&a.push.apply(a,Array.prototype.slice.call(arguments,0)),a}}),d.plugin(function(a,c){function d(a){var b=a[0];switch(b.toLowerCase()){case"t":return[b,0,0];case"m":return[b,1,0,0,1,0,0];case"r":return 4==a.length?[b,0,a[2],a[3]]:[b,0];case"s":return 5==a.length?[b,1,1,a[3],a[4]]:3==a.length?[b,1,1]:[b,1]}}function e(b,c,e){c=m(c).replace(/\.{3}|\u2026/g,b),b=a.parseTransformString(b)||[],c=a.parseTransformString(c)||[];for(var f,g,h,k,l=Math.max(b.length,c.length),n=[],o=[],p=0;l>p;p++){if(h=b[p]||d(c[p]),k=c[p]||d(h),h[0]!=k[0]||"r"==h[0].toLowerCase()&&(h[2]!=k[2]||h[3]!=k[3])||"s"==h[0].toLowerCase()&&(h[3]!=k[3]||h[4]!=k[4])){b=a._.transform2matrix(b,e()),c=a._.transform2matrix(c,e()),n=[["m",b.a,b.b,b.c,b.d,b.e,b.f]],o=[["m",c.a,c.b,c.c,c.d,c.e,c.f]];break}for(n[p]=[],o[p]=[],f=0,g=Math.max(h.length,k.length);g>f;f++)f in h&&(n[p][f]=h[f]),f in k&&(o[p][f]=k[f])}return{from:j(n),to:j(o),f:i(n)}}function f(a){return a}function g(a){return function(b){return+b.toFixed(3)+a}}function h(b){return a.rgb(b[0],b[1],b[2])}function i(a){var b,c,d,e,f,g,h=0,i=[];for(b=0,c=a.length;c>b;b++){for(f="[",g=['"'+a[b][0]+'"'],d=1,e=a[b].length;e>d;d++)g[d]="val["+h++ +"]";
+f+=g+"]",i[b]=f}return Function("val","return Snap.path.toString.call(["+i+"])")}function j(a){for(var b=[],c=0,d=a.length;d>c;c++)for(var e=1,f=a[c].length;f>e;e++)b.push(a[c][e]);return b}var k={},l=/[a-z]+$/i,m=String;k.stroke=k.fill="colour",c.prototype.equal=function(a,c){return b("snap.util.equal",this,a,c).firstDefined()},b.on("snap.util.equal",function(b,c){var d,n,o=m(this.attr(b)||""),p=this;if(o==+o&&c==+c)return{from:+o,to:+c,f:f};if("colour"==k[b])return d=a.color(o),n=a.color(c),{from:[d.r,d.g,d.b,d.opacity],to:[n.r,n.g,n.b,n.opacity],f:h};if("transform"==b||"gradientTransform"==b||"patternTransform"==b)return c instanceof a.Matrix&&(c=c.toTransformString()),a._.rgTransform.test(c)||(c=a._.svgTransform2string(c)),e(o,c,function(){return p.getBBox(1)});if("d"==b||"path"==b)return d=a.path.toCubic(o,c),{from:j(d[0]),to:j(d[1]),f:i(d[0])};if("points"==b)return d=m(o).split(a._.separator),n=m(c).split(a._.separator),{from:d,to:n,f:function(a){return a}};aUnit=o.match(l);var q=m(c).match(l);return aUnit&&aUnit==q?{from:parseFloat(o),to:parseFloat(c),f:g(aUnit)}:{from:this.asPX(b),to:this.asPX(b,c),f:f}})}),d.plugin(function(a,c,d,e){for(var f=c.prototype,g="hasOwnProperty",h=("createTouch"in e.doc),i=["click","dblclick","mousedown","mousemove","mouseout","mouseover","mouseup","touchstart","touchmove","touchend","touchcancel"],j={mousedown:"touchstart",mousemove:"touchmove",mouseup:"touchend"},k=(function(a,b){var c="y"==a?"scrollTop":"scrollLeft",d=b&&b.node?b.node.ownerDocument:e.doc;return d[c in d.documentElement?"documentElement":"body"][c]}),l=function(){this.returnValue=!1},m=function(){return this.originalEvent.preventDefault()},n=function(){this.cancelBubble=!0},o=function(){return this.originalEvent.stopPropagation()},p=function(){return e.doc.addEventListener?function(a,b,c,d){var e=h&&j[b]?j[b]:b,f=function(e){var f=k("y",d),i=k("x",d);if(h&&j[g](b))for(var l=0,n=e.targetTouches&&e.targetTouches.length;n>l;l++)if(e.targetTouches[l].target==a||a.contains(e.targetTouches[l].target)){var p=e;e=e.targetTouches[l],e.originalEvent=p,e.preventDefault=m,e.stopPropagation=o;break}var q=e.clientX+i,r=e.clientY+f;return c.call(d,e,q,r)};return b!==e&&a.addEventListener(b,f,!1),a.addEventListener(e,f,!1),function(){return b!==e&&a.removeEventListener(b,f,!1),a.removeEventListener(e,f,!1),!0}}:e.doc.attachEvent?function(a,b,c,d){var e=function(a){a=a||d.node.ownerDocument.window.event;var b=k("y",d),e=k("x",d),f=a.clientX+e,g=a.clientY+b;return a.preventDefault=a.preventDefault||l,a.stopPropagation=a.stopPropagation||n,c.call(d,a,f,g)};a.attachEvent("on"+b,e);var f=function(){return a.detachEvent("on"+b,e),!0};return f}:void 0}(),q=[],r=function(a){for(var c,d=a.clientX,e=a.clientY,f=k("y"),g=k("x"),i=q.length;i--;){if(c=q[i],h){for(var j,l=a.touches&&a.touches.length;l--;)if(j=a.touches[l],j.identifier==c.el._drag.id||c.el.node.contains(j.target)){d=j.clientX,e=j.clientY,(a.originalEvent?a.originalEvent:a).preventDefault();break}}else a.preventDefault();{var m=c.el.node;m.nextSibling,m.parentNode,m.style.display}d+=g,e+=f,b("snap.drag.move."+c.el.id,c.move_scope||c.el,d-c.el._drag.x,e-c.el._drag.y,d,e,a)}},s=function(c){a.unmousemove(r).unmouseup(s);for(var d,e=q.length;e--;)d=q[e],d.el._drag={},b("snap.drag.end."+d.el.id,d.end_scope||d.start_scope||d.move_scope||d.el,c);q=[]},t=i.length;t--;)!function(b){a[b]=f[b]=function(c,d){return a.is(c,"function")&&(this.events=this.events||[],this.events.push({name:b,f:c,unbind:p(this.node||document,b,c,d||this)})),this},a["un"+b]=f["un"+b]=function(a){for(var c=this.events||[],d=c.length;d--;)if(c[d].name==b&&(c[d].f==a||!a))return c[d].unbind(),c.splice(d,1),!c.length&&delete this.events,this;return this}}(i[t]);f.hover=function(a,b,c,d){return this.mouseover(a,c).mouseout(b,d||c)},f.unhover=function(a,b){return this.unmouseover(a).unmouseout(b)};var u=[];f.drag=function(c,d,e,f,g,h){function i(i,j,k){(i.originalEvent||i).preventDefault(),this._drag.x=j,this._drag.y=k,this._drag.id=i.identifier,!q.length&&a.mousemove(r).mouseup(s),q.push({el:this,move_scope:f,start_scope:g,end_scope:h}),d&&b.on("snap.drag.start."+this.id,d),c&&b.on("snap.drag.move."+this.id,c),e&&b.on("snap.drag.end."+this.id,e),b("snap.drag.start."+this.id,g||f||this,j,k,i)}if(!arguments.length){var j;return this.drag(function(a,b){this.attr({transform:j+(j?"T":"t")+[a,b]})},function(){j=this.transform().local})}return this._drag={},u.push({el:this,start:i}),this.mousedown(i),this},f.undrag=function(){for(var c=u.length;c--;)u[c].el==this&&(this.unmousedown(u[c].start),u.splice(c,1),b.unbind("snap.drag.*."+this.id));return!u.length&&a.unmousemove(r).unmouseup(s),this}}),d.plugin(function(a,c,d){var e=(c.prototype,d.prototype),f=/^\s*url\((.+)\)/,g=String,h=a._.$;a.filter={},e.filter=function(b){var d=this;"svg"!=d.type&&(d=d.paper);var e=a.parse(g(b)),f=a._.id(),i=(d.node.offsetWidth,d.node.offsetHeight,h("filter"));return h(i,{id:f,filterUnits:"userSpaceOnUse"}),i.appendChild(e.node),d.defs.appendChild(i),new c(i)},b.on("snap.util.getattr.filter",function(){b.stop();var c=h(this.node,"filter");if(c){var d=g(c).match(f);return d&&a.select(d[1])}}),b.on("snap.util.attr.filter",function(d){if(d instanceof c&&"filter"==d.type){b.stop();var e=d.node.id;e||(h(d.node,{id:d.id}),e=d.id),h(this.node,{filter:a.url(e)})}d&&"none"!=d||(b.stop(),this.node.removeAttribute("filter"))}),a.filter.blur=function(b,c){null==b&&(b=2);var d=null==c?b:[b,c];return a.format('<feGaussianBlur stdDeviation="{def}"/>',{def:d})},a.filter.blur.toString=function(){return this()},a.filter.shadow=function(b,c,d,e,f){return"string"==typeof d&&(e=d,f=e,d=4),"string"!=typeof e&&(f=e,e="#000"),e=e||"#000",null==d&&(d=4),null==f&&(f=1),null==b&&(b=0,c=2),null==c&&(c=b),e=a.color(e),a.format('<feGaussianBlur in="SourceAlpha" stdDeviation="{blur}"/><feOffset dx="{dx}" dy="{dy}" result="offsetblur"/><feFlood flood-color="{color}"/><feComposite in2="offsetblur" operator="in"/><feComponentTransfer><feFuncA type="linear" slope="{opacity}"/></feComponentTransfer><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>',{color:e,dx:b,dy:c,blur:d,opacity:f})},a.filter.shadow.toString=function(){return this()},a.filter.grayscale=function(b){return null==b&&(b=1),a.format('<feColorMatrix type="matrix" values="{a} {b} {c} 0 0 {d} {e} {f} 0 0 {g} {b} {h} 0 0 0 0 0 1 0"/>',{a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},a.filter.grayscale.toString=function(){return this()},a.filter.sepia=function(b){return null==b&&(b=1),a.format('<feColorMatrix type="matrix" values="{a} {b} {c} 0 0 {d} {e} {f} 0 0 {g} {h} {i} 0 0 0 0 0 1 0"/>',{a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},a.filter.sepia.toString=function(){return this()},a.filter.saturate=function(b){return null==b&&(b=1),a.format('<feColorMatrix type="saturate" values="{amount}"/>',{amount:1-b})},a.filter.saturate.toString=function(){return this()},a.filter.hueRotate=function(b){return b=b||0,a.format('<feColorMatrix type="hueRotate" values="{angle}"/>',{angle:b})},a.filter.hueRotate.toString=function(){return this()},a.filter.invert=function(b){return null==b&&(b=1),a.format('<feComponentTransfer><feFuncR type="table" tableValues="{amount} {amount2}"/><feFuncG type="table" tableValues="{amount} {amount2}"/><feFuncB type="table" tableValues="{amount} {amount2}"/></feComponentTransfer>',{amount:b,amount2:1-b})},a.filter.invert.toString=function(){return this()},a.filter.brightness=function(b){return null==b&&(b=1),a.format('<feComponentTransfer><feFuncR type="linear" slope="{amount}"/><feFuncG type="linear" slope="{amount}"/><feFuncB type="linear" slope="{amount}"/></feComponentTransfer>',{amount:b})},a.filter.brightness.toString=function(){return this()},a.filter.contrast=function(b){return null==b&&(b=1),a.format('<feComponentTransfer><feFuncR type="linear" slope="{amount}" intercept="{amount2}"/><feFuncG type="linear" slope="{amount}" intercept="{amount2}"/><feFuncB type="linear" slope="{amount}" intercept="{amount2}"/></feComponentTransfer>',{amount:b,amount2:.5-b/2})},a.filter.contrast.toString=function(){return this()}}),d}); \ No newline at end of file
diff --git a/web-ui/public/templates/compose/attachment_item.hbs b/web-ui/public/templates/compose/attachment_item.hbs
new file mode 100644
index 00000000..7a64f6f5
--- /dev/null
+++ b/web-ui/public/templates/compose/attachment_item.hbs
@@ -0,0 +1,4 @@
+<li data-ident="{{ this.ident }}" class="compose-view__attachments-list-item">
+ <a class="compose-view__attachments-list-item-label" href="/attachment/{{ this.ident }}?encoding={{ this.encoding }}&filename={{ this.name }}">{{ this.name }} <span class="attachment-size">({{ formatSize this.size}})</span></a>
+ {{#if removable}}<i class="fa fa-close remove-icon compose-view__attachments-list-item-icon"></i>{{/if}}
+</li>
diff --git a/web-ui/public/templates/compose/attachment_upload_item.hbs b/web-ui/public/templates/compose/attachment_upload_item.hbs
new file mode 100644
index 00000000..eb6c4ba6
--- /dev/null
+++ b/web-ui/public/templates/compose/attachment_upload_item.hbs
@@ -0,0 +1,5 @@
+<li class="compose-view__attachments-list-item">
+ <div id="attachment-upload-item-progress" class="compose-view__attachments-list-item-progress"><div class="compose-view__attachments-list-item-progress-bar"></div></div>
+ <a class="compose-view__attachments-list-item-label">{{ this.name }} <span class="attachment-size">({{ formatSize this.size}})</span></a>
+ <i id="attachment-upload-item-abort" class="fa fa-close remove-icon compose-view__attachments-list-item-icon"></i>
+</li>
diff --git a/web-ui/public/templates/compose/attachments_list.hbs b/web-ui/public/templates/compose/attachments_list.hbs
new file mode 100644
index 00000000..6f34df9e
--- /dev/null
+++ b/web-ui/public/templates/compose/attachments_list.hbs
@@ -0,0 +1,14 @@
+<div id="attachment-list" class="buttons-group columns compose-view__attachments">
+ <input id="fileupload" type="file" name="attachment" hidden>
+
+ <!-- The container for the uploaded files -->
+ <div class="compose-view__attachments-wrapper">
+ <ul id="attachment-list-item" class="compose-view__attachments-list">
+ {{#each attachments }}
+ {{> attachment_item this }}
+ {{/each }}
+ </ul>
+ <ul id="attachment-upload-item" class="compose-view__attachments-list compose-view__attachments-list--upload"></ul>
+ </div>
+
+</div>
diff --git a/web-ui/public/templates/compose/compose_box.hbs b/web-ui/public/templates/compose/compose_box.hbs
new file mode 100644
index 00000000..fcfbeaaf
--- /dev/null
+++ b/web-ui/public/templates/compose/compose_box.hbs
@@ -0,0 +1,32 @@
+<button class="close-mail-button">
+ <i class="fa fa-times"></i>
+</button>
+
+<div class="compose-view">
+
+ {{> recipients }}
+ <div class="clearfix">
+ <a id="to-trigger" class="hide">{{t 'to'}}</a>
+ <a id="ccs-trigger" class="hide">{{t 'cc'}}</a>
+ <a id="bccs-trigger" class="hide">{{t 'bcc'}}</a>
+ </div>
+
+ <div class="floatlabel">
+ <label class="floatlabel" for="subject">{{t 'subject'}}</label>
+ <input class="floatlabel" name="subject" type="text" id="subject" value="{{subject}}" placeholder="{{t 'subject'}}" tabindex="4"/>
+ </div>
+ <div class="floatlabel">
+ <label class="floatlabel" for="body">{{t 'body'}}</label>
+ <textarea class="floatlabel" name="body" id="text-box" placeholder="{{t 'body'}}" tabindex="5">{{body}}</textarea>
+ </div>
+
+ {{> attachments_list }}
+
+ <div class="buttons-group columns compose-view__buttons">
+ <button id="send-button" tabindex="6"><i class="fa fa-send"></i></button>
+ <div class="compose-view__buttons-attachment"><span id="attachment-button" tabindex="6"></span></div>
+ <button id="trash-button" tabindex="7">{{t 'trash-button'}}<i class="fa fa-trash-o"></i></button>
+ <div id="draft-save-status"></div>
+ </div>
+
+</div> <!-- ./compose-view -->
diff --git a/web-ui/public/templates/compose/feedback_box.hbs b/web-ui/public/templates/compose/feedback_box.hbs
new file mode 100644
index 00000000..346a6192
--- /dev/null
+++ b/web-ui/public/templates/compose/feedback_box.hbs
@@ -0,0 +1,18 @@
+<button class="close-mail-button">
+ <i class="fa fa-times"></i>
+</button>
+
+<div class="compose-view">
+ <div class="floatlabel">
+ <span id="feedback-subject">Feedback</span>
+ </div>
+
+ <div class="floatlabel">
+ <label class="floatlabel" for="text-box">Body</label>
+ <textarea class="floatlabel" name="body" id="text-box" placeholder="{{t 'feedback-placeholder'}}" tabindex="2">{{body}}</textarea>
+ </div>
+
+ <div class="buttons-group columns">
+ <button id="send-button" tabindex="6">{{t 'Submit Feedback'}} </button>
+ </div>
+</div> <!-- ./compose-view -->
diff --git a/web-ui/public/templates/compose/fixed_recipient.hbs b/web-ui/public/templates/compose/fixed_recipient.hbs
new file mode 100644
index 00000000..8b01717c
--- /dev/null
+++ b/web-ui/public/templates/compose/fixed_recipient.hbs
@@ -0,0 +1,8 @@
+<div class="fixed-recipient">
+ <span class="recipient-area">
+ <div class="recipient-value">
+ <span>{{ address }}</span> <a class="recipient-del" href="#" data-label="{{t 'click-to-remove'}}"/>
+ </div>
+ </span>
+ <input type="hidden" value="{{ address }}" name="{{ name }}" />
+</div>
diff --git a/web-ui/public/templates/compose/inline_box.hbs b/web-ui/public/templates/compose/inline_box.hbs
new file mode 100644
index 00000000..c9c114ec
--- /dev/null
+++ b/web-ui/public/templates/compose/inline_box.hbs
@@ -0,0 +1,20 @@
+<div id="subject-container">
+ <h4 id="reply-subject">{{subject}}</h4>
+ <input type="text" value="{{subject}}" style="display: none"/>
+</div>
+<textarea id="text-box" placeholder="{{t 'body'}}" tabindex=1>{{body}}</textarea>
+
+<a id="all-recipients" tabindex=2>
+ <strong>{{t 'to'}}:</strong> {{formatRecipients recipients}}
+</a>
+
+{{> recipients }}
+
+{{> attachments_list }}
+
+<div class="buttons-group columns compose-view__buttons">
+ <button id="send-button" tabindex=6><i class="fa fa-send"></i></button>
+ <div class="compose-view__buttons-attachment"><span id="attachment-button" tabindex="6"></span></div>
+ <button id="trash-button" tabindex=7>{{t 'trash-button'}}<i class="fa fa-trash-o"></i></button>
+ <div id="draft-save-status"></div>
+</div>
diff --git a/web-ui/public/templates/compose/no_mails_available.hbs b/web-ui/public/templates/compose/no_mails_available.hbs
new file mode 100644
index 00000000..c61152a4
--- /dev/null
+++ b/web-ui/public/templates/compose/no_mails_available.hbs
@@ -0,0 +1,7 @@
+<div class="no-mails-available-pane">
+ {{#if forSearch }}
+ {{t 'no-results-for'}}: '{{ forSearch }}'.
+ {{else}}
+ {{t 'no-emails-in'}} '{{t tag}}'.
+ {{/if}}
+</div>
diff --git a/web-ui/public/templates/compose/no_message_selected.hbs b/web-ui/public/templates/compose/no_message_selected.hbs
new file mode 100644
index 00000000..0b9beaf8
--- /dev/null
+++ b/web-ui/public/templates/compose/no_message_selected.hbs
@@ -0,0 +1,3 @@
+<div class="no-message-selected-pane">
+ <div class="no-message-selected-pane__text">{{t 'nothing-selected'}}.</div>
+</div>
diff --git a/web-ui/public/templates/compose/recipient_input.hbs b/web-ui/public/templates/compose/recipient_input.hbs
new file mode 100644
index 00000000..9416f11f
--- /dev/null
+++ b/web-ui/public/templates/compose/recipient_input.hbs
@@ -0,0 +1 @@
+<input type="text" />
diff --git a/web-ui/public/templates/compose/recipients.hbs b/web-ui/public/templates/compose/recipients.hbs
new file mode 100644
index 00000000..43aced1c
--- /dev/null
+++ b/web-ui/public/templates/compose/recipients.hbs
@@ -0,0 +1,33 @@
+<div id="recipients-fields" style="display:none">
+ <div id='recipients-to-area' class="recipients-area input-container columns large-12 no-padding">
+ <input class="recipients-navigation-handler"/>
+ <div class='compose-column-label'>
+ <label for="recipients-to-box" class="recipients-label column large-1">{{t 'to'}}: </label>
+ </div>
+ <div class='recipients-list compose-column-recipients'>
+ <input id='recipients-to-box' class="recipients-input" type="text" tabindex="1"/></div>
+ </div>
+ <label id="cc-bcc-collapse" class="collapse fa-angle-down" for="hide-and-show"></label>
+ <input id="hide-and-show" type="checkbox">
+ <div id="cc-and-bcc">
+ <div id="recipients-cc-area" class="recipients-area input-container columns large-12 no-padding">
+ <input class="recipients-navigation-handler"/>
+ <div class='compose-column-label'>
+ <label for="recipients-cc-box" class="recipients-label column large-1">{{t 'cc'}}: </label>
+ </div>
+ <div class='recipients-list compose-column-recipients'>
+ <input id='recipients-cc-box' class="recipients-input" type="text" tabindex="2"/>
+ </div>
+ </div>
+
+ <div id="recipients-bcc-area" class="recipients-area input-container columns large-12 no-padding">
+ <input class="recipients-navigation-handler"/>
+ <div class='compose-column-label'>
+ <label for ="recipients-bcc-box" class="recipients-label column large-1">{{t 'bcc'}}: </label>
+ </div>
+ <div class='recipients-list compose-column-recipients'>
+ <input id='recipients-bcc-box' class="recipients-input" type="text" tabindex="3"/>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/web-ui/public/templates/compose/reply_section.hbs b/web-ui/public/templates/compose/reply_section.hbs
new file mode 100644
index 00000000..45203d87
--- /dev/null
+++ b/web-ui/public/templates/compose/reply_section.hbs
@@ -0,0 +1,6 @@
+<div class="reply-container columns small-12 large-12">
+ <button id="reply-button" class="column small-12 large-4">{{t 'reply'}} <i class="fa fa-reply"></i></button>
+ <button id="reply-all-button" class="column small-12 large-4">{{t 'reply-to-all'}} <i class="fa fa-reply-all"></i></button>
+ <button id="forward-button" class="column small-12 large-4">{{t 'forward'}} <i class="fa fa-mail-forward"></i></button>
+ <div id="reply-box" style="display:none"></div>
+</div>
diff --git a/web-ui/public/templates/compose/upload_attachment_failed.hbs b/web-ui/public/templates/compose/upload_attachment_failed.hbs
new file mode 100644
index 00000000..dbb1437b
--- /dev/null
+++ b/web-ui/public/templates/compose/upload_attachment_failed.hbs
@@ -0,0 +1,6 @@
+<div id="upload-error" class="compose-view__attachments-error">
+ <i id="upload-error-close" class="fa fa-close compose-view__attachments-error-close"></i>
+ <span id="upload-error-message" class="compose-view__attachments-error-message">Upload failed. This file exceeds the 1MB limit.</span>
+ <a href="#" id="upload-file-button">Choose another file</a>
+ <a href="#" id="dismiss-button">Dismiss</a>
+</div>
diff --git a/web-ui/public/templates/feedback/feedback_trigger.hbs b/web-ui/public/templates/feedback/feedback_trigger.hbs
new file mode 100644
index 00000000..7f3f8ef1
--- /dev/null
+++ b/web-ui/public/templates/feedback/feedback_trigger.hbs
@@ -0,0 +1,8 @@
+<ul id="feedback">
+ <a title="Feedback" href="#">
+ <li>
+ <div class="fa fa-exclamation-circle"></div>
+ <i class="shortcut-label"></i> Feedback
+ </li>
+ </a>
+</ul>
diff --git a/web-ui/public/templates/mail_actions/actions_box.hbs b/web-ui/public/templates/mail_actions/actions_box.hbs
new file mode 100644
index 00000000..68a8d0bf
--- /dev/null
+++ b/web-ui/public/templates/mail_actions/actions_box.hbs
@@ -0,0 +1,7 @@
+<li><input type="checkbox" id="toggle-check-all-emails"/></li>
+<li><input type="button" id="mark-selected-as-read" value="{{t 'mark-as-read'}}" disabled="disabled"/></li>
+<li><input type="button" id="mark-selected-as-unread" value="{{t 'mark-as-unread'}}" disabled="disabled"/></li>
+<li><input type="button" id="delete-selected" value="{{t 'delete'}}" disabled="disabled"/></li>
+<li><input type="button" id="archive-selected" value="{{t 'archive'}}" disabled="disabled"/></li>
+<li id="pagination-trigger" class="right"></li>
+<li id="refresh-trigger" class="right"></li>
diff --git a/web-ui/public/templates/mail_actions/compose_trigger.hbs b/web-ui/public/templates/mail_actions/compose_trigger.hbs
new file mode 100644
index 00000000..06f05fca
--- /dev/null
+++ b/web-ui/public/templates/mail_actions/compose_trigger.hbs
@@ -0,0 +1,3 @@
+<button id="compose-mails-trigger">
+ {{t 'compose' }}
+</button>
diff --git a/web-ui/public/templates/mail_actions/pagination_trigger.hbs b/web-ui/public/templates/mail_actions/pagination_trigger.hbs
new file mode 100644
index 00000000..cbd8a089
--- /dev/null
+++ b/web-ui/public/templates/mail_actions/pagination_trigger.hbs
@@ -0,0 +1,3 @@
+<span id="left-arrow"><i class="fa fa-angle-left"></i></span>
+<span id="current-page">{{ currentPage }}</span>
+<span id="right-arrow"><i class="fa fa-angle-right"></i></span>
diff --git a/web-ui/public/templates/mail_actions/refresh_trigger.hbs b/web-ui/public/templates/mail_actions/refresh_trigger.hbs
new file mode 100644
index 00000000..dffc7090
--- /dev/null
+++ b/web-ui/public/templates/mail_actions/refresh_trigger.hbs
@@ -0,0 +1,3 @@
+<div id="refresh-mails-trigger">
+ <i class="fa fa-refresh" data-label="{{t 'refresh'}}"></i>
+</div>
diff --git a/web-ui/public/templates/mail_actions/trash_actions_box.hbs b/web-ui/public/templates/mail_actions/trash_actions_box.hbs
new file mode 100644
index 00000000..4e0ec332
--- /dev/null
+++ b/web-ui/public/templates/mail_actions/trash_actions_box.hbs
@@ -0,0 +1,5 @@
+<li><input type="checkbox" id="toggle-check-all-emails"/></li>
+<li><input type="button" id="delete-selected" value="{{t 'delete-permanently'}}" disabled="disabled"/></li>
+<li><input type="button" id="recover-selected" value="{{t 'move-to-inbox'}}" disabled="disabled"/></li>
+<li id="pagination-trigger" class="right"></li>
+<li id="refresh-trigger" class="right"></li>
diff --git a/web-ui/public/templates/mails/draft.hbs b/web-ui/public/templates/mails/draft.hbs
new file mode 100644
index 00000000..808ce3ff
--- /dev/null
+++ b/web-ui/public/templates/mails/draft.hbs
@@ -0,0 +1,41 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{t 'to'}}:
+ {{#if header.to }}
+ {{ header.to }}
+ {{else}}
+ {{t 'no-recipient'}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">
+ <i class="mail-list-entry__item-subject-icon fa fa-pencil"></i>
+ {{#if header.subject }}
+ {{header.subject}}
+ {{else}}
+ {{t 'no-subject'}}
+ {{/if}}
+ </div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
+
+
+
+
diff --git a/web-ui/public/templates/mails/full_view.hbs b/web-ui/public/templates/mails/full_view.hbs
new file mode 100644
index 00000000..40bfd4a2
--- /dev/null
+++ b/web-ui/public/templates/mails/full_view.hbs
@@ -0,0 +1,83 @@
+<button class="close-mail-button">
+ <i class="fa fa-times"></i>
+</button>
+
+<div id="fullView-{{ ident }}" class="mail-read-view {{statuses}}">
+ <header class="mail-read-view__header row">
+
+ <!-- TODO -->
+ <div class="column large-12 no-padding security-status">
+ {{#if signatureStatus}}
+ <span class="security-status__label {{ signatureStatus.cssClass }}">
+ {{t signatureStatus.label }}
+ </span>
+ {{/if}}
+ {{#if encryptionStatus}}
+ <span class="security-status__label {{ encryptionStatus.cssClass }}">
+ {{t encryptionStatus.label }}
+ </span>
+ {{/if}}
+ </div>
+
+ <div class="mail-read-view__header-recipients">
+ <span class="mail-read-view__header-recipients--highlight-sender">
+ {{#if header.from }}
+ {{ header.from }}
+ {{else}}
+ {{t 'you'}}
+ {{/if}}
+ </span>
+ <i class="fa fa-long-arrow-right"></i>
+ {{{formatRecipients header}}}
+ </div>
+
+ <div class="mail-read-view__header-date">
+ {{ formatDate header.date }}
+ </div>
+
+ <hr>
+
+ <div class="mail-read-view__header-subject">
+ <h3>{{ header.subject }}</h3>
+ </div>
+
+ <nav id="mail-actions" class="mail-read-view__header-actions"></nav>
+
+ <ul class="mail-read-view__header-tags">
+ <li class="mail-read-view__header-tags-label">
+ <i class="fa fa-tags"></i>
+ </li>
+
+ {{#each tags }}
+ <li class="mail-read-view__header-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+
+ <li class="mail-read-view__header-tags-name-input">
+ <input type="text" id="new-tag-input" placeholder="{{t 'add-tag-placeholder'}}"
+ />
+ </li>
+
+ <li class="mail-read-view__header-tags-new-button">
+ <button id="new-tag-button" class="no-style"><i class="fa fa-plus"></i></button>
+ </li>
+ </ul>
+ </header>
+
+ <iframe class="mail-read-view__body" id="read-sandbox" sandbox="allow-popups allow-scripts" src="sandbox/sandbox.html" scrolling="no"></iframe>
+
+ {{#if attachments}}
+ <hr>
+
+ <div id="attachmentsArea" class="mail-read-view__attachments">
+ <p class="mail-read-view__attachments-header"><i class="fa fa-paperclip"></i> {{ attachments.length }} attachment(s):</p>
+ <ul>
+ {{#each attachments }}
+ <li class="mail-read-view__attachments-item">
+ <a class="mail-read-view__attachments-item-label" href="/attachment/{{ this.ident }}?content_type={{ this.content-type }}&encoding={{ this.encoding }}&filename={{ this.name }}">{{ this.name }} ({{ formatSize this.size}})
+ <i class="fa fa-arrow-down mail-read-view__attachments-item-download"></i></a>
+ </li>
+ {{/each }}
+ </ul>
+ </div>
+ {{/if}}
+</div>
diff --git a/web-ui/public/templates/mails/mail_actions.hbs b/web-ui/public/templates/mails/mail_actions.hbs
new file mode 100644
index 00000000..0adfe853
--- /dev/null
+++ b/web-ui/public/templates/mails/mail_actions.hbs
@@ -0,0 +1,6 @@
+<button id="reply-button-top" class="mail-read-view__header-actions-button mail-read-view__header-actions-button--reply"><i class="fa fa-reply"></i></button>
+<button id="view-more-actions" class="mail-read-view__header-actions-button mail-read-view__header-actions-button--more"><i class="fa fa-caret-down"></i></button>
+<ul id="more-actions" class="mail-read-view__header-actions-dropdown">
+ <li id="reply-all-button-top" class="mail-read-view__header-actions-dropdown-entry">{{t 'reply-to-all'}}</li>
+ <li id="delete-button-top" class="mail-read-view__header-actions-dropdown-entry">{{t 'delete-this-message'}}</li>
+</ul>
diff --git a/web-ui/public/templates/mails/sent.hbs b/web-ui/public/templates/mails/sent.hbs
new file mode 100644
index 00000000..158b20c8
--- /dev/null
+++ b/web-ui/public/templates/mails/sent.hbs
@@ -0,0 +1,36 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{t 'to'}}:
+ {{#if header.to }}
+ {{ header.to }}
+ {{else}}
+ {{t 'no-recipient'}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">
+ {{#if header.subject }}
+ {{header.subject}}
+ {{else}}
+ {{t 'no-subject'}}
+ {{/if}}
+ </div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
+
diff --git a/web-ui/public/templates/mails/single.hbs b/web-ui/public/templates/mails/single.hbs
new file mode 100644
index 00000000..aaede844
--- /dev/null
+++ b/web-ui/public/templates/mails/single.hbs
@@ -0,0 +1,28 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{#if header.from }}
+ {{ header.from }}
+ {{else}}
+ {{t "you"}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">{{ header.subject }}</div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
diff --git a/web-ui/public/templates/mails/trash.hbs b/web-ui/public/templates/mails/trash.hbs
new file mode 100644
index 00000000..f8947b15
--- /dev/null
+++ b/web-ui/public/templates/mails/trash.hbs
@@ -0,0 +1,32 @@
+<div class="mail-list-entry__checkbox">
+ <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} />
+</div>
+<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}">
+ <div>
+ <div class="mail-list-entry__item-from">
+ {{#if header.from }}
+ {{ header.from }}
+ {{else}}
+ {{t "you"}}
+ {{/if}}
+ </div> <!-- /.mail-list-entry__item-from -->
+
+ <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date -->
+ </div>
+ <div>
+ <div class="mail-list-entry__item-subject">
+ <i class="mail-list-entry__item-subject-icon fa fa-trash-o"></i>
+ {{ header.subject }}
+ </div>
+
+ {{#if attachments}}
+ <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div>
+ {{/if}}
+ </div>
+ <ul class="mail-list-entry__item-tags">
+ {{#each tagsForListView }}
+ <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li>
+ {{/each }}
+ </ul> <!-- /.mail-list-entry__item-tags -->
+</a>
+
diff --git a/web-ui/public/templates/page/logout.hbs b/web-ui/public/templates/page/logout.hbs
new file mode 100644
index 00000000..0cc079bc
--- /dev/null
+++ b/web-ui/public/templates/page/logout.hbs
@@ -0,0 +1,9 @@
+<ul id="logout">
+ <form id="logout-form" method="POST" action="{{ logout_url }}">
+ <input type="hidden" name="csrftoken" value="{{ csrf_token }}" />
+ <li>
+ <div class="fa fa-sign-out"></div>
+ <i class="shortcut-label"></i> {{t 'logout'}}
+ </li>
+ </form>
+</ul>
diff --git a/web-ui/public/templates/page/logout_shortcut.hbs b/web-ui/public/templates/page/logout_shortcut.hbs
new file mode 100644
index 00000000..043ab0dc
--- /dev/null
+++ b/web-ui/public/templates/page/logout_shortcut.hbs
@@ -0,0 +1,6 @@
+<li>
+ <a class="left-off-canvas-toggle logout" >
+ <i class="fa fa-sign-out"></i>
+ <div class="shortcut-label">{{t 'logout'}}</div>
+ </a>
+</li>
diff --git a/web-ui/public/templates/page/user_settings_box.hbs b/web-ui/public/templates/page/user_settings_box.hbs
new file mode 100644
index 00000000..2152b779
--- /dev/null
+++ b/web-ui/public/templates/page/user_settings_box.hbs
@@ -0,0 +1,10 @@
+<header>
+ <span id="user-settings-close"><i class="fa fa-close"></i></span>
+ <i class="fa fa-user"></i>
+ <h1>{{t 'user-account'}}</h1>
+ <i class="shortcut-label"></i>
+</header>
+<i class="fa fa-envelope-o"></i><h2>{{t 'email-address'}}</h2>
+<p>{{ account_email }}</p>
+<i class="fa fa-key"></i><h2>{{t 'public-key-fingerprint'}}</h2>
+<p>{{ formatFingerPrint fingerprint }}</p>
diff --git a/web-ui/public/templates/page/user_settings_icon.hbs b/web-ui/public/templates/page/user_settings_icon.hbs
new file mode 100644
index 00000000..8f2f9215
--- /dev/null
+++ b/web-ui/public/templates/page/user_settings_icon.hbs
@@ -0,0 +1,8 @@
+<ul id="user-settings-icon">
+ <a title="my account" href='#'>
+ <li>
+ <div class="fa fa-user"></div>
+ <i class="shortcut-label"></i> {{t 'user-account'}}
+ </li>
+ </a>
+</ul>
diff --git a/web-ui/public/templates/page/version.hbs b/web-ui/public/templates/page/version.hbs
new file mode 100644
index 00000000..5f43f78a
--- /dev/null
+++ b/web-ui/public/templates/page/version.hbs
@@ -0,0 +1,2 @@
+{{t 'version'}}: UNKNOWN_VERSION <br/>
+<span id="version-date" data-since="COMMIT_DATE"></span>
diff --git a/web-ui/public/templates/search/search_trigger.hbs b/web-ui/public/templates/search/search_trigger.hbs
new file mode 100644
index 00000000..2261d154
--- /dev/null
+++ b/web-ui/public/templates/search/search_trigger.hbs
@@ -0,0 +1,3 @@
+<form>
+ <input type="search" placeholder="{{t 'search-placeholder'}}"></input>
+</form>
diff --git a/web-ui/public/templates/tags/shortcut.hbs b/web-ui/public/templates/tags/shortcut.hbs
new file mode 100644
index 00000000..1e82d6a9
--- /dev/null
+++ b/web-ui/public/templates/tags/shortcut.hbs
@@ -0,0 +1,9 @@
+<li class="{{ selected }}">
+ <a>
+ {{#if displayBadge }}
+ <span class="{{ badgeType }}-count">{{ count }}</span>
+ {{/if}}
+ <i class="fa fa-{{ icon }}"></i>
+ <div class="shortcut-label">{{ tagName }}</div>
+ </a>
+</li>
diff --git a/web-ui/public/templates/tags/tag.hbs b/web-ui/public/templates/tags/tag.hbs
new file mode 100644
index 00000000..ca397b9a
--- /dev/null
+++ b/web-ui/public/templates/tags/tag.hbs
@@ -0,0 +1,3 @@
+<li id="tag-{{ ident }}" class="custom-tag {{ selected }}">
+ <span class="tag-label">{{> tag_inner }}</span>
+</li>
diff --git a/web-ui/public/templates/tags/tag_inner.hbs b/web-ui/public/templates/tags/tag_inner.hbs
new file mode 100644
index 00000000..2e0958cb
--- /dev/null
+++ b/web-ui/public/templates/tags/tag_inner.hbs
@@ -0,0 +1,4 @@
+{{ tagName }}
+{{#if displayBadge }}
+<span class="{{ badgeType }}-count">{{ count }}</span>
+{{/if}}
diff --git a/web-ui/public/templates/tags/tag_list.hbs b/web-ui/public/templates/tags/tag_list.hbs
new file mode 100644
index 00000000..92a73283
--- /dev/null
+++ b/web-ui/public/templates/tags/tag_list.hbs
@@ -0,0 +1,6 @@
+<ul id="default-tag-list"></ul>
+<div class="tags-icon side-nav-toggle">
+ <i class="fa fa-tags"></i>
+ <span class="tag-label">{{t 'tags.tags'}}</span>
+</div>
+<ul id="custom-tag-list"></ul>
diff --git a/web-ui/public/templates/user_alerts/message.hbs b/web-ui/public/templates/user_alerts/message.hbs
new file mode 100644
index 00000000..abba1f91
--- /dev/null
+++ b/web-ui/public/templates/user_alerts/message.hbs
@@ -0,0 +1 @@
+<span class="message-panel__growl {{ message.class }}">{{ message.content }}</span>