Renaissance

_,,ad8888888888bba,_
                  ,ad88888I888888888888888ba,
                ,88888888I88888888888888888888a,
              ,d888888888I8888888888888888888888b,
             d88888PP"""" ""YY88888888888888888888b,
           ,d88"'__,,--------,,,,.;ZZZY8888888888888,
          ,8IIl'"                ;;l"ZZZIII8888888888,
         ,I88l;'                  ;lZZZZZ888III8888888,
       ,II88Zl;.                  ;llZZZZZ888888I888888,
      ,II888Zl;.                .;;;;;lllZZZ888888I8888b
     ,II8888Z;;                 `;;;;;''llZZ8888888I8888,
     II88888Z;'                        .;lZZZ8888888I888b
     II88888Z; _,aaa,      .,aaaaa,__.l;llZZZ88888888I888
     II88888IZZZZZZZZZ,  .ZZZZZZZZZZZZZZ;llZZ88888888I888,
     II88888IZZ<'(@@>Z|  |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
    ,II88888;   `""" ;|  |ZZ; `"""     ;;llZ8888888888I888
    II888888l            `;;          .;llZZ8888888888I888,
   ,II888888Z;           ;;;        .;;llZZZ8888888888I888I
   III888888Zl;    ..,   `;;       ,;;lllZZZ88888888888I888
   II88888888Z;;...;(_    _)      ,;;;llZZZZ88888888888I888,
   II88888888Zl;;;;;' `--'Z;.   .,;;;;llZZZZ88888888888I888b
   ]I888888888Z;;;;'   ";llllll;..;;;lllZZZZ88888888888I8888,
   II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
   II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
   II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
   `II8888888888888Zl;.    ,;;lllZZZZZZZZWMZ88888888888I88888
    II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
    `II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
     `II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
      `II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
       II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
      ,II8888888888888888b   .;;lllllll;;;.;..88888888I88888888b,
      II888888888888888PZI;.  .`;;;.;;;..; ...88888888I8888888888,
      II888888888888PZ;;';;.   ;. .;.  .;. .. Y8888888I88888888888b,
     ,II888888888PZ;;'                        `8888888I8888888888888b,
     II888888888'                              888888I8888888888888888
    ,II888888888                              ,888888I8888888888888888
   ,d88888888888                              d888888I8888888888ZZZZZZ
,ad888888888888I                              8888888I8888ZZZZZZZZZZZZ
888888888888888'                              888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P'                               Y888ZZZZZZZZZZZZZZZZZZZZ
888888888,  "                                 ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888,                                ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a,      _                    ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d'                  ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand  8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
This commit is contained in:
Maxime Quandalle 2015-05-12 19:20:58 +02:00
commit 2dbea30842
128 changed files with 10521 additions and 0 deletions

View file

@ -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

View file

@ -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');

View file

View file

@ -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();
}
});

View file

@ -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>

View file

@ -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'}}

View file

@ -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');

View file

@ -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

View file

@ -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)

View file

@ -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();
}
});

View file

@ -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'}}")

View file

@ -0,0 +1,7 @@
Template.headerBoard.helpers({
isStarred: function() {
var boardId = Session.get('currentBoard');
var user = Meteor.user();
return boardId && user && user.hasStarred(boardId);
}
});

View file

@ -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;
}
}
}

View file

@ -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);
}
});

View file

@ -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'}}

View file

@ -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

View file

@ -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);
}
});

View file

@ -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

View file

@ -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
}
});
}
});

View file

@ -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%

View file

@ -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();
}
});

View file

@ -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);
}
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}
});

View file

@ -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>

View file

@ -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('');
}
});

View file

@ -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

View file

@ -0,0 +1,6 @@
template(name='inlinedForm')
if isOpen.get
form(id=id class=classNames)
+Template.contentBlock
else
+Template.elseBlock

View file

@ -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');

View file

@ -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

View file

@ -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');

View file

@ -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 = '';
}
}
});

View file

@ -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

View file

@ -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');

View file

@ -0,0 +1,5 @@
template(name='list')
.list.js-list(id="js-list-{{_id}}")
.list-wrapper
+listHeader
+listBody

View file

@ -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');

View file

@ -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

View file

@ -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'}}")

View file

@ -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();
}
});

View file

@ -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();
}
}
});

View file

@ -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

View file

@ -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')
});

View file

@ -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;
// }

View file

@ -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);
}));

View file

@ -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

View file

@ -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);
}
});

View file

@ -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%

View file

@ -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)

View file

@ -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
}
]);
};

View file

@ -0,0 +1,5 @@
Router.route('/', {
name: 'Home',
redirectLoggedInUsers: true,
authenticated: true
});

View file

@ -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) }
}

View file

@ -0,0 +1,6 @@
.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass)
.sk-rect1
.sk-rect2
.sk-rect3
.sk-rect4
.sk-rect5

View file

@ -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>

View file

@ -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();
}
});

View file

View file

@ -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)

View file

@ -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);
// }
// });

View file

@ -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;
// }
// });

View file

@ -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');
}
}
}];
}
});

View file

@ -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();
}
});
});
});
};

View file

@ -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');

View file

@ -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

View file

@ -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>

View file

@ -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'}}

View file

@ -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)

View file

@ -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')
});

View file

@ -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

View file

@ -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'}}

View file

@ -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')
});

View file

@ -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();
}
});

View file

@ -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

View file

@ -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'
});

View file

@ -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>

35
client/config/accounts.js Normal file
View file

@ -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
}
]);

3
client/config/avatar.js Normal file
View file

@ -0,0 +1,3 @@
Avatar.options = {
fallbackType: 'initials'
};

28
client/config/router.js Normal file
View file

@ -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();
}
});

152
client/lib/emoji-values.js Normal file
View file

@ -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'];

133
client/lib/filter.js Normal file
View file

@ -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);

22
client/lib/i18n.js Normal file
View file

@ -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);
}
});

55
client/lib/keyboard.js Normal file
View file

@ -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);
}
});

1
client/lib/mixins.js Normal file
View file

@ -0,0 +1 @@
Mixins = {};

200
client/lib/popup.js Normal file
View file

@ -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();
}
});

96
client/lib/utils.js Normal file
View file

@ -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;
}
}
};

79
client/styles/cheat.styl Normal file
View file

@ -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

View file

@ -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

814
client/styles/main.styl Normal file
View file

@ -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

110
client/styles/temp.styl Normal file
View file

@ -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
}
}