summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/components/activities/activities.jade8
-rw-r--r--client/components/activities/activities.js77
-rw-r--r--client/components/activities/comments.jade0
-rw-r--r--client/components/activities/comments.js0
-rw-r--r--client/components/activities/events.js30
-rw-r--r--client/components/activities/templates.html154
-rw-r--r--client/components/boards/body.jade33
-rw-r--r--client/components/boards/body.js70
-rw-r--r--client/components/boards/body.styl54
-rw-r--r--client/components/boards/colors.styl34
-rw-r--r--client/components/boards/events.js96
-rw-r--r--client/components/boards/header.jade87
-rw-r--r--client/components/boards/header.js7
-rw-r--r--client/components/boards/header.styl137
-rw-r--r--client/components/boards/helpers.js45
-rw-r--r--client/components/boards/list.jade14
-rw-r--r--client/components/boards/list.styl85
-rw-r--r--client/components/boards/router.js34
-rw-r--r--client/components/cards/details.jade47
-rw-r--r--client/components/cards/details.js103
-rw-r--r--client/components/cards/details.styl161
-rw-r--r--client/components/cards/events.js285
-rw-r--r--client/components/cards/helpers.js48
-rw-r--r--client/components/cards/labels.styl183
-rw-r--r--client/components/cards/minicard.styl136
-rw-r--r--client/components/cards/popups.jade12
-rw-r--r--client/components/cards/router.js15
-rw-r--r--client/components/cards/templates.html336
-rw-r--r--client/components/forms/cachedValue.js22
-rw-r--r--client/components/forms/forms.styl636
-rw-r--r--client/components/forms/inlinedform.jade6
-rw-r--r--client/components/forms/inlinedform.js93
-rw-r--r--client/components/lists/body.jade50
-rw-r--r--client/components/lists/body.js73
-rw-r--r--client/components/lists/events.js16
-rw-r--r--client/components/lists/header.jade13
-rw-r--r--client/components/lists/header.js25
-rw-r--r--client/components/lists/main.jade5
-rw-r--r--client/components/lists/main.js81
-rw-r--r--client/components/lists/main.styl136
-rw-r--r--client/components/lists/menu.jade28
-rw-r--r--client/components/lists/menu.js46
-rw-r--r--client/components/main/events.js8
-rw-r--r--client/components/main/header.jade40
-rw-r--r--client/components/main/header.js10
-rw-r--r--client/components/main/header.styl266
-rw-r--r--client/components/main/helpers.js63
-rw-r--r--client/components/main/layouts.jade17
-rw-r--r--client/components/main/popup.js16
-rw-r--r--client/components/main/popup.styl585
-rw-r--r--client/components/main/popup.tpl.jade13
-rw-r--r--client/components/main/rendered.js40
-rw-r--r--client/components/main/router.js5
-rw-r--r--client/components/main/spinner.styl45
-rw-r--r--client/components/main/spinner.tpl.jade6
-rw-r--r--client/components/main/templates.html18
-rw-r--r--client/components/modal/events.js14
-rw-r--r--client/components/modal/helpers.js0
-rw-r--r--client/components/modal/modal.tpl.jade5
-rw-r--r--client/components/sidebar/events.js93
-rw-r--r--client/components/sidebar/helpers.js51
-rw-r--r--client/components/sidebar/infiniteScrolling.js37
-rw-r--r--client/components/sidebar/rendered.js21
-rw-r--r--client/components/sidebar/sidebar.js55
-rw-r--r--client/components/sidebar/sidebar.styl154
-rw-r--r--client/components/sidebar/templates.html.old307
-rw-r--r--client/components/sidebar/templates.jade103
-rw-r--r--client/components/users/avatar.jade7
-rw-r--r--client/components/users/events.js59
-rw-r--r--client/components/users/form.styl50
-rw-r--r--client/components/users/headerButtons.jade27
-rw-r--r--client/components/users/headerButtons.js5
-rw-r--r--client/components/users/helpers.js27
-rw-r--r--client/components/users/member.styl107
-rw-r--r--client/components/users/router.js29
-rw-r--r--client/components/users/templates.html118
-rw-r--r--client/config/accounts.js35
-rw-r--r--client/config/avatar.js3
-rw-r--r--client/config/router.js28
-rw-r--r--client/lib/emoji-values.js152
-rw-r--r--client/lib/filter.js133
-rw-r--r--client/lib/i18n.js22
-rw-r--r--client/lib/keyboard.js55
-rw-r--r--client/lib/mixins.js1
-rw-r--r--client/lib/popup.js200
-rw-r--r--client/lib/utils.js96
-rw-r--r--client/styles/cheat.styl79
-rw-r--r--client/styles/fancy-scrollbar.styl45
-rw-r--r--client/styles/main.styl814
-rw-r--r--client/styles/temp.styl110
90 files changed, 7695 insertions, 0 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
new file mode 100644
index 00000000..1c6b9faf
--- /dev/null
+++ b/client/components/activities/activities.jade
@@ -0,0 +1,8 @@
+template(name="activities")
+ .js-sidebar-activities
+ //- We should use Template.dynamic here but there is a bug with
+ //- blaze-components: https://github.com/peerlibrary/meteor-blaze-components/issues/30
+ if $eq mode "board"
+ +boardActivities
+ else
+ +cardActivities
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
new file mode 100644
index 00000000..c806e87b
--- /dev/null
+++ b/client/components/activities/activities.js
@@ -0,0 +1,77 @@
+var activitiesPerPage = 20;
+
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'activities';
+ },
+
+ onCreated: function() {
+ var self = this;
+ // XXX Should we use ReactiveNumber?
+ self.page = new ReactiveVar(1);
+ self.loadNextPageLocked = false;
+ var sidebar = self.componentParent(); // XXX for some reason not working
+ sidebar.callFirstWith(null, 'resetNextPeak');
+ self.autorun(function() {
+ var mode = self.data().mode;
+ var capitalizedMode = Utils.capitalize(mode);
+ var id = Session.get('current' + capitalizedMode);
+ var limit = self.page.get() * activitiesPerPage;
+ if (id === null)
+ return;
+
+ self.subscribe('activities', mode, id, limit, function() {
+ self.loadNextPageLocked = false;
+
+ // If the sibear peak hasn't increased, that mean that there are no more
+ // activities, and we can stop calling new subscriptions.
+ // XXX This is hacky! We need to know excatly and reactively how many
+ // activities there are, we probably want to denormalize this number
+ // dirrectly into card and board documents.
+ var a = sidebar.callFirstWith(null, 'getNextPeak');
+ sidebar.calculateNextPeak();
+ var b = sidebar.callFirstWith(null, 'getNextPeak');
+ if (a === b) {
+ sidebar.callFirstWith(null, 'resetNextPeak');
+ }
+ });
+ });
+ },
+
+ loadNextPage: function() {
+ if (this.loadNextPageLocked === false) {
+ this.page.set(this.page.get() + 1);
+ this.loadNextPageLocked = true;
+ }
+ },
+
+ boardLabel: function() {
+ return TAPi18n.__('this-board');
+ },
+
+ cardLabel: function() {
+ return TAPi18n.__('this-card');
+ },
+
+ cardLink: function() {
+ var card = this.currentData().card();
+ return Blaze.toHTML(HTML.A({
+ href: card.absoluteUrl(),
+ 'class': 'action-card'
+ }, card.title));
+ },
+
+ memberLink: function() {
+ return Blaze.toHTMLWithData(Template.memberName, {
+ user: this.currentData().member()
+ });
+ },
+
+ attachmentLink: function() {
+ var attachment = this.currentData().attachment();
+ return Blaze.toHTML(HTML.A({
+ href: attachment.url(),
+ 'class': 'js-open-attachment-viewer'
+ }, attachment.name()));
+ }
+}).register('activities');
diff --git a/client/components/activities/comments.jade b/client/components/activities/comments.jade
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/client/components/activities/comments.jade
diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/client/components/activities/comments.js
diff --git a/client/components/activities/events.js b/client/components/activities/events.js
new file mode 100644
index 00000000..ea98e65f
--- /dev/null
+++ b/client/components/activities/events.js
@@ -0,0 +1,30 @@
+Template.cardActivities.events({
+ 'click .js-edit-action': function(evt) {
+ var $this = $(evt.currentTarget);
+ var container = $this.parents('.phenom-comment');
+
+ // open and focus
+ container.addClass('editing');
+ container.find('textarea').focus();
+ },
+ 'click .js-confirm-delete-action': function() {
+ CardComments.remove(this._id);
+ },
+ 'submit form': function(evt) {
+ var $this = $(evt.currentTarget);
+ var container = $this.parents('.phenom-comment');
+ var text = container.find('textarea');
+
+ if ($.trim(text.val())) {
+ CardComments.update(this._id, {
+ $set: {
+ text: text.val()
+ }
+ });
+
+ // reset editing class
+ $('.editing').removeClass('editing');
+ }
+ evt.preventDefault();
+ }
+});
diff --git a/client/components/activities/templates.html b/client/components/activities/templates.html
new file mode 100644
index 00000000..8d3ff763
--- /dev/null
+++ b/client/components/activities/templates.html
@@ -0,0 +1,154 @@
+<template name="boardActivities">
+ {{# each currentBoard.activities }}
+ <div class="phenom phenom-action clearfix phenom-other">
+ {{> userAvatar user=user size="extra-small" class="creator js-show-mem-menu" }}
+ <div class="phenom-desc">
+ {{ > memberName user=user }}
+
+ {{# if $eq activityType 'createBoard' }}
+ {{_ 'activity-created' boardLabel}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'createList' }}
+ {{_ 'activity-added' list.title boardLabel}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'archivedList' }}
+ {{_ 'activity-archived' list.title}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'createCard' }}
+ {{{_ 'activity-added' cardLink boardLabel}}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'archivedCard' }}
+ {{{_ 'activity-archived' cardLink}}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'restoredCard' }}
+ {{{_ 'activity-sent' cardLink boardLabel}}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'moveCard' }}
+ {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'addBoardMember' }}
+ {{{_ 'activity-added' memberLink boardLabel}}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'removeBoardMember' }}
+ {{{_ 'activity-excluded' memberLink boardLabel}}}.
+ {{ /if }}
+
+ {{# if $eq activityType 'joinMember' }}
+ {{# if $eq currentUser._id member._id }}
+ {{{_ 'activity-joined' cardLink}}}.
+ {{ else }}
+ {{{_ 'activity-added' memberLink cardLink}}}.
+ {{/if}}
+ {{ /if }}
+
+ {{# if $eq activityType 'unjoinMember' }}
+ {{# if $eq currentUser._id member._id }}
+ {{{_ 'activity-unjoined' cardLink}}}.
+ {{ else }}
+ {{{_ 'activity-removed' memberLink cardLink}}}.
+ {{/if}}
+ {{ /if }}
+
+ {{# if $eq activityType 'addComment' }}
+ <div class="phenom-desc">
+ {{{_ 'activity-on' cardLink}}}
+ <div class="action-comment markeddown">
+ <a href="{{ card.absoluteUrl }}" class="current-comment show tdn">
+ <p>{{#viewer}}{{ comment.text }}{{/viewer}}</p>
+ </a>
+ </div>
+ </div>
+ {{ /if }}
+
+ {{# if $eq activityType 'addAttachment' }}
+ <div class="phenom-desc">
+ {{{_ 'activity-attached' attachmentLink cardLink}}}.
+ </div>
+ {{ /if }}
+ </div>
+ <p class="phenom-meta quiet">
+ <span class="date js-hide-on-sending">
+ {{ moment createdAt }}
+ </span>
+ </p>
+ </div>
+ {{ /each }}
+</template>
+
+<template name="cardActivities">
+ {{# each currentCard.comments }}
+ <div class="phenom phenom-action clearfix phenom-comment">
+ {{> userAvatar user=user size="small" class="creator js-show-mem-menu" }}
+ <form>
+ <div class="phenom-desc">
+ {{ > memberName user=user }}
+ <div class="action-comment markeddown">
+ <div class="current-comment">
+ {{#viewer}}{{ text }}{{/viewer}}
+ </div>
+ <textarea class="js-text" tabindex="1">{{ text }}</textarea>
+ </div>
+ </div>
+ <div class="edit-controls clearfix">
+ <input type="submit" class="primary confirm js-save-edit" value="{{_ 'save'}}" tabindex="2">
+ </div>
+ </form>
+ <p class="phenom-meta quiet">
+ <span class="date js-hide-on-sending">{{ moment createdAt }}</span>
+ {{# if currentUser }}
+ <span class="js-hide-on-sending">
+ - <a href="#" class="js-edit-action">{{_ "edit"}}</a>
+ - <a href="#" class="js-confirm-delete-action">{{_ "delete"}}</a>
+ </span>
+ {{/ if }}
+ </p>
+ </div>
+ {{/each}}
+
+ {{# each currentCard.activities }}
+ <div class="phenom phenom-action clearfix phenom-other">
+ {{> userAvatar user=user size="extra-small" class="creator js-show-mem-menu" }}
+ {{ > memberName user=user }}
+ {{# if $eq activityType 'createCard' }}
+ {{_ 'activity-added' cardLabel list.title}}.
+ {{ /if }}
+ {{# if $eq activityType 'joinMember' }}
+ {{# if $eq currentUser._id member._id }}
+ {{_ 'activity-joined' cardLabel}}.
+ {{ else }}
+ {{{_ 'activity-added' cardLabel memberLink}}}.
+ {{/if}}
+ {{/if}}
+ {{# if $eq activityType 'unjoinMember' }}
+ {{# if $eq currentUser._id member._id }}
+ {{_ 'activity-unjoined' cardLabel}}.
+ {{ else }}
+ {{{_ 'activity-removed' cardLabel memberLink}}}.
+ {{/if}}
+ {{ /if }}
+ {{# if $eq activityType 'archivedCard' }}
+ {{_ 'activity-archived' cardLabel}}.
+ {{ /if }}
+ {{# if $eq activityType 'restoredCard' }}
+ {{_ 'activity-sent' cardLabel boardLabel}}.
+ {{/ if }}
+ {{# if $eq activityType 'moveCard' }}
+ {{_ 'activity-moved' cardLabel oldList.title list.title}}.
+ {{/ if }}
+ {{# if $eq activityType 'addAttachment' }}
+ {{{_ 'activity-attached' attachmentLink cardLabel}}}.
+ {{# if attachment.isImage }}
+ <img src="{{ attachment.url }}" class="attachment-image-preview">
+ {{/if}}
+ {{/ if}}
+ </div>
+ {{/each}}
+</template>
diff --git a/client/components/boards/body.jade b/client/components/boards/body.jade
new file mode 100644
index 00000000..5406ee2f
--- /dev/null
+++ b/client/components/boards/body.jade
@@ -0,0 +1,33 @@
+//-
+ XXX This template can't be transformed into a component because it is
+ included by iron-router. That's a bug.
+template(name="board")
+ +boardComponent
+
+template(name="boardComponent")
+ if this
+ .board-wrapper(class=colorClass)
+ .board-canvas(class=sidebarSize)
+ .lists.js-lists
+ each lists
+ +list(this)
+ if currentUser.isBoardMember
+ +addlistForm
+ +boardSidebar
+ if currentCard
+ +cardSidebar(currentCard)
+ else
+ +message(label="board-no-found")
+
+template(name="addlistForm")
+ .list.js-list.add-list.js-add-list
+ +inlinedForm(autoclose=false)
+ input.list-name-input(type="text" placeholder="{{_ 'add-list'}}"
+ autocomplete="off" autofocus)
+ div.edit-controls.clearfix
+ button.primary.confirm.js-save-edit(type="submit") {{_ 'save'}}
+ a.fa.fa-times.dark-hover.cancel.js-close-inlined-form
+ else
+ .js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-list'}}
diff --git a/client/components/boards/body.js b/client/components/boards/body.js
new file mode 100644
index 00000000..2b4baf53
--- /dev/null
+++ b/client/components/boards/body.js
@@ -0,0 +1,70 @@
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'boardComponent';
+ },
+
+ openNewListForm: function() {
+ this.componentChildren('addlistForm')[0].open();
+ },
+
+ scrollLeft: function() {
+ // TODO
+ },
+
+ onRendered: function() {
+ var self = this;
+
+ self.scrollLeft();
+
+ if (Meteor.user().isBoardMember()) {
+ self.$('.js-lists').sortable({
+ tolerance: 'pointer',
+ appendTo: '.js-lists',
+ helper: 'clone',
+ items: '.js-list:not(.add-list)',
+ placeholder: 'list placeholder',
+ start: function(event, ui) {
+ $('.list.placeholder').height(ui.item.height());
+ Popup.close();
+ },
+ stop: function() {
+ self.$('.js-lists').find('.js-list:not(.add-list)').each(
+ function(i, list) {
+ var data = Blaze.getData(list);
+ Lists.update(data._id, {
+ $set: {
+ sort: i
+ }
+ });
+ }
+ );
+ }
+ });
+
+ // If there is no data in the board (ie, no lists) we autofocus the list
+ // creation form by clicking on the corresponding element.
+ if (self.data().lists().count() === 0) {
+ this.openNewListForm();
+ }
+ }
+ },
+
+ sidebarSize: function() {
+ var sidebar = this.componentChildren('boardSidebar')[0];
+ if (Session.get('currentCard') !== null)
+ return 'next-large-sidebar';
+ else if (sidebar && sidebar.isOpen())
+ return 'next-small-sidebar';
+ }
+}).register('boardComponent');
+
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'addlistForm';
+ },
+
+ // Proxy
+ open: function() {
+ this.componentChildren('inlinedForm')[0].open();
+ }
+}).register('addlistForm');
diff --git a/client/components/boards/body.styl b/client/components/boards/body.styl
new file mode 100644
index 00000000..cb351e46
--- /dev/null
+++ b/client/components/boards/body.styl
@@ -0,0 +1,54 @@
+@import 'nib'
+
+.board-wrapper
+ left: 0
+ top: 0
+ bottom: 0
+ right: 0
+ position: absolute
+ overflow: hidden
+
+ .board-canvas
+ position: absolute
+ left: 0
+ right: 0
+ top: 0
+ bottom: 0
+ transition: margin .1s
+
+ &.next-small-sidebar
+ margin-right: 248px
+
+ &.next-large-sidebar
+ opacity: 0.8
+ margin-right: 496px
+
+.lists
+ align-items: flex-start
+ display: flex
+ flex-direction: row
+ margin-bottom: 10px
+ overflow-x: auto
+ overflow-y: hidden
+ padding-bottom: 10px
+ position: absolute
+ top: 0
+ right: 0
+ bottom: 0
+ left: 0
+
+ &::-webkit-scrollbar
+ height: 13px
+ width: 13px
+
+ &::-webkit-scrollbar-thumb:vertical,
+ &::-webkit-scrollbar-thumb:horizontal
+ background: rgba(255, 255, 255, .4)
+
+ &::-webkit-scrollbar-track-piece
+ background: rgba(0, 0, 0, .15)
+
+ &::-webkit-scrollbar-button
+ display: block
+ height: 5px
+ width: 5px
diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl
new file mode 100644
index 00000000..1db44845
--- /dev/null
+++ b/client/components/boards/colors.styl
@@ -0,0 +1,34 @@
+// We define a set of six board colors that we took from the FlatUI palette.
+// http://flatuicolors.com
+
+setBoardColor(color)
+ &#header,
+ &.sk-spinner div,
+ .board-backgrounds-list &.background-box,
+ &.pop-over .pop-over-list li a:hover,
+ .board-list & a
+ background-color: color
+
+ & .minicard.is-selected .minicard-details
+ border-bottom: 2px solid color
+
+ button[type=submit].primary, input[type=submit].primary
+ background-color: darken(color, 20%)
+
+.board-color-nephritis
+ setBoardColor(#27AE60)
+
+.board-color-pomegranate
+ setBoardColor(#C0392B)
+
+.board-color-belize
+ setBoardColor(#2980B9)
+
+.board-color-wisteria
+ setBoardColor(#8E44AD)
+
+.board-color-midnight
+ setBoardColor(#2C3E50)
+
+.board-color-pumpkin
+ setBoardColor(#E67E22)
diff --git a/client/components/boards/events.js b/client/components/boards/events.js
new file mode 100644
index 00000000..6f9d7fc6
--- /dev/null
+++ b/client/components/boards/events.js
@@ -0,0 +1,96 @@
+var toggleBoardStar = function(boardId) {
+ var queryType = Meteor.user().hasStarred(boardId) ? '$pull' : '$addToSet';
+ var query = {};
+ query[queryType] = {
+ 'profile.starredBoards': boardId
+ };
+ Meteor.users.update(Meteor.userId(), query);
+};
+
+Template.boards.events({
+ 'click .js-star-board': function(evt) {
+ toggleBoardStar(this._id);
+ evt.preventDefault();
+ }
+});
+
+Template.headerBoard.events({
+ 'click .js-star-board': function() {
+ toggleBoardStar(this._id);
+ },
+ 'click .js-open-board-menu': Popup.open('boardMenu'),
+ 'click #permission-level:not(.no-edit)': Popup.open('boardChangePermission'),
+ 'click .js-filter-cards-indicator': function(evt) {
+ Session.set('currentWidget', 'filter');
+ evt.preventDefault();
+ },
+ 'click .js-filter-card-clear': function(evt) {
+ Filter.reset();
+ evt.stopPropagation();
+ }
+});
+
+Template.boardMenuPopup.events({
+ 'click .js-rename-board': Popup.open('boardChangeTitle'),
+ 'click .js-change-board-color': Popup.open('boardChangeColor')
+});
+
+Template.createBoardPopup.events({
+ 'submit #CreateBoardForm': function(evt, t) {
+ var title = t.$('#boardNewTitle');
+
+ // trim value title
+ if ($.trim(title.val())) {
+ // İnsert Board title
+ var boardId = Boards.insert({
+ title: title.val(),
+ permission: 'public'
+ });
+
+ // Go to Board _id
+ Utils.goBoardId(boardId);
+ }
+ evt.preventDefault();
+ }
+});
+
+Template.boardChangeTitlePopup.events({
+ 'submit #ChangeBoardTitleForm': function(evt, t) {
+ var title = t.$('.js-board-name').val().trim();
+ if (title) {
+ Boards.update(this._id, {
+ $set: {
+ title: title
+ }
+ });
+ Popup.close();
+ }
+ evt.preventDefault();
+ }
+});
+
+Template.boardChangePermissionPopup.events({
+ 'click .js-select': function(evt) {
+ var $this = $(evt.currentTarget);
+ var permission = $this.attr('name');
+
+ Boards.update(this._id, {
+ $set: {
+ permission: permission
+ }
+ });
+ Popup.close();
+ }
+});
+
+Template.boardChangeColorPopup.events({
+ 'click .js-select-background': function(evt) {
+ var currentBoardId = Session.get('currentBoard');
+ Boards.update(currentBoardId, {
+ $set: {
+ color: this.toString()
+ }
+ });
+ evt.preventDefault();
+ }
+});
diff --git a/client/components/boards/header.jade b/client/components/boards/header.jade
new file mode 100644
index 00000000..189cdac4
--- /dev/null
+++ b/client/components/boards/header.jade
@@ -0,0 +1,87 @@
+template(name="headerBoard")
+ h1.header-board-menu.js-open-board-menu
+ = title
+ span.fa.fa-angle-down
+
+ .board-header-btns.left
+ unless isSandstorm
+ a.board-header-btn.js-star-board(class="{{#if isStarred}}board-header-starred{{/if}}"
+ title="{{# if isStarred }}{{_ 'click-to-unstar'}}{{ else }}{{_ 'click-to-star'}}{{/ if }} {{_ 'starred-boards-description'}}")
+ span.board-header-btn-icon.icon-sm.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
+ //- XXX To implement
+ span.board-header-btn-text Starred
+ //-
+ XXX Normally we would disable this field for sandstorm, but we keep it
+ until sandstorm implements sharing capabilities
+
+ a.board-header-btn.perms-btn.js-change-vis(class="{{#unless currentUser.isBoardAdmin}}no-edit{{/ unless}}" id="permission-level")
+ span.board-header-btn-icon.icon-sm.fa(class="{{#if isPublic}}fa-globe{{else}}fa-lock{{/if}}")
+ span.board-header-btn-text {{_ permission}}
+
+ a.board-header-btn.js-search
+ span.board-header-btn-icon.icon-sm.fa.fa-tag
+ span.board-header-btn-text Labels
+
+ //- XXX Clicking here should open a search field
+ a.board-header-btn.js-search
+ span.board-header-btn-icon.icon-sm.fa.fa-search
+ span.board-header-btn-text {{_ 'search'}}
+
+ //- +boardMembersHeader
+
+template(name="boardMembersHeader")
+ .board-header-members
+ each currentBoard.members
+ +userAvatar(userId=userId draggable=true showBadges=true)
+ unless isSandstorm
+ if currentUser.isBoardAdmin
+ a.member.add-board-member.js-open-manage-board-members
+ i.fa.fa-plus
+
+template(name="boardMenuPopup")
+ ul.pop-over-list
+ li: a.js-rename-board {{_ 'rename-board'}}
+ li: a.js-change-board-color Change color
+ li: a Copy this board
+ li: a Rules
+
+template(name="boardChangeTitlePopup")
+ form#ChangeBoardTitleForm
+ label {{_ 'name'}}
+ input.js-board-name(type="text" value="{{ title }}" autofocus)
+ input.primary.wide.js-rename-board(type="submit" value="{{_ 'rename'}}")
+
+template(name="boardChangePermissionPopup")
+ ul.pop-over-list
+ li
+ a.js-select.light-hover(name="private")
+ span.icon-sm.fa.fa-lock.vis-icon
+ | {{_ 'private'}}
+ if check 'private'
+ span.icon-sm.fa.fa-check
+ span.sub-name {{_ 'private-desc'}}
+ li
+ a.js-select.light-hover(name="public")
+ span.icon-sm.fa.fa-globe.vis-icon
+ | {{_ 'public'}}
+ if check 'public'
+ span.icon-sm.fa.fa-check
+ span.sub-name {{_ 'public-desc'}}
+
+template(name="boardChangeColorPopup")
+ .board-backgrounds-list.clearfix
+ each backgroundColors
+ .board-background-select.js-select-background
+ span.background-box(class="board-color-{{this}}")
+ if isSelected
+ i.fa.fa-check
+
+template(name="createBoardPopup")
+ .content.clearfix
+ form#CreateBoardForm
+ label(for="boardNewTitle") {{_ 'title'}}
+ input#boardNewTitle.non-empty(type="text" name="name" placeholder="{{_ 'bucket-example'}}" autofocus)
+ p.quiet
+ span.icon-sm.fa.fa-globe
+ | {{{_ 'board-public-info'}}}
+ input.primary.wide(type="submit" value="{{_ 'create'}}")
diff --git a/client/components/boards/header.js b/client/components/boards/header.js
new file mode 100644
index 00000000..7d02df48
--- /dev/null
+++ b/client/components/boards/header.js
@@ -0,0 +1,7 @@
+Template.headerBoard.helpers({
+ isStarred: function() {
+ var boardId = Session.get('currentBoard');
+ var user = Meteor.user();
+ return boardId && user && user.hasStarred(boardId);
+ }
+});
diff --git a/client/components/boards/header.styl b/client/components/boards/header.styl
new file mode 100644
index 00000000..44c38a4b
--- /dev/null
+++ b/client/components/boards/header.styl
@@ -0,0 +1,137 @@
+@import 'nib'
+
+.board-header {
+ height: auto;
+ overflow: hidden;
+ padding: 10px 30px 10px 8px;
+ position: relative;
+ transition: padding .15s ease-in;
+}
+
+.board-header-btns {
+ position: relative;
+ display: block;
+}
+
+.board-header-btn {
+ border-radius: 3px;
+ color: #f6f6f6;
+ cursor: default;
+ float: left;
+ font-size: 12px;
+ height: 30px;
+ line-height: 32px;
+ margin: 2px 4px 0 0;
+ overflow: hidden;
+ padding-left: 30px;
+ position: relative;
+ text-decoration: none;
+}
+
+.board-header-btn:empty {
+ display: none;
+}
+
+.board-header-btn-without-icon {
+ padding-left: 8px;
+}
+
+.board-header-btn-icon {
+ background-clip: content-box;
+ background-origin: content-box;
+ color: #f6f6f6 !important;
+ padding: 6px;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.board-header-btn-text {
+ padding-right: 8px;
+}
+
+.board-header-btn:not(.no-edit) .text {
+ text-decoration: underline;
+}
+
+.board-header-btn:not(.no-edit):hover {
+ background: rgba(0, 0, 0, .12);
+ cursor: pointer;
+}
+
+.board-header-btn:hover {
+ color: #f6f6f6;
+}
+
+.board-header-btn.board-header-btn-enabled {
+ background-color: rgba(0, 0, 0, .1);
+
+ &:hover {
+ background-color: rgba(0, 0, 0, .3);
+ }
+
+ .board-header-btn-icon.icon-star {
+ color: #e6bf00 !important;
+ }
+}
+
+.board-header-btn-name {
+ cursor: default;
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 30px;
+ padding-left: 4px;
+ text-decoration: none;
+
+ .board-header-btn-text {
+ padding-left: 6px;
+ }
+}
+
+.board-header-btn-name-org-logo {
+ border-radius: 3px;
+ height: 30px;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 30px;
+
+ .board-header-btn-text {
+ padding-left: 32px;
+ }
+}
+
+.board-header-btn-org-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 400px;
+}
+
+.board-header-btn-filter-indicator {
+ background: #3d990f;
+ padding-right: 30px;
+ color: #fff;
+ text-shadow: 0;
+
+ &:hover {
+ background: #43a711 !important;
+ }
+
+ .board-header-btn-icon-close {
+ background: #43a711;
+ border-top-left-radius: 0;
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 0;
+ color: #fff;
+ padding: 6px;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ &:hover {
+ background: #48b512;
+ }
+ }
+}
diff --git a/client/components/boards/helpers.js b/client/components/boards/helpers.js
new file mode 100644
index 00000000..05be987d
--- /dev/null
+++ b/client/components/boards/helpers.js
@@ -0,0 +1,45 @@
+Template.boards.helpers({
+ boards: function() {
+ return Boards.find({}, {
+ sort: ['title']
+ });
+ },
+
+ starredBoards: function() {
+ var cursor = Boards.find({
+ _id: { $in: Meteor.user().profile.starredBoards || [] }
+ }, {
+ sort: ['title']
+ });
+ return cursor.count() === 0 ? null : cursor;
+ },
+
+ isStarred: function() {
+ var user = Meteor.user();
+ return user && user.hasStarred(this._id);
+ }
+});
+
+Template.boardChangePermissionPopup.helpers({
+ check: function(perm) {
+ return this.permission === perm;
+ }
+});
+
+Template.boardChangeColorPopup.helpers({
+ backgroundColors: function() {
+ return Boards.simpleSchema()._schema.color.allowedValues;
+ },
+
+ isSelected: function() {
+ var currentBoard = Boards.findOne(Session.get('currentBoard'));
+ return currentBoard.color === this.toString();
+ }
+});
+
+Blaze.registerHelper('currentBoard', function() {
+ var boardId = Session.get('currentBoard');
+ if (boardId) {
+ return Boards.findOne(boardId);
+ }
+});
diff --git a/client/components/boards/list.jade b/client/components/boards/list.jade
new file mode 100644
index 00000000..3a8fecd2
--- /dev/null
+++ b/client/components/boards/list.jade
@@ -0,0 +1,14 @@
+template(name="boards")
+ if boards
+ ul.board-list.clearfix
+ each boards
+ li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
+ a.js-open-board(href="{{ pathFor route='Board' boardId=_id }}")
+ span.details
+ span.board-list-item-name= title
+ i.fa.fa-star-o.js-star-board(
+ class="{{#if isStarred}}is-star-active{{/if}}"
+ title="{{_ 'star-board-title'}}")
+ else
+ p.quiet {{_ 'no-boards'}}
+ button.js-add-board {{_ 'add-board'}}
diff --git a/client/components/boards/list.styl b/client/components/boards/list.styl
new file mode 100644
index 00000000..c068dbb0
--- /dev/null
+++ b/client/components/boards/list.styl
@@ -0,0 +1,85 @@
+.board-list
+ margin: 25px auto
+ width: 1200px
+
+ li
+ float: left
+ width: 25%
+ box-sizing: border-box
+ position: relative
+
+ &.starred .fa-star-o
+ opacity: 1
+
+ a
+ background-color: #999
+ color: #f6f6f6
+ height: 90px
+ font-size: 16px
+ line-height: 22px
+ border-radius: 3px
+ display: block
+ font-weight: 700
+ min-height: 18px
+ padding: 8px 12px 8px 12px
+ margin: 0 16px 16px 0
+ position: relative
+ text-decoration: none
+
+ &.tile
+ background-size: auto
+ background-repeat: repeat
+
+ .details
+ height: 84px
+ padding-right: 36px
+ bottom: 0
+ left: 0
+ overflow: hidden
+ padding: 9px 12px
+ position: absolute
+ right: 0
+ top: 0
+
+ .board-list-item-sub-name
+ color: rgba(255, 255, 255, .5)
+ display: block
+ font-size: 14px
+ font-weight: 400
+ line-height: 22px
+
+ .fa-star-o
+ bottom: 0
+ font-size: 14px
+ height: 18px
+ line-height: 18px
+ opacity: 0
+ padding: 9px 9px
+ position: absolute
+ right: 0
+ top: 0
+ transition-duration: .15s
+ transition-property: color, font-size, background
+
+ .is-star-active
+ color: #e6bf00
+
+ li:hover a
+ color: #f6f6f6
+
+ .fa-star-o
+ color: #fff
+ opacity: .75
+
+ &:hover
+ font-size: 18px
+ opacity: 1
+
+ &.is-star-active
+ color: #e6bf00
+ opacity: 1
+
+ &:hover
+ color: #ffd91a
+ font-size: 16px
+ opacity: 1
diff --git a/client/components/boards/router.js b/client/components/boards/router.js
new file mode 100644
index 00000000..6845b7f2
--- /dev/null
+++ b/client/components/boards/router.js
@@ -0,0 +1,34 @@
+Meteor.subscribe('boards');
+
+BoardSubsManager = new SubsManager();
+
+Router.route('/boards', {
+ name: 'Boards',
+ template: 'boards',
+ authenticated: true,
+ onBeforeAction: function() {
+ Session.set('currentBoard', '');
+ Filter.reset();
+ this.next();
+ }
+});
+
+Router.route('/boards/:_id/:slug', {
+ name: 'Board',
+ template: 'board',
+ onAfterAction: function() {
+ Session.set('sidebarIsOpen', true);
+ Session.set('currentWidget', 'home');
+ Session.set('menuWidgetIsOpen', false);
+ },
+ waitOn: function() {
+ var params = this.params;
+ Session.set('currentBoard', params._id);
+ Session.set('currentCard', null);
+
+ return BoardSubsManager.subscribe('board', params._id, params.slug);
+ },
+ data: function() {
+ return Boards.findOne(this.params._id);
+ }
+});
diff --git a/client/components/cards/details.jade b/client/components/cards/details.jade
new file mode 100644
index 00000000..0de59297
--- /dev/null
+++ b/client/components/cards/details.jade
@@ -0,0 +1,47 @@
+template(name="cardSidebar")
+ .card-sidebar.sidebar
+ .card-detail.sidebar-content.js-card-sidebar-content
+ if cover
+ .card-detail-cover(style="background-image: url({{ card.cover.url }})")
+ .card-detail-header(class="{{#if currentUser.isBoardMember}}editable{{/if}}")
+ a.js-close-card-detail
+ i.fa.fa-times
+ h2.card-detail-title.js-card-title= title
+ p.card-detail-list.js-move-card
+ | {{_ 'in-list'}}
+ a.card-detail-list-title(
+ class="{{#if currentUser.isBoardMember}}js-open-move-from-header is-editable{{/if}}")
+ = list.title
+ hr
+ //- if card.members
+ .card-detail-item.card-detail-item-members.clearfix.js-card-detail-members
+ h3.card-detail-item-header {{_ 'members'}}
+ .js-card-detail-members-list.clearfix
+ each members
+ +userAvatar(userId=this size="small" cardId=../_id)
+ a.card-detail-item-add-button.dark-hover.js-details-edit-members
+ i.fa.fa-plus
+ //- We should use "editable" to avoide repetiting ourselves
+ .clearfix
+ if currentUser.isBoardMember
+ h3 Description
+ +inlinedForm(classNames="js-card-description")
+ i.fa.fa-times.js-close-inlined-form
+ textarea(autofocus)= description
+ button(type="submit") {{_ 'edit'}}
+ else
+ .js-open-inlined-form
+ a {{_ 'edit'}}
+ +viewer
+ = description
+ else if description
+ h3 Description
+ +viewer
+ = description
+ hr
+ if attachments.count
+ +WindowAttachmentsModule(card=this)
+ +WindowActivityModule(card=this)
+
+template(name="moveCardPopup")
+ +boardLists
diff --git a/client/components/cards/details.js b/client/components/cards/details.js
new file mode 100644
index 00000000..a4fe89a3
--- /dev/null
+++ b/client/components/cards/details.js
@@ -0,0 +1,103 @@
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'cardSidebar';
+ },
+
+ mixins: function() {
+ return [Mixins.InfiniteScrolling];
+ },
+
+ calculateNextPeak: function() {
+ var altitude = this.find('.js-card-sidebar-content').scrollHeight;
+ this.callFirstWith(this, 'setNextPeak', altitude);
+ },
+
+ reachNextPeak: function() {
+ var activitiesComponent = this.componentChildren('activities')[0];
+ activitiesComponent.loadNextPage();
+ },
+
+ events: function() {
+ return [{
+ 'click .js-move-card': Popup.open('moveCard'),
+ 'submit .js-card-description': function(evt) {
+ evt.preventDefault();
+ var cardId = Session.get('currentCard');
+ var form = this.componentChildren('inlinedForm')[0];
+ var newDescription = form.getValue();
+ Cards.update(cardId, {
+ $set: {
+ description: newDescription
+ }
+ });
+ form.close();
+ },
+ 'click .js-close-card-detail': function() {
+ Utils.goBoardId(Session.get('currentBoard'));
+ },
+ 'click .editable .js-card-title': function(event, t) {
+ var editable = t.$('.card-detail-title');
+
+ // add class editing and focus
+ $('.editing').removeClass('editing');
+ editable.addClass('editing');
+ editable.find('#title').focus();
+ },
+ 'click .js-edit-desc': function(event, t) {
+ var editable = t.$('.card-detail-item');
+
+ // editing remove based and add current editing.
+ $('.editing').removeClass('editing');
+ editable.addClass('editing');
+ editable.find('#desc').focus();
+
+ event.preventDefault();
+ },
+ 'click .js-cancel-edit': function(event, t) {
+ // remove editing hide.
+ $('.editing').removeClass('editing');
+ },
+ 'submit #WindowTitleEdit': function(event, t) {
+ var title = t.find('#title').value;
+ if ($.trim(title)) {
+ Cards.update(this.card._id, {
+ $set: {
+ title: title
+ }
+ }, function (err, res) {
+ if (!err) $('.editing').removeClass('editing');
+ });
+ }
+
+ event.preventDefault();
+ },
+ 'submit #WindowDescEdit': function(event, t) {
+ Cards.update(this.card._id, {
+ $set: {
+ description: t.find('#desc').value
+ }
+ }, function(err) {
+ if (!err) $('.editing').removeClass('editing');
+ });
+ event.preventDefault();
+ },
+ 'click .member': Popup.open('cardMember'),
+ 'click .js-details-edit-members': Popup.open('cardMembers'),
+ 'click .js-details-edit-labels': Popup.open('cardLabels')
+ }];
+ }
+}).register('cardSidebar');
+
+Template.moveCardPopup.events({
+ 'click .js-select-list': function() {
+ // XXX We should *not* get the currentCard from the global state, but
+ // instead from a “component” state.
+ var cardId = Session.get('currentCard');
+ var newListId = this._id;
+ Cards.update(cardId, {
+ $set: {
+ listId: newListId
+ }
+ });
+ }
+});
diff --git a/client/components/cards/details.styl b/client/components/cards/details.styl
new file mode 100644
index 00000000..faf15d79
--- /dev/null
+++ b/client/components/cards/details.styl
@@ -0,0 +1,161 @@
+@import 'nib'
+
+.card-detail.sidebar-content
+ width: 496px - 2 * 20px
+ top: -46px !important
+ z-index: 20 !important
+ // XXX Animate apparition
+
+ .card-detail-header
+ background: #F7F7F7
+ border-bottom: 1px solid darken(white, 10%)
+ position: absolute
+ min-height: 38px
+ top: 0
+ left: 0
+ right: 0
+ padding 7px 20px 0
+
+ i.fa
+ float: right
+ font-size: 1.3em
+ color: darken(white, 35%)
+ margin-top: 7px
+
+ .card-detail-title
+ font-weight: bold
+ font-size: 1.7em
+ margin: 3px 0 0
+ padding: 0
+
+ .card-detail-list
+ font-size: 0.85em
+ margin-bottom: 3px
+
+ a.card-detail-list-title
+ font-weight: bold
+
+ &.is-editable
+ display: inline-block
+ background: darken(white, 10%)
+ border-radius: 3px
+ padding: 0px 5px
+
+.new-comment
+ position: relative
+ margin: 0 0 20px 38px
+
+ .member
+ opacity: .7
+ position: absolute
+ top: 1px
+ left: -38px
+
+ .helper
+ bottom: 0
+ display: none
+ position: absolute
+ right: 9px
+
+ &.focus
+
+ .member
+ opacity: 1
+
+ .helper
+ display: inline-block
+
+ .new-comment-input
+ min-height: 108px
+ color: #4d4d4d
+ cursor: auto
+ overflow: hidden
+ word-wrap: break-word
+
+ .too-long
+ margin-top: 8px
+
+.new-comment-input
+ background-color: #fff
+ border: 0
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
+ color: #8c8c8c
+ height: 36px
+ margin: 4px 4px 6px 0
+ padding: 9px 11px
+ width: 100%
+
+ &:hover,
+ &:focus
+ background-color: #fff
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .33)
+ border: 0
+ cursor: pointer
+
+ &:focus
+ cursor: auto
+
+.list-voters.compact .voter
+ position: relative
+ min-height: 36px
+
+ .member
+ left: 0
+ position: absolute
+ top: 0
+
+ .title
+ display: block
+ line-height: 30px
+ left: 0
+ overflow: hidden
+ padding-left: 38px
+ position: absolute
+ text-overflow: ellipsis
+ top: 0
+ white-space: nowrap
+ width: 230px
+
+.list-voters .title
+ display: none
+
+.card-composer
+ padding-bottom: 8px
+
+.cc-controls
+ margin-top: 1px
+
+ input[type="submit"]
+ float: left
+ margin-top: 0
+ padding: 5px 18px
+
+ .icon-lg
+ float: left
+
+ .cc-opt
+ float: right
+
+.minicard-placeholder,
+.minicard.placeholder
+ background: silver
+ border: none
+ min-height: 18px
+
+ .hook
+ height: 18px
+ position: absolute
+ right: 0
+ top: 0
+ width: 18px
+
+input[type="text"].attachment-add-link-input
+ float: left
+ margin: 0 0 8px
+ width: 80%
+
+input[type="submit"].attachment-add-link-submit
+ float: left
+ margin: 0 0 8px 4px
+ padding: 6px 12px
+ width: 18%
diff --git a/client/components/cards/events.js b/client/components/cards/events.js
new file mode 100644
index 00000000..9c270e8d
--- /dev/null
+++ b/client/components/cards/events.js
@@ -0,0 +1,285 @@
+// Template.cards.events({
+// // 'click .js-cancel': function(event, t) {
+// // var composer = t.$('.card-composer');
+
+// // // Keep the old value in memory to display it again next time
+// // var inputCacheKey = "addCard-" + this.listId;
+// // var oldValue = composer.find('.js-card-title').val();
+// // InputsCache.set(inputCacheKey, oldValue);
+
+// // // add composer hide class
+// // composer.addClass('hide');
+// // composer.find('.js-card-title').val('');
+
+// // // remove hide open link class
+// // $('.js-open-card-composer').removeClass('hide');
+// // },
+// 'submit': function(evt, tpl) {
+// evt.preventDefault();
+// var textarea = $(evt.currentTarget).find('textarea');
+// var title = textarea.val();
+// var lastCard = tpl.find('.js-minicard:last-child');
+// var sort;
+// if (lastCard === null) {
+// sort = 0;
+// } else {
+// sort = Blaze.getData(lastCard).sort + 1;
+// }
+// // debugger
+
+// // Clear the form in-memory cache
+// // var inputCacheKey = "addCard-" + this.listId;
+// // InputsCache.set(inputCacheKey, '');
+
+// // title trim if not empty then
+// if ($.trim(title)) {
+// Cards.insert({
+// title: title,
+// listId: Template.currentData().listId,
+// boardId: Template.currentData().board._id,
+// sort: sort
+// }, function(err, _id) {
+// // In case the filter is active we need to add the newly
+// // inserted card in the list of exceptions -- cards that are
+// // not filtered. Otherwise the card will disappear instantly.
+// // See https://github.com/libreboard/libreboard/issues/80
+// Filter.addException(_id);
+// });
+
+// // empty and focus.
+// textarea.val('').focus();
+
+// // focus complete then scroll top
+// Utils.Scroll(tpl.find('.js-minicards')).top(1000, true);
+// }
+// }
+// });
+
+// Template.cards.events({
+// 'click .member': Popup.open('cardMember')
+// });
+
+Template.cardMemberPopup.events({
+ 'click .js-remove-member': function() {
+ Cards.update(this.cardId, {$pull: {members: this.userId}});
+ Popup.close();
+ }
+});
+
+Template.WindowActivityModule.events({
+ 'click .js-new-comment:not(.focus)': function(evt) {
+ var $this = $(evt.currentTarget);
+ $this.addClass('focus');
+ },
+ 'submit #CommentForm': function(evt, t) {
+ var text = t.$('.js-new-comment-input');
+ if ($.trim(text.val())) {
+ CardComments.insert({
+ boardId: this.card.boardId,
+ cardId: this.card._id,
+ text: text.val()
+ });
+ text.val('');
+ $('.focus').removeClass('focus');
+ }
+ evt.preventDefault();
+ }
+});
+
+Template.WindowSidebarModule.events({
+ 'click .js-change-card-members': Popup.open('cardMembers'),
+ 'click .js-edit-labels': Popup.open('cardLabels'),
+ 'click .js-archive-card': function(evt) {
+ // Update
+ Cards.update(this.card._id, {
+ $set: {
+ archived: true
+ }
+ });
+ evt.preventDefault();
+ },
+ 'click .js-unarchive-card': function(evt) {
+ Cards.update(this.card._id, {
+ $set: {
+ archived: false
+ }
+ });
+ evt.preventDefault();
+ },
+ 'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
+ Cards.remove(this.card._id);
+
+ // redirect board
+ Utils.goBoardId(this.card.board()._id);
+ Popup.close();
+ }),
+ 'click .js-more-menu': Popup.open('cardMore'),
+ 'click .js-attach': Popup.open('cardAttachments')
+});
+
+Template.WindowAttachmentsModule.events({
+ 'click .js-attach': Popup.open('cardAttachments'),
+ 'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
+ function() {
+ Attachments.remove(this._id);
+ Popup.close();
+ }
+ ),
+ // If we let this event bubble, Iron-Router will handle it and empty the
+ // page content, see #101.
+ 'click .js-open-viewer, click .js-download': function(event) {
+ event.stopPropagation();
+ },
+ 'click .js-add-cover': function() {
+ Cards.update(this.cardId, { $set: { coverId: this._id } });
+ },
+ 'click .js-remove-cover': function() {
+ Cards.update(this.cardId, { $unset: { coverId: '' } });
+ }
+});
+
+Template.cardMembersPopup.events({
+ 'click .js-select-member': function(evt) {
+ var cardId = Template.parentData(2).data._id;
+ var memberId = this.userId;
+ var operation;
+ if (Cards.find({ _id: cardId, members: memberId}).count() === 0)
+ operation = '$addToSet';
+ else
+ operation = '$pull';
+
+ var query = {};
+ query[operation] = {
+ members: memberId
+ };
+ Cards.update(cardId, query);
+ evt.preventDefault();
+ }
+});
+
+Template.cardLabelsPopup.events({
+ 'click .js-select-label': function(evt) {
+ var cardId = Template.parentData(2).data._id;
+ var labelId = this._id;
+ var operation;
+ if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0)
+ operation = '$addToSet';
+ else
+ operation = '$pull';
+
+ var query = {};
+ query[operation] = {
+ labelIds: labelId
+ };
+ Cards.update(cardId, query);
+ evt.preventDefault();
+ },
+ 'click .js-edit-label': Popup.open('editLabel'),
+ 'click .js-add-label': Popup.open('createLabel')
+});
+
+Template.formLabel.events({
+ 'click .js-palette-color': function(evt) {
+ var $this = $(evt.currentTarget);
+
+ // hide selected ll colors
+ $('.js-palette-select').addClass('hide');
+
+ // show select color
+ $this.find('.js-palette-select').removeClass('hide');
+ }
+});
+
+Template.createLabelPopup.events({
+ // Create the new label
+ 'submit .create-label': function(evt, tpl) {
+ var name = tpl.$('#labelName').val().trim();
+ var boardId = Session.get('currentBoard');
+ var selectLabelDom = tpl.$('.js-palette-select:not(.hide)').get(0);
+ var selectLabel = Blaze.getData(selectLabelDom);
+ Boards.update(boardId, {
+ $push: {
+ labels: {
+ _id: Random.id(6),
+ name: name,
+ color: selectLabel.color
+ }
+ }
+ });
+ Popup.back();
+ evt.preventDefault();
+ }
+});
+
+Template.editLabelPopup.events({
+ 'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() {
+ var boardId = Session.get('currentBoard');
+ Boards.update(boardId, {
+ $pull: {
+ labels: {
+ _id: this._id
+ }
+ }
+ });
+ Popup.back(2);
+ }),
+ 'submit .edit-label': function(evt, tpl) {
+ var name = tpl.$('#labelName').val().trim();
+ var boardId = Session.get('currentBoard');
+ var getLabel = Utils.getLabelIndex(boardId, this._id);
+ var selectLabelDom = tpl.$('.js-palette-select:not(.hide)').get(0);
+ var selectLabel = Blaze.getData(selectLabelDom);
+ var $set = {};
+
+ // set label index
+ $set[getLabel.key('name')] = name;
+
+ // set color
+ $set[getLabel.key('color')] = selectLabel.color;
+
+ // update
+ Boards.update(boardId, { $set: $set });
+
+ // return to the previous popup view trigger
+ Popup.back();
+
+ evt.preventDefault();
+ },
+ 'click .js-select-label': function() {
+ Cards.remove(this.cardId);
+
+ // redirect board
+ Utils.goBoardId(this.boardId);
+ }
+});
+
+Template.cardMorePopup.events({
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function() {
+ Cards.remove(this.card._id);
+
+ // redirect board
+ Utils.goBoardId(this.card.board()._id);
+ })
+});
+
+Template.cardAttachmentsPopup.events({
+ 'change .js-attach-file': function(evt) {
+ var card = this.card;
+ FS.Utility.eachFile(evt, function(f) {
+ var file = new FS.File(f);
+
+ // set Ids
+ file.boardId = card.boardId;
+ file.cardId = card._id;
+
+ // upload file
+ Attachments.insert(file);
+
+ Popup.close();
+ });
+ },
+ 'click .js-computer-upload': function(evt, t) {
+ t.find('.js-attach-file').click();
+ evt.preventDefault();
+ }
+});
diff --git a/client/components/cards/helpers.js b/client/components/cards/helpers.js
new file mode 100644
index 00000000..708b1b56
--- /dev/null
+++ b/client/components/cards/helpers.js
@@ -0,0 +1,48 @@
+Template.cardMembersPopup.helpers({
+ isCardMember: function() {
+ var cardId = Template.parentData()._id;
+ var cardMembers = Cards.findOne(cardId).members || [];
+ return _.contains(cardMembers, this.userId);
+ },
+ user: function() {
+ return Users.findOne(this.userId);
+ }
+});
+
+Template.cardLabelsPopup.helpers({
+ isLabelSelected: function(cardId) {
+ return _.contains(Cards.findOne(cardId).labelIds, this._id);
+ }
+});
+
+var labelColors;
+Meteor.startup(function() {
+ labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
+});
+
+Template.createLabelPopup.helpers({
+ // This is the default color for a new label. We search the first color that
+ // is not already used in the board (although it's not a problem if two
+ // labels have the same color).
+ defaultColor: function() {
+ var labels = this.labels || this.card.board().labels;
+ var usedColors = _.pluck(labels, 'color');
+ var availableColors = _.difference(labelColors, usedColors);
+ return availableColors.length > 1 ? availableColors[0] : 'green';
+ }
+});
+
+Template.formLabel.helpers({
+ labels: function() {
+ return _.map(labelColors, function(color) {
+ return { color: color, name: '' };
+ });
+ }
+});
+
+Blaze.registerHelper('currentCard', function() {
+ var cardId = Session.get('currentCard');
+ if (cardId) {
+ return Cards.findOne(cardId);
+ }
+});
diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl
new file mode 100644
index 00000000..27058b21
--- /dev/null
+++ b/client/components/cards/labels.styl
@@ -0,0 +1,183 @@
+@import 'nib'
+
+// XXX Use .board-widget-labels as a flexbox container
+.card-label
+ background-color: #b3b3b3
+ border-radius: 4px
+ color: white
+ display: inline-block
+ font-weight: 700
+ font-size: 13px
+ margin-right: 4px
+ padding: 3px 8px
+ position:relative
+ max-width: 100%
+ min-width: 8px
+ overflow: ellipsis
+ height: 18px
+
+ &:hover
+ color: white
+
+.card-label-green
+ background-color: #3cb500
+
+.card-label-yellow
+ background-color: #fad900
+
+.card-label-orange
+ background-color: #ff9f19
+
+.card-label-red
+ background-color: #eb4646
+
+.card-label-purple
+ background-color: #a632db
+
+.card-label-blue
+ background-color: #0079bf
+
+.card-label-pink
+ background-color: #ff78cb
+
+.card-label-sky
+ background-color: #00c2e0
+
+.card-label-black
+ background-color: #4d4d4d
+
+.card-label-lime
+ background-color: #51e898
+
+.edit-label,
+.create-label
+ .card-label
+ float: left
+ height: 25px
+ margin: 0px 3% 7px 0px
+ width: 10.5%
+ cursor: pointer
+
+.edit-labels
+ input[type="text"]
+ margin: 4px 0 6px 38px
+ width: 243px
+
+ .card-label
+ height: 30px
+ left: 0
+ padding: 1px 5px
+ position: absolute
+ top: 0
+ width: 24px
+
+ .labels-static .card-label
+ line-height: 30px
+ margin-bottom: 4px
+ position: relative
+ top: auto
+ left: 0
+ width: 260px
+
+.minicard-labels
+ position: relative
+ z-index: 30
+ top: -6px
+
+ .card-label
+ border-radius: 0
+ float: left
+ height: 4px
+ margin-bottom: 1px
+ padding: 0
+ width: 40px
+ line-height: 100px
+
+.card-detail-item-labels .card-label
+ border-radius: 3px
+ display: block
+ float: left
+ height: 20px
+ line-height: 20px
+ margin: 0 4px 4px 0
+ min-width: 30px
+ padding: 5px 10px
+ width: auto
+
+.editable-labels .card-label:hover
+ cursor: pointer
+ opacity: .75
+
+.edit-labels-pop-over
+ margin-bottom: 8px
+
+.edit-labels-pop-over .shortcut
+ display: inline-block
+
+.card-label-selectable
+ border-radius: 3px
+ cursor: pointer
+ margin: 0 50px 4px 0
+ min-height: 18px
+ padding: 8px
+ position: relative
+ transition: margin-right .1s
+
+ .card-label-selectable-icon
+ position: absolute
+ top: 8px
+ right: -20px
+
+ &.active:hover,
+ &.active,
+ &.active.selected:hover,
+ &.active.selected
+ margin-right: 38px
+ padding-right: 32px
+
+ .card-label-selectable-icon
+ right: 6px
+
+ &.active:hover:hover,
+ &.active:hover,
+ &.active.selected:hover:hover,
+ &.active.selected:hover
+ margin-right: 38px
+
+ &.selected,
+ &:hover
+ margin-right: 38px
+ opacity: .8
+
+.active .card-label-selectable
+ &,
+ &:hover
+ margin-right: 0
+
+ .card-label-selectable-icon
+ right: 8px
+
+.card-label-edit-button
+ border-radius: 3px
+ float: right
+ padding: 8px
+
+ &:hover
+ background: #dbdbdb
+
+.card-label-color-select-icon
+ left: 14px
+ position: absolute
+ top: 9px
+
+.phenom .card-label
+ display: inline-block
+ font-size: 12px
+ height: 14px
+ line-height: 13px
+ padding: 0 4px
+ min-width: 16px
+ overflow: ellipsis
+
+.board-widget .phenom .card-label
+ max-width: 130px
diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl
new file mode 100644
index 00000000..a78cd46f
--- /dev/null
+++ b/client/components/cards/minicard.styl
@@ -0,0 +1,136 @@
+.minicard
+ background-color: #fff
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
+ border-radius: 2px
+ cursor: pointer
+ margin-bottom: 9px
+ max-width: 300px
+ min-height: 20px
+ position: relative
+ z-index: 0
+ overflow: hidden
+
+ a
+ color: #4d4d4d
+
+ &.active-card
+ background-color: #f0f0f0
+ border-bottom-color: #c2c2c2
+
+ .minicard-operation
+ display: block
+
+ &.draggable-hover-card
+ background-color: #f0f0f0
+ border-bottom-color: #c2c2c2
+
+ .minicard-cover
+ background-position: center
+ background-repeat: no-repeat
+ background-size: cover
+ height: 145px
+ user-select: none
+ margin: -6px -8px 6px -8px
+ border-radius: top 2px
+
+ &.no-preview-size
+ background-size: auto
+ background-position: center
+
+ .minicard-details
+ padding: 6px 8px 2px
+ position: relative
+ z-index: 10
+
+
+ &.is-selected
+ .minicard-details
+ padding-bottom: 0
+
+ a.minicard-details
+ text-decoration:none
+
+ .minicard-details-overlay
+ background: transparent
+ bottom: 0
+ left: 0
+ position: absolute
+ right: 0
+ top: 0
+
+ .minicard-dropzone
+ display: none
+
+ .minicard.drophover .minicard-dropzone
+ background: rgba(255, 255, 255, .8)
+ // border-radius: 3px
+ // bottom: 0
+ // display: block
+ // font-weight: 700
+ // line-height: 100%
+ // left: 0
+ // margin: 0
+ // opacity: 1
+ // padding: 0
+ // position: absolute
+ // right: 0
+ // text-align: center
+ // top: 0
+ // z-index: 40
+
+ .minicard-title
+ display: block
+ font-weight: 400
+ margin: 0 0 4px
+ overflow: hidden
+ text-decoration: none
+ word-wrap: break-word
+
+ &::selection
+ background: transparent
+
+ .minicard-labels
+ padding-top: 3px
+ margin-top: 4px
+ float: right
+
+ .minicard-label
+ float: right
+ width: 8px
+ height: @width
+ border-radius: 2px
+ margin-left: 4px
+
+ .minicard-members
+ float: right
+ margin: 2px -8px -2px 0
+
+ .member
+ float: right
+ border-radius: 50%
+ height: 28px
+ width: @height
+
+ + .badges
+ margin-top: 10px
+
+ .minicard-members:empty
+ display: none
+
+.badges
+ float: left
+
+ &:empty
+ display: none
+
+textarea.minicard-composer-textarea,
+textarea.minicard-composer-textarea:focus
+ background: none
+ border: none
+ box-shadow: none
+ height: auto
+ margin-bottom: 4px
+ padding: 0
+ max-height: 162px
+ min-height: 54px
+ overflow-y: auto
diff --git a/client/components/cards/popups.jade b/client/components/cards/popups.jade
new file mode 100644
index 00000000..0b5aa4c0
--- /dev/null
+++ b/client/components/cards/popups.jade
@@ -0,0 +1,12 @@
+template(name="cardMembersPopup")
+ //- input.js-search-mem(autofocus placeholder="Search members…" type="text")
+ ul.pop-over-member-list.checkable.js-mem-list
+ each board.members
+ li.item.js-member-item(class="{{#if isCardMember}}active{{/if}}")
+ a.name.js-select-member(href="#")
+ +userAvatar(user=user size="small")
+ span.full-name
+ = user.profile.name
+ | (<span class="username">{{ user.username }}</span>)
+ if isCardMember
+ i.fa.fa-check
diff --git a/client/components/cards/router.js b/client/components/cards/router.js
new file mode 100644
index 00000000..48bb9a95
--- /dev/null
+++ b/client/components/cards/router.js
@@ -0,0 +1,15 @@
+Router.route('/boards/:boardId/:slug/:cardId', {
+ name: 'Card',
+ template: 'board',
+ waitOn: function() {
+ var params = this.params;
+ // XXX We probably shouldn't rely on Session
+ Session.set('currentBoard', params.boardId);
+ Session.set('currentCard', params.cardId);
+
+ return BoardSubsManager.subscribe('board', params.boardId, params.slug);
+ },
+ data: function() {
+ return Boards.findOne(this.params.boardId);
+ }
+});
diff --git a/client/components/cards/templates.html b/client/components/cards/templates.html
new file mode 100644
index 00000000..4c65e429
--- /dev/null
+++ b/client/components/cards/templates.html
@@ -0,0 +1,336 @@
+<template name="cardModal">
+ {{ > modal template='cardDetailWindow' card=this board=this.board }}
+</template>
+
+<template name="cardMemberPopup">
+ <div class="board-member-menu">
+ <div class="mini-profile-info">
+ {{> userAvatar user=user }}
+ <div class="info">
+ <h3 class="bottom" style="margin-right: 40px;">
+ <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a>
+ </h3>
+ <p class="quiet bottom">@{{ user.username }}</p>
+ </div>
+ </div>
+ {{# if currentUser.isBoardMember }}
+ <ul class="pop-over-list">
+ <li><a class="js-remove-member">{{_ 'remove-member-from-card'}}</a></li>
+ </ul>
+ {{/ if }}
+ </div>
+</template>
+
+<template name="cardMorePopup">
+ <p class="quiet bottom">
+ <span class="clearfix">
+ <span>{{_ 'link-card'}}</span>
+ <span class="icon-sm fa {{#if card.board.isPublic}}fa-globe{{else}}fa-lock{{/if}}"></span>
+ <input class="js-url js-autoselect inline-input" type="text" readonly="readonly" value="{{ card.rootUrl }}">
+ </span>
+ {{_ 'added'}} <span class="date" title="{{ card.createdAt }}">{{ moment card.createdAt 'LLL' }}</span> -
+ <a class="js-delete" href="#" title="{{_ 'card-delete-notice'}}">{{_ 'delete'}}</a>
+ </p>
+</template>
+
+<template name="cardLabelsPopup">
+ <div>
+ {{! <input id="labelSearch" name="search" class="js-autofocus js-label-search" placeholder="Search labels…" value="" type="text"> }}
+ <ul class="edit-labels-pop-over js-labels-list">
+ {{# each card.board.labels }}
+ <li>
+ <a href="#" class="card-label-edit-button icon-sm fa fa-pencil js-edit-label"></a>
+ <span class="card-label card-label-selectable card-label-{{color}} js-select-label {{# if isLabelSelected ../card._id }}active{{/ if }}">
+ {{name}}
+ {{# if currentUser.isBoardAdmin }}
+ <span class="card-label-selectable-icon icon-sm fa fa-check light"></span>
+ {{/ if }}
+ </span>
+ </li>
+ {{/ each}}
+ </ul>
+ <a class="quiet-button full js-add-label">{{_ 'label-create'}}</a>
+ </div>
+</template>
+
+<template name="cardAttachmentsPopup">
+ <div>
+ <ul class="pop-over-list">
+ <li>
+ <input type="file" name="file" class="js-attach-file hide" multiple>
+ <a class="js-computer-upload" href="#">
+ {{_ 'computer'}}
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
+
+<template name="formLabel">
+ <div class="colors clearfix">
+ <label for="labelName">{{_ 'name'}}</label>
+ <input id="labelName" type="text" name="name" class="js-label-name" value='{{ name }}' autofocus>
+ <label>{{_ "select-color"}}</label>
+ {{# each labels }}
+ <span class="card-label card-label--selectable card-label-{{ color }} palette-color js-palette-color">
+ <span class="card-label-color-select-icon icon-sm fa fa-check light js-palette-select {{#if $neq color ../color}}hide{{/if}}"></span>
+ </span>
+ {{/each}}
+ </div>
+</template>
+
+<template name="createLabelPopup">
+ <form class="create-label">
+ {{#with color=defaultColor}}
+ {{> formLabel}}
+ {{/with}}
+ <input type="submit" class="primary wide left" value="{{_ 'create'}}">
+ </form>
+</template>
+
+<template name="editLabelPopup">
+ <form class="edit-label">
+ {{> formLabel}}
+ <input type="submit" class="primary wide left" value="{{_ 'save'}}">
+ <span class="right">
+ <input type="submit" value="{{_ 'delete'}}" class="negate js-delete-label">
+ </span>
+ </form>
+</template>
+
+<template name="deleteLabelPopup">
+ <p>{{_ "label-delete-pop"}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
+</template>
+
+<template name="cardDeletePopup">
+ <p>{{_ "card-delete-pop"}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
+</template>
+
+<template name="attachmentDeletePopup">
+ <p>{{_ "attachment-delete-pop"}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}">
+</template>
+
+<template name="cardDetailSidebarOld">
+ <div class="card-detail-window clearfix">
+ {{# if card.cover }}
+ <div class="window-cover js-card-cover-box js-open-card-cover-in-viewer has-cover" style="background-image: url({{ card.cover.url }}); background-color: rgb(119, 119, 119); background-size: contain;">
+ </div>
+ {{ /if }}
+ {{ #if card.archived }}
+ <div class="window-archive-banner js-archive-banner">
+ <span class="icon-lg fa fa-archive window-archive-banner-icon"></span>
+ <p class="window-archive-banner-text">{{_ "card-archived"}}</p>
+ </div>
+ {{ /if }}
+ <div class="window-header clearfix">
+ <span class="window-header-icon icon-lg fa fa-calendar-o"></span>
+ <div class="window-title card-detail-title non-empty inline {{# if currentUser.isBoardMember }}editable{{/ if }}">
+ <h2 class="window-title-text current hide-on-edit js-card-title">{{ card.title }}</h2>
+ <div class="edit edit-heavy">
+ <form id="WindowTitleEdit">
+ <textarea type="text" class="field single-line" id="title">{{ card.title }}</textarea>
+ <div class="edit-controls clearfix">
+ <input type="submit" class="primary confirm js-title-save-edit" value="{{_ 'save'}}">
+ <a href="#" class="icon-lg fa fa-times dark-hover cancel js-cancel-edit"></a>
+ </div>
+ </form>
+ </div>
+ <div class="quiet hide-on-edit window-header-inline-content js-current-list">
+ <p class="inline-block bottom">
+ {{_ 'in-list'}}
+ <a href="#" class="{{# if currentUser.isBoardMember }}js-open-move-from-header{{else}}disabled{{/ if }}"><strong>{{ card.list.title }}</strong></a>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="window-main-col clearfix">
+ <div class="card-detail-data gutter clearfix">
+ <div class="card-detail-item card-detail-item-block clear clearfix editable">
+ {{# if card.members }}
+ <div class="card-detail-item card-detail-item-members clearfix js-card-detail-members">
+ <h3 class="card-detail-item-header">{{_ 'members'}}</h3>
+ <div class="js-card-detail-members-list clearfix">
+ {{# each card.members }}
+ {{> userAvatar userId=this size="small" cardId=../card._id }}
+ {{/ each }}
+ <a class="card-detail-item-add-button dark-hover js-details-edit-members">
+ <span class="icon-sm fa fa-plus"></span>
+ </a>
+ </div>
+ </div>
+ {{/ if }}
+ {{# if card.labels }}
+ <div class="card-detail-item card-detail-item-labels clearfix js-card-detail-labels">
+ <h3 class="card-detail-item-header">{{_ 'labels'}}</h3>
+ <div class="js-card-detail-labels-list clearfix editable-labels js-edit-label">
+ {{# each card.labels }}
+ <span class="card-label card-label-{{color}}" title="{{name}}">{{ name }}</span>
+ {{/ each }}
+ <a class="card-detail-item-add-button dark-hover js-details-edit-labels">
+ <span class="icon-sm fa fa-plus"></span>
+ </a>
+ </div>
+ </div>
+ {{/ if }}
+ <div class="card-detail-item card-detail-item-block clear clearfix editable" attr="desc">
+ {{# if card.description }}
+ <h3 class="card-detail-item-header js-show-with-desc">{{_ 'description'}}</h3>
+ {{# if currentUser.isBoardMember }}
+ <a href="#" class="card-detail-item-header-edit hide-on-edit js-show-with-desc js-edit-desc">{{_ 'edit'}}</a>
+ {{/ if }}
+ <div class="current markeddown hide-on-edit js-card-desc js-show-with-desc">
+ {{#viewer}}{{ card.description }}{{/viewer}}
+ </div>
+ {{ else }}
+ {{# if currentUser.isBoardMember }}
+ <p class="bottom">
+ <a href="#" class="hide-on-edit quiet-button w-img js-edit-desc js-hide-with-desc">
+ <span class="icon-sm fa fa-align-left"></span>
+ {{_ 'edit-description'}}
+ </a>
+ </p>
+ {{/ if }}
+ {{/ if }}
+ <div class="card-detail-edit edit">
+ <form id="WindowDescEdit">
+ {{#editor class="field single-line2" id="desc"}}{{ card.description }}{{/editor}}
+ <div class="edit-controls clearfix">
+ <input type="submit" class="primary confirm js-title-save-edit" value="{{_ 'save'}}">
+ <a href="#" class="icon-lg fa fa-times dark-hover cancel js-cancel-edit"></a>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ {{# if card.attachments.count }}
+ {{ > WindowAttachmentsModule card=card }}
+ {{/ if}}
+ {{ > WindowActivityModule card=card }}
+ </div>
+ {{# if currentUser.isBoardMember }}
+ {{ > WindowSidebarModule card=card }}
+ {{/if}}
+ </div>
+</template>
+
+<template name="WindowActivityModule">
+ <div class="card-detailwindow-module">
+ <div class="window-module-title window-module-title-no-divider">
+ <span class="window-module-title-icon icon-lg fa fa-comments-o"></span>
+ <h3>{{ _ 'activity'}}</h3>
+ </div>
+ {{# if currentUser.isBoardMember }}
+ <div class="new-comment js-new-comment">
+ {{> userAvatar user=currentUser size="small" class="member-no-menu" }}
+ <form id="CommentForm">
+ {{#editor class="new-comment-input js-new-comment-input"}}{{/editor}}
+ <div class="add-controls clearfix">
+ <input type="submit" class="primary confirm clear js-add-comment" value="{{_ 'comment'}}" tabindex="2">
+ </div>
+ </form>
+ </div>
+ {{/ if }}
+ {{ > activities mode="card" }}
+ </div>
+</template>
+
+<template name="WindowAttachmentsModule">
+ <div class="window-module js-attachments-section clearfix">
+ <div class="window-module-title window-module-title-no-divider">
+ <span class="window-module-title-icon icon-lg fa fa-paperclip"></span>
+ <h3 class="inline-block">{{_ 'attachments'}}</h3>
+ </div>
+ <div class="gutter">
+ <div class="clearfix js-attachment-list">
+ {{# each card.attachments }}
+ <div class="attachment-thumbnail">
+ {{# if isUploaded }}
+ <a href="{{ url download=true }}" class="attachment-thumbnail-preview js-open-viewer attachment-thumbnail-preview-is-cover">
+ {{# if isImage }}
+ <img src="{{ url }}">
+ {{ else }}
+ <span class="attachment-thumbnail-preview-ext">{{ extension }}</span>
+ {{ /if }}
+ </a>
+ <p class="attachment-thumbnail-details js-open-viewer">
+ <a href="" class="attachment-thumbnail-details-title js-attachment-thumbnail-details">
+ {{ name }}
+ <span class="block quiet">
+ {{_ 'added'}} <span class="date">{{ moment uploadedAt }}</span>
+ </span>
+ </a>
+ <span class="quiet attachment-thumbnail-details-options">
+ <a href="{{ url download=true }}" class="attachment-thumbnail-details-options-item dark-hover js-download">
+ <span class="icon-sm fa fa-download"></span>
+ <span class="attachment-thumbnail-details-options-item-text">{{_ 'download'}}</span>
+ </a>
+ {{# if isImage }}
+ <a class="attachment-thumbnail-details-options-item dark-hover {{#if $eq ../card.coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}">
+ <span class="icon-sm fa fa-thumb-tack"></span>
+ <span class="attachment-thumbnail-details-options-item-text">{{#if $eq ../card.coverId _id}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}</span>
+ </a>
+ {{/if}}
+ <a href="#" class="attachment-thumbnail-details-options-item attachment-thumbnail-details-options-item-delete dark-hover js-confirm-delete">
+ <span class="icon-sm fa fa-close"></span>
+ <span class="attachment-thumbnail-details-options-item-text">{{_ 'delete'}}</span>
+ </a>
+ </span>
+ </p>
+ {{ else }}
+ +spinner
+ {{/ if }}
+ </div>
+ {{/each}}
+ </div>
+ <p>
+ <a href="#" class="quiet-button js-attach">{{_ 'add-attachment' }}</a>
+ </p>
+ </div>
+ </div>
+</template>
+
+<template name="WindowSidebarModule">
+ <div class="window-sidebar" style="position: relative;">
+ <div class="window-module clearfix">
+ <h3>{{_ 'add'}}</h3>
+ <div class="clearfix">
+ <a href="#" class="button-link js-change-card-members" title="{{_ 'members-title'}}">
+ <span class="icon-sm fa fa-user"></span> {{_ 'members'}}
+ </a>
+ <a href="#" class="button-link js-edit-labels" title="{{_ 'labels-title'}}">
+ <span class="icon-sm fa fa-tags"></span> {{_ 'labels'}}
+ </a>
+ <a href="#" class="button-link js-attach" title="{{_ 'attachment-title'}}">
+ <span class="icon-sm fa fa-paperclip"></span> {{_ 'attachment'}}
+ </a>
+ </div>
+ </div>
+ <div class="window-module other-actions clearfix">
+ <h3>{{_ 'actions'}}</h3>
+ <div class="clearfix">
+ <hr>
+ {{ #if card.archived }}
+ <a href="#" class="button-link js-unarchive-card" title="{{_ 'send-to-board-title'}}">
+ <span class="icon-sm fa fa-recycle"></span> {{_ 'send-to-board'}}
+ </a>
+ <a href="#" class="button-link negate js-delete-card" title="{{_ 'delete-title'}}">
+ <span class="icon-sm fa fa-trash-o"></span> {{_ 'delete'}}
+ </a>
+ {{ else }}
+ <a href="#" class="button-link js-archive-card" title="{{_ 'archive-title'}}">
+ <span class="icon-sm fa fa-archive"></span> {{_ 'archive'}}
+ </a>
+ {{ /if }}
+ </div>
+ </div>
+ <div class="window-module clearfix">
+ <p class="quiet bottom">
+ <a href="#" class="quiet-button js-more-menu" title="{{_ 'share-and-more-title'}}">{{_ 'share-and-more'}}</a>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/client/components/forms/cachedValue.js b/client/components/forms/cachedValue.js
new file mode 100644
index 00000000..a2898d85
--- /dev/null
+++ b/client/components/forms/cachedValue.js
@@ -0,0 +1,22 @@
+var emptyValue = '';
+
+Mixins.CachedValue = BlazeComponent.extendComponent({
+ onCreated: function() {
+ this._cachedValue = emptyValue;
+ },
+
+ setCache: function(value) {
+ this._cachedValue = value;
+ },
+
+ getCache: function(defaultValue) {
+ if (this._cachedValue === emptyValue)
+ return defaultValue || '';
+ else
+ return this._cachedValue;
+ },
+
+ resetCache: function() {
+ this.setCache('');
+ }
+});
diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl
new file mode 100644
index 00000000..1084a4a6
--- /dev/null
+++ b/client/components/forms/forms.styl
@@ -0,0 +1,636 @@
+@import 'nib'
+
+textarea,
+input:not([type=file]),
+button
+ box-sizing: border-box
+ -webkit-appearance: none
+ background-color: #ebebeb
+ border: 1px solid #ccc
+ border-radius: 3px
+ display: block
+ margin-bottom: 12px
+ min-height: 34px
+ padding: 7px
+
+ &.full
+ width: 100%
+
+ &.input-error
+ background-color: #ece9e9
+ border-color: #ba1212
+
+ &:focus
+ outline: 0
+
+input[type="file"]
+ margin-bottom: 16px
+
+input[type="radio"]
+ -webkit-appearance: radio
+ min-height: inherit
+
+input[type="checkbox"]
+ -webkit-appearance: checkbox
+ margin-right: 4px
+
+input[type="text"],
+input[type="password"],
+input[type="email"]
+ transition: background 85ms ease-in,
+ border-color 85ms ease-in
+ width: 250px
+
+ &.inline-input
+ background: none
+ border: 0
+ margin: 0
+ padding: 2px
+ min-height: 0
+ height: 18px
+ width: 200px
+
+input[type="email"]:invalid
+ box-shadow: none
+
+input[type="text"],
+input[type="password"],
+input[type="email"],
+textarea
+
+ &:hover
+ border-color: #999
+
+ &.input-error
+ border-color: #ba1212
+
+ &:focus
+ background: #fff
+ border-color: #318ec4
+ box-shadow: 0 0 2px #318ec4
+
+ &.input-error
+ background-color: #f8f7f7
+ border-color: #ba1212
+ box-shadow: 0 0 2px #d11515
+
+ &:disabled
+ background-color: #dcdcdc
+ border-color: #bfbfbf
+ color: #8c8c8c
+ -webkit-touch-callout: none
+ user-select: none
+
+select
+ max-height: 300px
+ width: 256px
+ margin-bottom: 8px
+
+option[disabled]
+ color: #8c8c8c
+
+textarea
+ height: 150px
+ transition: background 85ms ease-in,
+ border-color 85ms ease-in
+ resize: vertical
+ width: 100%
+
+.button
+ border-radius: 3px
+ text-decoration: none
+ position: relative
+
+input[type="submit"],
+button
+ background: #cfcfcf
+ background: linear-gradient(#cfcfcf, #c2c2c2)
+ border: none
+ box-shadow: 0 1px 0 #8c8c8c
+ cursor: pointer
+ display: inline-block
+ font-weight: 700
+ line-height: 22px
+ margin: 8px 4px 0 0
+ padding: 7px 20px
+ text-align: center
+
+ .wide
+ padding-left: 30px
+ padding-right: 30px
+
+ &:hover,
+ &:focus
+ background: #c2c2c2
+ background: linear-gradient(#c2c2c2, #b5b5b5)
+
+ &:active
+ background: #b5b5b5
+ background: linear-gradient(#b5b5b5, #a8a8a8)
+ box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1)
+
+ &:hover,
+ &:focus,
+ &:active
+ background: #e6e6e6
+ background: linear-gradient(#e6e6e6, #e6e6e6)
+
+ &.primary
+ background: #005377
+ box-shadow: 0 1px 0 #4d4d4d
+ color: white
+
+ &:hover,
+ &:focus
+ background: #004766
+
+ &:active
+ background: #01628C
+
+ &.negate
+ &:hover,
+ &:focus
+ background: #990f0f
+ background: linear-gradient(#990f0f, #7d0c0c)
+ box-shadow: 0 1px 0 #4d4d4d
+ color: #fff
+
+ &:active
+ background: #7d0c0c
+ box-shadow: 0 1px 0 #4d4d4d
+ color: #fff
+
+input[type="submit"].disabled,
+input[type="submit"]:disabled,
+input[type="button"].disabled,
+button.disabled,
+.button.disabled
+
+ &,
+ &:hover,
+ &:active
+ background: #cfcfcf
+ cursor: default
+ box-shadow: none
+ color: #a8a8a8
+
+fieldset
+ border: 1px solid #bfbfbf
+ padding: 15px
+ margin-bottom: 15px
+
+input[type="hidden"]
+ display: none
+
+input[type="checkbox"],
+input[type="radio"]
+ display: inline
+
+.radio-div,
+.check-div
+ display: block
+ margin: 0 0 4px 20px
+ min-height: 20px
+ position: relative
+
+ input
+ left: -18px
+ min-height: 0
+ margin: 0
+ padding: 0
+ position: absolute
+ top: 2px
+
+ label
+ font-weight: 400
+
+label
+ display: block
+ font-weight: 700
+ margin-bottom: 4px
+
+ &.form-error
+ color: #ba1212
+
+input,
+textarea
+ &::-webkit-input-placeholder,
+ &::-moz-placeholder
+ color: #8c8c8c
+
+.edit-controls,
+.add-controls
+ margin-top: 0
+
+ button[type=submit]
+ float: left
+ height: 32px
+ margin-top: -2px
+ padding-top: 5px
+ padding-bottom: 5px
+
+ i.fa.fa-times
+ font-size: 20px
+
+ .option
+ border-color: transparent
+ border-radius: 3px
+ color: #8c8c8c
+ display: block
+ float: right
+ height: 30px
+ line-height: 30px
+ padding: 0 8px
+ margin: 0 2px
+
+ &:hover
+ background-color: #dbdbdb
+ color: #4d4d4d
+
+ &:active
+ background-color: #ccc
+
+.button-link
+ background: #fff
+ background: linear-gradient(#fff, #f5f5f5)
+ border-radius: 3px
+ box-sizing: border-box
+ user-select: none
+ border: 1px solid #e3e3e3
+ border-bottom-color: #c2c2c2
+ cursor: pointer
+ display: block
+ font-weight: 700
+ height: 34px
+ margin-top: 6px
+ max-width: 300px
+ padding: 7px
+ position: relative
+ text-decoration: none
+ overflow: ellipsis
+
+ .on
+ background: #48b512
+ background: linear-gradient(#48b512, #3d990f)
+ border-radius: 3px
+ color: #fff
+ display: none
+ font-size: 12px
+ font-weight: 700
+ height: 17px
+ line-height: @height
+ margin: 0
+ padding: 2px 4px
+ position: absolute
+ right: 5px
+ top: 5px
+ text-align: center
+
+ &.is-on
+ padding-right: 30px
+ max-width: 196px
+
+ .on
+ display: block
+
+ &.inline
+ color: #666
+ padding: 2px 14px
+ margin-left: 4px
+
+ &.setting
+ height: 52px
+ float: left
+ position: relative
+ margin-top: 0
+
+ &.disabled
+ background: #fff
+ border-color: #e9e9e9
+ color: #8c8c8c
+ cursor: default
+
+ select
+ display: none
+
+ &:hover .label
+ color: #8c8c8c
+
+ &,
+ &:hover,
+ &:active,
+ &.primary,
+ &.primary:hover,
+ &.primary:active
+ background: #cfcfcf
+ border-color: #c2c2c2
+ border-bottom-color: #b5b5b5
+ cursor: default
+ box-shadow: none
+ color: #a8a8a8
+
+ .label
+ color: #8c8c8c
+ display: block
+ font-size: 12px
+ line-height: 14px
+ margin-bottom: 0
+
+ &:hover .label
+ color: #eee
+
+ .value
+ display: block
+ font-size: 18px
+ line-height: 24px
+ overflow: hidden
+ text-overflow: ellipsis
+
+ label
+ display: none
+
+ select
+ border: none
+ cursor: pointer
+ height: 50px
+ left: 0
+ margin: 0
+ opacity: 0
+ position: absolute
+ top: 0
+ z-index: 2
+ width: 100%
+
+ &:hover
+ background: #318ec4
+ background: linear-gradient(#318ec4, #2b7cab)
+ border-color: #2e85b8
+ color: #fff
+
+ .on
+ background-image: none
+ background-color: rgba(255, 255, 255, .3)
+ border-color: transparent
+
+ .icon-sm
+ color: #fff
+
+ &:active
+ background: #2e85b8
+ background: linear-gradient(#2e85b8, #28739f)
+ border-color: #2b7cab
+ color: #fff
+
+ .button-link.negate
+
+ &:hover
+ background: #990f0f
+ background: linear-gradient(#990f0f, #7d0c0c)
+ border-color: @background
+
+ &:active
+ background: #7d0c0c
+ border-color: #990f0f
+
+
+ &.primary
+ background: #48b512
+ background: linear-gradient(#48b512, #3d990f)
+ border: 1px solid
+ border-color: #3d990f
+ color: #fff
+
+ &:hover
+ background: #3d990f
+ background: linear-gradient(#3d990f, #327d0c)
+ border-color: #3d990f
+
+ &.danger
+ background: #ba1212
+ background: linear-gradient(#ba1212, #8b0e0e)
+ border: 1px solid
+ border-color: #a21010
+ color: #fff
+
+ &:hover
+ background: #a21010
+ background: linear-gradient(#a21010, #740b0b)
+ border-color: #8b0e0e
+
+button
+
+ &.quiet-button,
+ &.loud-text-button
+ background: none
+ text-align: left
+ line-height: normal
+ border: none
+ box-shadow: none
+
+ &:active
+ color: #4d4d4d
+ background: #d3d3d3
+ box-shadow: none
+
+ &.quiet-button
+ font-weight: 400
+ text-decoration: underline
+
+ &.loud-text-button
+ width: 100%
+
+ &:hover
+ color: #111
+
+.emphasis-button,
+.quiet-button
+ border-radius: 3px
+ user-select: none
+ color: #8c8c8c
+ display: block
+ margin: 2px 0
+ padding: 6px 8px
+ position: relative
+
+ &.w-img
+ padding-left: 28px
+
+ .icon-sm
+ left: 6px
+ position: absolute
+ top: 6px
+
+ &:hover
+ color: #4d4d4d
+ background: #dcdcdc
+
+ &:active
+ color: #4d4d4d
+ background: #d3d3d3
+
+.quiet-button-large
+ padding: 16px 24px
+
+.emphasis-button
+ color: #74663e
+ background: #ecdfbb
+
+ &:hover
+ color: #53492d
+ background: #e7d6a7
+
+ &:active
+ color: #53492d
+ background: #e1cc93
+
+.big-link
+ border-radius: 3px
+ display: block
+ margin: 6px 0 6px 40px
+ padding: 11px
+ position: relative
+ text-decoration: none
+ font-size: 16px
+ line-height: 20px
+
+ .text
+ text-decoration: underline
+
+ &:hover
+ background: #dcdcdc
+
+ &.options
+ padding-right: 41px
+
+ .option
+ height: 30px
+ width: @height
+ position: absolute
+ right: 6px
+ top: 6px
+
+ &.none
+ color: #8c8c8c
+ text-decoration: none
+
+ &:hover
+ background: transparent
+
+ &.avatar-changer
+ padding-right: 51px
+
+ .member
+ border: 1px solid #ccc
+ border-radius: 3px
+ height: 40px
+ width: @height
+ position: absolute
+ right: 0
+ top: 0
+
+ .member-avatar
+ height: 40px
+ width: @height
+
+ .member-initials
+ font-size: 16px
+ height: 40px
+ line-height: @height
+ max-height: @height
+
+.show-more
+ border-radius: 3px
+ color: #8c8c8c
+ display: block
+ padding: 16px 8px 16px 40px
+ margin: 8px 0
+
+ &:hover
+ background: #dcdcdc
+ text-decoration: underline
+
+ &.compact
+ padding: 12px 8px
+ margin: 8px 0 0
+ text-align: center
+
+.board-widget .show-more
+ padding: 12px 8px 12px 40px
+
+.uploader
+ clear: both
+ cursor: pointer
+ position: relative
+ height: 34px
+ width: 100%
+
+ .realfile
+ cursor: pointer
+ height: 34px
+ line-height: @height
+ position: absolute
+ top: 0
+ left: 0
+ width: 100%
+ z-index: 2
+ font-size: 23px
+
+ input[type="file"]
+ cursor: pointer
+ height: 34px
+ line-height: @height
+ margin: 0
+ opacity: 0
+ padding: 0
+ width: 100%
+ z-index: 2
+ font-size: 23px
+
+ &:hover .fakefile
+ background: #318ec4
+ background: linear-gradient(#318ec4, #2b7cab)
+ border-color: #2e85b8
+ color: #fff
+
+.form-grid
+ display: flex
+ flex-wrap: wrap
+ width: 100%
+
+.form-grid-child
+ flex: 1
+ margin: 0 0 8px
+
+.form-grid-child-full
+ flex: 1 1 100%
+
+.form-grid-child-threequarters
+ flex: 3
+ margin-right: 8px
+
+.form-grid-child-twothirds
+ flex: 2
+ margin-right: 8px
+
+.dropdown-menu
+ border-radius: 2px
+ // padding-bottom: 3px
+ overflow: hidden
+
+ li
+ border-top: none
+
+ a
+ padding: 4px 12px 4px 8px
+
+ img
+ width: 18px
+ height: @width
+ margin-right: 5px
+ vertical-align: middle
+
+ &.active
+ background: #005377
+
+ a
+ color: white
diff --git a/client/components/forms/inlinedform.jade b/client/components/forms/inlinedform.jade
new file mode 100644
index 00000000..5ad9039e
--- /dev/null
+++ b/client/components/forms/inlinedform.jade
@@ -0,0 +1,6 @@
+template(name='inlinedForm')
+ if isOpen.get
+ form(id=id class=classNames)
+ +Template.contentBlock
+ else
+ +Template.elseBlock
diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js
new file mode 100644
index 00000000..2e2b2eba
--- /dev/null
+++ b/client/components/forms/inlinedform.js
@@ -0,0 +1,93 @@
+// A inlined form is used to provide a quick edition of single field for a given
+// document. Clicking on a edit button should display the form to edit the field
+// value. The form can then be submited, or just closed.
+//
+// When the form is closed we save non-submitted values in memory to avoid any
+// data loss.
+//
+// Usage:
+//
+// +inlineForm
+// // the content when the form is open
+// else
+// // the content when the form is close (optional)
+
+// We can only have one inlined form element opened at a time
+// XXX Could we avoid using a global here ? This is used in Mousetrap
+// keyboard.js
+currentlyOpenedForm = new ReactiveVar(null);
+
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'inlinedForm';
+ },
+
+ mixins: function() {
+ return [Mixins.CachedValue];
+ },
+
+ onCreated: function() {
+ this.isOpen = new ReactiveVar(false);
+ },
+
+ open: function() {
+ // Close currently opened form, if any
+ if (currentlyOpenedForm.get() !== null) {
+ currentlyOpenedForm.get().close();
+ }
+ this.isOpen.set(true);
+ currentlyOpenedForm.set(this);
+ },
+
+ close: function() {
+ this.saveValue();
+ this.isOpen.set(false);
+ currentlyOpenedForm.set(null);
+ },
+
+ getValue: function() {
+ return this.isOpen.get() && this.find('textarea,input[type=text]').value;
+ },
+
+ saveValue: function() {
+ this.callFirstWith(this, 'setCache', this.getValue());
+ },
+
+ events: function() {
+ return [{
+ 'click .js-close-inlined-form': this.close,
+ 'click .js-open-inlined-form': this.open,
+
+ // Close the inlined form by pressing escape.
+ //
+ // Keydown (and not keypress) in necessary here because the `keyCode`
+ // property is consistent in all browsers, (there is not keyCode for the
+ // `keypress` event in firefox)
+ 'keydown form input, keydown form textarea': function(evt) {
+ if (evt.keyCode === 27) {
+ evt.preventDefault();
+ this.close();
+ }
+ },
+
+ // Pressing Ctrl+Enter should submit the form
+ 'keydown form textarea': function(evt) {
+ if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
+ $(evt.currentTarget).parents('form:first').submit();
+ }
+ },
+
+ // Close the inlined form when after its submission
+ submit: function() {
+ var self = this;
+ // XXX Swith to an arrow function here when we'll have ES6
+ if (this.currentData().autoclose !== false) {
+ Tracker.afterFlush(function() {
+ self.close();
+ self.callFirstWith(self, 'resetCache');
+ });
+ }
+ }
+ }];
+ }
+}).register('inlinedForm');
diff --git a/client/components/lists/body.jade b/client/components/lists/body.jade
new file mode 100644
index 00000000..0e8efeeb
--- /dev/null
+++ b/client/components/lists/body.jade
@@ -0,0 +1,50 @@
+template(name="listBody")
+ .minicards.clearfix.js-minicards
+ if cards.count
+ +inlinedForm(autoclose=false position="top")
+ +addCardForm
+ each cards
+ .minicard.card.js-minicard.js-member-droppable(
+ class="{{#if isSelected}}is-selected{{/if}}")
+ a.minicard-details.clearfix.show(href=absoluteUrl)
+ if cover
+ .minicard-cover.js-card-cover(style="background-image: url({{cover.url}});")
+ if labels
+ .minicard-labels
+ each labels
+ .minicard-label(class="card-label-{{color}}" title="{{name}}")
+ .minicard-title= title
+ if members
+ .minicard-members.js-minicard-members
+ each members
+ +userAvatar(userId=this size="small" cardId="{{../_id}}")
+ .badges
+ if comments.count
+ .badge(title="{{_ 'card-comments-title' comments.count }}")
+ span.badge-icon.icon-sm.fa.fa-comment-o
+ .badge-text= comments.count
+ if description
+ .badge.badge-state-image-only(title=description)
+ span.badge-icon.icon-sm.fa.fa-align-left
+ if attachments.count
+ .badge
+ span.badge-icon.icon-sm.fa.fa-paperclip
+ span.badge-text= attachments.count
+ if currentUser.isBoardMember
+ +inlinedForm(autoclose=false position="bottom")
+ +addCardForm
+ else
+ a.open-card-composer.js-open-inlined-form
+ i.fa.fa-plus
+ | {{_ 'add-card'}}
+
+template(name="addCardForm")
+ .minicard.js-composer
+ .minicard-labels.js-minicard-composer-labels
+ .minicard-details.clearfix
+ textarea.minicard-composer-textarea.js-card-title(autofocus)
+ = getCache
+ .minicard-members.js-minicard-composer-members
+ .add-controls.clearfix
+ button.primary.confirm(type="submit") {{_ 'add'}}
+ a.fa.fa-times.dark-hover.cancel.js-close-inlined-form
diff --git a/client/components/lists/body.js b/client/components/lists/body.js
new file mode 100644
index 00000000..fa6ec096
--- /dev/null
+++ b/client/components/lists/body.js
@@ -0,0 +1,73 @@
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'listBody';
+ },
+
+ isSelected: function() {
+ return Session.equals('currentCard', this.currentData()._id);
+ },
+
+ addCard: function(evt) {
+ evt.preventDefault();
+ var textarea = $(evt.currentTarget).find('textarea');
+ var title = textarea.val();
+ var position = this.currentData().position;
+ var sortIndex;
+ if (position === 'top') {
+ sortIndex = Utils.getSortIndex(null, this.find('.js-minicard:first'));
+ } else if (position === 'bottom') {
+ sortIndex = Utils.getSortIndex(this.find('.js-minicard:last'), null);
+ }
+
+ // Clear the form in-memory cache
+ // var inputCacheKey = "addCard-" + this.listId;
+ // InputsCache.set(inputCacheKey, '');
+
+ // title trim if not empty then
+ if ($.trim(title)) {
+ Cards.insert({
+ title: title,
+ listId: this.data()._id,
+ boardId: this.data().board()._id,
+ sort: sortIndex
+ }, function(err, _id) {
+ // In case the filter is active we need to add the newly
+ // inserted card in the list of exceptions -- cards that are
+ // not filtered. Otherwise the card will disappear instantly.
+ // See https://github.com/libreboard/libreboard/issues/80
+ Filter.addException(_id);
+ });
+
+ // We keep the form opened, empty it, and scroll to it.
+ textarea.val('').focus();
+ Utils.Scroll(this.find('.js-minicards')).top(1000, true);
+ }
+ },
+
+ events: function() {
+ return [{
+ submit: this.addCard,
+ 'keydown form textarea': function(evt) {
+ // Pressing Enter should submit the card
+ if (evt.keyCode === 13) {
+ evt.preventDefault();
+ $(evt.currentTarget).parents('form:first').submit();
+
+ // Pressing Tab should open the form of the next column, and Maj+Tab go
+ // in the reverse order
+ } else if (evt.keyCode === 9) {
+ evt.preventDefault();
+ var isReverse = evt.shiftKey;
+ var list = $('#js-list-' + this.data()._id);
+ var nextList = list[isReverse ? 'prev' : 'next']('.js-list').get(0) ||
+ $('.js-list:' + (isReverse ? 'last' : 'first')).get(0);
+ var nextListComponent = BlazeComponent.getComponentForElement(nextList);
+
+ // XXX Get the real position
+ var position = 'bottom';
+ nextListComponent.openForm({position: position});
+ }
+ }
+ }];
+ }
+}).register('listBody');
diff --git a/client/components/lists/events.js b/client/components/lists/events.js
new file mode 100644
index 00000000..f636de75
--- /dev/null
+++ b/client/components/lists/events.js
@@ -0,0 +1,16 @@
+Template.addlistForm.events({
+ submit: function(event, t) {
+ event.preventDefault();
+ var title = t.find('.list-name-input');
+ if ($.trim(title.value)) {
+ Lists.insert({
+ title: title.value,
+ boardId: Session.get('currentBoard'),
+ sort: $('.list').length
+ });
+
+ Utils.Scroll('.js-lists').left(270, true);
+ title.value = '';
+ }
+ }
+});
diff --git a/client/components/lists/header.jade b/client/components/lists/header.jade
new file mode 100644
index 00000000..5196af5d
--- /dev/null
+++ b/client/components/lists/header.jade
@@ -0,0 +1,13 @@
+template(name="listHeader")
+ .list-header.js-list-header
+ +inlinedForm
+ +editListTitleForm
+ else
+ h2.list-header-name.js-open-inlined-form= title
+ a.list-header-menu-icon.fa.fa-bars.js-open-list-menu
+
+template(name="editListTitleForm")
+ input.field.single-line(type="text" value="{{getCache title}}" autofocus)
+ .edit-controls.clearfix
+ input.primary.confirm(type="submit" value="{{_ 'save'}}")
+ a.fa.fa-times.js-close-inlined-form
diff --git a/client/components/lists/header.js b/client/components/lists/header.js
new file mode 100644
index 00000000..014cfd80
--- /dev/null
+++ b/client/components/lists/header.js
@@ -0,0 +1,25 @@
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'listHeader';
+ },
+
+ editTitle: function(evt) {
+ evt.preventDefault();
+ var form = this.componentChildren('inlinedForm')[0];
+ var newTitle = form.getValue();
+ if ($.trim(newTitle)) {
+ Lists.update(this.currentData()._id, {
+ $set: {
+ title: newTitle
+ }
+ });
+ }
+ },
+
+ events: function() {
+ return [{
+ 'click .js-open-list-menu': Popup.open('listAction'),
+ submit: this.editTitle
+ }];
+ }
+}).register('listHeader');
diff --git a/client/components/lists/main.jade b/client/components/lists/main.jade
new file mode 100644
index 00000000..dd4bb49a
--- /dev/null
+++ b/client/components/lists/main.jade
@@ -0,0 +1,5 @@
+template(name='list')
+ .list.js-list(id="js-list-{{_id}}")
+ .list-wrapper
+ +listHeader
+ +listBody
diff --git a/client/components/lists/main.js b/client/components/lists/main.js
new file mode 100644
index 00000000..3d458055
--- /dev/null
+++ b/client/components/lists/main.js
@@ -0,0 +1,81 @@
+ListComponent = BlazeComponent.extendComponent({
+ template: function() {
+ return 'list';
+ },
+
+ openForm: function(options) {
+ options = options || {};
+ options.position = options.position || 'top';
+
+ var listComponent = this.componentChildren('listBody')[0];
+ var forms = listComponent.componentChildren('inlinedForm');
+
+ if (options.position === 'top') {
+ forms[0].open();
+ } else {
+ forms[forms.length - 1].open();
+ }
+ },
+
+ // XXX The jQuery UI sortable plugin is far from ideal here. First we include
+ // all jQuery components but only use one. Second, it modifies the DOM itself,
+ // resulting in Blaze abandoning reactive update of the nodes that have been
+ // moved which result in bugs if multiple users use the board in real time.
+ // I tried sortable:sortable but that was not better. Should we “simply” write
+ // the drag&drop code ourselves?
+ onRendered: function() {
+ if (Meteor.user().isBoardMember()) {
+ var $cards = this.$('.js-minicards');
+ $cards.sortable({
+ connectWith: ".js-minicards",
+ tolerance: 'pointer',
+ appendTo: '.js-lists',
+ helper: "clone",
+ items: '.js-minicard:not(.placeholder, .hide, .js-composer)',
+ placeholder: 'minicard placeholder',
+ start: function (event, ui) {
+ $('.minicard.placeholder').height(ui.item.height());
+ Popup.close();
+ },
+ stop: function(event, ui) {
+ // To attribute the new index number, we need to get the dom element of
+ // the previous and the following card -- if any.
+ var cardDomElement = ui.item.get(0);
+ var prevCardDomElement = ui.item.prev('.js-minicard').get(0);
+ var nextCardDomElement = ui.item.next('.js-minicard').get(0);
+ var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement);
+ var cardId = Blaze.getData(cardDomElement)._id;
+ var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
+ Cards.update(cardId, {
+ $set: {
+ listId: listId,
+ sort: sort
+ }
+ });
+ }
+ }).disableSelection();
+
+ Utils.liveEvent('mouseover', function($el) {
+ $el.find('.js-member-droppable').droppable({
+ hoverClass: "draggable-hover-card",
+ accept: '.js-member',
+ drop: function(event, ui) {
+ var memberId = Blaze.getData(ui.draggable.get(0)).userId;
+ var cardId = Blaze.getData(this)._id;
+ Cards.update(cardId, {$addToSet: {members: memberId}});
+ }
+ });
+
+ $el.find('.js-member-droppable').droppable({
+ hoverClass: "draggable-hover-card",
+ accept: '.js-label',
+ drop: function(event, ui) {
+ var labelId = Blaze.getData(ui.draggable.get(0))._id;
+ var cardId = Blaze.getData(this)._id;
+ Cards.update(cardId, {$addToSet: {labelIds: labelId}});
+ }
+ });
+ });
+ }
+ }
+}).register('list');
diff --git a/client/components/lists/main.styl b/client/components/lists/main.styl
new file mode 100644
index 00000000..18484174
--- /dev/null
+++ b/client/components/lists/main.styl
@@ -0,0 +1,136 @@
+@import 'nib'
+
+.list
+ box-sizing: border-box
+ display: flex
+ flex-direction: column
+ flex: 0 0 270px
+ position: relative
+ // Even if this background color is the same as the body we can't leave it
+ // transparent, because that won't work during a list drag.
+ background: darken(white, 10%)
+ height: 100%
+ border-right: 1px solid darken(white, 17%)
+ border-left: 1px solid darken(white, 4%)
+ padding: 12px 7px 5px
+ overflow-y: auto
+
+ &:first-child
+ margin-left: 5px
+ border-left: none
+
+ &:last-child
+ margin-right: 5px
+ border-right: none
+
+ &.editable
+ cursor: grab
+
+ .list-wrapper
+ cursor: default
+
+ &.add-list
+ &.fade
+ opacity: 0
+
+ .list-name-input
+ background: rgba(0, 0, 0, .05)
+ border-color: #aaa
+ box-shadow: inset 0 1px 8px rgba(0, 0, 0, .15)
+ display: block
+ margin: 0
+ transition: margin 85ms ease-in,
+ background 85ms ease-in
+ width: 100%
+
+ .edit-controls
+ height: 32px
+ transition: margin 85ms ease-in,
+ height 85ms ease-in
+ overflow: hidden
+ margin: 4px 0 0
+
+ input[type=submit]
+ margin-top: 0
+ min-height: 30px
+ height: 30px
+
+.list-header
+ flex: 0 0 auto
+ padding: 10px 26px 4px 6px
+ position: relative
+ min-height: 20px
+
+ .list-header-name
+ display: inline
+ font-size: 16px
+ line-height: 17px
+ margin: 0
+ font-weight: bold
+ min-height: 9px
+ min-width: 30px
+ overflow: hidden
+ text-overflow: ellipsis
+ word-wrap: break-word
+
+ .list-header-menu-icon
+ background-clip: content-box
+ background-origin: content-box
+ padding: 6px 8px
+ position: absolute
+ top: 3px
+ right: -5px
+ color: #a6a6a6
+
+ .list-header-num-cards
+ color: #8c8c8c
+ margin: 0
+
+.minicards
+ // flex: 1 1 auto
+ overflow-y: auto
+ overflow-x: hidden
+ padding: 4px 4px 1px
+ z-index: 1
+ height: 100%
+
+ &::-webkit-scrollbar-button
+ display: block
+ height: 4px
+
+.open-card-composer
+ border-top-left-radius: 0
+ border-top-right-radius: 0
+ border-bottom-right-radius: 3px
+ border-bottom-left-radius: 3px
+ color: #8c8c8c
+ display: block
+ // flex: 0 0 auto
+ margin: 2px -3px -3px
+ padding: 7px 10px
+ position: relative
+ text-decoration: none
+
+ &:hover
+ background: #c3c3c3
+ color: #222
+ text-decoration: underline
+
+
+ &::selection
+ background: transparent
+
+.list.placeholder
+ background-color: rgba(0, 0, 0, .2)
+ border-color: transparent
+ box-shadow: none
+ height: 100px
+
+.list.ui-sortable-helper
+ cursor: grabbing
+ box-shadow: -2px 2px 8px rgba(0, 0, 0, .3), 0 0 1px rgba(0, 0, 0, .5)
+ transform: rotate(4deg)
+
+
+.list.ui-sortable-helper .list-header-menu-icon
+ display: none
diff --git a/client/components/lists/menu.jade b/client/components/lists/menu.jade
new file mode 100644
index 00000000..ff7820a4
--- /dev/null
+++ b/client/components/lists/menu.jade
@@ -0,0 +1,28 @@
+template(name="listActionPopup")
+ ul.pop-over-list
+ li: a.js-add-card {{_ 'add-card'}}
+ li: a.highlight-icon.js-list-subscribe {{_ 'subscribe'}}
+ if cards.count
+ hr
+ ul.pop-over-list
+ li: a.js-move-cards {{_ 'list-move-cards'}}
+ li: a.js-archive-cards {{_ 'list-archive-cards'}}
+ hr
+ ul.pop-over-list
+ li: a.js-close-list {{_ 'archive-list'}}
+
+template(name="listMoveCardsPopup")
+ +boardLists
+
+template(name="boardLists")
+ ul.pop-over-list
+ each currentBoard.lists
+ li
+ if($eq ../_id _id)
+ a.disabled {{title}} ({{_ 'current'}})
+ else
+ a.js-select-list= title
+
+template(name="listArchiveCardsPopup")
+ p {{_ 'list-archive-cards-pop'}}
+ input.js-confirm.negate.full(type="submit" value="{{_ 'archive-all'}}")
diff --git a/client/components/lists/menu.js b/client/components/lists/menu.js
new file mode 100644
index 00000000..ef08cf76
--- /dev/null
+++ b/client/components/lists/menu.js
@@ -0,0 +1,46 @@
+Template.listActionPopup.events({
+ 'click .js-add-card': function() {
+ // XXX We need a better API and architecture here. See
+ // https://github.com/peerlibrary/meteor-blaze-components/issues/19
+ var listDom = document.getElementById('js-list-' + this._id);
+ var listComponent = Blaze.getView(listDom).templateInstance().get('component');
+ listComponent.openForm();
+ Popup.close();
+ },
+ 'click .js-list-subscribe': function() {},
+ 'click .js-move-cards': Popup.open('listMoveCards'),
+ 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() {
+ Cards.find({listId: this._id}).forEach(function(card) {
+ Cards.update(card._id, {
+ $set: {
+ archived: true
+ }
+ });
+ });
+ Popup.close();
+ }),
+ 'click .js-close-list': function(evt) {
+ evt.preventDefault();
+ Lists.update(this._id, {
+ $set: {
+ archived: true
+ }
+ });
+ Popup.close();
+ }
+});
+
+Template.listMoveCardsPopup.events({
+ 'click .js-select-list': function() {
+ var fromList = Template.parentData(2).data._id;
+ var toList = this._id;
+ Cards.find({listId: fromList}).forEach(function(card) {
+ Cards.update(card._id, {
+ $set: {
+ listId: toList
+ }
+ });
+ });
+ Popup.close();
+ }
+});
diff --git a/client/components/main/events.js b/client/components/main/events.js
new file mode 100644
index 00000000..beb90c5e
--- /dev/null
+++ b/client/components/main/events.js
@@ -0,0 +1,8 @@
+Template.editor.events({
+ // Pressing Ctrl+Enter should submit the form.
+ 'keydown textarea': function(event) {
+ if (event.keyCode === 13 && (event.metaKey || event.ctrlKey)) {
+ $(event.currentTarget).parents('form:first').submit();
+ }
+ }
+});
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
new file mode 100644
index 00000000..588c9b6e
--- /dev/null
+++ b/client/components/main/header.jade
@@ -0,0 +1,40 @@
+template(name="header")
+ #header(class=currentBoard.colorClass)
+ //-
+ If the user is connected we display a small "quick-access" top bar that
+ list all starred boards with a link to go there. This is inspired by the
+ Reddit "subreddit" bar.
+ The first link goes to the boards page.
+ if currentUser
+ #header-quick-access
+ ul
+ li
+ +linkTo(route="Boards")
+ span.fa.fa-home
+ | All boards
+ each currentUser.starredBoards
+ li.separator -
+ li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
+ +linkTo(route="Board" data=this)
+ = title
+ else
+ li.current Star a board to add a shortcut in this bar.
+
+ li
+ a.js-create-board
+ i.fa.fa-plus(title="Create a new board")
+
+ +headerUserBar
+
+ //-
+ The main bar is a colorful bar that provide all the meta-data for the
+ current page. This bar is contextual based.
+ If the user is not connected we display "sign in" and "log in" buttons.
+ #header-main-bar
+ if $.Session.get 'currentBoard'
+ +headerBoard
+ else
+ +headerTitle
+
+template(name="headerTitle")
+ h1 LibreBoard
diff --git a/client/components/main/header.js b/client/components/main/header.js
new file mode 100644
index 00000000..2a545309
--- /dev/null
+++ b/client/components/main/header.js
@@ -0,0 +1,10 @@
+Template.header.helpers({
+ // Reactively set the color of the page from the color of the current board.
+ headerTemplate: function() {
+ return 'headerBoard';
+ }
+});
+
+Template.header.events({
+ 'click .js-create-board': Popup.open('createBoard')
+});
diff --git a/client/components/main/header.styl b/client/components/main/header.styl
new file mode 100644
index 00000000..1177d930
--- /dev/null
+++ b/client/components/main/header.styl
@@ -0,0 +1,266 @@
+@import 'nib'
+
+global-reset()
+
+#header
+ color: white
+ transition: background-color 0.4s
+ background: #27AE60
+
+ #header-quick-access
+ background-color: rgba(0, 0, 0, 0.2)
+ padding: 4px 10px
+ height: 16px
+ font-size: 12px
+ display: flex
+
+ ul li, #header-user-bar
+ color: darken(white, 17%)
+
+ a
+ color: inherit
+ text-decoration: none
+
+ &:hover
+ color: white
+
+ ul
+ flex: 1
+ transition: opacity 0.2s
+ margin-left: 5px
+
+ li
+ display: block
+ float: left
+ width: auto
+ color: darken(white, 15%)
+ padding: 0 4px 1px 4px
+
+ &.separator
+ padding: 0 2px 1px 2px
+
+ &.current
+ font-style: italic
+
+ &:first-child .fa-home
+ margin-right: 5px
+
+ #header-main-bar
+ height: 30px
+ padding: 8px
+
+ h1
+ font-size: 19px
+ line-height: 1.7em
+ margin: 0 20px 0 10px
+ float: left
+
+ &.header-board-menu
+ cursor: pointer
+
+ .fa-angle-down
+ font-size: 0.8em
+ // line-height: 1.1em
+ margin-left: 5px
+
+ .board-header-starred .fa
+ color: yellow
+
+ .board-header-members
+ float: right
+
+ .member
+ display: block
+ width: 32px
+ height: @width
+
+ .add-board-member
+ color: white
+ display: flex
+ align-items: center
+ justify-content: center
+ border: 1px solid white
+ height: 32px - 2px
+ width: @height
+
+ i.fa-plus
+ margin-top: 2px
+
+ .header-btn:last-child
+ margin-right: 0
+
+
+
+// #header {
+// background: #138871;
+// height: 30px;
+// overflow: hidden;
+// padding: 5px;
+// position: relative;
+// z-index: 10;
+// }
+
+// .header-logo {
+// bottom: 0;
+// display: block;
+// height: 25px;
+// left: 50%;
+// position: absolute;
+// top: 8px;
+// width: 80px;
+// margin-left: - @width/2;
+// text-align: center;
+// z-index: 2;
+// opacity: .5;
+// transition: opacity ease-in 85ms;
+// color: white;
+// font-size: 22px;
+// text-decoration: none;
+// background-image: url('/logos/white_logo.png');
+
+// &:hover {
+// opacity: .8;
+// color: white;
+// }
+// }
+
+// .header-btn.header-btn-feedback {
+// background: rgba(255, 255, 255, .1);
+// background: linear-gradient(to bottom, rgba(255, 255, 255, .1) 0, rgba(255, 255, 255, .05) 100%);
+// padding-left: 22px;
+// margin-right: 16px;
+
+// .header-btn-icon {
+// top: 1px;
+// }
+// }
+
+.header-btn {
+ border-radius: 3px;
+ user-select: none;
+ background: rgba(255, 255, 255, .3);
+ background: linear-gradient(to bottom, rgba(255, 255, 255, .3) 0, rgba(255, 255, 255, .2) 100%);
+ color: #f3f3f3;
+ display: block;
+ float: left;
+ font-weight: 700;
+ height: 30px;
+ line-height: 30px;
+ padding: 0 10px;
+ position: relative;
+ margin-right: 8px;
+ min-width: 30px;
+ text-decoration: none;
+ cursor: pointer;
+
+ .header-btn-icon {
+ font-size: 16px;
+ line-height: 28px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ &.new-notifications {
+ background: #ba1212;
+
+ &:hover {
+ background: #d11515;
+ }
+ }
+
+ &.header-member .member {
+ margin: 0;
+ border-top-left-radius: 3px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 3px;
+
+ &:hover .member-avatar {
+ opacity: 1;
+ }
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, .4);
+ background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%);
+ color: #fff;
+
+ .header-btn-count {
+ background: #d11515;
+ }
+ }
+
+ &:active {
+ background: rgba(255, 255, 255, .4);
+ background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%);
+ }
+
+ &.upgrade {
+ margin-right: 16px;
+
+ .icon-sm {
+ padding: 6px 2px 6px 4px;
+ }
+ }
+
+ &.upgrade,
+ &.header-boards {
+ padding-left: 4px;
+ }
+
+ &.header-boards {
+ padding-right: 4px;
+ }
+
+ &.header-login,
+ &.header-signup {
+ padding: 0 12px;
+ }
+
+ &.header-signup {
+ background: #48b512;
+ background: linear-gradient(to bottom, #48b512 0, #3d990f 100%);
+
+ &:hover {
+ background: #3d990f;
+ background: linear-gradient(to bottom, #3d990f 0, #327d0c 100%);
+ }
+
+ &:active {
+ background: #327d0c;
+ }
+ }
+
+ &.header-go-to-boards {
+ padding: 0 8px 0 38px;
+ }
+
+ &.header-go-to-boards .member {
+ border-top-left-radius: 3px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 3px;
+ position: absolute;
+ left: 0;
+ }
+}
+
+// .header-btn-text {
+// padding: 0 8px;
+// }
+
+// .header-notification-list ul {
+// margin-top: 8px;
+// }
+
+// .header-notification-list .action-comment {
+// max-height: 250px;
+// overflow-y: auto;
+// }
+
+// .header-user {
+// position: absolute;
+// top: 5px;
+// right: 0;
+// }
diff --git a/client/components/main/helpers.js b/client/components/main/helpers.js
new file mode 100644
index 00000000..7ad602f1
--- /dev/null
+++ b/client/components/main/helpers.js
@@ -0,0 +1,63 @@
+var Helpers = {
+ error: function() {
+ return Session.get('error');
+ },
+
+ toLowerCase: function(text) {
+ return text && text.toLowerCase();
+ },
+
+ toUpperCase: function(text) {
+ return text && text.toUpperCase();
+ },
+
+ firstChar: function(text) {
+ return text && text[0].toUpperCase();
+ },
+
+ session: function(prop) {
+ return Session.get(prop);
+ },
+
+ getUser: function(userId) {
+ return Users.findOne(userId);
+ }
+};
+
+// Register all Helpers
+_.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); });
+
+// XXX I believe we should compute a HTML rendered field on the server that
+// would handle markdown, emojies and user mentions. We can simply have two
+// fields, one source, and one compiled version (in HTML) and send only the
+// compiled version to most users -- who don't need to edit.
+// In the meantime, all the transformation are done on the client using the
+// Blaze API.
+var at = HTML.CharRef({html: '&commat;', str: '@'});
+Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
+ var view = this;
+ var content = Blaze.toHTML(view.templateContentBlock);
+ var currentBoard = Session.get('currentBoard');
+ var knowedUsers = _.map(currentBoard.members, function(member) {
+ member.username = Users.findOne(member.userId).username;
+ return member;
+ });
+
+ var mentionRegex = /\B@(\w*)/gi;
+ var currentMention, knowedUser, href, linkClass, linkValue, link;
+ while (currentMention = mentionRegex.exec(content)) {
+
+ knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] });
+ if (! knowedUser)
+ continue;
+
+ linkValue = [' ', at, knowedUser.username];
+ href = Router.url('Profile', { username: knowedUser.username });
+ linkClass = 'atMention' + (knowedUser.userId === Meteor.userId() ? ' me' : '');
+ link = HTML.A({ href: href, 'class': linkClass }, linkValue);
+
+ content = content.replace(currentMention[0], Blaze.toHTML(link));
+ }
+
+ return HTML.Raw(content);
+}));
diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade
new file mode 100644
index 00000000..18df4d9e
--- /dev/null
+++ b/client/components/main/layouts.jade
@@ -0,0 +1,17 @@
+head
+ title LibreBoard
+ meta(name="viewport"
+ content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
+ link(rel="shortcut icon" href="/favicon.png")
+
+template(name="userFormsLayout")
+ h1.at-form-landing-logo
+ img(src="/logo.png" title="LibreBoard")
+ +yield
+
+template(name="defaultLayout")
+ #surface
+ +header
+ #content
+ +yield
+
diff --git a/client/components/main/popup.js b/client/components/main/popup.js
new file mode 100644
index 00000000..53695d10
--- /dev/null
+++ b/client/components/main/popup.js
@@ -0,0 +1,16 @@
+Popup.template.events({
+ click: function(evt) {
+ if (evt.originalEvent) {
+ evt.originalEvent.clickInPopup = true;
+ }
+ },
+ 'click .js-back-view': function() {
+ Popup.back();
+ },
+ 'click .js-close-popover': function() {
+ Popup.close();
+ },
+ 'click .js-confirm': function() {
+ this.__afterConfirmAction.call(this);
+ }
+});
diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl
new file mode 100644
index 00000000..8c9993af
--- /dev/null
+++ b/client/components/main/popup.styl
@@ -0,0 +1,585 @@
+@import 'nib'
+
+.pop-over
+ background: #fff
+ border-radius: 3px
+ border: 1px solid #dbdbdb
+ border-bottom-color: #c2c2c2
+ box-shadow: 0 1px 6px rgba(0, 0, 0, .3)
+ display: none
+ overflow: hidden
+ position: absolute
+ width: 300px
+ z-index: 99999
+ margin-top: 5px
+
+ hr
+ margin: 4px -10px
+ width: 275px + 2*10px
+
+ input[type="text"],
+ input[type="email"],
+ input[type="password"]
+ margin: 4px 0 12px
+ width: 100%
+
+ input[type="file"]
+ width: 240px
+
+ select
+ width: 100%
+ margin-bottom: 14px
+
+ textarea
+ height: 72px
+ margin: 4px 0 12px
+ width: 100%
+
+ .empty
+ margin: 0
+
+ img
+ max-width: 270px
+
+ .custom-image img
+ height: 18px
+ left: 9px
+ top: 9px
+ width: 18px
+
+ .title
+ line-height: 32px
+
+ .header
+ height: 36px
+ position: relative
+ margin-bottom: 8px
+ background: #F7F7F7
+ border-bottom: 1px solid #dcdcdc
+ color: darken(white, 60%)
+
+ .header-title
+ display: block
+ line-height: 32px
+ padding-top: 4px
+ margin: 0 10px
+ font-weight: bold
+ overflow: hidden
+ text-overflow: ellipsis
+ white-space: nowrap
+
+ .back-btn, .close-btn
+ &:hover .icon-sm
+ color: darken(white, 80%)
+
+ .back-btn
+ padding: 10px
+ float: left
+
+ .close-btn
+ padding: 10px 10px 10px 4px
+ position: absolute
+ top: 0
+ right: 0
+
+ .content
+ overflow-x: hidden
+ overflow-y: auto
+ padding: 0 10px 10px
+ max-height: 550px
+
+ .quiet
+ padding: 6px 6px 4px
+
+ &.search-over
+ background: #f0f0f0
+ min-height: 114px
+
+ .header
+ display: none
+
+ .content
+ padding: 8px 4px 8px 10px
+ margin-right: 8px
+
+ &::-webkit-scrollbar-button
+ display: block
+ height: 4px
+ width: 4px
+
+.select-members-list
+ margin-bottom: 8px
+
+.pop-over-list
+
+ &.navigable li.not-selectable>a:hover,
+ li.not-selectable>a:hover
+ color: #8c8c8c
+ cursor: default
+
+ .icon-sm
+ color: #a6a6a6
+
+ li > a
+ cursor: pointer
+ display: block
+ font-weight: 700
+ padding: 6px 10px
+ position: relative
+ margin: 0 -10px
+ text-decoration: none
+
+ .item-name
+ display: block
+ width: auto
+ padding-right: 22px
+
+ &:hover
+ background-color: #005377
+ color: #fff
+
+ .sub-name,
+ .quiet
+ color: #eee
+
+ .unread-indicator
+ background: #fff
+
+ .icon-sm
+ color: #fff
+
+ .sub-name
+ clear: both
+ color: #8c8c8c
+ display: block
+ font-size: 12px
+ font-weight: 400
+ line-height: 15px
+ margin-top: 4px
+
+ &.current
+ background-color: #e2e6e9
+
+ .unread-indicator
+ background: #2e85b8
+ background: linear-gradient(to bottom, #2e85b8 0, #2b7cab 100%)
+ border-radius: 7px
+ display: block
+ height: 14px
+ opacity: 0
+ position: absolute
+ right: 16px
+ top: 8px
+ width: 14px
+
+ &.any
+ opacity: 1
+
+ &:active
+ background-color: #2e85b8
+
+ &.disabled
+ color: #8c8c8c
+ cursor: default
+
+ .vis-icon
+ opacity: .35
+
+ .icon-sm
+ color: #a6a6a6
+
+ &:hover
+ background: none
+
+ .sub-name,
+ .quiet
+ color: #8c8c8c
+
+ .icon-sm
+ color: #a6a6a6
+
+ &:active
+ background: none
+
+ &.inset li > a
+ border-radius: 3px
+ margin: 0
+
+ .pop-over-list.checkable
+
+ .icon-check
+ display: none
+ position: absolute
+ top: 6px
+ right: 12px
+
+ li.active a
+ padding-right: 28px
+
+ .icon-check
+ display: block
+
+ &.left-check
+
+ .icon-check
+ right: auto
+ left: 10px
+
+ li a
+ padding-right: 10px
+ padding-left: 30px
+
+ li.active a
+ padding-right: 10px
+
+ &.normal-weight li>a
+ font-weight: 400
+
+ &.navigable
+
+ li > a:hover
+ background-color: transparent
+ color: #4d4d4d
+
+ .sub-name,
+ .quiet
+ color: #8c8c8c
+
+ .icon-sm
+ color: #a6a6a6
+
+ li.selected > a
+ background-color: #005377
+ color: #fff
+
+ .sub-name,
+ .quiet
+ color: #eee
+
+ li.selected > a
+
+ &.current
+ background-color: #005377
+
+ .unread-indicator
+ background: #fff
+
+ .icon-sm
+ color: #fff
+
+ &:active
+ background-color: #005377
+
+.pop-over.miniprofile
+
+ .header
+ border-bottom-color: transparent
+ height: 30px
+ position: absolute
+ right: 0
+ top: 0
+ width: 60px
+ z-index: 1
+
+ .header-title
+ display: none
+
+ .pop-over-list
+ padding-top: 8px
+
+.mini-profile-info
+ margin-top: 8px
+ min-height: 56px
+ position: relative
+
+ .member-large
+ position: absolute
+ top: 2px
+ left: 2px
+
+ .info
+ margin: 0 0 0 64px
+ word-wrap: break-word
+
+ h3 a
+ text-decoration: none
+
+ &:hover
+ text-decoration: underline
+
+.pop-over.avdetail .header
+ border-bottom-color: transparent
+ height: 20px
+ position: absolute
+ top: 8px
+ left: 8px
+ right: 8px
+ z-index: 0
+
+.pop-over.avdetail .header-title
+ display: none
+
+.pop-over.avdetail .content
+ text-align: center
+
+.pop-over.avdetail .mem-info
+ margin: 2px 24px 8px
+ position: relative
+ z-index: 1
+ width: 222px
+
+.pop-over.avdetail .mem-info h3 a
+ text-decoration: none
+
+.pop-over.avdetail .mem-info h3 a:hover
+ text-decoration: underline
+
+.pop-over-label-list li,
+.pop-over-member-list li
+
+ &.disabled a
+ cursor:default
+
+ &:not(.disabled):hover a
+ background-color: #005377
+ color: #fff
+
+
+.pop-over-label-list,
+.pop-over-member-list,
+.pop-over-emoji-list,
+.pop-over-card-list
+ li
+ a
+ border-radius: 3px
+ display: block
+ height: 30px
+ line-height: 30px
+ overflow: hidden
+ position: relative
+ text-overflow: ellipsis
+ text-decoration: none
+ white-space: nowrap
+ padding: 4px
+ margin-bottom: 2px
+
+ &.multi-line
+ line-height: 16px
+
+ .member
+ margin-right: 8px
+
+ .card-label
+ float: left
+ height: 30px
+ margin: 0 8px 0 0
+ padding: 0
+ width: 30px
+
+ .option,
+ .icon-check
+ background-clip: content-box
+ background-origin: content-box
+ padding: 11px
+ position: absolute
+ top: 0
+ right: 0
+
+ .sub-name
+ font-size: 12px
+
+
+ &:last-child a
+ margin-bottom: 0
+
+ &.disabled
+ opacity: .5
+
+ &.active a,
+ &.selected a
+ background: none
+ color: #4d4d4d
+ cursor: default
+
+ .quiet
+ color: #8c8c8c
+
+ &.email-invite
+
+ .member
+ display: none
+
+ a
+ padding: 0 10px
+
+ &.selected a
+ background-color: #005377
+ color: #fff
+
+ .quiet
+ color: #eee
+
+ .card-label
+ border-radius: 3px
+
+ .icon-check
+ color: #fff
+
+ &.active a .icon-check
+ display: block
+
+ &.unconfirmed a.name
+ line-height: 16px
+
+ &.options li
+
+ &.selected a
+ padding-right: 28px
+
+ .option
+ display: block
+ opacity: .5
+
+ &:hover
+ opacity: 1
+
+ &.disabled.selected a
+ padding-right: 0
+
+ .option
+ display: none
+
+
+ &.no-option.selected a
+ padding-right: 6px
+
+ .option
+ display: none
+
+ &.collapsed
+
+ &.checkable li.active a
+ padding-right: 0
+
+ li
+ float: left
+ margin: 0 3px 3px 0
+
+ a
+ padding: 0
+ margin: 0
+ width: 30px
+
+ .member
+ opacity: .8
+
+ .full-name
+ display: none
+
+ &.selected a .member,
+ &.active.selected a .member
+ border-color: #005377
+ opacity: .9
+
+ &.active a
+
+ .member
+ border-color: #2e85b8
+ opacity: 1
+
+ .icon-check
+ border-radius: 3px
+ background-color: #2e85b8
+ bottom: 0
+ color: #fff
+ display: block
+ padding: 0
+ right: 0
+ top: auto
+
+ &.checkable li.active a
+ padding-right: 28px
+
+ &.filtered li
+ display: none
+
+ &.matches-filter
+ display: block
+
+ &.limited li.exceeds-limit
+ display: none
+
+.pop-over-emoji-list li > a
+ padding: 2px 4px
+
+ .emoji
+ margin: 0 6px
+
+.pop-over-card-list li > a
+ padding: 2px 4px
+
+.login-signup-popover
+ padding: 15px
+
+ .form-tabs
+ display: none
+
+ h1
+ margin-bottom: 15px
+
+ p
+ margin: 8px 0
+
+ .form-parts-container
+ position: relative
+
+ .active-box
+ position: absolute
+ top: 0
+ background: #e2e2e2
+ border: 1px solid #c9c9c9
+ border-radius: 3px
+ z-index: 1
+ height: 100%
+ width: 49%
+ transition-property: all
+ transition-duration: .4s
+ opacity: 1
+
+ &.start
+ opacity: 0
+ left: 25%
+
+ .signup-form,
+ .login-form
+ position: relative
+ box-sizing: border-box
+ padding: 20px
+ width: 50%
+ z-index: 2
+ opacity: .3
+ transition-property: opacity
+ transition-duration: .2s
+
+ .active
+ opacity: 1
+
+
+ .js-signup-form-pos
+ left: 0
+
+ .login-form
+ position: absolute
+ top: 0
+
+ .login-form .icon-google
+ position: absolute
+ left: 5px
+ top: 3px
+
+ .login-form .button.google
+ padding-left: 40px
+ margin: 0 0 15px 0
+
+ .js-login-form-pos
+ left: 50%
diff --git a/client/components/main/popup.tpl.jade b/client/components/main/popup.tpl.jade
new file mode 100644
index 00000000..ba24db0a
--- /dev/null
+++ b/client/components/main/popup.tpl.jade
@@ -0,0 +1,13 @@
+.pop-over.clearfix(
+ class="{{#unless title}}miniprofile{{/unless}}"
+ class=currentBoard.colorClass
+ style="display:block; left:{{offset.left}}px; top:{{offset.top}}px;")
+ .header.clearfix
+ if hasPopupParent
+ a.back-btn.js-back-view
+ i.fa.fa-chevron-left
+ span.header-title= title
+ a.close-btn.js-close-popover
+ i.fa.fa-times
+ .content.clearfix
+ +Template.dynamic(template=popupName data=dataContext)
diff --git a/client/components/main/rendered.js b/client/components/main/rendered.js
new file mode 100644
index 00000000..787e8225
--- /dev/null
+++ b/client/components/main/rendered.js
@@ -0,0 +1,40 @@
+Template.editor.rendered = function() {
+ this.$('textarea').textcomplete([
+ // Emojies
+ {
+ match: /\B:([\-+\w]*)$/,
+ search: function(term, callback) {
+ callback($.map(Emoji.values, function(emoji) {
+ return emoji.indexOf(term) === 0 ? emoji : null;
+ }));
+ },
+ template: function(value) {
+ var image = '<img src="' + Emoji.baseImagePath + value + '.png"></img>';
+ return image + value;
+ },
+ replace: function(value) {
+ return ':' + value + ':';
+ },
+ index: 1
+ },
+
+ // User mentions
+ {
+ match: /\B@(\w*)$/,
+ search: function(term, callback) {
+ var currentBoard = Boards.findOne(Session.get('currentBoard'));
+ callback($.map(currentBoard.members, function(member) {
+ var username = Users.findOne(member.userId).username;
+ return username.indexOf(term) === 0 ? username : null;
+ }));
+ },
+ template: function(value) {
+ return value;
+ },
+ replace: function(username) {
+ return '@' + username + ' ';
+ },
+ index: 1
+ }
+ ]);
+};
diff --git a/client/components/main/router.js b/client/components/main/router.js
new file mode 100644
index 00000000..bae832e8
--- /dev/null
+++ b/client/components/main/router.js
@@ -0,0 +1,5 @@
+Router.route('/', {
+ name: 'Home',
+ redirectLoggedInUsers: true,
+ authenticated: true
+});
diff --git a/client/components/main/spinner.styl b/client/components/main/spinner.styl
new file mode 100644
index 00000000..f4b8cc86
--- /dev/null
+++ b/client/components/main/spinner.styl
@@ -0,0 +1,45 @@
+/*
+ * From https://github.com/tobiasahlin/SpinKit
+ *
+ * Usage:
+ *
+ * <div class="sk-spinner sk-spinner-wave">
+ * <div class="sk-rect1"></div>
+ * <div class="sk-rect2"></div>
+ * <div class="sk-rect3"></div>
+ * <div class="sk-rect4"></div>
+ * <div class="sk-rect5"></div>
+ * </div>
+ *
+ */
+
+.sk-spinner-wave {
+
+ &.sk-spinner {
+ width: 50px;
+ height: 50px;
+ margin: auto;
+ margin-top: 30vh;
+ text-align: center;
+ font-size: 10px;
+ }
+
+ div {
+ background-color: #333;
+ height: 100%;
+ width: 6px;
+ display: inline-block;
+
+ animation: sk-waveStretchDelay 1.2s infinite ease-in-out;
+ }
+
+ .sk-rect2 { animation-delay: -1.1s }
+ .sk-rect3 { animation-delay: -1.0s }
+ .sk-rect4 { animation-delay: -0.9s }
+ .sk-rect5 { animation-delay: -0.8s }
+}
+
+@keyframes sk-waveStretchDelay {
+ 0%, 40%, 100% { transform: scaleY(0.4) }
+ 20% { transform: scaleY(1.0) }
+}
diff --git a/client/components/main/spinner.tpl.jade b/client/components/main/spinner.tpl.jade
new file mode 100644
index 00000000..9310a6e5
--- /dev/null
+++ b/client/components/main/spinner.tpl.jade
@@ -0,0 +1,6 @@
+.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass)
+ .sk-rect1
+ .sk-rect2
+ .sk-rect3
+ .sk-rect4
+ .sk-rect5
diff --git a/client/components/main/templates.html b/client/components/main/templates.html
new file mode 100644
index 00000000..e9be0f93
--- /dev/null
+++ b/client/components/main/templates.html
@@ -0,0 +1,18 @@
+<template name="notfound">
+ {{ > message label='page-not-found'}}
+</template>
+
+<template name='message'>
+ <div class="big-message quiet {{ color }}">
+ <h1>{{_ label}}</h1>
+ {{#with pathFor route='Login'}}
+ <p>{{{_ 'page-maybe-private' this}}}</p>
+ {{/with}}
+ </div>
+</template>
+
+<template name="editor">
+ <textarea class="{{class}}" placeholder="{{_ 'comment-placeholder'}}" id="{{id}}" tabindex="1">{{> UI.contentBlock }}</textarea>
+</template>
+
+<template name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template>
diff --git a/client/components/modal/events.js b/client/components/modal/events.js
new file mode 100644
index 00000000..2943f841
--- /dev/null
+++ b/client/components/modal/events.js
@@ -0,0 +1,14 @@
+Template.modal.events({
+ 'click .window-overlay': function(event) {
+ // We only want to catch the event if the user click on the .window-overlay
+ // div itself, not a child (ie, not the overlay window)
+ if (event.target !== event.currentTarget)
+ return;
+ Utils.goBoardId(this.card.board()._id);
+ event.preventDefault();
+ },
+ 'click .js-close-window': function(event) {
+ Utils.goBoardId(this.card.board()._id);
+ event.preventDefault();
+ }
+});
diff --git a/client/components/modal/helpers.js b/client/components/modal/helpers.js
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/client/components/modal/helpers.js
diff --git a/client/components/modal/modal.tpl.jade b/client/components/modal/modal.tpl.jade
new file mode 100644
index 00000000..2f40ad75
--- /dev/null
+++ b/client/components/modal/modal.tpl.jade
@@ -0,0 +1,5 @@
+.window-overlay.show
+ .window
+ .window-wrapper.clearfix
+ a.icon-lg.fa.fa-times.dialog-close-button.js-close-window(title="{{_ 'modal-close-title'}}")
+ +UI.dynamic(template=template)
diff --git a/client/components/sidebar/events.js b/client/components/sidebar/events.js
new file mode 100644
index 00000000..1067421f
--- /dev/null
+++ b/client/components/sidebar/events.js
@@ -0,0 +1,93 @@
+Template.filterSidebar.events({
+ 'click .js-toggle-label-filter': function(event) {
+ Filter.labelIds.toogle(this._id);
+ Filter.resetExceptions();
+ event.preventDefault();
+ },
+ 'click .js-toogle-member-filter': function(event) {
+ Filter.members.toogle(this._id);
+ Filter.resetExceptions();
+ event.preventDefault();
+ },
+ 'click .js-clear-all': function(event) {
+ Filter.reset();
+ event.preventDefault();
+ }
+});
+
+var getMemberIndex = function(board, searchId) {
+ for (var i = 0; i < board.members.length; i++) {
+ if (board.members[i].userId === searchId)
+ return i;
+ }
+ throw new Meteor.Error('Member not found');
+};
+
+Template.memberPopup.events({
+ 'click .js-filter-member': function() {
+ Filter.members.toogle(this.userId);
+ Popup.close();
+ },
+ 'click .js-change-role': Popup.open('changePermissions'),
+ 'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
+ var currentBoard = Boards.findOne(Session.get('currentBoard'));
+ var memberIndex = getMemberIndex(currentBoard, this.userId);
+ var setQuery = {};
+ setQuery[['members', memberIndex, 'isActive'].join('.')] = false;
+ Boards.update(currentBoard._id, { $set: setQuery });
+ Popup.close();
+ }),
+ 'click .js-leave-member': function() {
+ // @TODO
+ Popup.close();
+ }
+});
+
+Template.membersWidget.events({
+ 'click .js-open-manage-board-members': Popup.open('addMember'),
+ 'click .member': Popup.open('member')
+});
+
+Template.labelsWidget.events({
+ 'click .js-label': Popup.open('editLabel'),
+ 'click .js-add-label': Popup.open('createLabel')
+});
+
+// Template.addMemberPopup.events({
+// 'click .pop-over-member-list li:not(.disabled)': function(event, t) {
+// var userId = this._id;
+// var boardId = t.data.board._id;
+// var currentMembersIds = _.pluck(t.data.board.members, 'userId');
+// if (currentMembersIds.indexOf(userId) === -1) {
+// Boards.update(boardId, {
+// $push: {
+// members: {
+// userId: userId,
+// isAdmin: false,
+// isActive: true
+// }
+// }
+// });
+// } else {
+// var memberIndex = getMemberIndex(t.data.board, userId);
+// var setQuery = {};
+// setQuery[['members', memberIndex, 'isActive'].join('.')] = true;
+// Boards.update(boardId, { $set: setQuery });
+// }
+// Popup.close();
+// }
+// });
+
+// Template.changePermissionsPopup.events({
+// 'click .js-set-admin, click .js-set-normal': function(event) {
+// var currentBoard = Boards.findOne(Session.get('currentBoard'));
+// var memberIndex = getMemberIndex(currentBoard, this.user._id);
+// var isAdmin = $(event.currentTarget).hasClass('js-set-admin');
+// var setQuery = {};
+// setQuery[['members', memberIndex, 'isAdmin'].join('.')] = isAdmin;
+// Boards.update(currentBoard._id, {
+// $set: setQuery
+// });
+// Popup.back(1);
+// }
+// });
diff --git a/client/components/sidebar/helpers.js b/client/components/sidebar/helpers.js
new file mode 100644
index 00000000..a76dad7f
--- /dev/null
+++ b/client/components/sidebar/helpers.js
@@ -0,0 +1,51 @@
+var widgetTitles = {
+ filter: 'filter-cards',
+ background: 'change-background'
+};
+
+Template.boardSidebar.helpers({
+ currentWidget: function() {
+ return Session.get('currentWidget') + 'Sidebar';
+ },
+ currentWidgetTitle: function() {
+ return TAPi18n.__(widgetTitles[Session.get('currentWidget')]);
+ }
+});
+
+// Template.addMemberPopup.helpers({
+// isBoardMember: function() {
+// var user = Users.findOne(this._id);
+// return user && user.isBoardMember();
+// }
+// });
+
+Template.memberPopup.helpers({
+ user: function() {
+ return Users.findOne(this.userId);
+ },
+ memberType: function() {
+ var type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
+ return TAPi18n.__(type).toLowerCase();
+ }
+});
+
+// Template.removeMemberPopup.helpers({
+// user: function() {
+// return Users.findOne(this.userId)
+// },
+// board: function() {
+// return currentBoard();
+// }
+// });
+
+// Template.changePermissionsPopup.helpers({
+// isAdmin: function() {
+// return this.user.isBoardAdmin();
+// },
+// isLastAdmin: function() {
+// if (! this.user.isBoardAdmin())
+// return false;
+// var nbAdmins = _.where(currentBoard().members, { isAdmin: true }).length;
+// return nbAdmins === 1;
+// }
+// });
diff --git a/client/components/sidebar/infiniteScrolling.js b/client/components/sidebar/infiniteScrolling.js
new file mode 100644
index 00000000..df3b8901
--- /dev/null
+++ b/client/components/sidebar/infiniteScrolling.js
@@ -0,0 +1,37 @@
+var peakAnticipation = 200;
+
+Mixins.InfiniteScrolling = BlazeComponent.extendComponent({
+ onCreated: function() {
+ this._nextPeak = Infinity;
+ },
+
+ setNextPeak: function(v) {
+ this._nextPeak = v;
+ },
+
+ getNextPeak: function() {
+ return this._nextPeak;
+ },
+
+ resetNextPeak: function() {
+ this._nextPeak = Infinity;
+ },
+
+ // To be overwritten by consumers of this mixin
+ reachNextPeak: function() {
+
+ },
+
+ events: function() {
+ return [{
+ scroll: function(evt) {
+ var domElement = evt.currentTarget;
+ var altitude = domElement.scrollTop + domElement.offsetHeight;
+ altitude += peakAnticipation;
+ if (altitude >= this.callFirstWith(null, 'getNextPeak')) {
+ this.callFirstWith(null, 'reachNextPeak');
+ }
+ }
+ }];
+ }
+});
diff --git a/client/components/sidebar/rendered.js b/client/components/sidebar/rendered.js
new file mode 100644
index 00000000..2b58bf33
--- /dev/null
+++ b/client/components/sidebar/rendered.js
@@ -0,0 +1,21 @@
+Template.membersWidget.rendered = function() {
+ if (! Meteor.user().isBoardMember())
+ return;
+
+ _.each(['.js-member', '.js-label'], function(className) {
+ Utils.liveEvent('mouseover', function($this) {
+ $this.find(className).draggable({
+ appendTo: 'body',
+ helper: 'clone',
+ revert: 'invalid',
+ revertDuration: 150,
+ snap: false,
+ snapMode: 'both',
+ start: function() {
+ Popup.close();
+ }
+ });
+ });
+ });
+};
+
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
new file mode 100644
index 00000000..3f0142d4
--- /dev/null
+++ b/client/components/sidebar/sidebar.js
@@ -0,0 +1,55 @@
+BlazeComponent.extendComponent({
+ template: function() {
+ return 'boardSidebar';
+ },
+
+ mixins: function() {
+ return [Mixins.InfiniteScrolling];
+ },
+
+ onCreated: function() {
+ this._isOpen = new ReactiveVar(true);
+ },
+
+ isOpen: function() {
+ return this._isOpen.get();
+ },
+
+ open: function() {
+ if (! this._isOpen.get()) {
+ this._isOpen.set(true);
+ }
+ },
+
+ hide: function() {
+ if (this._isOpen.get()) {
+ this._isOpen.set(false);
+ }
+ },
+
+ toogle: function() {
+ this._isOpen.set(! this._isOpen.get());
+ },
+
+ calculateNextPeak: function() {
+ var altitude = this.find('.js-board-sidebar-content').scrollHeight;
+ this.callFirstWith(this, 'setNextPeak', altitude);
+ },
+
+ reachNextPeak: function() {
+ var activitiesComponent = this.componentChildren('activities')[0];
+ activitiesComponent.loadNextPage();
+ },
+
+ isTongueHidden: function() {
+ return this.isOpen() && Filter.isActive();
+ },
+
+ events: function() {
+ // XXX Hacky, we need some kind of `super`
+ var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
+ return mixinEvents.concat([{
+ 'click .js-toogle-sidebar': this.toogle
+ }]);
+ }
+}).register('boardSidebar');
diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl
new file mode 100644
index 00000000..4b741dc7
--- /dev/null
+++ b/client/components/sidebar/sidebar.styl
@@ -0,0 +1,154 @@
+@import 'nib'
+
+.sidebar
+ .sidebar-content
+ padding: 10px 20px
+ background: white
+ box-shadow: -10px 0px 5px -10px darken(white, 30%)
+ z-index: 10
+ position: absolute
+ top: 0
+ bottom: 0
+ right: 0
+ left: 0
+ overflow-x: hidden
+ overflow-y: auto
+
+ h3
+ color: darken(white, 50%)
+
+ hr
+ margin: 8px 0
+
+.board-sidebar
+ width: 248px
+ position: absolute
+ top: 0
+ right: -@width
+ bottom: 0
+ transition: top .1s, right .1s, width .1s
+
+ &.is-open
+ right: 0
+
+.board-widget-nav
+ border-radius: 3px
+ background: #dcdcdc
+ overflow: hidden
+ padding: 0
+ position: relative
+
+ .toggle-widget-nav
+ border-radius: 3px
+ color: #8c8c8c
+ margin: 0
+ padding: 7px 10px
+ position: relative
+ cursor: pointer
+
+ .toggle-menu-icon
+ position: absolute
+ top: 8px
+ right: 8px
+
+ &:hover
+ background: #ccc
+ color: #4d4d4d
+
+ .nav-list
+ display: block
+ opacity: 1
+ max-height: 400px
+
+ hr
+ margin: 2px 0
+ color: #ccc
+ background: #ccc
+
+ .nav-list-item
+ display: block
+ font-weight: 700
+ line-height: 30px
+ overflow: hidden
+ padding: 0 8px 0 36px
+ position: relative
+ text-decoration: none
+
+ .icon-type
+ left: 10px
+ position: absolute
+ top: 6px
+
+ &:hover
+ background: #ccc
+
+ .icon-type
+ color: #686868
+
+ .nav-list-sub-item
+ font-weight: 400
+ color: #666
+
+ &:hover
+ color: #4d4d4d
+
+ &.collapsed
+
+ .nav-list
+ max-height: 0
+ opacity: 0
+
+ hr
+ margin: 0
+
+ .toggle-widget-nav
+ color: #4d4d4d
+
+
+.board-widget-title
+ display: block
+ min-height: 20px
+ margin-bottom: 6px
+
+.board-widget-content
+ position: relative
+ z-index: 1
+
+.board-widget h4
+ margin: 5px 0
+
+.board-widget-activity
+ margin-right: -4px
+
+.sidebar-tongue
+ display: block
+ width: 30px
+ height: @width
+ left: -@width
+ position: absolute
+ top: 12px
+ z-index: 15
+ background: white
+ border-radius: left 3px
+ box-shadow: -4px 0px 7px -4px darken(white, 30%)
+ color: darken(white, 50%)
+ transition: left .1s
+
+ i.fa
+ margin: 9px
+ transition: transform 0.5s
+
+ .board-sidebar.is-open &
+ left: -@width + 2px
+
+ // XXX Bug: we should add a padding left
+ &:hover
+ left: -@width + 5px
+
+ i.fa
+ transform: rotate(180deg)
+
+ &.is-hidden,
+ .board-sidebar.is-open &.is-hidden
+ z-index: 0
+ left: 5px
diff --git a/client/components/sidebar/templates.html.old b/client/components/sidebar/templates.html.old
new file mode 100644
index 00000000..d8b063f0
--- /dev/null
+++ b/client/components/sidebar/templates.html.old
@@ -0,0 +1,307 @@
+<template name="boardWidgets">
+ <a href="#" class="sidebar-show-btn dark-hover js-show-sidebar">
+ <span class="icon-sm fa fa-chevron-left"></span>
+ <span class="text">{{_ 'show-sidebar'}}</span>
+ </a>
+ <div class="board-widgets {{#if session 'sidebarIsOpen'}}show{{else}}hide{{/if}}">
+ <div>
+ <a href="#" class="sidebar-hide-btn dark-hover js-hide-sidebar" title="{{_ 'close-sidebar-title'}}">
+ <span class="icon-sm fa fa-chevron-right"></span>
+ </a>
+ {{#unless isTrue currentWidget "homeWidget"}}
+ <div class="board-widgets-title clearfix">
+ <a href="#" class="board-sidebar-back-btn js-pop-widget-view">
+ <span class="left-arrow"></span>{{_ 'back'}}
+ </a>
+ <h3 class="text">{{currentWidgetTitle}}</h3>
+ <hr>
+ </div>
+ {{/unless}}
+ <div class="board-widgets-content-wrapper">
+ <div class="board-widgets-content default fancy-scrollbar short{{#unless session 'menuWidgetIsOpen'}} short{{/unless}}">
+ {{> UI.dynamic template=currentWidget data=this }}
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<template name="homeWidget">
+{{ > menuWidget }}
+{{ > membersWidget }}
+{{ > activityWidget }}
+</template>
+
+<template name="menuWidget">
+ <div class="board-widget board-widget-nav clearfix{{#unless session 'menuWidgetIsOpen'}} collapsed{{/unless}}">
+ <h3 class="dark-hover toggle-widget-nav js-toggle-widget-nav">{{_ 'menu'}}
+ <span class="icon-sm fa fa-chevron-circle-down toggle-menu-icon"></span>
+ </h3>
+ <ul class="nav-list">
+ <hr style="margin-top: 0;">
+ <li>
+ <a href="#" class="nav-list-item js-open-archive">
+ <span class="icon-sm fa fa-archive icon-type"></span>
+ {{_ 'archived-items'}}
+ </a>
+ </li>
+ <li>
+ <a href="#" class="nav-list-item js-open-card-filter">
+ <span class="icon-sm fa fa-filter icon-type"></span>
+ {{_ 'filter-cards'}}
+ </a>
+ </li>
+ {{#if currentUser.isBoardAdmin}}
+ <hr>
+ <li>
+ <a class="nav-list-item nav-list-sub-item board-settings-background js-change-background">
+ <span class="board-settings-background-preview" style="background-color:{{board.background.color}}"></span>
+ {{_ 'change-background'}}…
+ </a>
+ </li>
+ {{#unless isSandstorm }}
+ <li>
+ <a class="nav-list-item nav-list-sub-item js-close-board" href="#">{{_ 'close-board'}}</a>
+ </li>
+ {{/unless}}
+ {{/if}}
+ {{!
+ XXX Language should be handled by sandstorm, but for now display a language selection link in the board menu.
+ This link is normally present in the header bar that is not displayed on sandstorm.
+ }}
+ {{#if isSandstorm}}
+ <hr>
+ <li>
+ <a class="nav-list-item nav-list-sub-item js-language">{{_ 'language'}}</a>
+ </li>
+ {{/if}}
+ </ul>
+ </div>
+</template>
+
+<template name="membersWidget">
+ <hr>
+ <div class="board-widget board-widget-members clearfix">
+ <div class="board-widget-title">
+ <h3>{{_ 'members'}}</h3>
+ </div>
+ <div class="board-widget-content">
+ <div class="board-widget-members js-list-board-members clearfix js-list-draggable-board-members">
+ {{# each board.members }}
+ {{> userAvatar userId=this.userId draggable=true size="small" showBadges=true}}
+ {{/ each }}
+ </div>
+ {{# unless isSandstrom }}
+ {{# if currentUser.isBoardAdmin }}
+ <a href="#" class="button-link js-open-manage-board-members">
+ <span class="icon-sm fa fa-user"></span> {{_ 'add-members'}}
+ </a>
+ {{/ if }}
+ {{/ unless }}
+ </div>
+ </div>
+</template>
+
+<template name="activityWidget">
+ {{# if board.activities.count }}
+ <hr>
+ <div class="board-widget board-widget-activity bottom clearfix">
+ <div class="board-widget-title">
+ <h3>{{_ 'activity'}}</h3>
+ </div>
+ <div class="board-widget-content">
+ <div class="activity-gradient-t"></div>
+ <div class="activity-gradient-b"></div>
+ <div class="board-actions-list fancy-scrollbar">
+ {{ > activities }}
+ </div>
+ </div>
+ </div>
+ {{/if}}
+</template>
+
+<template name="memberPopup">
+ <div class="board-member-menu">
+ <div class="mini-profile-info">
+ {{> userAvatar user=user}}
+ <div class="info">
+ <h3 class="bottom" style="margin-right: 40px;">
+ <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a>
+ </h3>
+ <p class="quiet bottom">@{{ user.username }}</p>
+ </div>
+ </div>
+ {{# if currentUser.isBoardMember }}
+ <ul class="pop-over-list">
+ {{# if currentUser.isBoardAdmin }}
+ <li>
+ <a class="js-change-role" href="#">
+ {{_ 'change-permissions'}} <span class="quiet" style="font-weight: normal;">({{ memberType }})</span>
+ </a>
+ </li>
+ {{/ if }}
+
+ <li>
+ {{# if currentUser.isBoardAdmin }}
+ <a class="js-remove-member">{{_ 'remove-from-board'}}</a>
+ {{ else }}
+ <a class="js-leave-member">{{_ 'leave-board'}}</a>
+ {{/ if }}
+ </li>
+ </ul>
+ {{/ if }}
+ </div>
+</template>
+
+<template name="filterWidget">
+ <ul class="pop-over-label-list checkable">
+ {{#each board.labels}}
+ <li class="item matches-filter">
+ <a class="name js-toggle-label-filter">
+ <span class="card-label card-label-{{color}}"></span>
+ <span class="full-name">
+ {{#if name}}
+ {{name}}
+ {{else}}
+ <span class="quiet">{{_ "label-default" color}}</span>
+ {{/if}}
+ </span>
+ {{#if Filter.labelIds.isSelected _id}}
+ <span class="icon-sm fa fa-check"></span>
+ {{/if}}
+ </a>
+ </li>
+ {{/each}}
+ </ul>
+ <hr>
+ <ul class="pop-over-member-list checkable">
+ {{#each board.members}}
+ {{#with getUser userId}}
+ <li class="item js-member-item {{#if Filter.members.isSelected _id}}active{{/if}}">
+ <a href="#" class="name js-toogle-member-filter">
+ {{> userAvatar user=this size="small" }}
+ <span class="full-name">
+ {{ profile.name }}
+ (<span class="username">{{ username }}</span>)
+ </span>
+ {{#if Filter.members.isSelected _id}}
+ <span class="icon-sm fa fa-check checked-icon"></span>
+ {{/if}}
+ </a>
+ </li>
+ {{/with}}
+ {{/each}}
+ </ul>
+ <hr>
+ <ul class="pop-over-list inset normal-weight">
+ <li>
+ <a class="js-clear-all {{#unless Filter.isActive}}disabled{{/unless}}" style="padding-left: 40px;">
+ {{_ 'filter-clear'}}
+ </a>
+ </li>
+ </ul>
+</template>
+
+<template name="backgroundWidget">
+ <div class="board-widgets-content-wrapper fancy-scrollbar">
+ <div class="board-widgets-content">
+ <div class="board-backgrounds-list clearfix">
+ {{#each backgroundColors}}
+ <div class="board-background-select js-select-background">
+ <span class="background-box " style="background-color: {{this}}; "></span>
+ </div>
+ {{/each}}
+ </div>
+ {{!--
+ <h2 class="clear">Photos</h2>
+ <div class="board-backgrounds-list relative clearfix js-gold-photos-list disabled">
+ <div class="board-background-select js-select-background">
+ <span class="background-box " style="background-image: url(&quot;{{url}}&quot;);">
+ <a class="background-option js-background-attribution" href={{href}} target="_blank" title={{title}}>
+ <img src="https://d78fikflryjgj.cloudfront.net/images/d906fe5c1274c56c5571d49705547587/cc.png" style="height: 14px; width: 14px; vertical-align: text-top;" title="http://creativecommons.org/licenses/by/2.0/deed.en">
+ <span class="text" style="margin-left: 2px;">{{author}}</span>
+ </a>
+ </span>
+ </div>
+ </div>
+ --}}
+ </div>
+ </div>
+</template>
+
+<template name="closeBoardPopup">
+ <p>{{_ 'close-board-pop'}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'close'}}">
+</template>
+
+<template name="removeMemberPopup">
+ <p>{{_ 'remove-member-pop'
+ name=user.profile.name
+ username=user.username
+ boardTitle=board.title}}</p>
+ <input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}">
+</template>
+
+<template name="addMemberPopup">
+ <div class="search-with-spinner">
+ {{> esInput index="users" }}
+ </div>
+
+ <div class="manage-member-section hide js-search-results" style="display: block;">
+ <ul class="pop-over-member-list options js-list">
+ {{# esEach index="users"}}
+ <li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}">
+ <a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})">
+ {{> userAvatar user=this size="small" }}
+ <span class="full-name">
+ {{ profile.name }} (<span class="username">{{ username }}</span>)
+ </span>
+ {{# if isBoardMember }}
+ <div class="extra-text quiet">({{_ 'joined'}})</div>
+ {{/if}}
+ <span class="icon-sm fa fa-chevron-right light option js-open-option"></span>
+ </a>
+ </li>
+ {{/esEach }}
+ </ul>
+ </div>
+
+ {{# ifEsIsSearching index='users' }}
+ <div class="tac">
+ <span class="tabbed-pane-main-col-loading-spinner spinner"></span>
+ </div>
+ {{ /ifEsIsSearching }}
+
+ {{# ifEsHasNoResults index="users" }}
+ <div class="manage-member-section js-no-results">
+ <p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p>
+ </div>
+ {{ /ifEsHasNoResults }}
+
+ <div class="manage-member-section js-helper">
+ <p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p>
+ </div>
+</template>
+
+<template name="changePermissionsPopup">
+ <ul class="pop-over-list">
+ <li>
+ <a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}">
+ {{_ 'admin'}}
+ {{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}}
+ <span class="sub-name">{{_ 'admin-desc'}}</span>
+ </a>
+ </li>
+ <li>
+ <a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}">
+ {{_ 'normal'}}
+ {{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}}
+ <span class="sub-name">{{_ 'normal-desc'}}</span>
+ </a>
+ </li>
+ </ul>
+ {{#if isLastAdmin}}
+ <hr>
+ <p class="quiet bottom">{{_ 'last-admin-desc'}}</p>
+ {{/if}}
+</template>
diff --git a/client/components/sidebar/templates.jade b/client/components/sidebar/templates.jade
new file mode 100644
index 00000000..23a1a87e
--- /dev/null
+++ b/client/components/sidebar/templates.jade
@@ -0,0 +1,103 @@
+template(name="boardSidebar")
+ .board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}}")
+ a.sidebar-tongue.js-toogle-sidebar(
+ class="{{#if isTongueHidden}}is-hidden{{/if}}")
+ i.fa.fa-chevron-left
+ .sidebar-content.js-board-sidebar-content
+ //- XXX https://github.com/peerlibrary/meteor-blaze-components/issues/30
+ if Filter.isActive
+ +filterSidebar
+ else
+ +homeSidebar
+
+template(name='homeSidebar')
+ +membersWidget
+ hr.clear
+ +labelsWidget
+ hr.clear
+ h3
+ i.fa.fa-comments-o
+ | {{_ 'activities'}}
+ +activities(mode="board")
+
+template(name="filterSidebar")
+ ul.pop-over-label-list.checkable
+ each currentBoard.labels
+ li.item.matches-filter
+ a.name.js-toggle-label-filter
+ span.card-label(class="card-label-{{color}}")
+ span.full-name
+ if name
+ = name
+ else
+ span.quiet {{_ "label-default" color}}
+ if Filter.labelIds.isSelected _id}}
+ span.icon-sm.fa.fa-check
+ hr
+ ul.pop-over-member-list.checkable
+ each currentBoard.members
+ if isActive
+ with getUser userId
+ li.item.js-member-item(
+ class="{{#if Filter.members.isSelected _id}}active{{/if}}")
+ a.name.js-toogle-member-filter
+ +userAvatar(user=this size="small")
+ span.full-name
+ = profile.name
+ | (<span class="username">{{ username }}</span>)
+ if Filter.members.isSelected _id
+ span.icon-sm.fa.fa-check
+ hr
+ a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}")
+ | {{_ 'filter-clear'}}
+
+template(name="membersWidget")
+ .board-widget.board-widget-members
+ h3
+ i.fa.fa-user
+ | {{_ 'members'}}
+ .board-widget-content
+ each currentBoard.members
+ +userAvatar(
+ userId=this.userId
+ draggable=true
+ size="small"
+ showBadges=true)
+ unless isSandstorm
+ if currentUser.isBoardAdmin
+ a.js-open-manage-board-members
+
+template(name="labelsWidget")
+ .board-widget.board-widget-labels
+ h3
+ i.fa.fa-tags
+ | {{_ 'labels'}}
+ .board-widget-content
+ each currentBoard.labels
+ a.card-label(class="card-label-{{color}}").js-label
+ span.card-label-name= name
+ a.card-label.js-add-label
+ i.fa.fa-plus
+
+template(name="memberPopup")
+ .board-member-menu: .mini-profile-info
+ +userAvatar(user=user)
+ .info
+ h3.bottom
+ a.js-profile(href="{{pathFor route='Profile' username=user.username}}")
+ = user.profile.name
+ p.quiet.bottom @#{user.username}
+ if currentUser.isBoardMember
+ ul.pop-over-list
+ li
+ a.js-filter-member Filter cards
+ if currentUser.isBoardAdmin
+ li
+ a.js-change-role
+ | {{_ 'change-permissions'}}
+ span.quiet (#{memberType})
+ li
+ if currentUser.isBoardAdmin
+ a.js-remove-member {{_ 'remove-from-board'}}
+ else
+ a.js-leave-member {{_ 'leave-board'}}
diff --git a/client/components/users/avatar.jade b/client/components/users/avatar.jade
new file mode 100644
index 00000000..70ef69e0
--- /dev/null
+++ b/client/components/users/avatar.jade
@@ -0,0 +1,7 @@
+template(name="userAvatar")
+ .member(class="{{class}} {{# if draggable }}js-member{{else}}js-member-on-card-menu{{/if}}"
+ title="{{userData.profile.name}} ({{userData.username}})")
+ +avatar(user=userData size=size)
+ if showBadges
+ span.member-status(class="{{# if userData.profile.status}}active{{/if}}")
+ span.member-type(class=memberType)
diff --git a/client/components/users/events.js b/client/components/users/events.js
new file mode 100644
index 00000000..14df9717
--- /dev/null
+++ b/client/components/users/events.js
@@ -0,0 +1,59 @@
+// XXX This should be handled by default (and in a better way) by useraccounts.
+// See https://github.com/meteor-useraccounts/core/issues/384
+Template.atForm.onRendered(function() {
+ this.find('input').focus();
+});
+
+Template.memberMenuPopup.events({
+ 'click .js-language': Popup.open('setLanguage'),
+ 'click .js-logout': function(evt) {
+ evt.preventDefault();
+
+ Meteor.logout(function() {
+ Router.go('Home');
+ });
+ }
+});
+
+Template.setLanguagePopup.events({
+ 'click .js-set-language': function(evt) {
+ Users.update(Meteor.userId(), {
+ $set: {
+ 'profile.language': this.tag
+ }
+ });
+ evt.preventDefault();
+ }
+});
+
+Template.profileEditForm.events({
+ 'click .js-edit-profile': function() {
+ Session.set('ProfileEditForm', true);
+ },
+ 'click .js-cancel-edit-profile': function() {
+ Session.set('ProfileEditForm', false);
+ },
+ 'submit #ProfileEditForm': function(evt, t) {
+ var name = t.find('#name').value;
+ var bio = t.find('#bio').value;
+
+ // trim and update
+ if ($.trim(name)) {
+ Users.update(this.profile()._id, {
+ $set: {
+ 'profile.name': name,
+ 'profile.bio': bio
+ }
+ }, function() {
+
+ // update complete close profileEditForm
+ Session.set('ProfileEditForm', false);
+ });
+ }
+ evt.preventDefault();
+ }
+});
+
+Template.memberName.events({
+ 'click .js-show-mem-menu': Popup.open('user')
+});
diff --git a/client/components/users/form.styl b/client/components/users/form.styl
new file mode 100644
index 00000000..845c810d
--- /dev/null
+++ b/client/components/users/form.styl
@@ -0,0 +1,50 @@
+.at-form-landing-logo
+ width: 275px
+ margin: auto
+ margin-top: 50px
+ margin-top: 17vh
+
+ img
+ width: 275px
+
+
+.at-form
+ margin: auto
+ width: 275px
+ padding: 25px
+ margin-top: 20px
+ padding-bottom: 10px
+ background: #fff
+ border-radius: 3px
+ border: 1px solid #dbdbdb
+ border-bottom-color: #c2c2c2
+ box-shadow: 0 1px 6px rgba(0, 0, 0, .3)
+
+ .at-link
+ color: darken(#27AE60, 40%)
+
+ label
+ margin-bottom: 3px
+
+ input
+ width: 100%
+
+ .at-title
+ background: #F7F7F7
+ margin: -25px
+ padding: 15px 25px 5px
+ margin-bottom: 20px
+ border-bottom: 1px solid #dcdcdc
+ color: darken(white, 70%)
+ font-weight: bold
+
+ .at-signup-link,
+ .at-signin-link,
+ .at-forgotPwd
+ font-size: 0.9em
+ margin-top: 15px
+ color: darken(white, 70%)
+
+ .at-signUp,
+ .at-signIn
+ font-weight: bold
diff --git a/client/components/users/headerButtons.jade b/client/components/users/headerButtons.jade
new file mode 100644
index 00000000..74c24ad5
--- /dev/null
+++ b/client/components/users/headerButtons.jade
@@ -0,0 +1,27 @@
+template(name="headerUserBar")
+ #header-user-bar
+ if currentUser
+ a.js-open-header-member-menu
+ if currentUser.profile.name
+ = currentUser.profile.name
+ else
+ = currentUser.username
+ i.fa.fa-chevron-down
+ else
+ a(href="{{pathFor route='signUp'}}") Sign in
+ span.separator -
+ a(href="{{pathFor route='signIn'}}") Log in
+
+template(name="memberHeader")
+ a.header-member.js-open-header-member-menu
+ span= currentUser.profile.name
+ +userAvatar(user=currentUser size="small")
+
+template(name="memberMenuPopup")
+ ul.pop-over-list
+ li: a(href="{{pathFor route='Profile' username=currentUser.username}}") {{_ 'profile'}}
+ li: a.js-language {{_ 'language'}}
+ li: a(href = "{{pathFor route='Settings'}}") {{_ 'settings'}}
+ hr
+ ul.pop-over-list
+ li: a.js-logout {{_ 'log-out'}}
diff --git a/client/components/users/headerButtons.js b/client/components/users/headerButtons.js
new file mode 100644
index 00000000..70594fb5
--- /dev/null
+++ b/client/components/users/headerButtons.js
@@ -0,0 +1,5 @@
+Template.headerUserBar.events({
+ 'click .js-sign-in': Popup.open('signup'),
+ 'click .js-log-in': Popup.open('login'),
+ 'click .js-open-header-member-menu': Popup.open('memberMenu')
+});
diff --git a/client/components/users/helpers.js b/client/components/users/helpers.js
new file mode 100644
index 00000000..33867298
--- /dev/null
+++ b/client/components/users/helpers.js
@@ -0,0 +1,27 @@
+Template.userAvatar.helpers({
+ userData: function() {
+ if (! this.user) {
+ this.user = Users.findOne(this.userId);
+ }
+ return this.user;
+ },
+ memberType: function() {
+ var userId = this.userId || this.user._id;
+ var user = Users.findOne(userId);
+ return user && user.isBoardAdmin() ? 'admin' : 'normal';
+ }
+});
+
+Template.setLanguagePopup.helpers({
+ languages: function() {
+ return _.map(TAPi18n.getLanguages(), function(lang, tag) {
+ return {
+ tag: tag,
+ name: lang.name
+ };
+ });
+ },
+ isCurrentLanguage: function() {
+ return this.tag === TAPi18n.getLanguage();
+ }
+});
diff --git a/client/components/users/member.styl b/client/components/users/member.styl
new file mode 100644
index 00000000..3dfdaa37
--- /dev/null
+++ b/client/components/users/member.styl
@@ -0,0 +1,107 @@
+@import 'nib'
+
+avatar-radius = 50%
+
+.member
+ border-radius: 3px
+ display: block
+ float: left
+ height: 30px
+ width: @height
+ margin: 0 4px 4px 0
+ position: relative
+ cursor: pointer
+ user-select: none
+ z-index: 1
+ text-decoration: none
+ border-radius: avatar-radius
+
+ .avatar
+ height: 100%
+ width: @height
+ display: flex
+ align-items: center
+ justify-content: center
+ overflow: hidden
+ border-radius: avatar-radius
+
+ .avatar-initials
+ font-weight: bold
+ max-width: 100%
+ max-height: 100%
+ font-size: 14px
+ line-height: 200%
+ background-color: #dbdbdb
+ color: #444444
+
+ .avatar-image
+ max-width: 100%
+ max-height: 100%
+
+ .member-status
+ background-color: #b3b3b3
+ border: 1px solid #fff
+ border-radius: 50%
+ height: 8px
+ width: @height
+ position: absolute
+ right: 0px
+ bottom: 0px
+ border: 1px solid white
+
+ &.active
+ background: #64c464
+ border-color: #daf1da
+
+ &.idle
+ background: #e4e467
+ border-color: #f7f7d4
+
+ &.disconnected
+ background: #bdbdbd
+ border-color: #ededed
+
+ &.extra-small
+ .avatar-initials
+ font-size: 9px
+ width: 18px
+ height: 18px
+ line-height: 18px
+
+ .avatar-image
+ width: 18px
+ height: 18px
+
+ &.small
+ width: 30px
+ height: 30px
+
+ .avatar-initials
+ font-size: 12px
+ line-height: 30px
+
+ &.large
+ height: 85px
+ line-height: 85px
+ width: 85px
+
+ .avatar
+ width: 85px
+ height: 85px
+
+ .avatar-initials
+ font-size: 16px
+ font-weight: 700
+ line-height: 85px
+ width: 85px
+
+.atMention
+ background: #dbdbdb
+ border-radius: 3px
+ padding: 1px 4px
+ margin: -1px 0
+ display: inline-block
+
+ &.me
+ background: #cfdfe8
+
diff --git a/client/components/users/router.js b/client/components/users/router.js
new file mode 100644
index 00000000..d59e174d
--- /dev/null
+++ b/client/components/users/router.js
@@ -0,0 +1,29 @@
+
+_.each(['signIn', 'signUp', 'resetPwd',
+ 'forgotPwd', 'enrollAccount', 'changePwd'], function(routeName) {
+ AccountsTemplates.configureRoute(routeName, {
+ layoutTemplate: 'userFormsLayout'
+ });
+});
+
+Router.route('/profile/:username', {
+ name: 'Profile',
+ template: 'profile',
+ waitOn: function() {
+ return Meteor.subscribe('profile', this.params.username);
+ },
+ data: function() {
+ var params = this.params;
+ return {
+ profile: function() {
+ return Users.findOne({ username: params.username });
+ }
+ };
+ }
+});
+
+Router.route('/settings', {
+ name: 'Settings',
+ template: 'settings',
+ layoutTemplate: 'AuthLayout'
+});
diff --git a/client/components/users/templates.html b/client/components/users/templates.html
new file mode 100644
index 00000000..5783eebf
--- /dev/null
+++ b/client/components/users/templates.html
@@ -0,0 +1,118 @@
+<template name="setLanguagePopup">
+<ul class="pop-over-list">
+ {{#each languages}}
+ <li class="{{# if isCurrentLanguage}}active{{/if}}">
+ <a class="js-set-language">
+ {{name}}
+ {{# if isCurrentLanguage}}
+ <span class="icon-sm fa fa-check"></span>
+ {{/if}}
+ </a>
+ </li>
+ {{/each}}
+</ul>
+</template>
+
+<template name='profile'>
+ {{ # if profile }}
+ <div class="tabbed-pane-header">
+ <div class="tabbed-pane-header-wrapper clearfix">
+ <a class="tabbed-pane-header-image profile-image ed js-change-avatar-profile" href="#">
+ {{> userAvatar user=profile size="large"}}
+ </a>
+ <div class="tabbed-pane-header-details">
+ <div class="js-current-details">
+ <div class="tabbed-pane-header-details-name">
+ <h1 class="inline"> {{ profile.profile.name }} </h1>
+ <p class="window-title-extra quiet"> @{{ profile.username }} </p>
+ </div>
+ <div class="tabbed-pane-header-details-content">
+ <p>{{ profile.profile.bio }}</p>
+ </div>
+ <div class="tabbed-pane-header-details-content"></div>
+ </div>
+ {{ > profileEditForm }}
+ </div>
+ </div>
+ </div>
+ {{ else }}
+ {{ > message label='user-profile-not-found' }}
+ {{ /if }}
+</template>
+
+<template name="settings">
+ {{ > profile profile=currentUser }}
+ <div class="tabbed-pane-main-col clearfix">
+ <div class="tabbed-pane-main-col-loading hide js-loading-page">
+ <span class="tabbed-pane-main-col-loading-spinner spinner"></span>
+ </div>
+ <div class="tabbed-pane-main-col-wrapper js-content">
+ <div class="window-module clearfix">
+ <div class="window-module-title">
+ <h3>{{_ "account-details"}}</h3>
+ </div>
+ <a class="big-link js-change-name-and-bio" href="#">
+ <span class="text">{{_ 'change-name-initials-bio'}}</span>
+ </a>
+ <a class="big-link js-change-avatar" href="#">
+ <span class="text">{{_ 'change-avatar'}}</span>
+ </a>
+ <a class="big-link js-change-password" href="#">
+ <span class="text">{{_ 'change-password'}}</span>
+ </a>
+ <a class="big-link js-change-email" href="#">
+ <span class="text">{{_ 'change-email'}}</span>
+ </a>
+ </div>
+ </div>
+ </div>
+</template>
+
+<template name="profileEditForm">
+ {{#if $eq currentUser.username profile.username }}
+ {{# if session 'ProfileEditForm' }}
+ <form id="ProfileEditForm" class="js-profile-form">
+ <p class="error js-profile-form-error hide"></p>
+ <label>{{_ "username"}}</label>
+ <input type="text" id="username" value="{{ profile.username }}" disabled>
+ <label>{{_ "fullname"}}</label>
+ <input type="text" id="name" value="{{ profile.profile.name }}">
+ <label>
+ {{_ "bio"}} <span class="quiet">({{_ 'optional'}})</span>
+ </label>
+ <textarea id="bio">{{ profile.profile.bio }}</textarea>
+ <input type="submit" class="primary wide js-submit-profile" value="{{_ 'save'}}">
+ <input type="button" class="js-cancel-edit-profile" value="{{_ 'cancel'}}">
+ </form>
+ {{ else }}
+ <a class="button-link tabbed-pane-header-details-edit js-edit-profile" href="#">
+ <span class="icon-sm fa fa-pencil"></span>
+ {{_ "edit-profile"}}
+ </a>
+ {{ /if }}
+ {{ /if }}
+</template>
+
+<template name="userPopup">
+ <div class="board-member-menu">
+ <div class="mini-profile-info">
+ {{> userAvatar user=user}}
+ <div class="info">
+ <h3 class="bottom" style="margin-right: 40px;">
+ <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a>
+ </h3>
+ <p class="quiet bottom">@{{ user.username }}</p>
+ </div>
+ </div>
+ </div>
+</template>
+
+
+<template name="memberName">
+ <a class="inline-object js-show-mem-menu" href="{{ pathFor route='Profile' username=user.username }}">
+ {{ user.profile.name }}
+ {{# if username }}
+ ({{ user.username }})
+ {{ /if }}
+ </a>
+</template>
diff --git a/client/config/accounts.js b/client/config/accounts.js
new file mode 100644
index 00000000..9e0d17d3
--- /dev/null
+++ b/client/config/accounts.js
@@ -0,0 +1,35 @@
+AccountsTemplates.configure({
+ confirmPassword: false,
+ enablePasswordChange: true,
+ sendVerificationEmail: true,
+ showForgotPasswordLink: true
+});
+
+AccountsTemplates.removeField('password');
+AccountsTemplates.removeField('email');
+AccountsTemplates.addFields([
+ {
+ _id: 'username',
+ type: 'text',
+ displayName: 'username',
+ required: true,
+ minLength: 5
+ },
+ {
+ _id: 'email',
+ type: 'email',
+ required: true,
+ displayName: 'email',
+ re: /.+@(.+){2,}\.(.+){2,}/,
+ errStr: 'Invalid email'
+ },
+ {
+ _id: 'password',
+ type: 'password',
+ placeholder: {
+ signUp: 'At least six characters'
+ },
+ required: true,
+ minLength: 6
+ }
+]);
diff --git a/client/config/avatar.js b/client/config/avatar.js
new file mode 100644
index 00000000..fc4ba58b
--- /dev/null
+++ b/client/config/avatar.js
@@ -0,0 +1,3 @@
+Avatar.options = {
+ fallbackType: 'initials'
+};
diff --git a/client/config/router.js b/client/config/router.js
new file mode 100644
index 00000000..c859013f
--- /dev/null
+++ b/client/config/router.js
@@ -0,0 +1,28 @@
+Router.configure({
+ loadingTemplate: 'spinner',
+ notFoundTemplate: 'notfound',
+ layoutTemplate: 'defaultLayout',
+
+ onBeforeAction: function() {
+ var options = this.route.options;
+
+ // Redirect logged in users to Boards view when they try to open Login or
+ // signup views.
+ if (Meteor.userId() && options.redirectLoggedInUsers) {
+ return this.redirect('Boards');
+ }
+
+ // Authenticated
+ if (! Meteor.userId() && options.authenticated) {
+ return this.redirect('atSignIn');
+ }
+
+ // Reset default sessions
+ Session.set('error', false);
+ Session.set('warning', false);
+
+ Popup.close();
+
+ this.next();
+ }
+});
diff --git a/client/lib/emoji-values.js b/client/lib/emoji-values.js
new file mode 100644
index 00000000..1f07ac62
--- /dev/null
+++ b/client/lib/emoji-values.js
@@ -0,0 +1,152 @@
+Emoji.values = ['+1', '-1', '100', '1234', '8ball', 'a', 'ab', 'abc', 'abcd',
+'accept', 'aerial_tramway', 'airplane', 'alarm_clock', 'alien', 'ambulance',
+'anchor', 'angel', 'anger', 'angry', 'anguished', 'ant', 'apple', 'aquarius',
+'aries', 'arrow_backward', 'arrow_double_down', 'arrow_double_up', 'arrow_down',
+'arrow_down_small', 'arrow_forward', 'arrow_heading_down', 'arrow_heading_up',
+'arrow_left', 'arrow_lower_left', 'arrow_lower_right', 'arrow_right',
+'arrow_right_hook', 'arrow_up', 'arrow_up_down', 'arrow_up_small',
+'arrow_upper_left', 'arrow_upper_right', 'arrows_clockwise',
+'arrows_counterclockwise', 'art', 'articulated_lorry', 'astonished', 'atm', 'b',
+'baby', 'baby_bottle', 'baby_chick', 'baby_symbol', 'baggage_claim', 'balloon',
+'ballot_box_with_check', 'bamboo', 'banana', 'bangbang', 'bank', 'bar_chart',
+'barber', 'baseball', 'basketball', 'bath', 'bathtub', 'battery', 'bear', 'bee',
+'beer', 'beers', 'beetle', 'beginner', 'bell', 'bento', 'bicyclist', 'bike',
+'bikini', 'bird', 'birthday', 'black_circle', 'black_joker', 'black_nib',
+'black_square', 'black_square_button', 'blossom', 'blowfish', 'blue_book',
+'blue_car', 'blue_heart', 'blush', 'boar', 'boat', 'bomb', 'book', 'bookmark',
+'bookmark_tabs', 'books', 'boom', 'boot', 'bouquet', 'bow', 'bowling', 'bowtie',
+'boy', 'bread', 'bride_with_veil', 'bridge_at_night', 'briefcase',
+'broken_heart', 'bug', 'bulb', 'bullettrain_front', 'bullettrain_side', 'bus',
+'busstop', 'bust_in_silhouette', 'busts_in_silhouette', 'cactus', 'cake',
+'calendar', 'calling', 'camel', 'camera', 'cancer', 'candy', 'capital_abcd',
+'capricorn', 'car', 'card_index', 'carousel_horse', 'cat', 'cat2', 'cd',
+'chart', 'chart_with_downwards_trend', 'chart_with_upwards_trend',
+'checkered_flag', 'cherries', 'cherry_blossom', 'chestnut', 'chicken',
+'children_crossing', 'chocolate_bar', 'christmas_tree', 'church', 'cinema',
+'circus_tent', 'city_sunrise', 'city_sunset', 'cl', 'clap', 'clapper',
+'clipboard', 'clock1', 'clock10', 'clock1030', 'clock11', 'clock1130',
+'clock12', 'clock1230', 'clock130', 'clock2', 'clock230', 'clock3', 'clock330',
+'clock4', 'clock430', 'clock5', 'clock530', 'clock6', 'clock630', 'clock7',
+'clock730', 'clock8', 'clock830', 'clock9', 'clock930', 'closed_book',
+'closed_lock_with_key', 'closed_umbrella', 'cloud', 'clubs', 'cn', 'cocktail',
+'coffee', 'cold_sweat', 'collision', 'computer', 'confetti_ball', 'confounded',
+'confused', 'congratulations', 'construction', 'construction_worker',
+'convenience_store', 'cookie', 'cool', 'cop', 'copyright', 'corn', 'couple',
+'couple_with_heart', 'couplekiss', 'cow', 'cow2', 'credit_card', 'crocodile',
+'crossed_flags', 'crown', 'cry', 'crying_cat_face', 'crystal_ball', 'cupid',
+'curly_loop', 'currency_exchange', 'curry', 'custard', 'customs', 'cyclone',
+'dancer', 'dancers', 'dango', 'dart', 'dash', 'date', 'de', 'deciduous_tree',
+'department_store', 'diamond_shape_with_a_dot_inside', 'diamonds',
+'disappointed', 'disappointed_relieved', 'dizzy', 'dizzy_face', 'do_not_litter',
+'dog', 'dog2', 'dollar', 'dolls', 'dolphin', 'donut', 'door', 'doughnut',
+'dragon', 'dragon_face', 'dress', 'dromedary_camel', 'droplet', 'dvd', 'e-mail',
+'ear', 'ear_of_rice', 'earth_africa', 'earth_americas', 'earth_asia', 'egg',
+'eggplant', 'eight', 'eight_pointed_black_star', 'eight_spoked_asterisk',
+'electric_plug', 'elephant', 'email', 'end', 'envelope', 'es', 'euro',
+'european_castle', 'european_post_office', 'evergreen_tree', 'exclamation',
+'expressionless', 'eyeglasses', 'eyes', 'facepunch', 'factory', 'fallen_leaf',
+'family', 'fast_forward', 'fax', 'fearful', 'feelsgood', 'feet', 'ferris_wheel',
+'file_folder', 'finnadie', 'fire', 'fire_engine', 'fireworks',
+'first_quarter_moon', 'first_quarter_moon_with_face', 'fish', 'fish_cake',
+'fishing_pole_and_fish', 'fist', 'five', 'flags', 'flashlight', 'floppy_disk',
+'flower_playing_cards', 'flushed', 'foggy', 'football', 'fork_and_knife',
+'fountain', 'four', 'four_leaf_clover', 'fr', 'free', 'fried_shrimp', 'fries',
+'frog', 'frowning', 'fu', 'fuelpump', 'full_moon', 'full_moon_with_face',
+'game_die', 'gb', 'gem', 'gemini', 'ghost', 'gift', 'gift_heart', 'girl',
+'globe_with_meridians', 'goat', 'goberserk', 'godmode', 'golf', 'grapes',
+'green_apple', 'green_book', 'green_heart', 'grey_exclamation', 'grey_question',
+'grimacing', 'grin', 'grinning', 'guardsman', 'guitar', 'gun', 'haircut',
+'hamburger', 'hammer', 'hamster', 'hand', 'handbag', 'hankey', 'hash',
+'hatched_chick', 'hatching_chick', 'headphones', 'hear_no_evil', 'heart',
+'heart_decoration', 'heart_eyes', 'heart_eyes_cat', 'heartbeat', 'heartpulse',
+'hearts', 'heavy_check_mark', 'heavy_division_sign', 'heavy_dollar_sign',
+'heavy_exclamation_mark', 'heavy_minus_sign', 'heavy_multiplication_x',
+'heavy_plus_sign', 'helicopter', 'herb', 'hibiscus', 'high_brightness',
+'high_heel', 'hocho', 'honey_pot', 'honeybee', 'horse', 'horse_racing',
+'hospital', 'hotel', 'hotsprings', 'hourglass', 'hourglass_flowing_sand',
+'house', 'house_with_garden', 'hurtrealbad', 'hushed', 'ice_cream', 'icecream',
+'id', 'ideograph_advantage', 'imp', 'inbox_tray', 'incoming_envelope',
+'information_desk_person', 'information_source', 'innocent', 'interrobang',
+'iphone', 'it', 'izakaya_lantern', 'jack_o_lantern', 'japan', 'japanese_castle',
+'japanese_goblin', 'japanese_ogre', 'jeans', 'joy', 'joy_cat', 'jp', 'key',
+'keycap_ten', 'kimono', 'kiss', 'kissing', 'kissing_cat', 'kissing_closed_eyes',
+'kissing_face', 'kissing_heart', 'kissing_smiling_eyes', 'koala', 'koko', 'kr',
+'large_blue_circle', 'large_blue_diamond', 'large_orange_diamond',
+'last_quarter_moon', 'last_quarter_moon_with_face', 'laughing', 'leaves',
+'ledger', 'left_luggage', 'left_right_arrow', 'leftwards_arrow_with_hook',
+'lemon', 'leo', 'leopard', 'libra', 'light_rail', 'link', 'lips', 'lipstick',
+'lock', 'lock_with_ink_pen', 'lollipop', 'loop', 'loudspeaker', 'love_hotel',
+'love_letter', 'low_brightness', 'm', 'mag', 'mag_right', 'mahjong', 'mailbox',
+'mailbox_closed', 'mailbox_with_mail', 'mailbox_with_no_mail', 'man',
+'man_with_gua_pi_mao', 'man_with_turban', 'mans_shoe', 'maple_leaf', 'mask',
+'massage', 'meat_on_bone', 'mega', 'melon', 'memo', 'mens', 'metal', 'metro',
+'microphone', 'microscope', 'milky_way', 'minibus', 'minidisc',
+'mobile_phone_off', 'money_with_wings', 'moneybag', 'monkey', 'monkey_face',
+'monorail', 'moon', 'mortar_board', 'mount_fuji', 'mountain_bicyclist',
+'mountain_cableway', 'mountain_railway', 'mouse', 'mouse2', 'movie_camera',
+'moyai', 'muscle', 'mushroom', 'musical_keyboard', 'musical_note',
+'musical_score', 'mute', 'nail_care', 'name_badge', 'neckbeard', 'necktie',
+'negative_squared_cross_mark', 'neutral_face', 'new', 'new_moon',
+'new_moon_with_face', 'newspaper', 'ng', 'nine', 'no_bell', 'no_bicycles',
+'no_entry', 'no_entry_sign', 'no_good', 'no_mobile_phones', 'no_mouth',
+'no_pedestrians', 'no_smoking', 'non-potable_water', 'nose', 'notebook',
+'notebook_with_decorative_cover', 'notes', 'nut_and_bolt', 'o', 'o2', 'ocean',
+'octocat', 'octopus', 'oden', 'office', 'ok', 'ok_hand', 'ok_woman',
+'older_man', 'older_woman', 'on', 'oncoming_automobile', 'oncoming_bus',
+'oncoming_police_car', 'oncoming_taxi', 'one', 'open_file_folder', 'open_hands',
+'open_mouth', 'ophiuchus', 'orange_book', 'outbox_tray', 'ox', 'page_facing_up',
+'page_with_curl', 'pager', 'palm_tree', 'panda_face', 'paperclip', 'parking',
+'part_alternation_mark', 'partly_sunny', 'passport_control', 'paw_prints',
+'peach', 'pear', 'pencil', 'pencil2', 'penguin', 'pensive', 'performing_arts',
+'persevere', 'person_frowning', 'person_with_blond_hair',
+'person_with_pouting_face', 'phone', 'pig', 'pig2', 'pig_nose', 'pill',
+'pineapple', 'pisces', 'pizza', 'plus1', 'point_down', 'point_left',
+'point_right', 'point_up', 'point_up_2', 'police_car', 'poodle', 'poop',
+'post_office', 'postal_horn', 'postbox', 'potable_water', 'pouch',
+'poultry_leg', 'pound', 'pouting_cat', 'pray', 'princess', 'punch',
+'purple_heart', 'purse', 'pushpin', 'put_litter_in_its_place', 'question',
+'rabbit', 'rabbit2', 'racehorse', 'radio', 'radio_button', 'rage', 'rage1',
+'rage2', 'rage3', 'rage4', 'railway_car', 'rainbow', 'raised_hand',
+'raised_hands', 'raising_hand', 'ram', 'ramen', 'rat', 'recycle', 'red_car',
+'red_circle', 'registered', 'relaxed', 'relieved', 'repeat', 'repeat_one',
+'restroom', 'revolving_hearts', 'rewind', 'ribbon', 'rice', 'rice_ball',
+'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster', 'rooster',
+'rose', 'rotating_light', 'round_pushpin', 'rowboat', 'ru', 'rugby_football',
+'runner', 'running', 'running_shirt_with_sash', 'sa', 'sagittarius', 'sailboat',
+'sake', 'sandal', 'santa', 'satellite', 'satisfied', 'saxophone', 'school',
+'school_satchel', 'scissors', 'scorpius', 'scream', 'scream_cat', 'scroll',
+'seat', 'secret', 'see_no_evil', 'seedling', 'seven', 'shaved_ice', 'sheep',
+'shell', 'ship', 'shipit', 'shirt', 'shit', 'shoe', 'shower', 'signal_strength',
+'six', 'six_pointed_star', 'ski', 'skull', 'sleeping', 'sleepy', 'slot_machine',
+'small_blue_diamond', 'small_orange_diamond', 'small_red_triangle',
+'small_red_triangle_down', 'smile', 'smile_cat', 'smiley', 'smiley_cat',
+'smiling_imp', 'smirk', 'smirk_cat', 'smoking', 'snail', 'snake', 'snowboarder',
+'snowflake', 'snowman', 'sob', 'soccer', 'soon', 'sos', 'sound',
+'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles',
+'sparkling_heart', 'speak_no_evil', 'speaker', 'speech_balloon', 'speedboat',
+'squirrel', 'star', 'star2', 'stars', 'station', 'statue_of_liberty',
+'steam_locomotive', 'stew', 'straight_ruler', 'strawberry', 'stuck_out_tongue',
+'stuck_out_tongue_closed_eyes', 'stuck_out_tongue_winking_eye', 'sun_with_face',
+'sunflower', 'sunglasses', 'sunny', 'sunrise', 'sunrise_over_mountains',
+'surfer', 'sushi', 'suspect', 'suspension_railway', 'sweat', 'sweat_drops',
+'sweat_smile', 'sweet_potato', 'swimmer', 'symbols', 'syringe', 'tada',
+'tanabata_tree', 'tangerine', 'taurus', 'taxi', 'tea', 'telephone',
+'telephone_receiver', 'telescope', 'tennis', 'tent', 'thought_balloon', 'three',
+'thumbsdown', 'thumbsup', 'ticket', 'tiger', 'tiger2', 'tired_face', 'tm',
+'toilet', 'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'tractor',
+'traffic_light', 'train', 'train2', 'tram', 'triangular_flag_on_post',
+'triangular_ruler', 'trident', 'triumph', 'trolleybus', 'trollface', 'trophy',
+'tropical_drink', 'tropical_fish', 'truck', 'trumpet', 'tshirt', 'tulip',
+'turtle', 'tv', 'twisted_rightwards_arrows', 'two', 'two_hearts',
+'two_men_holding_hands', 'two_women_holding_hands', 'u5272', 'u5408', 'u55b6',
+'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7981', 'u7a7a', 'uk',
+'umbrella', 'unamused', 'underage', 'unlock', 'up', 'us', 'v',
+'vertical_traffic_light', 'vhs', 'vibration_mode', 'video_camera', 'video_game',
+'violin', 'virgo', 'volcano', 'vs', 'walking', 'waning_crescent_moon',
+'waning_gibbous_moon', 'warning', 'watch', 'water_buffalo', 'watermelon',
+'wave', 'wavy_dash', 'waxing_crescent_moon', 'waxing_gibbous_moon', 'wc',
+'weary', 'wedding', 'whale', 'whale2', 'wheelchair', 'white_check_mark',
+'white_circle', 'white_flower', 'white_square', 'white_square_button',
+'wind_chime', 'wine_glass', 'wink', 'wolf', 'woman', 'womans_clothes',
+'womans_hat', 'womens', 'worried', 'wrench', 'x', 'yellow_heart', 'yen', 'yum',
+'zap', 'zero', 'zzz'];
diff --git a/client/lib/filter.js b/client/lib/filter.js
new file mode 100644
index 00000000..507a2bb7
--- /dev/null
+++ b/client/lib/filter.js
@@ -0,0 +1,133 @@
+// Filtered view manager
+// We define local filter objects for each different type of field (SetFilter,
+// RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose
+// goal is to filter complete documents by using the local filters for each
+// fields.
+
+// Use a "set" filter for a field that is a set of documents uniquely
+// identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
+var SetFilter = function() {
+ this._dep = new Tracker.Dependency();
+ this._selectedElements = [];
+};
+
+_.extend(SetFilter.prototype, {
+ isSelected: function(val) {
+ this._dep.depend();
+ return this._selectedElements.indexOf(val) > -1;
+ },
+
+ add: function(val) {
+ if (this.indexOfVal(val) === -1) {
+ this._selectedElements.push(val);
+ this._dep.changed();
+ }
+ },
+
+ remove: function(val) {
+ var indexOfVal = this._indexOfVal(val);
+ if (this.indexOfVal(val) !== -1) {
+ this._selectedElements.splice(indexOfVal, 1);
+ this._dep.changed();
+ }
+ },
+
+ toogle: function(val) {
+ var indexOfVal = this._indexOfVal(val);
+ if (indexOfVal === -1) {
+ this._selectedElements.push(val);
+ } else {
+ this._selectedElements.splice(indexOfVal, 1);
+ }
+
+ this._dep.changed();
+ },
+
+ reset: function() {
+ this._selectedElements = [];
+ this._dep.changed();
+ },
+
+ _indexOfVal: function(val) {
+ return this._selectedElements.indexOf(val);
+ },
+
+ _isActive: function() {
+ this._dep.depend();
+ return this._selectedElements.length !== 0;
+ },
+
+ _getMongoSelector: function() {
+ this._dep.depend();
+ return { $in: this._selectedElements };
+ }
+});
+
+// The global Filter object.
+// XXX It would be possible to re-write this object more elegantly, and removing
+// the need to provide a list of `_fields`. We also should move methods into the
+// object prototype.
+Filter = {
+ // XXX I would like to rename this field into `labels` to be consistent with
+ // the rest of the schema, but we need to set some migrations architecture
+ // before changing the schema.
+ labelIds: new SetFilter(),
+ members: new SetFilter(),
+
+ _fields: ['labelIds', 'members'],
+
+ // We don't filter cards that have been added after the last filter change. To
+ // implement this we keep the id of these cards in this `_exceptions` fields
+ // and use a `$or` condition in the mongo selector we return.
+ _exceptions: [],
+ _exceptionsDep: new Tracker.Dependency(),
+
+ isActive: function() {
+ var self = this;
+ return _.any(self._fields, function(fieldName) {
+ return self[fieldName]._isActive();
+ });
+ },
+
+ getMongoSelector: function() {
+ var self = this;
+
+ if (! self.isActive())
+ return {};
+
+ var filterSelector = {};
+ _.forEach(self._fields, function(fieldName) {
+ var filter = self[fieldName];
+ if (filter._isActive())
+ filterSelector[fieldName] = filter._getMongoSelector();
+ });
+
+ var exceptionsSelector = {_id: {$in: this._exceptions}};
+ this._exceptionsDep.depend();
+
+ return {$or: [filterSelector, exceptionsSelector]};
+ },
+
+ reset: function() {
+ var self = this;
+ _.forEach(self._fields, function(fieldName) {
+ var filter = self[fieldName];
+ filter.reset();
+ });
+ self.resetExceptions();
+ },
+
+ addException: function(_id) {
+ if (this.isActive()) {
+ this._exceptions.push(_id);
+ this._exceptionsDep.changed();
+ }
+ },
+
+ resetExceptions: function() {
+ this._exceptions = [];
+ this._exceptionsDep.changed();
+ }
+};
+
+Blaze.registerHelper('Filter', Filter);
diff --git a/client/lib/i18n.js b/client/lib/i18n.js
new file mode 100644
index 00000000..7d7e3ebb
--- /dev/null
+++ b/client/lib/i18n.js
@@ -0,0 +1,22 @@
+// We save the user language preference in the user profile, and use that to set
+// the language reactively. If the user is not connected we use the language
+// information provided by the browser, and default to english.
+
+Tracker.autorun(function() {
+ var language;
+ var currentUser = Meteor.user();
+ if (currentUser) {
+ language = currentUser.profile && currentUser.profile.language;
+ } else {
+ language = navigator.language || navigator.userLanguage;
+ }
+
+ if (language) {
+
+ TAPi18n.setLanguage(language);
+
+ // XXX
+ var shortLanguage = language.split('-')[0];
+ T9n.setLanguage(shortLanguage);
+ }
+});
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
new file mode 100644
index 00000000..c1267938
--- /dev/null
+++ b/client/lib/keyboard.js
@@ -0,0 +1,55 @@
+// XXX Pressing `?` should display a list of all shortcuts available.
+//
+// XXX There is no reason to define these shortcuts globally, they should be
+// attached to a template (most of them will go in the `board` template).
+
+// Pressing `Escape` should close the last opened “element” and only the last
+// one -- curently we handle popups and the card detailed view of the sidebar.
+Mousetrap.bind('esc', function() {
+ if (currentlyOpenedForm.get() !== null) {
+ currentlyOpenedForm.get().close();
+
+ } else if (Popup.isOpen()) {
+ Popup.back();
+
+ // XXX We should have a higher level API
+ } else if (Session.get('currentCard')) {
+ Utils.goBoardId(Session.get('currentBoard'));
+ }
+});
+
+Mousetrap.bind('w', function() {
+ if (! Session.get('currentCard')) {
+ Sidebar.toogle();
+ } else {
+ Utils.goBoardId(Session.get('currentBoard'));
+ Sidebar.hide();
+ }
+});
+
+Mousetrap.bind('q', function() {
+ var currentBoardId = Session.get('currentBoard');
+ var currentUserId = Meteor.userId();
+ if (currentBoardId && currentUserId) {
+ Filter.members.toogle(currentUserId);
+ }
+});
+
+Mousetrap.bind('x', function() {
+ if (Filter.isActive()) {
+ Filter.reset();
+ }
+});
+
+Mousetrap.bind(['down', 'up'], function(evt, key) {
+ if (! Session.get('currentCard')) {
+ return;
+ }
+
+ var nextFunc = (key === 'down' ? 'next' : 'prev');
+ var nextCard = $('.js-minicard.is-selected')[nextFunc]('.js-minicard').get(0);
+ if (nextCard) {
+ var nextCardId = Blaze.getData(nextCard)._id;
+ Utils.goCardId(nextCardId);
+ }
+});
diff --git a/client/lib/mixins.js b/client/lib/mixins.js
new file mode 100644
index 00000000..8d16be53
--- /dev/null
+++ b/client/lib/mixins.js
@@ -0,0 +1 @@
+Mixins = {};
diff --git a/client/lib/popup.js b/client/lib/popup.js
new file mode 100644
index 00000000..dd2a43b0
--- /dev/null
+++ b/client/lib/popup.js
@@ -0,0 +1,200 @@
+// A simple tracker dependency that we invalidate every time the window is
+// resized. This is used to reactively re-calculate the popup position in case
+// of a window resize.
+var windowResizeDep = new Tracker.Dependency();
+$(window).on('resize', function() { windowResizeDep.changed(); });
+
+Popup = {
+ /// This function returns a callback that can be used in an event map:
+ ///
+ /// Template.tplName.events({
+ /// 'click .elementClass': Popup.open("popupName")
+ /// });
+ ///
+ /// The popup inherit the data context of its parent.
+ open: function(name) {
+ var self = this;
+ var popupName = name + 'Popup';
+
+ return function(evt) {
+ // If a popup is already openened, clicking again on the opener element
+ // should close it -- and interupt the current `open` function.
+ if (self.isOpen() &&
+ self._getTopStack().openerElement === evt.currentTarget) {
+ return self.close();
+ }
+
+ // We determine the `openerElement` (the DOM element that is being clicked
+ // and the one we take in reference to position the popup) from the event
+ // if the popup has no parent, or from the parent `openerElement` if it
+ // has one. This allows us to position a sub-popup exactly at the same
+ // position than its parent.
+ var openerElement;
+ if (self._hasPopupParent()) {
+ openerElement = self._getTopStack().openerElement;
+ } else {
+ self._stack = [];
+ openerElement = evt.currentTarget;
+ }
+
+ // We modify the event to prevent the popup being closed when the event
+ // bubble up to the document element.
+ evt.originalEvent.clickInPopup = true;
+ evt.preventDefault();
+
+ // We push our popup data to the stack. The top of the stack is always
+ // used as the data source for our current popup.
+ self._stack.push({
+ __isPopup: true,
+ popupName: popupName,
+ hasPopupParent: self._hasPopupParent(),
+ title: self._getTitle(popupName),
+ openerElement: openerElement,
+ offset: self._getOffset(openerElement),
+ dataContext: this.currentData && this.currentData() || this
+ });
+
+ // If there are no popup currently opened we use the Blaze API to render
+ // one into the DOM. We use a reactive function as the data parameter that
+ // just return the top element on the stack and depends on our internal
+ // dependency that is being invalidated every time the top element of the
+ // stack has changed and we want to update the popup.
+ //
+ // Otherwise if there is already a popup open we just need to invalidate
+ // our internal dependency, and since we just changed the top element of
+ // our internal stack, the popup will be updated with the new data.
+ if (! self.isOpen()) {
+ self.current = Blaze.renderWithData(self.template, function() {
+ self._dep.depend();
+ return self._stack[self._stack.length - 1];
+ }, document.body);
+
+ } else {
+ self._dep.changed();
+ }
+ };
+ },
+
+ /// This function returns a callback that can be used in an event map:
+ ///
+ /// Template.tplName.events({
+ /// 'click .elementClass': Popup.afterConfirm("popupName", function() {
+ /// // What to do after the user has confirmed the action
+ /// })
+ /// });
+ afterConfirm: function(name, action) {
+ var self = this;
+
+ return function(evt, tpl) {
+ var context = this;
+ context.__afterConfirmAction = action;
+ self.open(name).call(context, evt, tpl);
+ };
+ },
+
+ /// The public reactive state of the popup.
+ isOpen: function() {
+ this._dep.changed();
+ return !! this.current;
+ },
+
+ /// In case the popup was opened from a parent popup we can get back to it
+ /// with this `Popup.back()` function. You can go back several steps at once
+ /// by providing a number to this function, e.g. `Popup.back(2)`. In this case
+ /// intermediate popup won't even be rendered on the DOM. If the number of
+ /// steps back is greater than the popup stack size, the popup will be closed.
+ back: function(n) {
+ n = n || 1;
+ var self = this;
+ if (self._stack.length > n) {
+ _.times(n, function() { self._stack.pop(); });
+ self._dep.changed();
+ } else {
+ self.close();
+ }
+ },
+
+ /// Close the current opened popup.
+ close: function() {
+ if (this.isOpen()) {
+ Blaze.remove(this.current);
+ this.current = null;
+ this._stack = [];
+ }
+ },
+
+ // The template we use for every popup
+ template: Template.popup,
+
+ // We only want to display one popup at a time and we keep the view object in
+ // this `Popup._current` variable. If there is no popup currently opened the
+ // value is `null`.
+ _current: null,
+
+ // It's possible to open a sub-popup B from a popup A. In that case we keep
+ // the data of popup A so we can return back to it. Every time we open a new
+ // popup the stack grows, every time we go back the stack decrease, and if we
+ // close the popup the stack is reseted to the empty stack [].
+ _stack: [],
+
+ // We invalidate this internal dependency every time the top of the stack has
+ // changed and we want to render a popup with the new top-stack data.
+ _dep: new Tracker.Dependency(),
+
+ // An utility fonction that returns the top element of the internal stack
+ _getTopStack: function() {
+ return this._stack[this._stack.length - 1];
+ },
+
+ // We use the blaze API to determine if the current popup has been opened from
+ // a parent popup. The number we give to the `Template.parentData` has been
+ // determined experimentally and is susceptible to change if you modify the
+ // `Popup.template`
+ _hasPopupParent: function() {
+ var tryParentData = Template.parentData(3);
+ return !! (tryParentData && tryParentData.__isPopup);
+ },
+
+ // We automatically calculate the popup offset from the reference element
+ // position and dimensions. We also reactively use the window dimensions to
+ // ensure that the popup is always visible on the screen.
+ _getOffset: function(element) {
+ var $element = $(element);
+ return function() {
+ windowResizeDep.depend();
+ var offset = $element.offset();
+ var popupWidth = 300 + 15;
+ return {
+ left: Math.min(offset.left, $(window).width() - popupWidth),
+ top: offset.top + $element.outerHeight()
+ };
+ };
+ },
+
+ // We get the title from the translation files. Instead of returning the
+ // result, we return a function that compute the result and since `TAPi18n.__`
+ // is a reactive data source, the title will be changed reactively.
+ _getTitle: function(popupName) {
+ return function() {
+ var translationKey = popupName + '-title';
+
+ // XXX There is no public API to check if there is an available
+ // translation for a given key. So we try to translate the key and if the
+ // translation output equals the key input we deduce that no translation
+ // was available and returns `false`. There is a (small) risk a false
+ // positives.
+ var title = TAPi18n.__(translationKey);
+ return title !== translationKey ? title : false;
+ };
+ }
+};
+
+// We automatically close a potential opened popup on any left click on the
+// document. To avoid closing it unexpectedly we modify the bubbled event in
+// case the click event happen in the popup or in a button that open a popup.
+$(document).on('click', function(evt) {
+ if (evt.which === 1 && ! (evt.originalEvent &&
+ evt.originalEvent.clickInPopup)) {
+ Popup.close();
+ }
+});
diff --git a/client/lib/utils.js b/client/lib/utils.js
new file mode 100644
index 00000000..9e92e999
--- /dev/null
+++ b/client/lib/utils.js
@@ -0,0 +1,96 @@
+Utils = {
+ error: function(err) {
+ Session.set('error', (err && err.message || false));
+ },
+
+ // scroll
+ Scroll: function(selector) {
+ var $el = $(selector);
+ return {
+ top: function(px, add) {
+ var t = $el.scrollTop();
+ $el.animate({ scrollTop: (add ? (t + px) : px) });
+ },
+ left: function(px, add) {
+ var l = $el.scrollLeft();
+ $el.animate({ scrollLeft: (add ? (l + px) : px) });
+ }
+ };
+ },
+
+ Warning: {
+ get: function() {
+ return Session.get('warning');
+ },
+ open: function(desc) {
+ Session.set('warning', { desc: desc });
+ },
+ close: function() {
+ Session.set('warning', false);
+ }
+ },
+
+ // XXX We should remove these two methods
+ goBoardId: function(_id) {
+ var board = Boards.findOne(_id);
+ return board && Router.go('Board', {
+ _id: board._id,
+ slug: board.slug
+ });
+ },
+
+ goCardId: function(_id) {
+ var card = Cards.findOne(_id);
+ var board = Boards.findOne(card.boardId);
+ return board && Router.go('Card', {
+ cardId: card._id,
+ boardId: board._id,
+ slug: board.slug
+ });
+ },
+
+ liveEvent: function(events, callback) {
+ $(document).on(events, function() {
+ callback($(this));
+ });
+ },
+
+ capitalize: function(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ },
+
+ getLabelIndex: function(boardId, labelId) {
+ var board = Boards.findOne(boardId);
+ var labels = {};
+ _.each(board.labels, function(a, b) {
+ labels[a._id] = b;
+ });
+ return {
+ index: labels[labelId],
+ key: function(key) {
+ return 'labels.' + labels[labelId] + '.' + key;
+ }
+ };
+ },
+
+ // Determine the new sort index
+ getSortIndex: function(prevCardDomElement, nextCardDomElement) {
+ // If we drop the card to an empty column
+ if (! prevCardDomElement && ! nextCardDomElement) {
+ return 0;
+ // If we drop the card in the first position
+ } else if (! prevCardDomElement) {
+ return Blaze.getData(nextCardDomElement).sort - 1;
+ // If we drop the card in the last position
+ } else if (! nextCardDomElement) {
+ return Blaze.getData(prevCardDomElement).sort + 1;
+ }
+ // In the general case take the average of the previous and next element
+ // sort indexes.
+ else {
+ var prevSortIndex = Blaze.getData(prevCardDomElement).sort;
+ var nextSortIndex = Blaze.getData(nextCardDomElement).sort;
+ return (prevSortIndex + nextSortIndex) / 2;
+ }
+ }
+};
diff --git a/client/styles/cheat.styl b/client/styles/cheat.styl
new file mode 100644
index 00000000..9d881b44
--- /dev/null
+++ b/client/styles/cheat.styl
@@ -0,0 +1,79 @@
+@import 'nib'
+
+.clear
+ clear: both
+
+.clearfix
+ clearfix()
+
+.hide
+ display: none
+
+.show
+ display: block
+
+.bold
+ font-weight: 700
+
+.center
+ text-align: center
+
+.left
+ float: left
+
+.right
+ float: right
+
+.first
+ margin-left: 0
+ padding-left: 0
+
+.last
+ margin-right: 0
+ padding-right: 0
+
+.top
+ margin-top: 0
+ padding-top: 0
+
+.bottom
+ margin-bottom: 0
+ padding-bottom: 0
+
+.relative
+ position: relative
+
+.block
+ display: block
+
+.inline
+ display: inline
+
+.inline-block
+ display: inline-block
+
+.pointer
+ cursor: pointer
+
+.ellip
+ overflow: hidden
+ text-overflow: ellipsis
+ white-space: nowrap
+
+.underline
+ text-decoration: underline
+
+.lowercase
+ text-transform: lowercase
+
+.invisible
+ visibility: hidden
+
+.wrapword
+ word-wrap: break-word
+
+.grab
+ cursor: grab
+
+.grabbing
+ cursor: grabbing
diff --git a/client/styles/fancy-scrollbar.styl b/client/styles/fancy-scrollbar.styl
new file mode 100644
index 00000000..c7a30018
--- /dev/null
+++ b/client/styles/fancy-scrollbar.styl
@@ -0,0 +1,45 @@
+.fancy-scrollbar
+ -webkit-overflow-scrolling: touch
+
+ .fancy-scrollbar::-webkit-scrollbar
+ height: 9px
+ width: 9px
+
+ &::-webkit-scrollbar-button:start:decrement,
+ &::-webkit-scrollbar-button:end:increment
+ background: transparent
+ display: none
+
+ &::-webkit-scrollbar-track-piece
+ background: #dbdbdb
+
+ &:vertical:start
+ border-top-left-radius: 5px
+ border-top-right-radius: 5px
+ border-bottom-right-radius: 0
+ border-bottom-left-radius: 0
+
+ &:vertical:end
+ border-top-left-radius: 0
+ border-top-right-radius: 0
+ border-bottom-right-radius: 5px
+ border-bottom-left-radius: 5px
+
+ &:horizontal:start
+ border-top-left-radius: 5px
+ border-top-right-radius: 0
+ border-bottom-right-radius: 0
+ border-bottom-left-radius: 5px
+
+ &:horizontal:end
+ border-top-left-radius: 0
+ border-top-right-radius: 5px
+ border-bottom-right-radius: 5px
+ border-bottom-left-radius: 0
+
+ &::-webkit-scrollbar-thumb:vertical,
+ &::-webkit-scrollbar-thumb:horizontal
+ background: #c2c2c2
+ border-radius: 5px
+ display: block
+ height: 50px
diff --git a/client/styles/main.styl b/client/styles/main.styl
new file mode 100644
index 00000000..0f12e35e
--- /dev/null
+++ b/client/styles/main.styl
@@ -0,0 +1,814 @@
+@import 'nib'
+
+html, body, input, select, textarea, button
+ font: 14px "Helvetica Neue", Arial, Helvetica, sans-serif
+ line-height: 18px
+ color: #4d4d4d
+
+html
+ font-size: 100%
+ -webkit-text-size-adjust: 100%
+
+p
+ margin: 0
+
+ol,
+ul
+ list-style: none
+ margin: 0
+ padding: 0
+
+blockquote, q
+ quotes: none
+
+ &:before,
+ &:after
+ content: none
+
+ins
+ text-decoration: none
+
+del
+ text-decoration: line-through
+
+table
+ border-collapse: collapse
+ border-spacing: 0
+ width: 100%
+
+hr
+ height: 1px
+ border: 0
+ border: none
+ width: 100%
+ background: #dbdbdb
+ color: #dbdbdb
+ margin: 15px 0
+ padding: 0
+
+article,
+aside,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section
+ display: block
+
+caption, th, td
+ text-align: left
+ font-weight: 400
+
+a img
+ border: none
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section,
+summary
+ display: block
+
+html
+ max-height: 100%
+
+body
+ background: darken(white, 10%)
+ margin: 0
+ position: relative
+ z-index: 0
+ overflow-y: auto
+
+#surface
+ display: flex
+ flex-direction: column
+ min-height: 100vh
+
+#content
+ position: relative
+ flex: 1
+
+div::selection
+ background: transparent
+
+h1
+ font-size: 22px
+ line-height: 1.2em
+ margin: 0 0 10px
+
+h2
+ font-size: 18px
+ line-height: 1.2em
+ margin: 0 0 9px
+
+h3, h4, h5, h6
+ font-size: 16px
+ line-height: 1.25em
+ margin: 0 0 6px
+
+.quiet, .quiet a
+ color: #8c8c8c
+
+.error, .error a
+ color: #eb3800
+
+.warning
+ background: #f0ecdb
+ border-radius: 3px
+ color: #aa8f09
+ padding: 6px 8px
+
+ a
+ color: #aa8f09
+
+a
+ color: #444
+ cursor: pointer
+ text-decoration: none
+
+ &:hover
+ color: #111
+
+ &.disabled,
+ &.disabled:hover
+ color: #8c8c8c
+ cursor: default
+ text-decoration: none
+
+table, p
+ margin-bottom: 8px
+
+pre
+ margin: 15px 0
+ white-space: pre
+ max-height: 516px
+
+pre,
+code,
+tt
+ font-family: bitstream vera sans mono, andale mono, lucida console, monospace
+ line-height: 1.25em
+
+blockquote
+ margin: 8px 0 8px 8px
+ border-left: 1px solid #ccc
+ color: #666
+ padding: 0 0 0 8px
+
+table, td, th
+ vertical-align: top
+ border-top: 1px solid #ccc
+ border-left: 1px solid #ccc
+
+td, th
+ padding: 5px
+ border-right: 1px solid #ccc
+ border-bottom: 1px solid #ccc
+
+th
+ font-weight: 700
+
+thead
+ background: #fff
+ background: linear-gradient(to bottom, #fff 0, #f0f0f0 100%)
+
+tbody
+ background-color: #fff
+
+dl, dt
+ margin-bottom: 8px
+
+dd
+ margin: 0 0 16px 24px
+
+.emoji
+ height: 18px
+ width: 18px
+ vertical-align: text-bottom
+
+.edit
+ display: none
+ position: relative
+
+.editable .current
+ cursor: pointer
+
+.editable.editing
+ cursor: auto
+
+.edits-warning, .edits-error
+ display: none
+ clear: both
+
+.editing .edit
+ display: block
+ float: left
+ padding-bottom: 9px
+ z-index: 100
+ width: 100%
+
+.editing .edits-warning
+ display: none!important
+
+.editing .edit .field,
+.editing .edit .field:active
+ background: rgba(0, 0, 0, .03)
+ box-shadow: inset 0 1px 6px rgba(0, 0, 0, .1)
+ border-color: rgba(0, 0, 0, .15)
+ margin-bottom: 4px
+
+.edit-heavy .field
+ font-size: 15px
+ font-weight: 700
+ line-height: 18px
+
+
+.board-backgrounds-list
+
+ .board-background-select
+ box-sizing: border-box
+ display: block
+ float: left
+ width: 50%
+ padding-top: 12px
+ position: relative
+ z-index: 1
+
+ &:nth-child(-n + 2)
+ padding-top: 0
+
+ &:nth-child(2n)
+ padding-left: 6px
+
+ &:nth-child(2n+1)
+ padding-right: 6px
+
+ .background-box
+ border-radius: 3px
+ background-size: cover
+ display: block
+ height: 74px
+ position: relative
+ width: 100%
+ cursor: pointer
+ display: flex
+ align-items: center
+ justify-content: center
+
+ i.fa-check
+ font-size: 25px
+ color: white
+
+.new-comment
+ position: relative
+ margin: 0 0 20px 38px
+
+ .member
+ opacity: .7
+ position: absolute
+ top: 1px
+ left: -38px
+
+ .helper
+ bottom: 0
+ display: none
+ position: absolute
+ right: 9px
+
+ &.focus
+
+ .member
+ opacity: 1
+
+ .helper
+ display: inline-block
+
+ .new-comment-input
+ min-height: 108px
+ color: #4d4d4d
+ cursor: auto
+ overflow: hidden
+ word-wrap: break-word
+
+ .too-long
+ margin-top: 8px
+
+.new-comment-input
+ background-color: #fff
+ border: 0
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
+ color: #8c8c8c
+ height: 36px
+ margin: 4px 4px 6px 0
+ padding: 9px 11px
+ width: 100%
+
+ &:hover,
+ &:focus
+ background-color: #fff
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .33)
+ border: 0
+ cursor: pointer
+
+ &:focus
+ cursor: auto
+
+.editing-members
+ float: right
+
+ .edit-in-progress
+ display: inline-block
+ border: 1px solid #ccc
+ background: #ddd
+ margin: 0 4px
+ border-radius: 2px
+
+ .inline-member
+ cursor: default
+
+ .inline-member-av
+ width: 18px
+ height: 18px
+ margin: 0 0 -4px 0
+
+ .initials
+ margin-left: 3px
+
+ .icon
+ animation: pulsate 1s ease-in alternate
+ animation-iteration-count: infinite
+
+@keyframes pulsate
+ 0%
+ opacity: 1
+
+ to
+ opacity: .4
+
+.list-voters.compact .voter
+ position: relative
+ min-height: 36px
+
+ .member
+ left: 0
+ position: absolute
+ top: 0
+
+ .title
+ display: block
+ line-height: 30px
+ left: 0
+ overflow: hidden
+ padding-left: 38px
+ position: absolute
+ text-overflow: ellipsis
+ top: 0
+ white-space: nowrap
+ width: 230px
+
+.list-voters .title
+ display: none
+
+.card-composer
+ padding-bottom: 8px
+
+.cc-controls
+ margin-top: 1px
+
+ input[type="submit"]
+ float: left
+ margin-top: 0
+ padding: 5px 18px
+
+ .icon-lg
+ float: left
+
+ .cc-opt
+ float: right
+
+.minicard-placeholder,
+.minicard.placeholder
+ background: silver
+ border: none
+ min-height: 18px
+
+ .hook
+ height: 18px
+ position: absolute
+ right: 0
+ top: 0
+ width: 18px
+
+.chrome .minicard.ui-sortable-helper,
+.safari .minicard.ui-sortable-helper
+ box-shadow: -2px 2px 6px rgba(0, 0, 0, .2)
+
+input[type="text"].attachment-add-link-input
+ float: left
+ margin: 0 0 8px
+ width: 80%
+
+input[type="submit"].attachment-add-link-submit
+ float: left
+ margin: 0 0 8px 4px
+ padding: 6px 12px
+ width: 18%
+
+.card-detail-badge
+ background-color: #dbdbdb
+ border-radius: 3px
+ color: #737373
+ cursor: default
+ display: block
+ height: 20px
+ line-height: 20px
+ margin: 0 4px 4px 0
+ padding: 5px 10px
+ text-align: center
+ text-decoration: none
+
+ &:hover
+ color: #737373
+
+ &.badge-state-clickable
+ text-decoration: underline
+
+.badge-state-clickable:hover
+ color: #262626
+ cursor: pointer
+ text-decoration: underline
+
+.card-detail-badge-aging:first-letter
+ text-transform: uppercase
+
+.badge
+ color: #8c8c8c
+ float: left
+ height: 18px
+ margin: 0 3px 3px 0
+ padding: 0 4px 0 0
+ position: relative
+ text-decoration: none
+
+.badge-icon
+ float: left
+
+.badge-text
+ float: left
+ font-size: 12px
+
+.badge-state-image-only
+ padding: 0
+
+ .badge-icon
+ margin-right: 0
+
+.badge-state-clickable
+ cursor: pointer
+
+ .badge-text
+ text-decoration: underline
+
+.badge-state-complete
+ background-color: #4aba12
+ border-radius: 3px
+ color: #fff
+
+ .badge-icon
+ color: #fff
+
+.badge-state-unread-notification
+ background-color: #990f0f
+ border-radius: 3px
+ color: #fff
+
+ .badge-icon
+ color: #fff
+
+.badge-state-voted
+ background-color: #dbdbdb
+ border-radius: 3px
+ color: #8c8c8c
+
+ .badge-icon
+ color: #999
+
+.badge-state-due-soon, .badge-state-due-soon:hover
+ background-color: #e6bf00
+ border-radius: 3px
+ color: #fff
+
+ .badge-icon
+ color: #fff
+
+.badge-state-due-now, .badge-state-due-now:hover
+ background-color: #990f0f
+ border-radius: 3px
+ color: #fff
+
+ .badge-icon
+ color: #fff
+
+.badge-state-due-past, .badge-state-due-past:hover
+ background-color: #ad8585
+ border-radius: 3px
+ color: #fff
+
+ .badge-icon
+ color: #fff
+
+.checklist-list:empty
+ display: none
+
+.checklist
+ margin-bottom: 16px
+
+.checklist.placeholder
+ background: #dcdcdc
+ border-radius: 3px
+
+.checklist.ui-sortable-helper
+ background: rgba(240, 240, 240, .85)
+ border-radius: 3px
+
+ .checklist-title,
+ .current,
+ .window-module-title
+ cursor: grabbing
+
+ .icon-menu
+ visibility: hidden
+
+.checklist-items-list
+ min-height: 2px
+
+.checklist-item
+ clear: both
+ margin: 0 0 6px
+ padding: 0 0 4px 38px
+ position: relative
+ transform-origin: left bottom
+ transition-property: transform, opacity, height, padding, margin
+ transition-duration: .14s
+ transition-timing-function: ease-in
+
+ &.placeholder
+ background: #dcdcdc
+ border-radius: 3px
+ margin: -5px -5px 5px 5px
+ padding: 5px 0
+
+ &.ui-sortable-helper
+ background: rgba(240, 240, 240, .85)
+ border-radius: 3px
+ margin: -3px -3px -3px 7px
+ padding: 3px 3px 3px 33px
+
+ .checklist-item-checkbox
+ top: 2px
+ left: 2px
+
+.hide-completed-items .checklist-item-fade-out
+ height: 0
+ margin: 0
+ opacity: 0
+ padding: 0
+ transform: rotate(-5deg) translateX(-10px) translateY(-10px)
+
+.checklist-item-checkbox
+ background: #fff
+ border-radius: 3px
+ box-shadow: 0 2px 3px rgba(0, 0, 0, .1)
+ border: 1px solid #ccc
+ border-bottom-color: #b3b3b3
+ font-weight: 700
+ position: absolute
+ left: 6px
+ line-height: 18px
+ overflow: hidden
+ text-align: center
+ text-indent: 100%
+ top: -2px
+ height: 18px
+ width: 18px
+ white-space: nowrap
+
+ &.enabled:hover
+ background-color: #f0f0f0
+ border-color: #ccc
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
+ color: #8c8c8c
+ cursor: pointer
+ text-indent: 0
+
+ &.enabled:active
+ background-color: #e3e3e3
+ border-color: #ccc
+ box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1)
+ color: #4d4d4d
+ text-indent: 0
+
+.checklist-item-details-text
+ min-height: 18px
+ margin-bottom: 0
+
+ &.enabled:hover
+ color: #4d4d4d
+ cursor: pointer
+
+ &:empty
+ content: "No name"
+ color: #8c8c8c
+
+.checklist-item-state-complete
+
+ .checklist-item-details-text
+ color: #8c8c8c
+ font-style: italic
+ text-decoration: line-through
+
+ img
+ opacity: .3
+
+ .checklist-item-checkbox
+ background-color: #f0f0f0
+ border-color: #dbdbdb
+ border-bottom-color: #ccc
+ box-shadow: none
+ text-indent: 0
+
+ &.enabled:hover
+ background-color: #e6e6e6
+ border-color: #ccc
+ box-shadow: none
+
+ &.enabled:active
+ background-color: #dbdbdb
+ box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1)
+
+.hide-completed-items .checklist-item-state-complete
+ display: none
+
+.checklist-new-item-text,
+.checklist-new-item-text:hover
+ background: transparent
+ border-color: transparent
+ box-shadow: none
+ color: #8c8c8c
+ cursor: pointer
+ margin-bottom: 4px
+ max-height: 32px
+ overflow: hidden
+ resize: none
+ text-decoration: none
+
+ .checklist-new-item.focus &
+ background: #fff
+ border-color: #2b7cab
+ box-shadow: 0 0 3px #2b7cab
+ color: #4d4d4d
+ cursor: text
+ max-height: none
+ resize: vertical
+
+.checklist-progress
+ margin-bottom: 12px
+ position: relative
+
+.checklist-progress-percentage
+ color: #8c8c8c
+ font-size: 11px
+ line-height: 10px
+ position: absolute
+ left: 0
+ top: -1px
+ text-align: center
+ width: 38px
+
+.checklist-progress-bar
+ background: #dbdbdb
+ border-radius: 3px
+ clear: both
+ height: 8px
+ margin: 0 0 0 38px
+ overflow: hidden
+ position: relative
+
+.checklist-progress-bar-current
+ background: #479fd1
+ background: linear-gradient(to bottom, #479fd1 0, #2288c3 100%)
+ bottom: 0
+ left: 0
+ position: absolute
+ top: 0
+ transition: width .14s ease-in, background .14s ease-in
+
+.checklist-progress-bar-current-complete
+ background: #24a828
+
+.checklist-completed-text
+ display: block
+ margin: 8px 0 0 38px
+
+.checklist .edit
+ clear: both
+ margin-top: -5px
+
+.explorer .av-btn
+ background: url(about:blank)
+
+.atMention
+ background: #dbdbdb
+ border-radius: 3px
+ padding: 1px 4px
+ margin: -1px 0
+ display: inline-block
+
+ &.me
+ background: #cfdfe8
+
+.helper
+ background-color: #e6e6e6
+ border-radius: 3px
+ color: #8c8c8c
+ font-size: 13px
+ line-height: 15px
+ margin: 4px 0 0
+ padding: 6px 8px
+ width: auto
+
+ a
+ color: #8c8c8c
+
+ &:hover
+ color: #666
+
+.empty-list, .empty
+ background: #e6e6e6
+ border: 1px dashed #ccc
+ border-radius: 3px
+ color: #8c8c8c
+ display: block
+ padding: 6px
+ text-align: center
+
+.empty-list
+ border-radius: 6px
+ padding: 25px 6px
+
+.search-results-page-contents .empty-list
+ margin: 12px 0 0 52px
+
+.window-module .empty-list
+ margin: 8px 0 0 38px
+
+.loading
+ margin: 19px auto
+ text-align: center
+
+.big-message
+ display: block
+ margin: 75px auto
+ text-align: center
+ max-width: 600px
+
+ h1
+ font-size: 26px
+ margin-bottom: 24px
+
+ p
+ font-size: 18px
+ line-height: 22px
+
+ &.with-picture
+ margin-top: 35px
+
+ h1
+ margin-top: 20px
+
+ .callout
+ margin: 20px 0
+
+.callout
+ background: #e3e3e3
+ border-radius: 5px
+ padding: 20px
+
+ ol
+ text-align: left
+ list-style-type: decimal
+ margin-left: 25px
+ font-size: 16px
+
+ li
+ margin: 10px 0
+
+.gutter
+ margin-left: 38px
diff --git a/client/styles/temp.styl b/client/styles/temp.styl
new file mode 100644
index 00000000..9dab7802
--- /dev/null
+++ b/client/styles/temp.styl
@@ -0,0 +1,110 @@
+/**
+ * We should merge these declarations in the appropriate stylus files.
+ */
+
+.dn {
+ display:none;
+}
+
+.header-btn-btn {
+ padding-left:23px!important;
+}
+
+.bgnone {
+ background:none!important;
+}
+
+.tac {
+ text-align:center;
+
+ h1 {
+ font-size: 2em;
+ }
+}
+
+.tdn {
+ text-decoration:none;
+}
+
+.header-member {
+ min-width:105px!important;
+ text-align:center;
+}
+
+.primarys {
+ font-size:20px;
+ line-height: 1.44em;
+ padding: .6em 1.3em!important;
+ border-radius: 3px!important;
+ box-shadow: 0 2px 0 #4d4d4d!important;
+}
+
+.layout-twothirds-center {
+ display: block;
+ max-width: 585px;
+ margin: 0 auto;
+ position: relative;
+ font-size:20px;
+ line-height: 100px;
+}
+
+#WindowTitleEdit .single-line, .single-line2 {
+ overflow: hidden;
+ word-wrap: break-word;
+ resize: none;
+ height: 60px;
+}
+
+.single-line2 {
+ overflow: hidden;
+ word-wrap: break-word;
+ resize: none;
+ height: 108px;
+}
+
+#header-search {
+ float: left;
+ margin: 1px 8px 0 0;
+ position: relative;
+ z-index: 1;
+
+ label {
+ display:none;
+ }
+ input[type="text"] {
+ background:rgba(255,255,255,0.5);
+ border-top-left-radius:3px;
+ border-top-right-radius:0;
+ border-bottom-right-radius:0;
+ border-bottom-left-radius:3px;
+ border:none;
+ float:left;
+ font-size:13px;
+ height:29px;
+ min-height:29px;
+ line-height:19px;
+ width:160px;
+ margin:0;
+
+ &:hover{
+ background:rgba(255,255,255,0.7);
+ }
+
+ &:focus{
+ background:#e8ebee;
+ -webkit-box-shadow:none;
+ box-shadow:none
+ }
+ }
+
+ .header-btn{
+ border-top-left-radius:0;
+ border-top-right-radius:3px;
+ border-bottom-right-radius:3px;
+ border-bottom-left-radius:0
+ }
+
+ input[type="submit"]{
+ display:none
+ }
+}