Major CSS revamp

- Get rid of Bootstrap (except we still rely on some JS)
- Use BEM class names
- Rework variables/theme config
- Fix various bugs, including some on mobile

The CSS is still not ideal – it needs to be cleaned up some more. But
that can be a focus for after beta.
This commit is contained in:
Toby Zerner
2015-07-17 14:47:49 +09:30
parent 76678f72f2
commit a9ded36b57
206 changed files with 4337 additions and 8830 deletions

View File

@ -1,26 +1,30 @@
var gulp = require('flarum-gulp'); var gulp = require('flarum-gulp');
var nodeDir = 'node_modules';
var bowerDir = '../bower_components';
gulp({ gulp({
files: [ files: [
'node_modules/babel-core/external-helpers.js', nodeDir + '/babel-core/external-helpers.js',
'../bower_components/es6-promise-polyfill/promise.js',
'../bower_components/es6-micro-loader/dist/system-polyfill.js',
'../bower_components/mithril/mithril.js', bowerDir + '/es6-promise-polyfill/promise.js',
'../bower_components/jquery/dist/jquery.js', bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
'../bower_components/jquery.hotkeys/jquery.hotkeys.js',
'../bower_components/color-thief/js/color-thief.js',
'../bower_components/moment/moment.js',
'../bower_components/bootstrap/js/affix.js', bowerDir + '/mithril/mithril.js',
'../bower_components/bootstrap/js/dropdown.js', bowerDir + '/jquery/dist/jquery.js',
'../bower_components/bootstrap/js/modal.js', bowerDir + '/jquery.hotkeys/jquery.hotkeys.js',
'../bower_components/bootstrap/js/tooltip.js', bowerDir + '/color-thief/js/color-thief.js',
'../bower_components/bootstrap/js/transition.js', bowerDir + '/moment/moment.js',
'../bower_components/spin.js/spin.js', bowerDir + '/bootstrap/js/affix.js',
'../bower_components/spin.js/jquery.spin.js', bowerDir + '/bootstrap/js/dropdown.js',
'../bower_components/fastclick/lib/fastclick.js' bowerDir + '/bootstrap/js/modal.js',
bowerDir + '/bootstrap/js/tooltip.js',
bowerDir + '/bootstrap/js/transition.js',
bowerDir + '/spin.js/spin.js',
bowerDir + '/spin.js/jquery.spin.js',
bowerDir + '/fastclick/lib/fastclick.js'
], ],
moduleFiles: [ moduleFiles: [
'src/**/*.js', 'src/**/*.js',

View File

@ -17,11 +17,11 @@ export default class Activity extends Component {
const activity = this.props.activity; const activity = this.props.activity;
return ( return (
<div className="activity"> <div className="Activity">
{avatar(this.user(), {className: 'activity-icon'})} {avatar(this.user(), {className: 'Activity-avatar'})}
<div className="activity-info"> <div className="Activity-header">
<strong>{this.description()}</strong> <strong className="Activity-description">{this.description()}</strong>
{humanTime(activity.time())} {humanTime(activity.time())}
</div> </div>

View File

@ -47,10 +47,10 @@ export default class ActivityPage extends UserPage {
footer = LoadingIndicator.component(); footer = LoadingIndicator.component();
} else if (this.moreResults) { } else if (this.moreResults) {
footer = ( footer = (
<div className="load-more"> <div className="ActivityPage-loadMore">
{Button.component({ {Button.component({
children: 'Load More', children: 'Load More',
className: 'btn btn-default', className: 'Button--default',
onclick: this.loadMore.bind(this) onclick: this.loadMore.bind(this)
})} })}
</div> </div>
@ -58,8 +58,8 @@ export default class ActivityPage extends UserPage {
} }
return ( return (
<div className="user-activity"> <div className="ActivityPage">
<ul className="activity-list"> <ul className="ActivityPage-list">
{this.activity.map(activity => { {this.activity.map(activity => {
const ActivityComponent = app.activityComponents[activity.contentType()]; const ActivityComponent = app.activityComponents[activity.contentType()];
return ActivityComponent ? <li>{ActivityComponent.component({activity})}</li> : ''; return ActivityComponent ? <li>{ActivityComponent.component({activity})}</li> : '';

View File

@ -37,15 +37,14 @@ export default class AvatarEditor extends Component {
const user = this.props.user; const user = this.props.user;
return ( return (
<div className={'avatar-editor dropdown ' + this.props.className + (this.loading ? ' loading' : '')}> <div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
{avatar(user)} {avatar(user)}
<a className="dropdown-toggle" <a className="Dropdown-toggle"
href="javascript:;"
data-toggle="dropdown" data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}> onclick={this.quickUpload.bind(this)}>
{this.loading ? LoadingIndicator.component() : icon('pencil', {className: 'icon'})} {this.loading ? LoadingIndicator.component() : icon('pencil')}
</a> </a>
<ul className="dropdown-menu"> <ul className="Dropdown-menu Menu">
{listItems(this.controlItems().toArray())} {listItems(this.controlItems().toArray())}
</ul> </ul>
</div> </div>

View File

@ -24,7 +24,7 @@ export default class ChangeEmailModal extends Modal {
} }
className() { className() {
return 'modal-sm change-email-modal'; return 'ChangeEmailModal Modal--small';
} }
title() { title() {
@ -36,11 +36,11 @@ export default class ChangeEmailModal extends Modal {
const emailProviderName = this.email().split('@')[1]; const emailProviderName = this.email().split('@')[1];
return ( return (
<div className="modal-body"> <div className="Modal-body">
<div class="form-centered"> <div class="Form Form--centered">
<p class="help-text">We've sent a confirmation email to <strong>{this.email()}</strong>. If it doesn't arrive soon, check your spam folder.</p> <p class="helpText">We've sent a confirmation email to <strong>{this.email()}</strong>. If it doesn't arrive soon, check your spam folder.</p>
<div class="form-group"> <div class="Form-group">
<a href={'http://' + emailProviderName} className="btn btn-primary btn-block">Go to {emailProviderName}</a> <a href={'http://' + emailProviderName} className="Button Button--primary Button--block">Go to {emailProviderName}</a>
</div> </div>
</div> </div>
</div> </div>
@ -48,17 +48,17 @@ export default class ChangeEmailModal extends Modal {
} }
return ( return (
<div className="modal-body"> <div className="Modal-body">
<div class="form-centered"> <div class="Form Form--centered">
<div class="form-group"> <div class="Form-group">
<input type="email" name="email" className="form-control" <input type="email" name="email" className="FormControl"
placeholder={app.session.user.email()} placeholder={app.session.user.email()}
value={this.email()} value={this.email()}
onchange={m.withAttr('value', this.email)} onchange={m.withAttr('value', this.email)}
disabled={this.loading}/> disabled={this.loading}/>
</div> </div>
<div class="form-group"> <div class="Form-group">
<button type="submit" className="btn btn-primary btn-block" disabled={this.loading}>Save Changes</button> <button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>Save Changes</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ import Modal from 'flarum/components/Modal';
*/ */
export default class ChangePasswordModal extends Modal { export default class ChangePasswordModal extends Modal {
className() { className() {
return 'modal-sm change-password-modal'; return 'ChangePasswordModal Modal--small';
} }
title() { title() {
@ -15,11 +15,11 @@ export default class ChangePasswordModal extends Modal {
content() { content() {
return ( return (
<div className="modal-body"> <div className="Modal-body">
<div className="form-centered"> <div className="Form Form--centered">
<p className="help-text">Click the button below and check your email for a link to change your password.</p> <p className="helpText">Click the button below and check your email for a link to change your password.</p>
<div className="form-group"> <div className="Form-group">
<button type="submit" className="btn btn-primary btn-block" disabled={this.loading}>Send Password Reset Email</button> <button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>Send Password Reset Email</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ import EditPostComposer from 'flarum/components/EditPostComposer';
import Composer from 'flarum/components/Composer'; import Composer from 'flarum/components/Composer';
import ItemList from 'flarum/utils/ItemList'; import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems'; import listItems from 'flarum/helpers/listItems';
import icon from 'flarum/helpers/icon'; import Button from 'flarum/components/Button';
/** /**
* The `CommentPost` component displays a standard `comment`-typed post. This * The `CommentPost` component displays a standard `comment`-typed post. This
@ -38,10 +38,10 @@ export default class CommentPost extends Post {
content() { content() {
return [ return [
<header className="post-header"><ul>{listItems(this.headerItems().toArray())}</ul></header>, <header className="Post-header"><ul>{listItems(this.headerItems().toArray())}</ul></header>,
<div className="post-body">{m.trust(this.props.post.contentHtml())}</div>, <div className="Post-body">{m.trust(this.props.post.contentHtml())}</div>,
<aside className="post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></aside>, <footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>,
<aside className="post-actions"><ul>{listItems(this.actionItems().toArray())}</ul></aside> <aside className="Post-actions"><ul>{listItems(this.actionItems().toArray())}</ul></aside>
]; ];
} }
@ -50,10 +50,10 @@ export default class CommentPost extends Post {
return { return {
className: classList({ className: classList({
'comment-post': true, 'CommentPost': true,
'is-hidden': post.isHidden(), 'hidden': post.isHidden(),
'is-edited': post.isEdited(), 'edited': post.isEdited(),
'reveal-content': this.revealContent, 'revealContent': this.revealContent,
'editing': app.composer.component instanceof EditPostComposer && 'editing': app.composer.component instanceof EditPostComposer &&
app.composer.component.props.post === post && app.composer.component.props.post === post &&
app.composer.position !== Composer.PositionEnum.MINIMIZED app.composer.position !== Composer.PositionEnum.MINIMIZED
@ -89,11 +89,11 @@ export default class CommentPost extends Post {
// of the post's content. // of the post's content.
if (post.isHidden()) { if (post.isHidden()) {
items.add('toggle', ( items.add('toggle', (
<button Button.component({
className="btn btn-default btn-more" className: 'Button Button--default Button--more',
onclick={this.toggleContent.bind(this)}> icon: 'ellipsis-h',
{icon('ellipsis-h')} onclick: this.toggleContent.bind(this)
</button> })
)); ));
} }

View File

@ -62,7 +62,7 @@ class Composer extends Component {
view() { view() {
const classes = { const classes = {
'minimized': this.position === Composer.PositionEnum.MINIMIZED, 'minimized': this.position === Composer.PositionEnum.MINIMIZED,
'full-screen': this.position === Composer.PositionEnum.FULLSCREEN 'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN
}; };
classes.visible = this.position === Composer.PositionEnum.NORMAL || classes.minimized || classes.fullScreen; classes.visible = this.position === Composer.PositionEnum.NORMAL || classes.minimized || classes.fullScreen;
@ -76,10 +76,10 @@ class Composer extends Component {
}; };
return ( return (
<div className={'composer ' + classList(classes)}> <div className={'Composer ' + classList(classes)}>
<div className="composer-handle"/> <div className="Composer-handle" config={this.configHandle.bind(this)}/>
<ul className="composer-controls">{listItems(this.controlItems().toArray())}</ul> <ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul>
<div className="composer-content" onclick={showIfMinimized}> <div className="Composer-content" onclick={showIfMinimized}>
{this.component ? this.component.render() : ''} {this.component ? this.component.render() : ''}
</div> </div>
</div> </div>
@ -113,20 +113,8 @@ class Composer extends Component {
return (this.component && this.component.preventExit()) || null; return (this.component && this.component.preventExit()) || null;
}; };
// Add the necessary event handlers to the composer's handle so that it can
// be used to resize the composer.
const composer = this;
const handlers = {}; const handlers = {};
this.$('.composer-handle').css('cursor', 'row-resize')
.bind('dragstart mousedown', e => e.preventDefault())
.mousedown(function(e) {
composer.mouseStart = e.clientY;
composer.heightStart = composer.$().height();
composer.handle = $(this);
$('body').css('cursor', 'row-resize');
});
$(window).on('resize', handlers.onresize = this.updateHeight.bind(this)).resize(); $(window).on('resize', handlers.onresize = this.updateHeight.bind(this)).resize();
$(document) $(document)
@ -142,6 +130,28 @@ class Composer extends Component {
}; };
} }
/**
* Add the necessary event handlers to the composer's handle so that it can
* be used to resize the composer.
*
* @param {DOMElement} element
* @param {Boolean} isInitialized
*/
configHandle(element, isInitialized) {
if (isInitialized) return;
const composer = this;
$(element).css('cursor', 'row-resize')
.bind('dragstart mousedown', e => e.preventDefault())
.mousedown(function(e) {
composer.mouseStart = e.clientY;
composer.heightStart = composer.$().height();
composer.handle = $(this);
$('body').css('cursor', 'row-resize');
});
}
/** /**
* Resize the composer according to mouse movement. * Resize the composer according to mouse movement.
* *
@ -185,15 +195,17 @@ class Composer extends Component {
* of any flexible elements inside the composer's body. * of any flexible elements inside the composer's body.
*/ */
updateHeight() { updateHeight() {
// TODO: update this in a way that is independent of the TextEditor being
// present.
const height = this.computedHeight(); const height = this.computedHeight();
const $flexible = this.$('.flexible-height'); const $flexible = this.$('.TextEditor-flexible');
this.$().height(height); this.$().height(height);
if ($flexible.length) { if ($flexible.length) {
const headerHeight = $flexible.offset().top - this.$().offset().top; const headerHeight = $flexible.offset().top - this.$().offset().top;
const paddingBottom = parseInt($flexible.css('padding-bottom'), 10); const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);
const footerHeight = this.$('.text-editor-controls').outerHeight(true); const footerHeight = this.$('.TextEditor-controls').outerHeight(true);
$flexible.height(height - headerHeight - paddingBottom - footerHeight); $flexible.height(height - headerHeight - paddingBottom - footerHeight);
} }
@ -209,7 +221,7 @@ class Composer extends Component {
this.position !== Composer.PositionEnum.MINIMIZED; this.position !== Composer.PositionEnum.MINIMIZED;
const paddingBottom = visible const paddingBottom = visible
? this.computedHeight() - parseInt($('#page').css('padding-bottom'), 10) ? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
: 0; : 0;
$('#content').css({paddingBottom}); $('#content').css({paddingBottom});
} }
@ -431,7 +443,7 @@ class Composer extends Component {
icon: 'minus minimize', icon: 'minus minimize',
title: 'Minimize', title: 'Minimize',
onclick: this.minimize.bind(this), onclick: this.minimize.bind(this),
wrapperClass: 'back-control' itemClassName: 'App-backControl'
})); }));
items.add('fullScreen', ComposerButton.component({ items.add('fullScreen', ComposerButton.component({

View File

@ -58,13 +58,13 @@ export default class ComposerBody extends Component {
this.editor.props.disabled = this.loading; this.editor.props.disabled = this.loading;
return ( return (
<div> <div className="ComposerBody">
{avatar(this.props.user, {className: 'composer-avatar'})} {avatar(this.props.user, {className: 'ComposerBody-avatar'})}
<div className="composer-body"> <div className="ComposerBody-content">
<ul className="composer-header">{listItems(this.headerItems().toArray())}</ul> <ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
<div className="composer-editor">{this.editor.render()}</div> <div className="ComposerBody-editor">{this.editor.render()}</div>
</div> </div>
{LoadingIndicator.component({className: 'composer-loading' + (this.loading ? ' active' : '')})} {LoadingIndicator.component({className: 'ComposerBody-loading' + (this.loading ? ' active' : '')})}
</div> </div>
); );
} }

View File

@ -8,6 +8,6 @@ export default class ComposerButton extends Button {
static initProps(props) { static initProps(props) {
super.initProps(props); super.initProps(props);
props.className = props.className || 'btn btn-icon btn-link'; props.className = props.className || 'Button Button--icon Button--link';
} }
} }

View File

@ -19,7 +19,7 @@ export default class DeleteAccountModal extends Modal {
} }
className() { className() {
return 'modal-sm delete-account-modal'; return 'DeleteAccountModal Modal--small';
} }
title() { title() {
@ -28,24 +28,24 @@ export default class DeleteAccountModal extends Modal {
content() { content() {
return ( return (
<div className="modal-body"> <div className="Modal-body">
<div className="form-centered"> <div className="Form Form--centered">
<div className="help-text"> <div className="helpText">
<p>Hold up! If you delete your account, there&#39;s no going back. Keep in mind:</p> <p>Hold up! If you delete your account, there&#39;s no going back. Keep in mind:</p>
<ul> <ul>
<li>Your username will be released, so someone else will be able to sign up with your name.</li> <li>Your username will be released, so someone else will be able to sign up with your name.</li>
<li>All of your posts will remain, but no longer associated with your account.</li> <li>All of your posts will remain, but no longer associated with your account.</li>
</ul> </ul>
</div> </div>
<div className="form-group"> <div className="Form-group">
<input className="form-control" <input className="FormControl"
name="confirm" name="confirm"
placeholder="Type &quot;DELETE&quot; to proceed" placeholder="Type 'DELETE' to proceed"
oninput={m.withAttr('value', this.confirmation)}/> oninput={m.withAttr('value', this.confirmation)}/>
</div> </div>
<div className="form-group"> <div className="Form-group">
<button type="submit" <button type="submit"
className="btn btn-primary btn-block" className="Button Button--primary Button--block"
disabled={this.loading || this.confirmation() !== 'DELETE'}> disabled={this.loading || this.confirmation() !== 'DELETE'}>
Delete Account Delete Account
</button> </button>

View File

@ -37,7 +37,7 @@ export default class DiscussionComposer extends ComposerBody {
items.add('title', ( items.add('title', (
<h3> <h3>
<input className="form-control" <input className="FormControl"
value={this.title()} value={this.title()}
oninput={m.withAttr('value', this.title)} oninput={m.withAttr('value', this.title)}
placeholder={this.props.titlePlaceholder} placeholder={this.props.titlePlaceholder}

View File

@ -12,9 +12,9 @@ import listItems from 'flarum/helpers/listItems';
export default class DiscussionHero extends Component { export default class DiscussionHero extends Component {
view() { view() {
return ( return (
<header className="hero discussion-hero"> <header className="Hero DiscussionHero">
<div className="container"> <div className="container">
<ul className="discussion-hero-items">{listItems(this.items().toArray())}</ul> <ul className="DiscussionHero-items">{listItems(this.items().toArray())}</ul>
</div> </div>
</header> </header>
); );
@ -31,10 +31,10 @@ export default class DiscussionHero extends Component {
const badges = discussion.badges().toArray(); const badges = discussion.badges().toArray();
if (badges.length) { if (badges.length) {
items.add('badges', <ul className="badges">{listItems(badges)}</ul>); items.add('badges', <ul className="DiscussionHero-badges">{listItems(badges)}</ul>);
} }
items.add('title', <h2 className="discussion-title">{discussion.title()}</h2>); items.add('title', <h2 className="DiscussionHero-title">{discussion.title()}</h2>);
return items; return items;
} }

View File

@ -52,20 +52,16 @@ export default class DiscussionList extends Component {
if (this.loading) { if (this.loading) {
loading = LoadingIndicator.component(); loading = LoadingIndicator.component();
} else if (this.moreResults) { } else if (this.moreResults) {
loading = ( loading = Button.component({
<div className="load-more"> children: 'Load More',
{Button.component({ className: 'Button',
children: 'Load More', onclick: this.loadMore.bind(this)
className: 'btn btn-default', });
onclick: this.loadMore.bind(this)
})}
</div>
);
} }
return ( return (
<div className="discussion-list"> <div className="DiscussionList">
<ul> <ul className="DiscussionList-discussions">
{this.discussions.map(discussion => { {this.discussions.map(discussion => {
return ( return (
<li key={discussion.id()} data-id={discussion.id()}> <li key={discussion.id()} data-id={discussion.id()}>
@ -74,7 +70,9 @@ export default class DiscussionList extends Component {
); );
})} })}
</ul> </ul>
{loading} <div className="DiscussionList-loadMore">
{loading}
</div>
</div> </div>
); );
} }

View File

@ -45,28 +45,27 @@ export default class DiscussionListItem extends Component {
const isUnread = discussion.isUnread(); const isUnread = discussion.isUnread();
const showUnread = !this.showRepliesCount() && isUnread; const showUnread = !this.showRepliesCount() && isUnread;
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1); const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : ''; const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
const controls = DiscussionControls.controls(discussion, this).toArray(); const controls = DiscussionControls.controls(discussion, this).toArray();
return this.subtree.retain() || ( return this.subtree.retain() || (
<div className={'discussion-list-item' + (this.active() ? ' active' : '')}> <div className={'DiscussionListItem ' + (this.active() ? 'active' : '')}>
{controls.length ? Dropdown.component({ {controls.length ? Dropdown.component({
icon: 'ellipsis-v', icon: 'ellipsis-v',
children: controls, children: controls,
className: 'contextual-controls', className: 'DiscussionListItem-controls',
buttonClassName: 'btn btn-default btn-naked btn-controls slidable-underneath slidable-underneath-right', buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right'
menuClassName: 'dropdown-menu-right'
}) : ''} }) : ''}
<a className={'slidable-underneath slidable-underneath-left elastic' + (isUnread ? '' : ' disabled')} <a className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
onclick={this.markAsRead.bind(this)}> onclick={this.markAsRead.bind(this)}>
{icon('check', {className: 'icon'})} {icon('check')}
</a> </a>
<div className={'discussion-summary slidable-slider' + (isUnread ? ' unread' : '')}> <div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
<a href={startUser ? app.route.user(startUser) : '#'} <a href={startUser ? app.route.user(startUser) : '#'}
className="author" className="DiscussionListItem-author"
title={'Started by ' + (startUser ? startUser.username() : '[deleted]') + ' ' + humanTime(discussion.startTime())} title={'Started by ' + (startUser ? startUser.username() : '[deleted]') + ' ' + humanTime(discussion.startTime())}
config={function(element) { config={function(element) {
$(element).tooltip({placement: 'right'}); $(element).tooltip({placement: 'right'});
@ -75,23 +74,25 @@ export default class DiscussionListItem extends Component {
{avatar(startUser, {title: ''})} {avatar(startUser, {title: ''})}
</a> </a>
<ul className="badges">{listItems(discussion.badges().toArray())}</ul> <ul className="DiscussionListItem-badges badges">
{listItems(discussion.badges().toArray())}
</ul>
<a href={app.route.discussion(discussion, jumpTo)} <a href={app.route.discussion(discussion, jumpTo)}
config={m.route} config={m.route}
className="main"> className="DiscussionListItem-main">
<h3 className="title">{highlight(discussion.title(), this.props.params.q)}</h3> <h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.props.params.q)}</h3>
<ul className="info">{listItems(this.infoItems().toArray())}</ul> <ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
</a> </a>
<span className="count" <span className="DiscussionListItem-count"
onclick={this.markAsRead.bind(this)} onclick={this.markAsRead.bind(this)}
title={showUnread ? 'Mark as Read' : ''}> title={showUnread ? 'Mark as Read' : ''}>
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())} {abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
</span> </span>
{relevantPosts && relevantPosts.length {relevantPosts && relevantPosts.length
? <div className="relevant-posts"> ? <div className="DiscussionListItem-relevantPosts">
{relevantPosts.map(post => PostPreview.component({post, highlight: this.props.params.q}))} {relevantPosts.map(post => PostPreview.component({post, highlight: this.props.params.q}))}
</div> </div>
: ''} : ''}
@ -108,9 +109,9 @@ export default class DiscussionListItem extends Component {
// This allows the user to drag the row to either side of the screen to // This allows the user to drag the row to either side of the screen to
// reveal controls. // reveal controls.
if ('ontouchstart' in window) { if ('ontouchstart' in window) {
const slidableInstance = slidable(this.$().addClass('slidable')); const slidableInstance = slidable(this.$().addClass('Slidable'));
this.$('.contextual-controls') this.$('.DiscussionListItem-controls')
.on('hidden.bs.dropdown', () => slidableInstance.reset()); .on('hidden.bs.dropdown', () => slidableInstance.reset());
} }
} }

View File

@ -92,25 +92,27 @@ export default class DiscussionPage extends mixin(Component, evented) {
const discussion = this.discussion; const discussion = this.discussion;
return ( return (
<div> <div className="DiscussionPage">
{app.cache.discussionList {app.cache.discussionList
? <div className="index-area paned" config={this.configPane.bind(this)}> ? <div className="DiscussionPage-list" config={this.configPane.bind(this)}>
{app.cache.discussionList.render()} {app.cache.discussionList.render()}
</div> </div>
: ''} : ''}
<div className="discussion-area"> <div className="DiscussionPage-discussion">
{discussion {discussion
? [ ? [
DiscussionHero.component({discussion}), DiscussionHero.component({discussion}),
<div className="container"> <div className="container">
<nav className="discussion-nav"> <nav className="DiscussionPage-nav">
<ul>{listItems(this.sidebarItems().toArray())}</ul> <ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav> </nav>
{this.stream.render()} <div className="DiscussionPage-stream">
{this.stream.render()}
</div>
</div> </div>
] ]
: LoadingIndicator.component({className: 'loading-indicator-block'})} : LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</div> </div>
</div> </div>
); );
@ -119,10 +121,10 @@ export default class DiscussionPage extends mixin(Component, evented) {
config(isInitialized, context) { config(isInitialized, context) {
if (isInitialized) return; if (isInitialized) return;
context.retain = true; // context.retain = true;
$('body').addClass('discussion-page'); $('#app').addClass('App--discussion');
context.onunload = () => $('body').removeClass('discussion-page'); context.onunload = () => $('#app').removeClass('App--discussion');
} }
/** /**
@ -198,7 +200,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
.filter(record => record.type === 'posts' && record.relationships && record.relationships.discussion) .filter(record => record.type === 'posts' && record.relationships && record.relationships.discussion)
.map(record => app.store.getById('posts', record.id)) .map(record => app.store.getById('posts', record.id))
.sort((a, b) => a.id() - b.id()) .sort((a, b) => a.id() - b.id())
.splice(20); .slice(0, 20);
} }
// Set up the post stream for this discussion, along with the first page of // Set up the post stream for this discussion, along with the first page of
@ -240,7 +242,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
// If the discussion we are viewing is listed in the discussion list, then // If the discussion we are viewing is listed in the discussion list, then
// we will make sure it is visible in the viewport – if it is not we will // we will make sure it is visible in the viewport – if it is not we will
// scroll the list down to it. // scroll the list down to it.
const $discussion = $list.find('.discussion-list-item.active'); const $discussion = $list.find('.DiscussionListItem.active');
if ($discussion.length) { if ($discussion.length) {
const listTop = $list.offset().top; const listTop = $list.offset().top;
const listBottom = listTop + $list.outerHeight(); const listBottom = listTop + $list.outerHeight();
@ -265,15 +267,15 @@ export default class DiscussionPage extends mixin(Component, evented) {
SplitDropdown.component({ SplitDropdown.component({
children: DiscussionControls.controls(this.discussion, this).toArray(), children: DiscussionControls.controls(this.discussion, this).toArray(),
icon: 'ellipsis-v', icon: 'ellipsis-v',
className: 'primary-control', className: 'App-primaryControl',
buttonClassName: 'btn btn-primary' buttonClassName: 'Button--primary'
}) })
); );
items.add('scrubber', items.add('scrubber',
PostStreamScrubber.component({ PostStreamScrubber.component({
stream: this.stream, stream: this.stream,
className: 'title-control' className: 'App-titleControl'
}) })
); );

View File

@ -18,6 +18,6 @@ export default class DiscussionRenamedPost extends EventPost {
const oldTitle = post.content()[0]; const oldTitle = post.content()[0];
const newTitle = post.content()[1]; const newTitle = post.content()[1];
return ['changed the title from ', m('strong.old-title', oldTitle), ' to ', m('strong.new-title', newTitle), '.']; return ['changed the title from ', m('strong.DiscussionRenamedPost-old', oldTitle), ' to ', m('strong.DiscussionRenamedPost-new', newTitle), '.'];
} }
} }

View File

@ -1,5 +1,5 @@
import highlight from 'flarum/helpers/highlight'; import highlight from 'flarum/helpers/highlight';
import Button from 'flarum/components/Button'; import LinkButton from 'flarum/components/LinkButton';
/** /**
* The `DiscussionsSearchSource` finds and displays discussion search results in * The `DiscussionsSearchSource` finds and displays discussion search results in
@ -28,13 +28,12 @@ export default class DiscussionsSearchSource {
const results = this.results[query] || []; const results = this.results[query] || [];
return [ return [
<li className="dropdown-header">Discussions</li>, <li className="Dropdown-header">Discussions</li>,
<li> <li>
{Button.component({ {LinkButton.component({
icon: 'search', icon: 'search',
children: 'Search all discussions for "' + query + '"', children: 'Search all discussions for "' + query + '"',
href: app.route('index', {q: query}), href: app.route('index', {q: query})
config: m.route
})} })}
</li>, </li>,
results.map(discussion => { results.map(discussion => {
@ -42,10 +41,10 @@ export default class DiscussionsSearchSource {
const post = relevantPosts && relevantPosts[0]; const post = relevantPosts && relevantPosts[0];
return ( return (
<li className="discussion-search-result" data-index={'discussions' + discussion.id()}> <li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<a href={app.route.discussion(discussion, post && post.number())} config={m.route}> <a href={app.route.discussion(discussion, post && post.number())} config={m.route}>
<div className="title">{highlight(discussion.title(), query)}</div> <div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{post ? <div className="excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''} {post ? <div className="DiscussionSearchResult-excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''}
</a> </a>
</li> </li>
); );

View File

@ -11,7 +11,7 @@ import icon from 'flarum/helpers/icon';
* - All of the props for ComposerBody * - All of the props for ComposerBody
* - `post` * - `post`
*/ */
export default class EditComposer extends ComposerBody { export default class EditPostComposer extends ComposerBody {
static initProps(props) { static initProps(props) {
super.initProps(props); super.initProps(props);
@ -27,7 +27,7 @@ export default class EditComposer extends ComposerBody {
items.add('title', ( items.add('title', (
<h3> <h3>
{icon('pencil')} {icon('pencil')}{' '}
<a href={app.route.discussion(post.discussion(), post.number())} config={m.route}> <a href={app.route.discussion(post.discussion(), post.number())} config={m.route}>
Post #{post.number()} in {post.discussion().title()} Post #{post.number()} in {post.discussion().title()}
</a> </a>

View File

@ -16,7 +16,7 @@ import icon from 'flarum/helpers/icon';
export default class EventPost extends Post { export default class EventPost extends Post {
attrs() { attrs() {
return { return {
className: 'event-post ' + this.props.post.contentType() + '-post' className: 'EventPost EventPost--' + this.props.post.contentType()
}; };
} }
@ -25,9 +25,9 @@ export default class EventPost extends Post {
const username = usernameHelper(user); const username = usernameHelper(user);
return [ return [
icon(this.icon(), {className: 'event-post-icon'}), icon(this.icon(), {className: 'EventPost-icon'}),
<div class="event-post-info"> <div class="EventPost-info">
{user ? <a className="post-user" href={app.route.user(user)} config={m.route}>{username}</a> : username} {user ? <a className="EventPost-user" href={app.route.user(user)} config={m.route}>{username}</a> : username}{' '}
{this.description()} {this.description()}
</div> </div>
]; ];

View File

@ -10,7 +10,7 @@ import listItems from 'flarum/helpers/listItems';
export default class FooterPrimary extends Component { export default class FooterPrimary extends Component {
view() { view() {
return ( return (
<ul className="footer-controls"> <ul className="Footer-controls">
{listItems(this.items().toArray())} {listItems(this.items().toArray())}
</ul> </ul>
); );

View File

@ -10,7 +10,7 @@ import listItems from 'flarum/helpers/listItems';
export default class FooterSecondary extends Component { export default class FooterSecondary extends Component {
view() { view() {
return ( return (
<ul className="footer-controls"> <ul className="Footer-controls">
{listItems(this.items().toArray())} {listItems(this.items().toArray())}
</ul> </ul>
); );

View File

@ -29,40 +29,44 @@ export default class ForgotPasswordModal extends Modal {
} }
className() { className() {
return 'modal-sm forgot-password'; return 'ForgotPasswordModal Modal--small';
} }
title() { title() {
return 'Forgot Password'; return 'Forgot Password';
} }
body() { content() {
if (this.success) { if (this.success) {
const emailProviderName = this.email().split('@')[1]; const emailProviderName = this.email().split('@')[1];
return ( return (
<div className="form-centered"> <div className="Modal-body">
<p className="help-text">We've sent you an email containing a link to reset your password. Check your spam folder if you don't receive it within the next minute or two.</p> <div className="Form Form--centered">
<div className="form-group"> <p className="helpText">We've sent you an email containing a link to reset your password. Check your spam folder if you don't receive it within the next minute or two.</p>
<a href={'http://' + emailProviderName} className="btn btn-primary btn-block">Go to {emailProviderName}</a> <div className="Form-group">
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">Go to {emailProviderName}</a>
</div>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="form-centered"> <div className="Modal-body">
<p className="help-text">Enter your email address and we will send you a link to reset your password.</p> <div className="Form Form--centered">
<div className="form-group"> <p className="helpText">Enter your email address and we will send you a link to reset your password.</p>
<input className="form-control" name="email" type="email" placeholder="Email" <div className="Form-group">
value={this.email()} <input className="FormControl" name="email" type="email" placeholder="Email"
onchange={m.withAttr('value', this.email)} value={this.email()}
disabled={this.loading} /> onchange={m.withAttr('value', this.email)}
</div> disabled={this.loading} />
<div className="form-group"> </div>
<button type="submit" className="btn btn-primary btn-block" disabled={this.loading}> <div className="Form-group">
Recover Password <button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>
</button> Recover Password
</button>
</div>
</div> </div>
</div> </div>
); );
@ -92,7 +96,7 @@ export default class ForgotPasswordModal extends Modal {
}, },
response => { response => {
this.loading = false; this.loading = false;
this.handleErrors(response.errors); this.handleErrors(response);
} }
); );
} }

View File

@ -9,7 +9,7 @@ import listItems from 'flarum/helpers/listItems';
export default class HeaderPrimary extends Component { export default class HeaderPrimary extends Component {
view() { view() {
return ( return (
<ul className="header-controls"> <ul className="Header-controls">
{listItems(this.items().toArray())} {listItems(this.items().toArray())}
</ul> </ul>
); );

View File

@ -15,7 +15,7 @@ import listItems from 'flarum/helpers/listItems';
export default class HeaderSecondary extends Component { export default class HeaderSecondary extends Component {
view() { view() {
return ( return (
<ul className="header-controls"> <ul className="Header-controls">
{listItems(this.items().toArray())} {listItems(this.items().toArray())}
</ul> </ul>
); );
@ -38,7 +38,7 @@ export default class HeaderSecondary extends Component {
items.add('signUp', items.add('signUp',
Button.component({ Button.component({
children: 'Sign Up', children: 'Sign Up',
className: 'btn btn-link', className: 'Button Button--link',
onclick: () => app.modal.show(new SignUpModal()) onclick: () => app.modal.show(new SignUpModal())
}) })
); );
@ -46,7 +46,7 @@ export default class HeaderSecondary extends Component {
items.add('logIn', items.add('logIn',
Button.component({ Button.component({
children: 'Log In', children: 'Log In',
className: 'btn btn-link', className: 'Button Button--link',
onclick: () => app.modal.show(new LogInModal()) onclick: () => app.modal.show(new LogInModal())
}) })
); );

View File

@ -59,16 +59,16 @@ export default class IndexPage extends Component {
view() { view() {
return ( return (
<div className="index-area"> <div className="IndexPage">
{this.hero()} {this.hero()}
<div className="container"> <div className="container">
<nav className="side-nav index-nav" config={affixSidebar}> <nav className="IndexPage-nav sideNav" config={affixSidebar}>
<ul>{listItems(this.sidebarItems().toArray())}</ul> <ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav> </nav>
<div className="offset-content index-results"> <div className="IndexPage-results sideNavOffset">
<div className="index-toolbar"> <div className="IndexPage-toolbar">
<ul className="index-toolbar-view">{listItems(this.viewItems().toArray())}</ul> <ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="index-toolbar-action">{listItems(this.actionItems().toArray())}</ul> <ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
</div> </div>
{app.cache.discussionList.render()} {app.cache.discussionList.render()}
</div> </div>
@ -80,10 +80,10 @@ export default class IndexPage extends Component {
config(isInitialized, context) { config(isInitialized, context) {
if (isInitialized) return; if (isInitialized) return;
$('body').addClass('index-page'); $('#app').addClass('App--index');
context.onunload = () => { context.onunload = () => {
$('body').removeClass('index-page'); $('#app').removeClass('App--index')
$('.global-page').css('min-height', ''); .css('min-height', '');
}; };
app.setTitle(''); app.setTitle('');
@ -91,10 +91,10 @@ export default class IndexPage extends Component {
// Work out the difference between the height of this hero and that of the // Work out the difference between the height of this hero and that of the
// previous hero. Maintain the same scroll position relative to the bottom // previous hero. Maintain the same scroll position relative to the bottom
// of the hero so that the 'fixed' sidebar doesn't jump around. // of the hero so that the 'fixed' sidebar doesn't jump around.
const heroHeight = this.$('.hero').outerHeight(); const heroHeight = this.$('.Hero').outerHeight();
const scrollTop = app.cache.scrollTop; const scrollTop = app.cache.scrollTop;
$('.global-page').css('min-height', $(window).height() + heroHeight); $('#app').css('min-height', $(window).height() + heroHeight);
$(window).scrollTop(scrollTop - (app.cache.heroHeight - heroHeight)); $(window).scrollTop(scrollTop - (app.cache.heroHeight - heroHeight));
app.cache.heroHeight = heroHeight; app.cache.heroHeight = heroHeight;
@ -103,7 +103,7 @@ export default class IndexPage extends Component {
// have set the `lastDiscussion` property. If this is the case, we want to // have set the `lastDiscussion` property. If this is the case, we want to
// scroll down to that discussion so that it's in view. // scroll down to that discussion so that it's in view.
if (this.lastDiscussion) { if (this.lastDiscussion) {
const $discussion = this.$('.discussion-summary[data-id=' + this.lastDiscussion.id() + ']'); const $discussion = this.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);
if ($discussion.length) { if ($discussion.length) {
const indexTop = $('#header').outerHeight(); const indexTop = $('#header').outerHeight();
@ -141,8 +141,8 @@ export default class IndexPage extends Component {
Button.component({ Button.component({
children: 'Start a Discussion', children: 'Start a Discussion',
icon: 'edit', icon: 'edit',
className: 'btn btn-primary new-discussion', className: 'Button Button--primary IndexPage-newDiscussion',
itemClassName: 'primary-control', itemClassName: 'App-primaryControl',
onclick: this.newDiscussion.bind(this) onclick: this.newDiscussion.bind(this)
}) })
); );
@ -150,7 +150,8 @@ export default class IndexPage extends Component {
items.add('nav', items.add('nav',
SelectDropdown.component({ SelectDropdown.component({
children: this.navItems(this).toArray(), children: this.navItems(this).toArray(),
itemClassName: 'title-control' buttonClassName: 'Button',
className: 'App-titleControl'
}) })
); );
@ -201,15 +202,6 @@ export default class IndexPage extends Component {
}) })
); );
items.add('refresh',
Button.component({
title: 'Refresh',
icon: 'refresh',
className: 'btn btn-default btn-icon',
onclick: () => app.cache.discussionList.refresh()
})
);
return items; return items;
} }
@ -222,12 +214,21 @@ export default class IndexPage extends Component {
actionItems() { actionItems() {
const items = new ItemList(); const items = new ItemList();
items.add('refresh',
Button.component({
title: 'Refresh',
icon: 'refresh',
className: 'Button Button--icon',
onclick: () => app.cache.discussionList.refresh()
})
);
if (app.session.user) { if (app.session.user) {
items.add('markAllAsRead', items.add('markAllAsRead',
Button.component({ Button.component({
title: 'Mark All as Read', title: 'Mark All as Read',
icon: 'check', icon: 'check',
className: 'btn btn-default btn-icon', className: 'Button Button--icon',
onclick: this.markAllAsRead.bind(this) onclick: this.markAllAsRead.bind(this)
}) })
); );

View File

@ -8,16 +8,16 @@ import avatar from 'flarum/helpers/avatar';
export default class LoadingPost extends Component { export default class LoadingPost extends Component {
view() { view() {
return ( return (
<div className="post comment-post loading-post"> <div className="Post CommentPost LoadingPost">
<header className="post-header"> <header className="Post-header">
{avatar()} {avatar(null, {className: 'PostUser-avatar'})}
<div className="fake-text"/> <div className="fakeText"/>
</header> </header>
<div className="post-body"> <div className="Post-body">
<div className="fake-text"/> <div className="fakeText"/>
<div className="fake-text"/> <div className="fakeText"/>
<div className="fake-text"/> <div className="fakeText"/>
</div> </div>
</div> </div>
); );

View File

@ -31,50 +31,49 @@ export default class LogInModal extends Modal {
} }
className() { className() {
return 'modal-sm login-modal'; return 'LogInModal Modal--small';
} }
title() { title() {
return 'Log In'; return 'Log In';
} }
body() { content() {
return (
<div className="form-centered">
<div className="form-group">
<input className="form-control" name="email" placeholder="Username or Email"
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="form-group">
<input className="form-control" name="password" type="password" placeholder="Password"
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />
</div>
<div className="form-group">
<button className="btn btn-primary btn-block"
type="submit"
disabled={this.loading}>
Log In
</button>
</div>
</div>
);
}
footer() {
return [ return [
<p className="forgot-password-link"> <div className="Modal-body">
<a href="javascript:;" onclick={this.forgotPassword.bind(this)}>Forgot password?</a> <div className="Form Form--centered">
</p>, <div className="Form-group">
<p className="sign-up-link"> <input className="FormControl" name="email" placeholder="Username or Email"
Don't have an account? value={this.email()}
<a href="javascript:;" onclick={this.signUp.bind(this)}>Sign Up</a> onchange={m.withAttr('value', this.email)}
</p> disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder="Password"
value={this.password()}
onchange={m.withAttr('value', this.password)}
disabled={this.loading} />
</div>
<div className="Form-group">
<button className="Button Button--primary Button--block"
type="submit"
disabled={this.loading}>
Log In
</button>
</div>
</div>
</div>,
<div className="Modal-footer">
<p className="LogInModal-forgotPassword">
<a onclick={this.forgotPassword.bind(this)}>Forgot password?</a>
</p>
<p className="LogInModal-signUp">
Don't have an account?{' '}
<a onclick={this.signUp.bind(this)}>Sign Up</a>
</p>
</div>
]; ];
} }
@ -84,7 +83,7 @@ export default class LogInModal extends Modal {
*/ */
forgotPassword() { forgotPassword() {
const email = this.email(); const email = this.email();
const props = email.indexOf('@') !== -1 ? {email} : null; const props = email.indexOf('@') !== -1 ? {email} : undefined;
app.modal.show(new ForgotPasswordModal(props)); app.modal.show(new ForgotPasswordModal(props));
} }
@ -101,7 +100,7 @@ export default class LogInModal extends Modal {
app.modal.show(new SignUpModal(props)); app.modal.show(new SignUpModal(props));
} }
focus() { onready() {
this.$('[name=' + (this.email() ? 'password' : 'email') + ']').select(); this.$('[name=' + (this.email() ? 'password' : 'email') + ']').select();
} }
@ -123,17 +122,17 @@ export default class LogInModal extends Modal {
if (response && response.code === 'confirm_email') { if (response && response.code === 'confirm_email') {
this.alert = Alert.component({ this.alert = Alert.component({
message: ['You need to confirm your email before you can log in. We\'ve sent a confirmation email to ', <strong>{response.email}</strong>, '. If it doesn\'t arrive soon, check your spam folder.'] children: ['You need to confirm your email before you can log in. We\'ve sent a confirmation email to ', <strong>{response.email}</strong>, '. If it doesn\'t arrive soon, check your spam folder.']
}); });
} else { } else {
this.alert = Alert.component({ this.alert = Alert.component({
type: 'warning', type: 'error',
message: 'Your login details were incorrect.' children: 'Your login details were incorrect.'
}); });
} }
m.redraw(); m.redraw();
this.focus(); this.onready();
} }
); );
} }

View File

@ -19,12 +19,12 @@ export default class Notification extends Component {
const href = this.href(); const href = this.href();
return ( return (
<div className={'notification notification-' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')} <div className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
onclick={this.markAsRead.bind(this)}> onclick={this.markAsRead.bind(this)}>
<a href={href} config={href.indexOf('://') === -1 ? m.route : undefined}> <a href={href} config={href.indexOf('://') === -1 ? m.route : undefined}>
{avatar(notification.sender())} {avatar(notification.sender())}
{icon(this.icon(), {className: 'icon'})} {icon(this.icon(), {className: 'Notification-icon'})}
<span className="content">{this.content()}</span> <span className="Notification-content">{this.content()}</span>
{humanTime(notification.time())} {humanTime(notification.time())}
</a> </a>
</div> </div>

View File

@ -58,12 +58,12 @@ export default class NotificationGrid extends Component {
view() { view() {
return ( return (
<table className="notification-grid"> <table className="NotificationGrid">
<thead> <thead>
<tr> <tr>
<td/> <td/>
{this.methods.map(method => ( {this.methods.map(method => (
<th className="toggle-group" onclick={this.toggleMethod.bind(this, method.name)}> <th className="NotificationGrid-groupToggle" onclick={this.toggleMethod.bind(this, method.name)}>
{icon(method.icon)} {method.label} {icon(method.icon)} {method.label}
</th> </th>
))} ))}
@ -73,11 +73,11 @@ export default class NotificationGrid extends Component {
<tbody> <tbody>
{this.types.map(type => ( {this.types.map(type => (
<tr> <tr>
<td className="toggle-group" onclick={this.toggleType.bind(this, type.name)}> <td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}>
{type.label} {type.label}
</td> </td>
{this.methods.map(method => ( {this.methods.map(method => (
<td className="checkbox-cell"> <td className="NotificationGrid-checkbox">
{this.inputs[this.preferenceKey(type.name, method.name)].render()} {this.inputs[this.preferenceKey(type.name, method.name)].render()}
</td> </td>
))} ))}
@ -91,13 +91,12 @@ export default class NotificationGrid extends Component {
config(isInitialized) { config(isInitialized) {
if (isInitialized) return; if (isInitialized) return;
var self = this; this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
this.$('thead .toggle-group').bind('mouseenter mouseleave', function(e) { const i = parseInt($(this).index(), 10) + 1;
var i = parseInt($(this).index()) + 1; $(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter');
self.$('table').find('td:nth-child('+i+')').toggleClass('highlighted', e.type === 'mouseenter');
}); });
this.$('tbody .toggle-group').bind('mouseenter mouseleave', function(e) { this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
$(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter'); $(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter');
}); });
} }

View File

@ -53,42 +53,42 @@ export default class NotificationList extends Component {
} }
return ( return (
<div className="notification-list"> <div className="NotificationList">
<div className="notifications-header"> <div className="NotificationList-header">
<div className="primary-control"> <div className="App-primaryControl">
{Button.component({ {Button.component({
className: 'btn btn-icon btn-link btn-sm', className: 'Button Button--icon Button--link',
icon: 'check', icon: 'check',
title: 'Mark All as Read', title: 'Mark All as Read',
onclick: this.markAllAsRead.bind(this) onclick: this.markAllAsRead.bind(this)
})} })}
</div> </div>
<h4 className="title-control">Notifications</h4> <h4 className="App-titleControl App-titleControl--text">Notifications</h4>
</div> </div>
<div className="notifications-content"> <div className="NotificationList-content">
{groups.length {groups.length
? groups.map(group => { ? groups.map(group => {
const badges = group.discussion && group.discussion.badges().toArray(); const badges = group.discussion && group.discussion.badges().toArray();
return ( return (
<div className="notification-group"> <div className="NotificationGroup">
{group.discussion {group.discussion
? ( ? (
<a className="notification-group-header" <a className="NotificationGroup-header"
href={app.route.discussion(group.discussion)} href={app.route.discussion(group.discussion)}
config={m.route}> config={m.route}>
{badges && badges.length ? <ul className="badges">{listItems(badges)}</ul> : ''} {badges && badges.length ? <ul className="NotificationGroup-badges">{listItems(badges)}</ul> : ''}
{group.discussion.title()} {group.discussion.title()}
</a> </a>
) : ( ) : (
<div className="notification-group-header"> <div className="NotificationGroup-header">
{app.forum.attribute('title')} {app.forum.attribute('title')}
</div> </div>
)} )}
<ul className="notification-group-list"> <ul className="NotificationGroup-content">
{group.notifications.map(notification => { {group.notifications.map(notification => {
const NotificationComponent = app.notificationComponents[notification.contentType()]; const NotificationComponent = app.notificationComponents[notification.contentType()];
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : ''; return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
@ -98,8 +98,8 @@ export default class NotificationList extends Component {
); );
}) })
: !this.loading : !this.loading
? <div className="no-notifications">No Notifications</div> ? <div className="NotificationList-empty">No Notifications</div>
: LoadingIndicator.component({className: 'loading-indicator-block'})} : LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</div> </div>
</div> </div>
); );

View File

@ -19,15 +19,15 @@ export default class NotificationsDropdown extends Component {
const unread = user.unreadNotificationsCount(); const unread = user.unreadNotificationsCount();
return ( return (
<div className="dropdown btn-group notifications-dropdown"> <div className="Dropdown NotificationsDropdown">
<a href="javascript:;" <a href="javascript:;"
className={'dropdown-toggle btn btn-default btn-rounded btn-naked btn-icon' + (unread ? ' unread' : '')} className={'Dropdown-toggle Button Button--flat NotificationsDropdown-button' + (unread ? ' unread' : '')}
data-toggle="dropdown" data-toggle="dropdown"
onclick={this.onclick.bind(this)}> onclick={this.onclick.bind(this)}>
<span className="notifications-icon">{unread || icon('bell')}</span> <span className="Button-icon">{unread || icon('bell')}</span>
<span className="label">Notifications</span> <span className="Button-label">Notifications</span>
</a> </a>
<div className="dropdown-menu dropdown-menu-right"> <div className="Dropdown-menu Dropdown-menu--right">
{this.showing ? NotificationList.component() : ''} {this.showing ? NotificationList.component() : ''}
</div> </div>
</div> </div>

View File

@ -15,6 +15,6 @@ export default class NotificationsPage extends Component {
} }
view() { view() {
return <div>{NotificationList.component()}</div>; return <div className="NotificationsPage">{NotificationList.component()}</div>;
} }
} }

View File

@ -37,7 +37,7 @@ export default class Post extends Component {
const controls = PostControls.controls(this.props.post, this).toArray(); const controls = PostControls.controls(this.props.post, this).toArray();
const attrs = this.attrs(); const attrs = this.attrs();
attrs.className = 'post ' + (attrs.className || ''); attrs.className = 'Post ' + (attrs.className || '');
return ( return (
<article {...attrs}> <article {...attrs}>
@ -45,9 +45,9 @@ export default class Post extends Component {
<div> <div>
{controls.length ? Dropdown.component({ {controls.length ? Dropdown.component({
children: controls, children: controls,
className: 'contextual-controls', className: 'Post-controls',
buttonClass: 'btn btn-default btn-icon btn-controls btn-naked', buttonClassName: 'Button Button--icon Button--flat',
menuClass: 'pull-right' menuClassName: 'Dropdown-menu--right'
}) : ''} }) : ''}
{this.content()} {this.content()}

View File

@ -17,7 +17,7 @@ export default class PostEdited extends Component {
const title = 'Edited ' + (editUser ? 'by ' + editUser.username() + ' ' : '') + humanTime(post.editTime()); const title = 'Edited ' + (editUser ? 'by ' + editUser.username() + ' ' : '') + humanTime(post.editTime());
return ( return (
<span className="post-edited" title={title}>{icon('pencil')}</span> <span className="PostEdited" title={title}>{icon('pencil')}</span>
); );
} }

View File

@ -21,23 +21,23 @@ export default class PostMeta extends Component {
// When the dropdown menu is shown, select the contents of the permalink // When the dropdown menu is shown, select the contents of the permalink
// input so that the user can quickly copy the URL. // input so that the user can quickly copy the URL.
const selectPermalink = function() { const selectPermalink = function() {
setTimeout(() => $(this).parent().find('.permalink').select()); setTimeout(() => $(this).parent().find('.PostMeta-permalink').select());
m.redraw.strategy('none'); m.redraw.strategy('none');
}; };
return ( return (
<div className="dropdown post-meta"> <div className="Dropdown PostMeta">
<a href="javascript:;" data-toggle="dropdown" className="dropdown-toggle" onclick={selectPermalink}> <a className="Dropdown-toggle" onclick={selectPermalink} data-toggle="dropdown">
{humanTime(time)} {humanTime(time)}
</a> </a>
<div className="dropdown-menu"> <div className="Dropdown-menu dropdown-menu">
<span className="number">Post #{post.number()}</span> <span className="PostMeta-number">Post #{post.number()}</span>{' '}
{fullTime(time)} {fullTime(time)}
{touch {touch
? <a href="btn btn-default permalink" href={permalink}>{permalink}</a> ? <a href="Button PostMeta-permalink" href={permalink}>{permalink}</a>
: <input className="form-control permalink" value="permalink" onclick={e => e.stopPropagation()} />} : <input className="FormControl PostMeta-permalink" value={permalink} onclick={e => e.stopPropagation()} />}
</div> </div>
</div> </div>
); );

View File

@ -19,12 +19,12 @@ export default class PostPreview extends Component {
const excerpt = highlight(post.contentPlain(), this.props.highlight, 200); const excerpt = highlight(post.contentPlain(), this.props.highlight, 200);
return ( return (
<a className="post-preview" href={app.route.post(post)} config={m.route} onclick={this.props.onclick}> <a className="PostPreview" href={app.route.post(post)} config={m.route} onclick={this.props.onclick}>
<span className="post-preview-content"> <span className="PostPreview-content">
{avatar(user)} {avatar(user)}
{username(user)} {username(user)}
{humanTime(post.time())} {humanTime(post.time())}
<span className="excerpt">{excerpt}</span> <span className="PostPreview-excerpt">{excerpt}</span>
</span> </span>
</a> </a>
); );

View File

@ -79,7 +79,7 @@ class PostStream extends mixin(Component, evented) {
m.redraw(true); m.redraw(true);
return promise.then(() => { return promise.then(() => {
anchorScroll(this.$('.post-stream-item:' + (backwards ? 'last' : 'first')), () => m.redraw(true)); anchorScroll(this.$('.PostStream-item:' + (backwards ? 'last' : 'first')), () => m.redraw(true));
this.scrollToIndex(index, noAnimation, backwards).done(this.unpause.bind(this)); this.scrollToIndex(index, noAnimation, backwards).done(this.unpause.bind(this));
}); });
@ -204,7 +204,7 @@ class PostStream extends mixin(Component, evented) {
if (dt > 1000 * 60 * 60 * 24 * 4) { if (dt > 1000 * 60 * 60 * 24 * 4) {
content = [ content = [
<div className="time-gap"> <div className="PostStream-timeGap">
<span>{moment.duration(dt).humanize()} later</span> <span>{moment.duration(dt).humanize()} later</span>
</div>, </div>,
content content
@ -218,7 +218,7 @@ class PostStream extends mixin(Component, evented) {
content = PostLoading.component(); content = PostLoading.component();
} }
return <div className="post-stream-item" {...attrs}>{content}</div>; return <div className="PostStream-item" {...attrs}>{content}</div>;
})} })}
{ {
@ -228,7 +228,7 @@ class PostStream extends mixin(Component, evented) {
(!app.session.user || this.discussion.canReply()) && (!app.session.user || this.discussion.canReply()) &&
!app.composingReplyTo(this.discussion) !app.composingReplyTo(this.discussion)
? ( ? (
<div className="post-stream-item" key="reply"> <div className="PostStream-item" key="reply">
{ReplyPlaceholder.component({discussion: this.discussion})} {ReplyPlaceholder.component({discussion: this.discussion})}
</div> </div>
) : '' ) : ''
@ -265,7 +265,7 @@ class PostStream extends mixin(Component, evented) {
const loadAheadDistance = 500; const loadAheadDistance = 500;
if (this.visibleStart > 0) { if (this.visibleStart > 0) {
const $item = this.$('.post-stream-item[data-index=' + this.visibleStart + ']'); const $item = this.$('.PostStream-item[data-index=' + this.visibleStart + ']');
if ($item.length && $item.offset().top > viewportTop - loadAheadDistance) { if ($item.length && $item.offset().top > viewportTop - loadAheadDistance) {
this.loadPrevious(); this.loadPrevious();
@ -273,7 +273,7 @@ class PostStream extends mixin(Component, evented) {
} }
if (this.visibleEnd < this.count()) { if (this.visibleEnd < this.count()) {
const $item = this.$('.post-stream-item[data-index=' + (this.visibleEnd - 1) + ']'); const $item = this.$('.PostStream-item[data-index=' + (this.visibleEnd - 1) + ']');
if ($item.length && $item.offset().top + $item.outerHeight(true) < viewportTop + viewportHeight + loadAheadDistance) { if ($item.length && $item.offset().top + $item.outerHeight(true) < viewportTop + viewportHeight + loadAheadDistance) {
this.loadNext(); this.loadNext();
@ -334,7 +334,7 @@ class PostStream extends mixin(Component, evented) {
if (start < this.visibleStart || end > this.visibleEnd) return; if (start < this.visibleStart || end > this.visibleEnd) return;
const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart; const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
anchorScroll(`.post-stream-item[data-index=${anchorIndex}]`, () => m.redraw(true)); anchorScroll(`.PostStream-item[data-index=${anchorIndex}]`, () => m.redraw(true));
this.unpause(); this.unpause();
}; };
@ -386,7 +386,7 @@ class PostStream extends mixin(Component, evented) {
* @return {Promise} * @return {Promise}
*/ */
loadNearNumber(number) { loadNearNumber(number) {
if (this.posts().some(post => post && post.number() === number)) { if (this.posts().some(post => post && Number(post.number()) === Number(number))) {
return m.deferred().resolve().promise; return m.deferred().resolve().promise;
} }
@ -431,7 +431,7 @@ class PostStream extends mixin(Component, evented) {
let startNumber; let startNumber;
let endNumber; let endNumber;
this.$('.post-stream-item').each(function() { this.$('.PostStream-item').each(function() {
const $item = $(this); const $item = $(this);
const top = $item.offset().top; const top = $item.offset().top;
const height = $item.outerHeight(true); const height = $item.outerHeight(true);
@ -472,7 +472,7 @@ class PostStream extends mixin(Component, evented) {
* @return {jQuery.Deferred} * @return {jQuery.Deferred}
*/ */
scrollToNumber(number, noAnimation) { scrollToNumber(number, noAnimation) {
const $item = this.$(`.post-stream-item[data-number=${number}]`); const $item = this.$(`.PostStream-item[data-number=${number}]`);
return this.scrollToItem($item, noAnimation).done(this.flashItem.bind(this, $item)); return this.scrollToItem($item, noAnimation).done(this.flashItem.bind(this, $item));
} }
@ -487,7 +487,7 @@ class PostStream extends mixin(Component, evented) {
* @return {jQuery.Deferred} * @return {jQuery.Deferred}
*/ */
scrollToIndex(index, noAnimation, bottom) { scrollToIndex(index, noAnimation, bottom) {
const $item = this.$(`.post-stream-item[data-index=${index}]`); const $item = this.$(`.PostStream-item[data-index=${index}]`);
return this.scrollToItem($item, noAnimation, true, bottom); return this.scrollToItem($item, noAnimation, true, bottom);
} }

View File

@ -72,9 +72,9 @@ export default class PostStreamScrubber extends Component {
const unreadPercent = Math.min(this.count() - this.index, unreadCount) / this.count(); const unreadPercent = Math.min(this.count() - this.index, unreadCount) / this.count();
const viewing = [ const viewing = [
<span className="index">{retain || formatNumber(this.visibleIndex())}</span>, <span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
' of ', ' of ',
<span className="count">{formatNumber(this.count())}</span>, <span className="Scrubber-count">{formatNumber(this.count())}</span>,
' posts ' ' posts '
]; ];
@ -95,34 +95,34 @@ export default class PostStreamScrubber extends Component {
} }
return ( return (
<div className={'post-stream-scrubber dropdown ' + (this.disabled() ? 'disabled ' : '') + (this.props.className || '')}> <div className={'PostStreamScrubber Dropdown ' + (this.disabled() ? 'disabled ' : '') + (this.props.className || '')}>
<a href="javascript:;" className="btn btn-default dropdown-toggle" data-toggle="dropdown"> <button className="Button Dropdown-toggle" data-toggle="dropdown">
{viewing} {icon('sort')} {viewing} {icon('sort')}
</a> </button>
<div className="dropdown-menu"> <div className="Dropdown-menu dropdown-menu">
<div className="scrubber"> <div className="Scrubber">
<a href="javascript:;" className="scrubber-first" onclick={this.goToFirst.bind(this)}> <a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
{icon('angle-double-up')} Original Post {icon('angle-double-up')} Original Post
</a> </a>
<div className="scrubber-scrollbar"> <div className="Scrubber-scrollbar">
<div className="scrubber-before"/> <div className="Scrubber-before"/>
<div className="scrubber-handle"> <div className="Scrubber-handle">
<div className="scrubber-bar"/> <div className="Scrubber-bar"/>
<div className="scrubber-info"> <div className="Scrubber-info">
<strong>{viewing}</strong> <strong>{viewing}</strong>
<span class="description">{retain || this.description}</span> <span class="Scrubber-description">{retain || this.description}</span>
</div> </div>
</div> </div>
<div className="scrubber-after"/> <div className="Scrubber-after"/>
<div className="scrubber-unread" config={styleUnread}> <div className="Scrubber-unread" config={styleUnread}>
{formatNumber(unreadCount)} unread {formatNumber(unreadCount)} unread
</div> </div>
</div> </div>
<a href="javascript:;" className="scrubber-last" onclick={this.goToLast.bind(this)}> <a className="Scrubber-last" onclick={this.goToLast.bind(this)}>
{icon('angle-double-down')} Now {icon('angle-double-down')} Now
</a> </a>
</div> </div>
@ -208,7 +208,7 @@ export default class PostStreamScrubber extends Component {
// properties to a 'default' state. These values reflect what would be // properties to a 'default' state. These values reflect what would be
// seen if the browser were scrolled right up to the top of the page, // seen if the browser were scrolled right up to the top of the page,
// and the viewport had a height of 0. // and the viewport had a height of 0.
const $items = stream.$('> .post-stream-item[data-index]'); const $items = stream.$('> .PostStream-item[data-index]');
let index = $items.first().data('index') || 0; let index = $items.first().data('index') || 0;
let visible = 0; let visible = 0;
let period = ''; let period = '';
@ -273,7 +273,7 @@ export default class PostStreamScrubber extends Component {
// When any part of the whole scrollbar is clicked, we want to jump to // When any part of the whole scrollbar is clicked, we want to jump to
// that position. // that position.
this.$('.scrubber-scrollbar') this.$('.Scrubber-scrollbar')
.bind('click', this.onclick.bind(this)) .bind('click', this.onclick.bind(this))
// Now we want to make the scrollbar handle draggable. Let's start by // Now we want to make the scrollbar handle draggable. Let's start by
@ -289,7 +289,7 @@ export default class PostStreamScrubber extends Component {
this.mouseStart = 0; this.mouseStart = 0;
this.indexStart = 0; this.indexStart = 0;
this.$('.scrubber-handle') this.$('.Scrubber-handle')
.css('cursor', 'move') .css('cursor', 'move')
.bind('mousedown touchstart', this.onmousedown.bind(this)) .bind('mousedown touchstart', this.onmousedown.bind(this))
@ -331,8 +331,8 @@ export default class PostStreamScrubber extends Component {
const visible = this.visible || 1; const visible = this.visible || 1;
const $scrubber = this.$(); const $scrubber = this.$();
$scrubber.find('.index').text(formatNumber(this.visibleIndex())); $scrubber.find('.Scrubber-index').text(formatNumber(this.visibleIndex()));
$scrubber.find('.description').text(this.description); $scrubber.find('.Scrubber-description').text(this.description);
$scrubber.toggleClass('disabled', this.disabled()); $scrubber.toggleClass('disabled', this.disabled());
const heights = {}; const heights = {};
@ -342,7 +342,7 @@ export default class PostStreamScrubber extends Component {
const func = animate ? 'animate' : 'css'; const func = animate ? 'animate' : 'css';
for (const part in heights) { for (const part in heights) {
const $part = $scrubber.find(`.scrubber-${part}`); const $part = $scrubber.find(`.Scrubber-${part}`);
$part.stop(true, true)[func]({height: heights[part] + '%'}, 'fast'); $part.stop(true, true)[func]({height: heights[part] + '%'}, 'fast');
// jQuery likes to put overflow:hidden, but because the scrollbar handle // jQuery likes to put overflow:hidden, but because the scrollbar handle
@ -371,7 +371,7 @@ export default class PostStreamScrubber extends Component {
// minimum percentage per visible post. If this is greater than the actual // minimum percentage per visible post. If this is greater than the actual
// percentage per post, then we need to adjust the 'before' percentage to // percentage per post, then we need to adjust the 'before' percentage to
// account for it. // account for it.
const minPercentVisible = 50 / this.$('.scrubber-scrollbar').outerHeight() * 100; const minPercentVisible = 50 / this.$('.Scrubber-scrollbar').outerHeight() * 100;
const percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible); const percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible);
const percentPerPost = count === visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible); const percentPerPost = count === visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible);
@ -387,11 +387,11 @@ export default class PostStreamScrubber extends Component {
// Adjust the height of the scrollbar so that it fills the height of // Adjust the height of the scrollbar so that it fills the height of
// the sidebar and doesn't overlap the footer. // the sidebar and doesn't overlap the footer.
const scrubber = this.$(); const scrubber = this.$();
const scrollbar = this.$('.scrubber-scrollbar'); const scrollbar = this.$('.Scrubber-scrollbar');
scrollbar.css('max-height', $(window).height() - scrollbar.css('max-height', $(window).height() -
scrubber.offset().top + $(window).scrollTop() - scrubber.offset().top + $(window).scrollTop() -
parseInt($('.global-page').css('padding-bottom'), 10) - parseInt($('#app').css('padding-bottom'), 10) -
(scrubber.outerHeight() - scrollbar.outerHeight())); (scrubber.outerHeight() - scrollbar.outerHeight()));
} }
@ -411,7 +411,7 @@ export default class PostStreamScrubber extends Component {
// finally convert it into an index. Add this delta index onto // finally convert it into an index. Add this delta index onto
// the index at which the drag was started, and then scroll there. // the index at which the drag was started, and then scroll there.
const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart; const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart;
const deltaPercent = deltaPixels / this.$('.scrubber-scrollbar').outerHeight() * 100; const deltaPercent = deltaPixels / this.$('.Scrubber-scrollbar').outerHeight() * 100;
const deltaIndex = deltaPercent / this.percentPerPost().index; const deltaIndex = deltaPercent / this.percentPerPost().index;
const newIndex = Math.min(this.indexStart + deltaIndex, this.count() - 1); const newIndex = Math.min(this.indexStart + deltaIndex, this.count() - 1);
@ -441,14 +441,14 @@ export default class PostStreamScrubber extends Component {
// 1. Get the offset of the click from the top of the scrollbar, as a // 1. Get the offset of the click from the top of the scrollbar, as a
// percentage of the scrollbar's height. // percentage of the scrollbar's height.
const $scrollbar = this.$('.scrubber-scrollbar'); const $scrollbar = this.$('.Scrubber-scrollbar');
const offsetPixels = (e.clientY || e.originalEvent.touches[0].clientY) - $scrollbar.offset().top + $('body').scrollTop(); const offsetPixels = (e.clientY || e.originalEvent.touches[0].clientY) - $scrollbar.offset().top + $('body').scrollTop();
let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100; let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100;
// 2. We want the handle of the scrollbar to end up centered on the click // 2. We want the handle of the scrollbar to end up centered on the click
// position. Thus, we calculate the height of the handle in percent and // position. Thus, we calculate the height of the handle in percent and
// use that to find a new offset percentage. // use that to find a new offset percentage.
offsetPercent = offsetPercent - parseFloat($scrollbar.find('.scrubber-handle')[0].style.height) / 2; offsetPercent = offsetPercent - parseFloat($scrollbar.find('.Scrubber-handle')[0].style.height) / 2;
// 3. Now we can convert the percentage into an index, and tell the stream- // 3. Now we can convert the percentage into an index, and tell the stream-
// content component to jump to that index. // content component to jump to that index.

View File

@ -11,7 +11,7 @@ import listItems from 'flarum/helpers/listItems';
* *
* - `post` * - `post`
*/ */
export default class PostHeaderUser extends Component { export default class PostUser extends Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -29,7 +29,7 @@ export default class PostHeaderUser extends Component {
if (!user) { if (!user) {
return ( return (
<div className="post-user"> <div className="PostUser">
<h3>{avatar(user)} {username(user)}</h3> <h3>{avatar(user)} {username(user)}</h3>
</div> </div>
); );
@ -40,19 +40,19 @@ export default class PostHeaderUser extends Component {
if (!post.isHidden() && this.cardVisible) { if (!post.isHidden() && this.cardVisible) {
card = UserCard.component({ card = UserCard.component({
user, user,
className: 'user-card-popover fade', className: 'UserCard--popover',
controlsButtonClassName: 'btn btn-default btn-icon btn-controls btn-naked' controlsButtonClassName: 'Button Button--icon Button--flat'
}); });
} }
return ( return (
<div className="post-user"> <div className="PostUser">
<h3> <h3>
<a href={app.route.user(user)} config={m.route}> <a href={app.route.user(user)} config={m.route}>
{avatar(user)} {username(user)} {avatar(user, {className: 'PostUser-avatar'})}{' '}{username(user)}
</a> </a>
</h3> </h3>
<ul className="badges"> <ul className="PostUser-badges badges">
{listItems(user.badges().toArray())} {listItems(user.badges().toArray())}
</ul> </ul>
{card} {card}
@ -66,11 +66,11 @@ export default class PostHeaderUser extends Component {
let timeout; let timeout;
this.$() this.$()
.on('mouseover', 'h3 a, .user-card', () => { .on('mouseover', 'h3 a, .UserCard', () => {
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(this.showCard.bind(this), 500); timeout = setTimeout(this.showCard.bind(this), 500);
}) })
.on('mouseout', 'h3 a, .user-card', () => { .on('mouseout', 'h3 a, .UserCard', () => {
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(this.hideCard.bind(this), 250); timeout = setTimeout(this.hideCard.bind(this), 250);
}); });
@ -84,14 +84,14 @@ export default class PostHeaderUser extends Component {
m.redraw(); m.redraw();
setTimeout(() => this.$('.user-card').addClass('in')); setTimeout(() => this.$('.UserCard').addClass('in'));
} }
/** /**
* Hide the user card. * Hide the user card.
*/ */
hideCard() { hideCard() {
this.$('.user-card').removeClass('in') this.$('.UserCard').removeClass('in')
.one('transitionend', () => { .one('transitionend', () => {
this.cardVisible = false; this.cardVisible = false;
m.redraw(); m.redraw();

View File

@ -22,13 +22,13 @@ export default class PostedActivity extends Activity {
const post = this.props.activity.subject(); const post = this.props.activity.subject();
return ( return (
<a className="activity-content posted-activity-preview" <a className="Activity-content PostedActivity-preview"
href={app.route.post(post)} href={app.route.post(post)}
config={m.route}> config={m.route}>
<ul className="posted-activity-header"> <ul className="PostedActivity-header">
{listItems(this.headerItems().toArray())} {listItems(this.headerItems().toArray())}
</ul> </ul>
<div className="posted-activity-body"> <div className="PostedActivity-body">
{m.trust(truncate(post.contentPlain(), 200))} {m.trust(truncate(post.contentPlain(), 200))}
</div> </div>
</a> </a>

View File

@ -27,7 +27,7 @@ export default class ReplyComposer extends ComposerBody {
items.add('title', ( items.add('title', (
<h3> <h3>
{icon('reply')} <a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a> {icon('reply')}{' '}<a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a>
</h3> </h3>
)); ));

View File

@ -22,9 +22,9 @@ export default class ReplyPlaceholder extends Component {
}; };
return ( return (
<article className="post reply-post" onclick={reply} onmousedown={triggerClick}> <article className="Post ReplyPlaceholder" onclick={reply} onmousedown={triggerClick}>
<header className="post-header"> <header className="Post-header">
{avatar(app.session.user)} {avatar(app.session.user, {className: 'PostUser-avatar'})}{' '}
Write a Reply... Write a Reply...
</header> </header>
</article> </article>

View File

@ -75,25 +75,25 @@ export default class Search extends Component {
} }
return ( return (
<div className={'search dropdown ' + classList({ <div className={'Search Dropdown ' + classList({
open: this.value() && this.hasFocus, open: this.value() && this.hasFocus,
active: !!currentSearch, active: !!currentSearch,
loading: !!this.loadingSources loading: !!this.loadingSources
})}> })}>
<div className="search-input"> <div className="Search-input">
<input className="form-control" <input className="FormControl"
placeholder="Search Forum" placeholder="Search Forum"
value={this.value()} value={this.value()}
oninput={m.withAttr('value', this.value)} oninput={m.withAttr('value', this.value)}
onfocus={() => this.hasFocus = true} onfocus={() => this.hasFocus = true}
onblur={() => this.hasFocus = false}/> onblur={() => this.hasFocus = false}/>
{this.loadingSources {this.loadingSources
? LoadingIndicator.component({size: 'tiny', className: 'btn btn-icon btn-link'}) ? LoadingIndicator.component({size: 'tiny', className: 'Button Button--icon Button--link'})
: currentSearch : currentSearch
? <button className="clear btn btn-icon btn-link" onclick={this.clear.bind(this)}>{icon('times-circle')}</button> ? <button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>{icon('times-circle')}</button>
: ''} : ''}
</div> </div>
<ul className="dropdown-menu dropdown-menu-right search-results"> <ul className="Dropdown-menu Search-results">
{this.sources.map(source => source.view(this.value()))} {this.sources.map(source => source.view(this.value()))}
</ul> </ul>
</div> </div>
@ -108,12 +108,12 @@ export default class Search extends Component {
const search = this; const search = this;
this.$('.search-results') this.$('.Search-results')
.on('mousedown', e => e.preventDefault()) .on('mousedown', e => e.preventDefault())
.on('click', () => this.$('input').blur()) .on('click', () => this.$('input').blur())
// Whenever the mouse is hovered over a search result, highlight it. // Whenever the mouse is hovered over a search result, highlight it.
.on('mouseenter', '> li:not(.dropdown-header)', function() { .on('mouseenter', '> li:not(.Dropdown-header)', function() {
search.setIndex( search.setIndex(
search.selectableItems().index(this) search.selectableItems().index(this)
); );
@ -169,7 +169,7 @@ export default class Search extends Component {
search.searched.push(query); search.searched.push(query);
m.redraw(); m.redraw();
}, 500); }, 250);
}); });
} }
@ -215,7 +215,7 @@ export default class Search extends Component {
* @return {jQuery} * @return {jQuery}
*/ */
selectableItems() { selectableItems() {
return this.$('.search-results > li:not(.dropdown-header)'); return this.$('.Search-results > li:not(.Dropdown-header)');
} }
/** /**
@ -237,7 +237,7 @@ export default class Search extends Component {
*/ */
getItem(index) { getItem(index) {
const $items = this.selectableItems(); const $items = this.selectableItems();
let $item = $items.filter(`[data-index=${index}]`); let $item = $items.filter(`[data-index="${index}"]`);
if (!$item.length) { if (!$item.length) {
$item = $items.eq(index); $item = $items.eq(index);

View File

@ -1,6 +1,7 @@
import avatar from 'flarum/helpers/avatar'; import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username'; import username from 'flarum/helpers/username';
import Dropdown from 'flarum/components/Dropdown'; import Dropdown from 'flarum/components/Dropdown';
import LinkButton from 'flarum/components/LinkButton';
import Button from 'flarum/components/Button'; import Button from 'flarum/components/Button';
import ItemList from 'flarum/utils/ItemList'; import ItemList from 'flarum/utils/ItemList';
import Separator from 'flarum/components/Separator'; import Separator from 'flarum/components/Separator';
@ -14,8 +15,9 @@ export default class SessionDropdown extends Dropdown {
static initProps(props) { static initProps(props) {
super.initProps(props); super.initProps(props);
props.buttonClassName = 'btn btn-default btn-naked btn-rounded btn-user'; props.className = 'SessionDropdown';
props.menuClassName = 'dropdown-menu-right'; props.buttonClassName = 'Button Button--user Button--flat';
props.menuClassName = 'Dropdown-menu--right';
} }
view() { view() {
@ -29,7 +31,7 @@ export default class SessionDropdown extends Dropdown {
return [ return [
avatar(user), ' ', avatar(user), ' ',
<span className="label">{username(user)}</span> <span className="Button-label">{username(user)}</span>
]; ];
} }
@ -43,32 +45,31 @@ export default class SessionDropdown extends Dropdown {
const user = app.session.user; const user = app.session.user;
items.add('profile', items.add('profile',
Button.component({ LinkButton.component({
icon: 'user', icon: 'user',
children: 'Profile', children: 'Profile',
href: app.route.user(user), href: app.route.user(user)
config: m.route
}), }),
100 100
); );
items.add('settings', items.add('settings',
Button.component({ LinkButton.component({
icon: 'cog', icon: 'cog',
children: 'Settings', children: 'Settings',
href: app.route('settings'), href: app.route('settings')
config: m.route
}), }),
50 50
); );
if (user.groups().some(group => Number(group.id()) === Group.ADMINISTRATOR_ID)) { if (user.groups().some(group => Number(group.id()) === Group.ADMINISTRATOR_ID)) {
items.add('administration', items.add('administration',
Button.component({ LinkButton.component({
icon: 'wrench', icon: 'wrench',
children: 'Administration', children: 'Administration',
href: app.forum.attribute('baseUrl') + '/admin', href: app.forum.attribute('baseUrl') + '/admin',
target: '_blank' target: '_blank',
config: () => {}
}), }),
0 0
); );

View File

@ -24,7 +24,7 @@ export default class SettingsPage extends UserPage {
content() { content() {
return ( return (
<div className="settings"> <div className="SettingsPage">
<ul>{listItems(this.settingsItems().toArray())}</ul> <ul>{listItems(this.settingsItems().toArray())}</ul>
</div> </div>
); );
@ -41,7 +41,7 @@ export default class SettingsPage extends UserPage {
items.add('account', items.add('account',
FieldSet.component({ FieldSet.component({
label: 'Account', label: 'Account',
className: 'settings-account', className: 'Settings-account',
children: this.accountItems().toArray() children: this.accountItems().toArray()
}) })
); );
@ -49,7 +49,7 @@ export default class SettingsPage extends UserPage {
items.add('notifications', items.add('notifications',
FieldSet.component({ FieldSet.component({
label: 'Notifications', label: 'Notifications',
className: 'settings-account', className: 'Settings-notifications',
children: [NotificationGrid.component({user: this.user})] children: [NotificationGrid.component({user: this.user})]
}) })
); );
@ -57,7 +57,7 @@ export default class SettingsPage extends UserPage {
items.add('privacy', items.add('privacy',
FieldSet.component({ FieldSet.component({
label: 'Privacy', label: 'Privacy',
className: 'settings-privacy', className: 'Settings-privacy',
children: this.privacyItems().toArray() children: this.privacyItems().toArray()
}) })
); );
@ -76,7 +76,7 @@ export default class SettingsPage extends UserPage {
items.add('changePassword', items.add('changePassword',
Button.component({ Button.component({
children: 'Change Password', children: 'Change Password',
className: 'btn btn-default', className: 'Button',
onclick: () => app.modal.show(new ChangePasswordModal()) onclick: () => app.modal.show(new ChangePasswordModal())
}) })
); );
@ -84,7 +84,7 @@ export default class SettingsPage extends UserPage {
items.add('changeEmail', items.add('changeEmail',
Button.component({ Button.component({
children: 'Change Email', children: 'Change Email',
className: 'btn btn-default', className: 'Button',
onclick: () => app.modal.show(new ChangeEmailModal()) onclick: () => app.modal.show(new ChangeEmailModal())
}) })
); );
@ -92,7 +92,7 @@ export default class SettingsPage extends UserPage {
items.add('deleteAccount', items.add('deleteAccount',
Button.component({ Button.component({
children: 'Delete Account', children: 'Delete Account',
className: 'btn btn-default btn-danger', className: 'Button Button--danger',
onclick: () => app.modal.show(new DeleteAccountModal()) onclick: () => app.modal.show(new DeleteAccountModal())
}) })
); );

View File

@ -45,39 +45,50 @@ export default class SignUpModal extends Modal {
} }
className() { className() {
return 'modal-sm signup-modal' + (this.welcomeUser ? ' signup-modal-success' : ''); return 'Modal--small SignUpModal' + (this.welcomeUser ? ' SignUpModal--success' : '');
} }
title() { title() {
return 'Sign Up'; return 'Sign Up';
} }
content() {
return [
<div className="Modal-body">
{this.body()}
</div>,
<div className="Modal-footer">
{this.footer()}
</div>
];
}
body() { body() {
const body = [( const body = [(
<div className="form-centered"> <div className="Form Form--centered">
<div className="form-group"> <div className="Form-group">
<input className="form-control" name="username" placeholder="Username" <input className="FormControl" name="username" placeholder="Username"
value={this.username()} value={this.username()}
onchange={m.withAttr('value', this.email)} onchange={m.withAttr('value', this.email)}
disabled={this.loading} /> disabled={this.loading} />
</div> </div>
<div className="form-group"> <div className="Form-group">
<input className="form-control" name="email" type="email" placeholder="Email" <input className="FormControl" name="email" type="email" placeholder="Email"
value={this.email()} value={this.email()}
onchange={m.withAttr('value', this.email)} onchange={m.withAttr('value', this.email)}
disabled={this.loading} /> disabled={this.loading} />
</div> </div>
<div className="form-group"> <div className="Form-group">
<input className="form-control" name="password" type="password" placeholder="Password" <input className="FormControl" name="password" type="password" placeholder="Password"
value={this.password()} value={this.password()}
onchange={m.withAttr('value', this.password)} onchange={m.withAttr('value', this.password)}
disabled={this.loading} /> disabled={this.loading} />
</div> </div>
<div className="form-group"> <div className="Form-group">
<button className="btn btn-primary btn-block" <button className="Button Button--primary Button--block"
type="submit" type="submit"
disabled={this.loading}> disabled={this.loading}>
Sign Up Sign Up
@ -96,17 +107,17 @@ export default class SignUpModal extends Modal {
}; };
body.push( body.push(
<div className="signup-welcome" style={{background: user.color()}} config={fadeIn}> <div className="SignUpModal-welcome" style={{background: user.color()}} config={fadeIn}>
<div className="darken-overlay"/> <div className="darkenBackground"/>
<div className="container"> <div className="container">
{avatar(user)} {avatar(user)}
<h3>Welcome, {user.username()}!</h3> <h3>Welcome, {user.username()}!</h3>
{user.isConfirmed() ? [ {user.isConfirmed() ? [
<p>We've sent a confirmation email to <strong>{user.email()}</strong>. If it doesn't arrive soon, check your spam folder.</p>, <p>We've sent a confirmation email to <strong>{user.email()}</strong>. If it doesn't arrive soon, check your spam folder.</p>,
<p><a href={`http://${emailProviderName}`} className="btn btn-primary">Go to {emailProviderName}</a></p> <p><a href={`http://${emailProviderName}`} className="Button Button--primary">Go to {emailProviderName}</a></p>
] : ( ] : (
<p><button className="btn btn-primary" onclick={this.hide.bind(this)}>Dismiss</button></p> <p><button className="Button Button--primary" onclick={this.hide.bind(this)}>Dismiss</button></p>
)} )}
</div> </div>
</div> </div>
@ -118,9 +129,9 @@ export default class SignUpModal extends Modal {
footer() { footer() {
return [ return [
<p className="log-in-link"> <p className="SignUpModal-logIn">
Already have an account? Already have an account?{' '}
<a href="javascript:;" onclick={this.logIn.bind(this)}>Log In</a> <a onclick={this.logIn.bind(this)}>Log In</a>
</p> </p>
]; ];
} }

View File

@ -20,8 +20,8 @@ export default class TerminalPost extends Component {
return ( return (
<span> <span>
{username(user)} {username(user)}{' '}
{lastPost ? 'replied' : 'started'} {lastPost ? 'replied ' : 'started '}
{humanTime(time)} {humanTime(time)}
</span> </span>
); );

View File

@ -34,15 +34,15 @@ export default class TextEditor extends Component {
view() { view() {
return ( return (
<div className="text-editor"> <div className="TextEditor">
<textarea className="form-control flexible-height" <textarea className="FormControl TextEditor-flexible"
config={this.configTextarea.bind(this)} config={this.configTextarea.bind(this)}
oninput={m.withAttr('value', this.oninput.bind(this))} oninput={m.withAttr('value', this.oninput.bind(this))}
placeholder={this.props.placeholder || ''} placeholder={this.props.placeholder || ''}
disabled={!!this.props.disabled} disabled={!!this.props.disabled}
value={this.value()}/> value={this.value()}/>
<ul className="text-editor-controls"> <ul className="TextEditor-controls">
{listItems(this.controlItems().toArray())} {listItems(this.controlItems().toArray())}
</ul> </ul>
</div> </div>
@ -76,7 +76,7 @@ export default class TextEditor extends Component {
Button.component({ Button.component({
children: this.props.submitLabel, children: this.props.submitLabel,
icon: 'check', icon: 'check',
className: 'btn btn-primary', className: 'Button Button--primary',
onclick: this.onsubmit.bind(this) onclick: this.onsubmit.bind(this)
}) })
); );

View File

@ -29,27 +29,27 @@ export default class UserBio extends Component {
let content; let content;
if (this.editing) { if (this.editing) {
content = <textarea className="form-control" placeholder="Write something about yourself" rows="3"/>; content = <textarea className="FormControl" placeholder="Write something about yourself" rows="3"/>;
} else { } else {
let subContent; let subContent;
if (this.loading) { if (this.loading) {
subContent = <p className="placeholder">Saving</p>; subContent = <p className="UserBio-placeholder">Saving</p>;
} else { } else {
const bioHtml = user.bioHtml(); const bioHtml = user.bioHtml();
if (bioHtml) { if (bioHtml) {
subContent = m.trust(bioHtml); subContent = m.trust(bioHtml);
} else if (this.props.editable) { } else if (this.props.editable) {
subContent = <p className="placeholder">Write something about yourself</p>; subContent = <p className="UserBio-placeholder">Write something about yourself</p>;
} }
} }
content = <div className="bio-content">{subContent}</div>; content = <div className="UserBio-content">{subContent}</div>;
} }
return ( return (
<div className={'bio ' + classList({ <div className={'UserBio ' + classList({
editable: this.isEditable(), editable: this.isEditable(),
editing: this.editing editing: this.editing
})} })}

View File

@ -28,32 +28,38 @@ export default class UserCard extends Component {
const controls = UserControls.controls(user, this).toArray(); const controls = UserControls.controls(user, this).toArray();
return ( return (
<div className={'user-card ' + (this.props.className || '')} <div className={'UserCard ' + (this.props.className || '')}
style={{backgroundColor: user.color()}}> style={{backgroundColor: user.color()}}>
<div className="darken-overlay"/> <div className="darkenBackground">
<div className="container"> <div className="container">
{controls.length ? Dropdown.component({ {controls.length ? Dropdown.component({
children: controls, children: controls,
className: 'contextual-controls', className: 'UserCard-controls App-primaryControl',
menuClass: 'dropdown-menu-right', menuClassName: 'Dropdown-menu--right',
buttonClass: this.props.controlsButtonClassName buttonClassName: this.props.controlsButtonClassName
}) : ''} }) : ''}
<div className="user-profile"> <div className="UserCard-profile">
<h2 className="user-identity"> <h2 className="UserCard-identity">
{this.props.editable {this.props.editable
? [AvatarEditor.component({user, className: 'user-avatar'}), username(user)] ? [AvatarEditor.component({user, className: 'UserCard-avatar'}), username(user)]
: ( : (
<a href={app.route.user(user)} config={m.route}> <a href={app.route.user(user)} config={m.route}>
{avatar(user, {className: 'user-avatar'})} <div className="UserCard-avatar">{avatar(user)}</div>
{username(user)} {username(user)}
</a> </a>
)} )}
</h2> </h2>
<ul className="badges user-badges">{listItems(user.badges().toArray())}</ul> <ul className="UserCard-badges badges">
<ul className="user-info">{listItems(this.infoItems().toArray())}</ul> {listItems(user.badges().toArray())}
</ul>
<ul className="UserCard-info">
{listItems(this.infoItems().toArray())}
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -81,7 +87,7 @@ export default class UserCard extends Component {
const online = user.isOnline(); const online = user.isOnline();
items.add('lastSeen', ( items.add('lastSeen', (
<span className={'user-last-seen' + (online ? ' online' : '')}> <span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
{online {online
? [icon('circle'), ' Online'] ? [icon('circle'), ' Online']
: [icon('clock-o'), ' ', humanTime(lastSeenTime)]} : [icon('clock-o'), ' ', humanTime(lastSeenTime)]}

View File

@ -33,24 +33,24 @@ export default class UserPage extends Component {
view() { view() {
return ( return (
<div> <div className="UserPage">
{this.user ? [ {this.user ? [
UserCard.component({ UserCard.component({
user: this.user, user: this.user,
className: 'hero user-hero', className: 'Hero UserHero',
editable: this.user.canEdit(), editable: this.user.canEdit(),
controlsButtonClassName: 'btn btn-default' controlsButtonClassName: 'Button'
}), }),
<div className="container"> <div className="container">
<nav className="side-nav user-nav" config={affixSidebar}> <nav className="sideNav UserPage-nav" config={affixSidebar}>
<ul>{listItems(this.sidebarItems().toArray())}</ul> <ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav> </nav>
<div className="offset-content user-content"> <div className="sideNavOffset UserPage-content">
{this.content()} {this.content()}
</div> </div>
</div> </div>
] : [ ] : [
LoadingIndicator.component({className: 'loading-indicator-block'}) LoadingIndicator.component({className: 'LoadingIndicator--block'})
]} ]}
</div> </div>
); );
@ -59,8 +59,8 @@ export default class UserPage extends Component {
config(isInitialized, context) { config(isInitialized, context) {
if (isInitialized) return; if (isInitialized) return;
$('body').addClass('user-page'); $('#app').addClass('App--user');
context.onunload = () => $('body').removeClass('user-page'); context.onunload = () => $('#app').removeClass('App--user');
} }
/** /**
@ -117,7 +117,8 @@ export default class UserPage extends Component {
items.add('nav', items.add('nav',
SelectDropdown.component({ SelectDropdown.component({
children: this.navItems().toArray(), children: this.navItems().toArray(),
itemClass: 'title-control' className: 'App-titleControl',
buttonClassName: 'Button'
}) })
); );
@ -144,7 +145,7 @@ export default class UserPage extends Component {
items.add('discussions', items.add('discussions',
LinkButton.component({ LinkButton.component({
href: app.route('user.discussions', {username: user.username()}), href: app.route('user.discussions', {username: user.username()}),
children: ['Discussions', <span className="count">{user.discussionsCount()}</span>], children: ['Discussions', <span className="Button-badge">{user.discussionsCount()}</span>],
icon: 'reorder' icon: 'reorder'
}) })
); );
@ -152,7 +153,7 @@ export default class UserPage extends Component {
items.add('posts', items.add('posts',
LinkButton.component({ LinkButton.component({
href: app.route('user.posts', {username: user.username()}), href: app.route('user.posts', {username: user.username()}),
children: ['Posts', <span className="count">{user.commentsCount()}</span>], children: ['Posts', <span className="Button-badge">{user.commentsCount()}</span>],
icon: 'comment-o' icon: 'comment-o'
}) })
); );

View File

@ -22,9 +22,9 @@ export default class UsersSearchResults {
if (!results.length) return ''; if (!results.length) return '';
return [ return [
<li className="dropdown-header">Users</li>, <li className="Dropdown-header">Users</li>,
results.map(user => ( results.map(user => (
<li className="user-search-result" data-index={'users' + user.id()}> <li className="UserSearchResult" data-index={'users' + user.id()}>
<a href={app.route.user(user)} config={m.route}> <a href={app.route.user(user)} config={m.route}>
{avatar(user)} {avatar(user)}
{highlight(user.username(), query)} {highlight(user.username(), query)}

View File

@ -20,15 +20,15 @@ export default class WelcomeHero extends Component {
}; };
return ( return (
<header className="hero welcome-hero"> <header className="Hero WelcomeHero">
<div class="container"> <div class="container">
<button className="close btn btn-icon btn-link" onclick={slideUp}> <button className="Hero-close Button Button--icon Button--link" onclick={slideUp}>
{icon('times')} {icon('times')}
</button> </button>
<div className="container-narrow"> <div className="containerNarrow">
<h2>{app.forum.attribute('welcomeTitle')}</h2> <h2 className="Hero-title">{app.forum.attribute('welcomeTitle')}</h2>
<div className="subtitle">{m.trust(app.forum.attribute('welcomeMessage'))}</div> <div className="Hero-subtitle">{m.trust(app.forum.attribute('welcomeMessage'))}</div>
</div> </div>
</div> </div>
</header> </header>

View File

@ -12,7 +12,7 @@ import FooterPrimary from 'flarum/components/FooterPrimary';
import FooterSecondary from 'flarum/components/FooterSecondary'; import FooterSecondary from 'flarum/components/FooterSecondary';
import Composer from 'flarum/components/Composer'; import Composer from 'flarum/components/Composer';
import ModalManager from 'flarum/components/ModalManager'; import ModalManager from 'flarum/components/ModalManager';
import Alerts from 'flarum/components/Alerts'; import AlertManager from 'flarum/components/AlertManager';
/** /**
* The `boot` initializer boots up the forum app. It initializes some app * The `boot` initializer boots up the forum app. It initializes some app
@ -23,18 +23,18 @@ import Alerts from 'flarum/components/Alerts';
export default function boot(app) { export default function boot(app) {
m.startComputation(); m.startComputation();
m.mount(document.getElementById('page-navigation'), Navigation.component({className: 'back-control', drawer: true})); m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
m.mount(document.getElementById('header-navigation'), Navigation.component()); m.mount(document.getElementById('header-navigation'), Navigation.component());
m.mount(document.getElementById('header-primary'), HeaderPrimary.component()); m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component()); m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
m.mount(document.getElementById('footer-primary'), FooterPrimary.component()); m.mount(document.getElementById('footer-primary'), FooterPrimary.component());
m.mount(document.getElementById('footer-secondary'), FooterSecondary.component()); m.mount(document.getElementById('footer-secondary'), FooterSecondary.component());
app.pane = new Pane(document.getElementById('page')); app.pane = new Pane(document.getElementById('app'));
app.drawer = new Drawer(); app.drawer = new Drawer();
app.composer = m.mount(document.getElementById('composer'), Composer.component()); app.composer = m.mount(document.getElementById('composer'), Composer.component());
app.modal = m.mount(document.getElementById('modal'), ModalManager.component()); app.modal = m.mount(document.getElementById('modal'), ModalManager.component());
app.alerts = m.mount(document.getElementById('alerts'), Alerts.component()); app.alerts = m.mount(document.getElementById('alerts'), AlertManager.component());
m.route.mode = 'pathname'; m.route.mode = 'pathname';
m.route(document.getElementById('content'), '/', mapRoutes(app.routes)); m.route(document.getElementById('content'), '/', mapRoutes(app.routes));
@ -47,15 +47,22 @@ export default function boot(app) {
if (e.ctrlKey || e.metaKey || e.which === 2) return; if (e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault(); e.preventDefault();
app.history.home(); app.history.home();
app.drawer.hide();
}); });
const offsetTop = $('#app').offset().top + 1;
// Add a class to the body which indicates that the page has been scrolled // Add a class to the body which indicates that the page has been scrolled
// down. // down.
new ScrollListener(top => $('body').toggleClass('scrolled', top > 0)).start(); new ScrollListener(top => $('#app').toggleClass('scrolled', top > offsetTop)).start();
// Initialize FastClick, which makes links and buttons much more responsive on // Initialize FastClick, which makes links and buttons much more responsive on
// touch devices. // touch devices.
$(() => FastClick.attach(document.body)); $(() => FastClick.attach(document.body));
$('#app').affix({
offset: {top: offsetTop}
});
app.booted = true; app.booted = true;
} }

View File

@ -25,7 +25,7 @@ export default {
['user', 'moderation', 'destructive'].forEach(section => { ['user', 'moderation', 'destructive'].forEach(section => {
const controls = this[section + 'Controls'](discussion, context).toArray(); const controls = this[section + 'Controls'](discussion, context).toArray();
if (controls.length) { if (controls.length) {
items.add(section, controls); controls.forEach(item => items.add(item.itemName, item));
items.add(section + 'Separator', Separator.component()); items.add(section + 'Separator', Separator.component());
} }
}); });

View File

@ -7,7 +7,7 @@ export default class Drawer {
constructor() { constructor() {
// Set up an event handler so that whenever the content area is tapped, // Set up an event handler so that whenever the content area is tapped,
// the drawer will close. // the drawer will close.
$('.global-content').click(e => { $('#content').click(e => {
if (this.isOpen()) { if (this.isOpen()) {
e.preventDefault(); e.preventDefault();
this.hide(); this.hide();
@ -22,7 +22,7 @@ export default class Drawer {
* @public * @public
*/ */
isOpen() { isOpen() {
return $('body').hasClass('drawer-open'); return $('#app').hasClass('drawerOpen');
} }
/** /**
@ -31,7 +31,9 @@ export default class Drawer {
* @public * @public
*/ */
hide() { hide() {
$('body').removeClass('drawer-open'); $('#app').removeClass('drawerOpen');
if (this.$backdrop) this.$backdrop.remove();
} }
/** /**
@ -40,15 +42,13 @@ export default class Drawer {
* @public * @public
*/ */
show() { show() {
$('body').addClass('drawer-open'); $('#app').addClass('drawerOpen');
}
/** this.$backdrop = $('<div/>')
* Toggle the drawer. .addClass('drawer-backdrop fade')
* .appendTo('body')
* @public .click(() => this.hide());
*/
toggle() { setTimeout(() => this.$backdrop.addClass('in'));
$('body').toggleClass('drawer-open');
} }
} }

View File

@ -93,6 +93,6 @@ export default class History {
home() { home() {
this.stack.splice(1); this.stack.splice(1);
m.route(this.stack[0].url); m.route('/');
} }
} }

View File

@ -122,8 +122,8 @@ export default class Pane {
*/ */
render() { render() {
this.$element this.$element
.toggleClass('pane-pinned', this.pinned) .toggleClass('panePinned', this.pinned)
.toggleClass('has-pane', this.active) .toggleClass('hasPane', this.active)
.toggleClass('pane-showing', this.showing); .toggleClass('paneShowing', this.showing);
} }
} }

View File

@ -23,7 +23,7 @@ export default {
['user', 'moderation', 'destructive'].forEach(section => { ['user', 'moderation', 'destructive'].forEach(section => {
const controls = this[section + 'Controls'](post, context).toArray(); const controls = this[section + 'Controls'](post, context).toArray();
if (controls.length) { if (controls.length) {
items.add(section, controls); controls.forEach(item => items.add(item.itemName, item));
items.add(section + 'Separator', Separator.component()); items.add(section + 'Separator', Separator.component());
} }
}); });

View File

@ -22,7 +22,7 @@ export default {
['user', 'moderation', 'destructive'].forEach(section => { ['user', 'moderation', 'destructive'].forEach(section => {
const controls = this[section + 'Controls'](discussion, context).toArray(); const controls = this[section + 'Controls'](discussion, context).toArray();
if (controls.length) { if (controls.length) {
items.add(section, controls); controls.forEach(item => items.add(item.itemName, item));
items.add(section + 'Separator', Separator.component()); items.add(section + 'Separator', Separator.component());
} }
}); });

View File

@ -9,8 +9,8 @@ export default function affixSidebar(element, isInitialized) {
if (isInitialized) return; if (isInitialized) return;
const $sidebar = $(element); const $sidebar = $(element);
const $header = $('.global-header'); const $header = $('#header');
const $footer = $('.global-footer'); const $footer = $('#footer');
// Don't affix the sidebar if it is taller than the viewport (otherwise // Don't affix the sidebar if it is taller than the viewport (otherwise
// there would be no way to scroll through its content). // there would be no way to scroll through its content).

View File

@ -40,7 +40,7 @@ export default function slidable(element) {
$(this).css('transform', 'translate(' + x + 'px, 0)'); $(this).css('transform', 'translate(' + x + 'px, 0)');
}; };
$element.find('.slidable-slider').animate({'background-position-x': newPos}, options); $element.find('.Slidable-content').animate({'background-position-x': newPos}, options);
}; };
/** /**
@ -57,12 +57,12 @@ export default function slidable(element) {
}); });
}; };
$element.find('.slidable-slider') $element.find('.Slidable-content')
.on('touchstart', function(e) { .on('touchstart', function(e) {
// Update the references to the elements underneath the slider, provided // Update the references to the elements underneath the slider, provided
// they're not disabled. // they're not disabled.
$underneathLeft = $element.find('.slidable-underneath-left:not(.disabled)'); $underneathLeft = $element.find('.Slidable-underneath--left:not(.disabled)');
$underneathRight = $element.find('.slidable-underneath-right:not(.disabled)'); $underneathRight = $element.find('.Slidable-underneath--right:not(.disabled)');
startX = e.originalEvent.targetTouches[0].clientX; startX = e.originalEvent.targetTouches[0].clientX;
startY = e.originalEvent.targetTouches[0].clientY; startY = e.originalEvent.targetTouches[0].clientY;
@ -89,8 +89,10 @@ export default function slidable(element) {
// If there are controls underneath the either side, then we'll show/hide // If there are controls underneath the either side, then we'll show/hide
// them depending on the slider's position. We also make the controls // them depending on the slider's position. We also make the controls
// icon get a bit bigger the further they slide. // icon get a bit bigger the further they slide.
const toggle = ($underneath, active) => { const toggle = ($underneath, side) => {
if ($underneath.length) { if ($underneath.length) {
const active = side === 'left' ? pos > 0 : pos < 0;
if (active && $underneath.hasClass('elastic')) { if (active && $underneath.hasClass('elastic')) {
pos -= pos * 0.5; pos -= pos * 0.5;
} }
@ -99,12 +101,12 @@ export default function slidable(element) {
const scale = Math.max(0, Math.min(1, (Math.abs(pos) - 25) / threshold)); const scale = Math.max(0, Math.min(1, (Math.abs(pos) - 25) / threshold));
$underneath.find('.icon').css('transform', 'scale(' + scale + ')'); $underneath.find('.icon').css('transform', 'scale(' + scale + ')');
} else { } else {
pos = Math.min(0, pos); pos = Math[side === 'left' ? 'min' : 'max'](0, pos);
} }
}; };
toggle($underneathLeft, pos > 0); toggle($underneathLeft, 'left');
toggle($underneathRight, pos < 0); toggle($underneathRight, 'right');
$(this).css('transform', 'translate(' + pos + 'px, 0)'); $(this).css('transform', 'translate(' + pos + 'px, 0)');
$(this).css('background-position-x', pos + 'px'); $(this).css('background-position-x', pos + 'px');

View File

@ -197,8 +197,8 @@ export default class App {
return m.request(options).then(null, response => { return m.request(options).then(null, response => {
if (response instanceof Error) { if (response instanceof Error) {
this.alerts.show(this.requestError = new Alert({ this.alerts.show(this.requestError = new Alert({
type: 'warning', type: 'error',
message: response.message children: response.message
})); }));
} }

View File

@ -10,7 +10,7 @@ import extract from 'flarum/utils/extract';
* The alert may have the following special props: * The alert may have the following special props:
* *
* - `type` The type of alert this is. Will be used to give the alert a class * - `type` The type of alert this is. Will be used to give the alert a class
* name of `alert-{type}`. * name of `Alert--{type}`.
* - `controls` An array of controls to show in the alert. * - `controls` An array of controls to show in the alert.
* - `dismissible` Whether or not the alert can be dismissed. * - `dismissible` Whether or not the alert can be dismissed.
* - `ondismiss` A callback to run when the alert is dismissed. * - `ondismiss` A callback to run when the alert is dismissed.
@ -22,7 +22,7 @@ export default class Alert extends Component {
const attrs = Object.assign({}, this.props); const attrs = Object.assign({}, this.props);
const type = extract(attrs, 'type'); const type = extract(attrs, 'type');
attrs.className = 'alert alert-' + type + ' ' + (attrs.className || ''); attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const children = extract(attrs, 'children'); const children = extract(attrs, 'children');
const controls = extract(attrs, 'controls') || []; const controls = extract(attrs, 'controls') || [];
@ -37,17 +37,17 @@ export default class Alert extends Component {
if (dismissible || dismissible === undefined) { if (dismissible || dismissible === undefined) {
dismissControl.push(Button.component({ dismissControl.push(Button.component({
icon: 'times', icon: 'times',
className: 'btn btn-link btn-icon dismiss', className: 'Button Button--link Button--icon Alert-dismiss',
onclick: ondismiss onclick: ondismiss
})); }));
} }
return ( return (
<div {...attrs}> <div {...attrs}>
<span className="alert-body"> <span className="Alert-body">
{children} {children}
</span> </span>
<ul className="alert-controls"> <ul className="Alert-controls">
{listItems(controls.concat(dismissControl))} {listItems(controls.concat(dismissControl))}
</ul> </ul>
</div> </div>

View File

@ -2,10 +2,10 @@ import Component from 'flarum/Component';
import Alert from 'flarum/components/Alert'; import Alert from 'flarum/components/Alert';
/** /**
* The `Alerts` component provides an area in which `Alert` components can be * The `AlertManager` component provides an area in which `Alert` components can
* shown and dismissed. * be shown and dismissed.
*/ */
export default class Alerts extends Component { export default class AlertManager extends Component {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -20,8 +20,8 @@ export default class Alerts extends Component {
view() { view() {
return ( return (
<div className="alerts"> <div className="AlertManager">
{this.components.map(component => <div className="alerts-item">{component}</div>)} {this.components.map(component => <div className="AlertManager-alert">{component}</div>)}
</div> </div>
); );
} }
@ -34,7 +34,7 @@ export default class Alerts extends Component {
*/ */
show(component) { show(component) {
if (!(component instanceof Alert)) { if (!(component instanceof Alert)) {
throw new Error('The Alerts component can only show Alert components'); throw new Error('The AlertManager component can only show Alert components');
} }
component.props.ondismiss = this.dismiss.bind(this, component); component.props.ondismiss = this.dismiss.bind(this, component);

View File

@ -9,7 +9,7 @@ import extract from 'flarum/utils/extract';
* A badge may have the following special props: * A badge may have the following special props:
* *
* - `type` The type of badge this is. This will be used to give the badge a * - `type` The type of badge this is. This will be used to give the badge a
* class name of `badge-{type}`. * class name of `Badge--{type}`.
* - `icon` The name of an icon to show inside the badge. * - `icon` The name of an icon to show inside the badge.
* *
* All other props will be assigned as attributes on the badge element. * All other props will be assigned as attributes on the badge element.
@ -20,7 +20,8 @@ export default class Badge extends Component {
const type = extract(attrs, 'type'); const type = extract(attrs, 'type');
const iconName = extract(attrs, 'icon'); const iconName = extract(attrs, 'icon');
attrs.className = 'badge badge-' + type + ' ' + (attrs.className || ''); attrs.className = 'Badge Badge--' + type + ' ' + (attrs.className || '');
attrs.title = extract(attrs, 'label');
// Give the badge a unique key so that when badges are displayed together, // Give the badge a unique key so that when badges are displayed together,
// and then one is added/removed, Mithril will correctly redraw the series // and then one is added/removed, Mithril will correctly redraw the series
@ -29,7 +30,7 @@ export default class Badge extends Component {
return ( return (
<span {...attrs}> <span {...attrs}>
{iconName ? icon(iconName, {className: 'icon'}) : ''} {iconName ? icon(iconName, {className: 'Badge-icon'}) : ''}
</span> </span>
); );
} }

View File

@ -24,10 +24,9 @@ export default class Button extends Component {
delete attrs.children; delete attrs.children;
attrs.className = (attrs.className || ''); attrs.className = (attrs.className || '');
attrs.href = attrs.href || 'javascript:;';
const iconName = extract(attrs, 'icon'); const iconName = extract(attrs, 'icon');
if (iconName) attrs.className += ' has-icon'; if (iconName) attrs.className += ' hasIcon';
const disabled = extract(attrs, 'disabled'); const disabled = extract(attrs, 'disabled');
if (disabled) { if (disabled) {
@ -35,7 +34,7 @@ export default class Button extends Component {
delete attrs.onclick; delete attrs.onclick;
} }
return <a {...attrs}>{this.getButtonContent()}</a>; return <button {...attrs}>{this.getButtonContent()}</button>;
} }
/** /**
@ -48,8 +47,8 @@ export default class Button extends Component {
const iconName = this.props.icon; const iconName = this.props.icon;
return [ return [
iconName ? icon(iconName) : '', iconName ? icon(iconName, {className: 'Button-icon'}) : '',
<span className="label">{this.props.children}</span> this.props.children ? <span className="Button-label">{this.props.children}</span> : ''
]; ];
} }
} }

View File

@ -27,7 +27,7 @@ export default class Checkbox extends Component {
} }
view() { view() {
let className = 'checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || ''); let className = 'Checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
if (this.loading) className += ' loading'; if (this.loading) className += ' loading';
if (this.props.disabled) className += ' disabled'; if (this.props.disabled) className += ' disabled';
@ -37,7 +37,7 @@ export default class Checkbox extends Component {
checked={this.props.state} checked={this.props.state}
disabled={this.props.disabled} disabled={this.props.disabled}
onchange={m.withAttr('checked', this.onchange.bind(this))}/> onchange={m.withAttr('checked', this.onchange.bind(this))}/>
<div className="checkbox-display"> <div className="Checkbox-display">
{this.getDisplay()} {this.getDisplay()}
</div> </div>
{this.props.children} {this.props.children}

View File

@ -18,6 +18,8 @@ import listItems from 'flarum/helpers/listItems';
*/ */
export default class Dropdown extends Component { export default class Dropdown extends Component {
static initProps(props) { static initProps(props) {
super.initProps(props);
props.className = props.className || ''; props.className = props.className || '';
props.buttonClassName = props.buttonClassName || ''; props.buttonClassName = props.buttonClassName || '';
props.contentClassName = props.contentClassName || ''; props.contentClassName = props.contentClassName || '';
@ -26,11 +28,13 @@ export default class Dropdown extends Component {
} }
view() { view() {
const items = listItems(this.props.children);
return ( return (
<div className={'dropdown btn-group ' + this.props.className}> <div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length}>
{this.getButton()} {this.getButton()}
<ul className={'dropdown-menu ' + this.props.menuClassName}> <ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
{listItems(this.props.children)} {items}
</ul> </ul>
</div> </div>
); );
@ -44,12 +48,12 @@ export default class Dropdown extends Component {
*/ */
getButton() { getButton() {
return ( return (
<a href="javascript:;" <button
className={'dropdown-toggle ' + this.props.buttonClassName} className={'Dropdown-toggle ' + this.props.buttonClassName}
data-toggle="dropdown" data-toggle="dropdown"
onclick={this.props.onclick}> onclick={this.props.onclick}>
{this.getButtonContent()} {this.getButtonContent()}
</a> </button>
); );
} }
@ -61,9 +65,9 @@ export default class Dropdown extends Component {
*/ */
getButtonContent() { getButtonContent() {
return [ return [
icon(this.props.icon), icon(this.props.icon, {className: 'Button-icon'}),
<span className="label">{this.props.label}</span>, <span className="Button-label">{this.props.label}</span>, ' ',
icon('caret-down', {className: 'caret'}) icon('caret-down', {className: 'Button-caret'})
]; ];
} }
} }

View File

@ -18,6 +18,14 @@ export default class LinkButton extends Button {
props.config = props.config || m.route; props.config = props.config || m.route;
} }
view() {
const vdom = super.view();
vdom.tag = 'a';
return vdom;
}
/** /**
* Determine whether a component with the given props is 'active'. * Determine whether a component with the given props is 'active'.
* *

View File

@ -12,7 +12,7 @@ export default class LoadingIndicator extends Component {
view() { view() {
const attrs = Object.assign({}, this.props); const attrs = Object.assign({}, this.props);
attrs.className = 'loading-indicator ' + (attrs.className || ''); attrs.className = 'LoadingIndicator ' + (attrs.className || '');
delete attrs.size; delete attrs.size;
return <div {...attrs}>{m.trust('&nbsp;')}</div>; return <div {...attrs}>{m.trust('&nbsp;')}</div>;

View File

@ -1,6 +1,7 @@
import Component from 'flarum/Component'; import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator'; import LoadingIndicator from 'flarum/components/LoadingIndicator';
import Alert from 'flarum/components/Alert'; import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import icon from 'flarum/helpers/icon'; import icon from 'flarum/helpers/icon';
/** /**
@ -34,27 +35,29 @@ export default class Modal extends Component {
} }
return ( return (
<div className={'modal-dialog ' + this.className()}> <div className={'Modal modal-dialog ' + this.className()}>
<div className="modal-content"> <div className="Modal-content">
<div className="close back-control"> <div className="Modal-close Page-backControl">
<a href="javascript:;" className="btn btn-icon btn-link" onclick={this.hide.bind(this)}> {Button.component({
{icon('times', {className: 'icon'})} icon: 'times',
</a> onclick: this.hide.bind(this),
className: 'Button Button--icon Button--link'
})}
</div> </div>
<form onsubmit={this.onsubmit.bind(this)}> <form onsubmit={this.onsubmit.bind(this)}>
<div className="modal-header"> <div className="Modal-header">
<h3 className="title-control">{this.title()}</h3> <h3 className="Page-titleControl Page-titleControl--text">{this.title()}</h3>
</div> </div>
{alert ? <div className="modal-alert">{this.alert}</div> : ''} {alert ? <div className="Modal-alert">{this.alert}</div> : ''}
{this.content()} {this.content()}
</form> </form>
</div> </div>
{LoadingIndicator.component({ {LoadingIndicator.component({
className: 'modal-loading' + (this.loading ? ' active' : '') className: 'Modal-loading ' + (this.loading ? 'active' : '')
})} })}
</div> </div>
); );
@ -99,7 +102,7 @@ export default class Modal extends Component {
* Focus on the first input when the modal is ready to be used. * Focus on the first input when the modal is ready to be used.
*/ */
onready() { onready() {
this.$(':input:first').select(); this.$('form :input:first').select();
} }
/** /**
@ -113,9 +116,11 @@ export default class Modal extends Component {
* Show an alert describing errors returned from the API, and give focus to * Show an alert describing errors returned from the API, and give focus to
* the first relevant field. * the first relevant field.
* *
* @param {Array} errors * @param {Object} response
*/ */
handleErrors(errors) { handleErrors(response) {
const errors = response && response.errors;
if (errors) { if (errors) {
this.alert(new Alert({ this.alert(new Alert({
type: 'warning', type: 'warning',
@ -126,9 +131,9 @@ export default class Modal extends Component {
m.redraw(); m.redraw();
if (errors) { if (errors) {
this.$('[name=' + errors[0].path + ']').select(); this.$('form [name=' + errors[0].path + ']').select();
} else { } else {
this.$(':input:first').select(); this.$('form :input:first').select();
} }
} }
} }

View File

@ -9,7 +9,7 @@ import Modal from 'flarum/components/Modal';
export default class ModalManager extends Component { export default class ModalManager extends Component {
view() { view() {
return ( return (
<div className="modal"> <div className="ModalManager modal fade">
{this.component && this.component.render()} {this.component && this.component.render()}
</div> </div>
); );

View File

@ -21,14 +21,12 @@ export default class Navigation extends Component {
const {history, pane} = app; const {history, pane} = app;
return ( return (
<div className={'navigation ' + (this.props.className || '')} <div className={'Navigation ButtonGroup ' + (this.props.className || '')}
onmouseenter={pane && pane.show.bind(pane)} onmouseenter={pane && pane.show.bind(pane)}
onmouseleave={pane && pane.onmouseleave.bind(pane)}> onmouseleave={pane && pane.onmouseleave.bind(pane)}>
<div className="btn-group"> {history.canGoBack()
{history.canGoBack() ? [this.getBackButton(), this.getPaneButton()]
? [this.getBackButton(), this.getPaneButton()] : this.getDrawerButton()}
: this.getDrawerButton()}
</div>
</div> </div>
); );
} }
@ -50,7 +48,7 @@ export default class Navigation extends Component {
const {history} = app; const {history} = app;
return Button.component({ return Button.component({
className: 'btn btn-default btn-icon navigation-back', className: 'Button Button--icon Navigation-back',
onclick: history.back.bind(history), onclick: history.back.bind(history),
icon: 'chevron-left' icon: 'chevron-left'
}); });
@ -68,7 +66,7 @@ export default class Navigation extends Component {
if (!pane || !pane.active) return ''; if (!pane || !pane.active) return '';
return Button.component({ return Button.component({
className: 'btn btn-default btn-icon navigation-pin' + (pane.pinned ? ' active' : ''), className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
onclick: pane.togglePinned.bind(pane), onclick: pane.togglePinned.bind(pane),
icon: 'thumb-tack' icon: 'thumb-tack'
}); });
@ -87,9 +85,12 @@ export default class Navigation extends Component {
const user = app.session.user; const user = app.session.user;
return Button.component({ return Button.component({
className: 'btn btn-default btn-icon navigation-drawer' + className: 'Button Button--icon Navigation-drawer' +
(user && user.unreadNotificationsCount() ? ' unread' : ''), (user && user.unreadNotificationsCount() ? ' unread' : ''),
onclick: drawer.toggle.bind(drawer), onclick: e => {
e.stopPropagation();
drawer.show();
},
icon: 'reorder' icon: 'reorder'
}); });
} }

View File

@ -14,11 +14,11 @@ export default class Select extends Component {
const {options, onchange, value} = this.props; const {options, onchange, value} = this.props;
return ( return (
<span className="select"> <span className="Select">
<select className="form-control" onchange={m.withAttr('value', onchange.bind(this))} value={value}> <select className="Select-input FormControl" onchange={m.withAttr('value', onchange.bind(this))} value={value}>
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)} {Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
</select> </select>
{icon('sort', {className: 'caret'})} {icon('sort', {className: 'Select-caret'})}
</span> </span>
); );
} }

View File

@ -10,16 +10,18 @@ export default class SelectDropdown extends Dropdown {
static initProps(props) { static initProps(props) {
super.initProps(props); super.initProps(props);
props.className += ' select-dropdown'; props.className += ' Dropdown--select';
} }
getButtonContent() { getButtonContent() {
const activeChild = this.props.children.filter(child => child.props.active)[0]; const activeChild = this.props.children.filter(child => child.props.active)[0];
const label = activeChild && activeChild.props.label; let label = activeChild && activeChild.props.children;
if (label instanceof Array) label = label[0];
return [ return [
<span className="label">{label}</span>, <span className="Button-label">{label}</span>, ' ',
icon('sort', {className: 'caret'}) icon('sort', {className: 'Button-caret'})
]; ];
} }
} }

View File

@ -5,7 +5,7 @@ import Component from 'flarum/Component';
*/ */
class Separator extends Component { class Separator extends Component {
view() { view() {
return <li className="divider"/>; return <li className="Dropdown-separator"/>;
} }
} }

View File

@ -10,8 +10,8 @@ export default class SplitDropdown extends Dropdown {
static initProps(props) { static initProps(props) {
super.initProps(props); super.initProps(props);
props.className += ' split-dropdown'; props.className += ' Dropdown--split';
props.menuClassName += ' dropdown-menu-right'; props.menuClassName += ' Dropdown-menu--right';
} }
getButton() { getButton() {
@ -20,16 +20,16 @@ export default class SplitDropdown extends Dropdown {
// the first child. // the first child.
const firstChild = this.getFirstChild(); const firstChild = this.getFirstChild();
const buttonProps = Object.assign({}, firstChild.props); const buttonProps = Object.assign({}, firstChild.props);
buttonProps.className = (buttonProps.className || '') + ' ' + this.props.buttonClassName; buttonProps.className = (buttonProps.className || '') + ' SplitDropdown-button Button ' + this.props.buttonClassName;
return [ return [
Button.component(buttonProps), Button.component(buttonProps),
<a href="javascript:;" <button
className={'dropdown-toggle btn-icon ' + this.props.buttonClassName} className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName}
data-toggle="dropdown"> data-toggle="dropdown">
{icon(this.props.icon)} {icon(this.props.icon, {className: 'Button-icon'})}
{icon('caret-down', {className: 'caret'})} {icon('caret-down', {className: 'Button-caret'})}
</a> </button>
]; ];
} }

View File

@ -8,10 +8,10 @@ export default class Switch extends Checkbox {
static initProps(props) { static initProps(props) {
super.initProps(props); super.initProps(props);
props.className += ' switch'; props.className = (props.className || '') + ' Checkbox--switch';
} }
getDisplay() { getDisplay() {
return ''; return this.loading ? super.getDisplay() : '';
} }
} }

View File

@ -6,7 +6,7 @@
* @return {Object} * @return {Object}
*/ */
export default function avatar(user, attrs = {}) { export default function avatar(user, attrs = {}) {
attrs.className = 'avatar ' + (attrs.className || ''); attrs.className = 'Avatar ' + (attrs.className || '');
let content = ''; let content = '';
// If the `title` attribute is set to null or false, we don't want to give the // If the `title` attribute is set to null or false, we don't want to give the

View File

@ -1,4 +1,5 @@
import Separator from 'flarum/components/Separator'; import Separator from 'flarum/components/Separator';
import classList from 'flarum/utils/classList';
function isSeparator(item) { function isSeparator(item) {
return item && item.component === Separator; return item && item.component === Separator;
@ -28,10 +29,20 @@ function withoutUnnecessarySeparators(items) {
export default function listItems(items) { export default function listItems(items) {
return withoutUnnecessarySeparators(items).map(item => { return withoutUnnecessarySeparators(items).map(item => {
const isListItem = item.component && item.component.isListItem; const isListItem = item.component && item.component.isListItem;
const active = item.component && item.component.isActive && item.component.isActive(item.props);
const className = item.props ? item.props.itemClassName : item.itemClassName; const className = item.props ? item.props.itemClassName : item.itemClassName;
return isListItem return [
? item isListItem
: <li className={(item.itemName ? 'item-' + item.itemName : '') + ' ' + (className || '')}>{item}</li>; ? item
: <li className={classList([
(item.itemName ? 'item-' + item.itemName : ''),
className,
(active ? 'active' : '')
])}>
{item}
</li>,
' '
];
}); });
}; }

View File

@ -10,10 +10,16 @@
* @return {String} * @return {String}
*/ */
export default function classList(classes) { export default function classList(classes) {
const classNames = []; let classNames;
for (const i in classes) { if (classes instanceof Array) {
if (classes[i]) classNames.push(i); classNames = classes.filter(name => name);
} else {
classNames = [];
for (const i in classes) {
if (classes[i]) classNames.push(i);
}
} }
return classNames.join(' '); return classNames.join(' ');

View File

@ -14,9 +14,9 @@
top: @header-height; top: @header-height;
bottom: 0; bottom: 0;
width: @admin-pane-width; width: @admin-pane-width;
box-shadow: 0 2px 6px @fl-shadow-color; box-shadow: 0 2px 6px @shadow-color;
background: @fl-body-bg; background: @body-bg;
border-top: 1px solid @fl-body-control-bg; border-top: 1px solid @control-bg;
& .dropdown-select .dropdown-menu > li { & .dropdown-select .dropdown-menu > li {
& > a { & > a {
@ -26,14 +26,14 @@
white-space: normal; white-space: normal;
} }
& > a, & > a:hover, &.active > a { & > a, & > a:hover, &.active > a {
color: @fl-body-muted-color; color: @muted-color;
} }
&.active > a { &.active > a {
background: @fl-body-secondary-color; background: @control-bg;
font-weight: normal; font-weight: normal;
& .label, & .icon { & .label, & .icon {
color: @fl-body-color; color: @text-color;
} }
& .label { & .label {
font-weight: bold; font-weight: bold;

View File

@ -0,0 +1,76 @@
.ActivityPage-loadMore .LoadingIndicator {
height: 46px;
}
.ActivityPage-list {
border-left: 3px solid @control-bg;
list-style: none;
margin: 0 0 0 16px;
padding: 0;
> li {
margin-bottom: 30px;
padding-left: 32px;
@media @phone {
padding-left: 24px;
}
}
}
.Activity-avatar {
.Avatar--size(32px);
float: left;
margin-left: -50px;
.box-shadow(0 0 0 3px @body-bg);
margin-top: -5px;
@media @phone {
margin-left: -42px;
}
}
.Activity-header {
color: @muted-color;
margin-bottom: 10px;
}
.Activity-description {
margin-right: 5px;
}
.Activity-content {
display: block;
padding: 15px;
background: @control-bg;
border-radius: @border-radius;
color: @muted-color;
&, &:hover {
text-decoration: none;
}
}
.PostedActivity-header {
margin: 0 0 5px;
padding: 0;
list-style: none;
> li {
display: inline-block;
margin-right: 5px;
}
h3 {
font-size: 14px;
font-weight: bold;
margin: 0;
&, & a {
color: @heading-color;
}
.Activity-content:hover & {
text-decoration: underline;
}
}
}
.PostedActivity-body {
color: @muted-color;
& :last-child {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,34 @@
.AvatarEditor {
position: relative;
.Dropdown-toggle {
opacity: 0;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: 100%;
background: rgba(0, 0, 0, 0.6);
text-align: center;
text-decoration: none;
border: 0;
}
&:hover .Dropdown-toggle, &.open .Dropdown-toggle, &.loading .Dropdown-toggle {
opacity: 1;
}
.LoadingIndicator {
color: #fff;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
@media @tablet-up {
.Dropdown-menu {
left: 35%;
top: 65%;
}
}
}

View File

@ -1,28 +1,28 @@
// ------------------------------------ // ------------------------------------
// Composer // Composer
.composer { .Composer {
pointer-events: auto; pointer-events: auto;
.box-shadow(0 2px 6px @fl-shadow-color); .box-shadow(0 2px 6px @shadow-color);
&.minimized { &.minimized {
height: 50px; height: 50px;
cursor: pointer; cursor: pointer;
} }
} }
.composer-controls { .Composer-controls {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.composer-content { .ComposerBody-content {
.minimized & { .minimized & {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
} }
.composer-header { .ComposerBody-header {
list-style: none; list-style: none;
padding: 1px 0; padding: 1px 0;
margin: 0 0 10px; margin: 0 0 10px;
@ -31,20 +31,20 @@
pointer-events: none; pointer-events: none;
} }
& > li { > li {
display: inline-block; display: inline-block;
margin-right: -4px; margin-right: -4px;
} }
& h3 { h3 {
margin: 0; margin: 0;
line-height: 1.5em; line-height: 1.5em;
&, & input, & a { &, input, a {
color: @fl-secondary-color; color: @secondary-color;
font-size: 14px; font-size: 14px;
font-weight: normal; font-weight: normal;
} }
& input { input {
font-size: 16px; font-size: 16px;
&, &[disabled], &:focus { &, &[disabled], &:focus {
@ -54,7 +54,7 @@
height: auto; height: auto;
} }
} }
& .fa { .icon {
font-size: 14px; font-size: 14px;
} }
} }
@ -62,7 +62,7 @@
.fa-minus.minimize { .fa-minus.minimize {
vertical-align: -5px; vertical-align: -5px;
} }
.composer-controls { .Composer-controls {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 10px; top: 10px;
@ -75,16 +75,16 @@
top: 7px; top: 7px;
} }
} }
.composer-loading { .ComposerBody-loading {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(255, 255, 255, 0.9); background: fade(@body-bg, 90%);
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
border-radius: @border-radius-base @border-radius-base 0 0; border-radius: @border-radius @border-radius 0 0;
.transition(opacity 0.2s); .transition(opacity 0.2s);
&.active { &.active {
@ -92,7 +92,7 @@
pointer-events: auto; pointer-events: auto;
} }
} }
.composer-editor { .ComposerBody-editor {
.minimized & { .minimized & {
visibility: hidden; visibility: hidden;
} }
@ -102,22 +102,22 @@
// screen. The controls are hidden (except for the 'x', which is the back- // screen. The controls are hidden (except for the 'x', which is the back-
// control), and the avatar hidden. // control), and the avatar hidden.
@media @phone { @media @phone {
.composer { .Composer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: @zindex-composer; z-index: @zindex-composer;
background: @fl-body-bg; background: @body-bg;
&:not(.minimized) { &:not(.minimized) {
top: 0; top: 0;
height: 100vh !important; height: 100vh !important;
padding-top: @mobile-header-height; padding-top: @header-height-phone;
&:before { &:before {
content: " "; content: " ";
.toolbar(); .header-background();
opacity: 0; opacity: 0;
.visible& { .visible& {
@ -125,29 +125,29 @@
} }
} }
& .composer-controls { & .Composer-controls {
z-index: @zindex-navbar-fixed + 1; z-index: @zindex-header + 1;
& li:not(.back-control) { li:not(.App-backControl) {
display: none; display: none;
} }
} }
} }
} }
.composer-content { .ComposerBody-content {
.minimized & { .minimized & {
margin-right: 50px; margin-right: 50px;
} }
} }
.composer-avatar { .ComposerBody-avatar {
display: none; display: none;
} }
.composer-header { .ComposerBody-header {
margin-bottom: 0; margin-bottom: 0;
& > li { > li {
display: block; display: block;
border-bottom: 1px solid @fl-body-secondary-color; border-bottom: 1px solid @control-bg;
padding: 10px 15px; padding: 10px 15px;
.minimized & { .minimized & {
@ -155,19 +155,19 @@
padding: 15px; padding: 15px;
} }
} }
& h3 { h3 {
&, & a, & input { &, a, input {
font-size: 14px; font-size: 14px;
} }
& input { input {
width: 100% !important; width: 100% !important;
} }
} }
} }
.composer-editor { .ComposerBody-editor {
padding: 15px; padding: 15px;
& textarea { textarea {
height: 50vh !important; height: 50vh !important;
} }
} }
@ -175,8 +175,8 @@
// On larger screens, show the composer as a window at the bottom of the // On larger screens, show the composer as a window at the bottom of the
// content area. We hide a lot of the content when the composer is minimized. // content area. We hide a lot of the content when the composer is minimized.
@media @tablet, @desktop, @desktop-hd { @media @tablet-up {
.composer-container { .App-composer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
@ -185,21 +185,21 @@
pointer-events: none; pointer-events: none;
.transition(left 0.2s); .transition(left 0.2s);
} }
.composer { .Composer {
border-radius: @border-radius-base @border-radius-base 0 0; border-radius: @border-radius @border-radius 0 0;
background: fade(@fl-body-bg, 95%); background: fade(@body-bg, 95%);
transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray
position: relative; position: relative;
height: 300px; height: 300px;
.transition(~"background 0.2s, box-shadow 0.2s"); .transition(~"background 0.2s, box-shadow 0.2s");
&.active, &.full-screen { &.active, &.fullScreen {
background: @fl-body-bg; background: @body-bg;
} }
&.active:not(.full-screen) { &.active:not(.fullScreen) {
box-shadow: 0 0 0 2px @fl-body-primary-color, 0 2px 6px @fl-shadow-color; box-shadow: 0 0 0 2px @primary-color, 0 2px 6px @shadow-color;
} }
&.full-screen { &.fullScreen {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
@ -209,69 +209,70 @@
height: auto; height: auto;
} }
} }
.composer-controls { .Composer-controls {
.full-screen & .btn { .fullScreen & .Button {
padding: 13px; padding: 13px;
width: auto;
& .fa { .Button-icon {
font-size: 20px; font-size: 20px;
} }
} }
} }
.composer-header { .ComposerBody-header {
.full-screen & { .fullScreen & {
margin-bottom: 20px; margin-bottom: 20px;
} }
} }
.composer-content { .Composer-content {
padding: 20px 20px 0; padding: 20px 20px 0;
.minimized & { .minimized & {
padding: 12px 20px; padding: 12px 20px;
} }
.full-screen & { .fullScreen & {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 30px; padding: 30px;
} }
} }
.composer-handle { .Composer-handle {
height: 15px; height: 15px;
margin-bottom: -17px; margin-bottom: -17px;
position: relative; position: relative;
.minimized &, .full-screen & { .minimized &, .fullScreen & {
display: none; display: none;
} }
} }
.composer-avatar { .ComposerBody-avatar {
float: left; float: left;
.avatar-size(64px); .Avatar--size(64px);
.minimized &, .full-screen & { .minimized &, .fullScreen & {
display: none; display: none;
} }
} }
.composer-body { .ComposerBody-content {
margin-left: 90px; margin-left: 90px;
.minimized &, .full-screen & { .minimized &, .fullScreen & {
margin-left: 0; margin-left: 0;
} }
} }
.composer-editor { .ComposerBody-editor {
.full-screen & textarea { .fullScreen & textarea {
font-size: 16px; font-size: 16px;
} }
} }
} }
@media @desktop, @desktop-hd { @media @desktop-up {
.composer:not(.full-screen) { .Composer:not(.fullScreen) {
margin-left: -20px; margin-left: -20px;
margin-right: 180px; margin-right: 180px;
.index-page & { .App--index & {
margin-left: 205px; margin-left: 205px;
margin-right: -20px; margin-right: -20px;
} }
@ -279,21 +280,21 @@
} }
@media @desktop-hd { @media @desktop-hd {
.has-pane.pane-pinned .composer-container { .hasPane.panePinned .App-composer {
left: @index-pane-width; left: @pane-width;
} }
} }
// ------------------------------------ // ------------------------------------
// Text Editor // Text Editor
.text-editor { .TextEditor {
& textarea { & textarea {
border-radius: 0; border-radius: 0;
padding: 0 0 10px; padding: 0 0 10px;
border: 0; border: 0;
resize: none; resize: none;
color: @fl-body-color; color: @text-color;
font-size: 14px; font-size: 14px;
line-height: 1.7; line-height: 1.7;
@ -303,7 +304,7 @@
} }
} }
} }
.text-editor-controls { .TextEditor-controls {
margin: 0; margin: 0;
padding: 15px 0; padding: 15px 0;
list-style-type: none; list-style-type: none;
@ -313,19 +314,19 @@
} }
} }
@media @tablet, @desktop, @desktop-hd { @media @tablet-up {
.text-editor-controls { .TextEditor-controls {
margin: 0 -20px 0 -110px; margin: 0 -20px 0 -110px;
padding: 15px 20px; padding: 15px 20px;
border-top: 1px solid @fl-body-secondary-color; border-top: 1px solid @control-bg;
.full-screen & { .fullScreen & {
margin: 0; margin: 0;
border-top: 0; border-top: 0;
padding: 20px 0; padding: 20px 0;
} }
& .btn-primary { & .Button--primary {
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
} }

View File

@ -0,0 +1,19 @@
.DiscussionHero {
.badges {
margin-right: 5px;
margin-bottom: -2px;
}
}
.DiscussionHero-items {
padding: 0;
margin: 0;
list-style: none;
& > li {
display: inline-block;
}
}
.DiscussionHero-title {
display: inline;
vertical-align: middle;
}

View File

@ -0,0 +1,22 @@
// ------------------------------------
// Discussions List
.DiscussionList-discussions {
margin: 0;
padding: 0;
list-style-type: none;
position: relative;
}
.DiscussionList-loadMore {
text-align: center;
margin-top: 10px;
}
.DiscussionList-loadMore .LoadingIndicator {
height: 46px;
}
@media @phone {
.DiscussionList {
margin: 0 -15px;
}
}

View File

@ -0,0 +1,253 @@
.DiscussionListItem {
.tooltip .tooltip-inner {
max-width: none;
}
}
.DiscussionListItem a {
text-decoration: none;
}
.DiscussionListItem-content {
position: relative;
color: @muted-color;
}
.DiscussionListItem-main {
color: inherit;
text-decoration: none;
}
.DiscussionListItem-author {
float: left;
margin-top: 15px;
}
.DiscussionListItem-badges {
float: left;
margin-top: 10px;
text-align: right;
white-space: nowrap;
pointer-events: none;
.badge {
margin-left: -15px;
position: relative;
pointer-events: auto;
}
}
.DiscussionListItem-main {
display: inline-block;
width: 100%;
padding: 12px 0;
}
.DiscussionListItem-title {
margin: 0 0 5px;
line-height: 1.3;
color: @heading-color;
font-weight: normal;
.unread & {
font-weight: bold;
}
}
.DiscussionListItem-info {
list-style-type: none;
padding: 0;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
> li {
display: inline;
}
.username {
font-weight: bold;
}
}
.DiscussionListItem-count {
float: right;
margin-top: 12px;
text-decoration: none;
.unread & {
cursor: pointer;
}
}
.DiscussionListItem-relevantPosts {
padding-bottom: 15px;
@media @phone {
margin-left: -45px;
margin-right: -35px;
}
.PostPreview {
background: @control-bg;
display: block;
padding: 10px 15px;
border-bottom: 2px dotted @body-bg;
color: @muted-color;
transition: border-color 0.2s;
.DiscussionListItem:hover & {
border-color: lighten(@control-bg, 3%);
}
.Avatar, time {
display: none;
}
.PostPreview-content {
padding-left: 0;
}
&:first-child {
border-radius: @border-radius @border-radius 0 0;
}
&:hover {
background: darken(@control-bg, 3%);
text-decoration: none;
}
}
}
@media @phone {
.DiscussionListItem-controls {
display: none;
}
.DiscussionListItem-content {
padding-left: 15px + 45px;
padding-right: 15px + 35px;
&:active {
background: @control-bg;
}
}
.DiscussionListItem-author {
margin-left: -45px;
.Avatar {
.Avatar--size(32px);
}
}
.DiscussionListItem-badges {
margin-left: -45px;
width: 38px;
.badge {
.Badge--size(20px);
margin-left: -13px;
}
}
.DiscussionListItem-main {
margin-right: -45px;
}
.DiscussionListItem-title {
font-size: 14px;
text-decoration: none !important;
}
.DiscussionListItem-count {
margin-right: -35px;
background: @control-bg;
color: @control-color;
border-radius: @border-radius;
font-size: 12px;
padding: 2px 6px;
.unread& {
background: @primary-color;
color: #fff;
font-weight: bold;
&:active {
opacity: 0.5;
}
}
}
}
@media @tablet-up {
.DiscussionListItem {
position: relative;
margin-right: -15px;
padding-right: 15px;
padding-left: 15px;
margin-left: -15px;
border-radius: @border-radius;
transition: background 0.2s;
&:hover {
background: @control-bg;
}
&:hover .DiscussionListItem-controls,
.DiscussionListItem-controls.open {
opacity: 1;
}
.DiscussionListItem-controls.open {
z-index: 3;
}
}
.DiscussionListItem-controls {
position: absolute;
right: 0;
top: 15px;
z-index: 1;
opacity: 0;
transition: opacity 0.2s;
.Dropdown-toggle {
display: block;
}
.Dropdown-menu {
right: 0;
left: auto;
}
}
.DiscussionListItem-content {
padding-left: 52px;
padding-right: 75px;
}
.DiscussionListItem-author {
margin-left: -52px;
.Avatar {
.Avatar--size(36px);
}
}
.DiscussionListItem-badges {
margin-left: -55px;
width: 48px;
}
.DiscussionListItem-main {
margin-right: -65px;
}
.DiscussionListItem-title {
font-size: 16px;
}
.DiscussionListItem-count {
margin-top: 21px;
margin-right: -65px;
width: 55px;
color: @muted-color;
font-size: 14px;
padding-left: 21px;
&:before {
.fa();
content: @fa-var-comment-o;
float: left;
margin-left: -21px;
margin-top: 3px;
}
.unread & {
color: @heading-color;
font-weight: bold;
&:before {
content: @fa-var-comment;
}
&:hover:before {
content: @fa-var-check;
}
}
}
}

View File

@ -0,0 +1,145 @@
.DiscussionPage-nav {
> ul {
padding: 0;
margin: 0;
list-style: none;
}
}
@media @phone {
.DiscussionPage-nav {
margin: 0 -15px;
border-bottom: 1px solid @control-bg;
> ul > li {
margin: 15px;
display: inline-block;
&.item-controls, &.item-scrubber {
margin: 0;
display: block;
}
}
}
}
@media @tablet-up {
.DiscussionPage-nav {
float: right;
&, > ul {
width: 150px;
}
> ul {
position: fixed;
margin-top: 30px;
z-index: 1;
> li {
margin-bottom: 10px;
}
}
.ButtonGroup, .Button {
width: 100%;
}
.ButtonGroup:not(.itemCount1) {
.SplitDropdown-button {
width: 77%;
}
.Dropdown-toggle {
width: 22%;
}
}
}
}
@media @tablet-up {
.DiscussionPage-stream {
margin-right: 200px;
}
}
// ------------------------------------
// Discussions Pane
@media @phone {
.DiscussionPage-list {
display: none;
}
}
@media @tablet-up {
.DiscussionPage-list {
left: -@pane-width;
width: 100%;
position: fixed;
z-index: @zindex-pane;
overflow: auto;
top: @header-height;
bottom: 0;
width: @pane-width;
background: @body-bg;
padding-bottom: 40px;
border-top: 1px solid @control-bg;
.box-shadow(2px 2px 6px -2px @shadow-color);
.transition(left 0.2s);
.paneShowing & {
left: 0;
}
.DiscussionListItem {
margin: 0;
padding: 0;
border-radius: 0;
&.active {
background: @control-bg;
}
}
.DiscussionListItem-controls {
top: 5px;
}
.DiscussionListItem-content {
padding-left: 52px + 15px;
padding-right: 65px + 15px;
}
.DiscussionListItem-title {
font-size: 14px;
}
.DiscussionListItem-relevantPosts {
margin-left: -52px;
margin-right: -65px;
}
.DiscussionListItem-count {
margin-top: 11px;
}
}
}
@media @desktop-hd {
.DiscussionPage-list {
.panePinned & {
left: 0;
z-index: @zindex-composer - 1;
.transition(none);
}
}
// When the pane is pinned, move the other page content inwards
.App-content, .App-footer {
.hasPane.panePinned & {
margin-left: @pane-width;
.container {
max-width: 100%;
padding-left: 30px;
padding-right: 30px;
}
}
}
.App-header .container {
transition: width 0.2s;
.hasPane.panePinned & {
width: 100%;
}
}
}

47
less/forum/Hero.less Normal file
View File

@ -0,0 +1,47 @@
.Hero {
margin-top: -1px;
background: @hero-bg;
text-align: center;
color: @hero-color;
h2 {
margin: 0;
font-size: 16px;
font-weight: normal;
line-height: 1.5em;
}
.container {
padding-top: 20px;
padding-bottom: 20px;
}
}
.Hero-close {
float: right;
margin-top: -10px;
color: inherit;
}
.Hero-subtitle {
margin: 8px 0 0;
line-height: 1.5em;
}
@media @phone {
.Hero-close {
margin-right: -10px;
}
}
@media @tablet-up {
.Hero {
h2 {
font-size: 22px;
}
.container {
padding-top: 40px;
padding-bottom: 30px;
}
}
.Hero-subtitle {
font-size: 15px;
}
}

37
less/forum/IndexPage.less Normal file
View File

@ -0,0 +1,37 @@
// ------------------------------------
// Sidebar
@media @desktop-up {
.IndexPage-nav .item-newDiscussion .Button {
display: block;
width: 100%;
margin-bottom: 20px;
}
}
// ------------------------------------
// Results
.IndexPage-toolbar {
margin-bottom: 15px;
}
.IndexPage-toolbar-view, .IndexPage-toolbar-action {
display: inline-block;
margin: 0;
list-style: none;
padding: 0;
> li {
display: inline-block;
}
}
.IndexPage-toolbar-view > li {
margin-right: 5px;
}
.IndexPage-toolbar-action {
float: right;
> li {
margin-left: 5px;
}
}

View File

@ -0,0 +1,49 @@
.NotificationGrid {
background: @control-bg;
border-radius: @border-radius;
td, th {
border-bottom: 1px solid @body-bg;
color: @control-color;
}
td, th, .Checkbox {
padding: 10px 15px;
}
.NotificationGrid-checkbox {
padding: 0;
}
thead {
th {
text-align: center;
padding: 15px 25px;
}
.icon {
display: block;
font-size: 14px;
width: auto;
margin-bottom: 5px;
}
}
}
.NotificationGrid-groupToggle {
cursor: pointer;
.icon {
font-size: 14px;
margin-right: 2px;
}
}
.NotificationGrid-checkbox {
.Checkbox {
display: block;
}
.Checkbox-display {
text-align: center;
cursor: pointer;
}
&.highlighted .Checkbox, .Checkbox:hover {
&:not(.disabled) {
background: darken(@control-bg, 4%);
}
}
}

View File

@ -0,0 +1,121 @@
.NotificationList {
& .loading-indicator {
height: 100px;
}
}
.NotificationList-header {
@media @tablet-up {
padding: 12px 15px;
border-bottom: 1px solid @control-bg;
h4 {
font-size: 12px;
text-transform: uppercase;
font-weight: bold;
margin: 0;
color: @muted-color;
}
.Button {
float: right;
margin-top: -11px;
margin-right: -11px;
}
}
}
.NotificationList-empty {
color: @muted-color;
text-align: center;
padding: 50px 0;
font-size: 16px;
}
.NotificationGroup {
border-top: 1px solid @control-bg;
margin-top: -1px;
&:not(:last-child) {
margin-bottom: 20px;
}
}
.NotificationGroup-header {
font-weight: bold;
color: @heading-color !important;
padding: 6px 15px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.NotificationGroup-badges {
margin-left: -2px;
margin-right: 18px;
vertical-align: 1px;
.badge {
margin-right: -13px;
position: relative;
.Badge--size(21px);
}
}
.NotificationGroup-content {
list-style: none;
margin: 0;
padding: 0;
}
.Notification {
> a {
display: block;
padding: 8px 15px 8px 70px;
color: @muted-color;
overflow: hidden;
.unread& {
background: @control-bg;
}
&:hover {
text-decoration: none;
background: @control-bg;
}
}
.Avatar {
.Avatar--size(24px);
float: left;
margin: -2px 0 -2px -55px;
}
time {
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
}
.Notification-icon {
float: left;
margin-left: -23px;
font-size: 14px;
margin-top: 2px;
}
.Notification-content {
margin-right: 5px;
.username {
font-weight: bold;
}
}
.drawerToggle.unreadNotifications {
position: relative;
&:after {
content: ' ';
display: block;
position: absolute;
background: @primary-color;
top: 8px;
right: 6px;
width: 14px;
height: 14px;
border-radius: 7px;
border: 2px solid @body-bg;
}
}

View File

@ -0,0 +1,39 @@
.NotificationsDropdown {
.Dropdown-menu {
padding: 0;
overflow: hidden;
.NotificationList-content {
max-height: 70vh;
overflow: auto;
padding-bottom: 10px;
}
}
& .Dropdown-toggle .Button-label {
margin-left: 10px;
}
}
@media @tablet-up {
.NotificationsDropdown {
.Dropdown-menu {
width: 400px;
}
.Dropdown-toggle {
.Button--icon();
}
}
}
.NotificationsDropdown-button.unread .Button-icon {
display: inline-block;
border-radius: 12px;
height: 24px;
width: 24px;
text-align: center;
padding: 2px 0;
font-weight: bold;
margin: -2px 0;
background: @primary-color;
color: #fff;
font-size: 13px;
}

398
less/forum/Post.less Normal file
View File

@ -0,0 +1,398 @@
// ------------------------------------
// Posts
.Post {
padding: 30px 0;
transition: 0.2s box-shadow, top 0.2s, opacity 0.2s;
position: relative;
top: 0;
&.editing {
top: 5px;
opacity: 0.2;
}
}
.Post-controls {
float: right;
margin-top: -8px;
margin-left: 10px;
}
.Post-header {
margin-bottom: 10px;
color: @muted-color;
&, a {
color: @muted-color;
}
> ul {
list-style-type: none;
padding: 0;
margin: 0;
> li {
display: inline;
margin-right: 10px;
}
}
}
.PostUser {
margin: 0;
display: inline;
font-weight: normal;
position: relative;
h3 {
display: inline;
}
h3, h3 a {
color: @heading-color;
font-weight: bold;
font-size: 15px;
}
.UserCard {
position: absolute;
top: -10px;
left: -100px;
z-index: @zindex-dropdown;
.transition(~"opacity 0.2s, transform 0.2s");
transform: scale(0.95);
transform-origin: left top;
opacity: 0;
&.in {
transform: scale(1);
opacity: 1;
}
}
}
.PostUser-badges {
text-align: right;
white-space: nowrap;
pointer-events: none;
.Badge {
margin-left: -15px;
position: relative;
pointer-events: auto;
}
}
.Post-body {
font-size: 14px;
line-height: 1.7;
position: relative;
p, ul, ol, blockquote {
margin-bottom: 1em;
}
a {
border-bottom: 1px solid @control-bg;
font-weight: 600;
&:hover, &:focus, &:active {
text-decoration: none;
border-color: @link-color;
}
}
blockquote {
font-size: inherit;
border: 0;
background: @control-bg;
color: @control-color;
border-radius: @border-radius;
padding: 10px 15px;
border-top: 2px dotted @body-bg;
border-bottom: 2px dotted @body-bg;
margin: 1em 0;
}
pre {
border: 0;
padding: 15px;
background: darken(@body-bg, 3%);
color: #666;
font-size: 90%;
border-radius: @border-radius;
}
h1 {
font-size: 160%;
}
h2 {
font-size: 120%;
font-weight: bold;
}
h3 {
font-size: 100%;
font-weight: bold;
text-transform: uppercase;
}
h4, h5, h6 {
font-size: 100%;
font-weight: bold;
}
img {
max-width: 100%;
}
}
.Post.hidden {
.Post-header, .Post-header a, .Post-user h3, .Post-user h3 a {
color: @muted-more-color;
}
.Post-body, .Post-footer, h3 .Avatar, .PostUser-badges {
position: absolute;
visibility: hidden;
opacity: 0;
margin-top: -5px;
.transition(~"margin-top 0.2s, opacity 0.2s");
}
&.revealContent {
.Post-body, .Post-footer, h3 .Avatar, .PostUser-badges {
position: static;
visibility: visible;
opacity: 0.5;
margin-top: 0;
}
}
.Post-header .Button--more {
background: fade(@muted-more-color, 30%);
color: @muted-more-color;
}
}
.PostMeta {
display: inline;
}
.PostMeta .Dropdown-menu {
width: 400px;
padding: 10px;
color: @muted-color;
@media @phone {
padding: 15px !important;
}
}
.PostMeta-number {
color: @text-color;
font-weight: bold;
}
.PostMeta-time {
margin-left: 5px;
}
.PostMeta-permalink {
margin-top: 10px;
a& {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
.EventPost-icon {
float: left;
}
.EventPost {
&, a {
color: @muted-color;
}
a {
font-weight: bold;
}
}
.EventPost-info {
font-size: 15px;
}
.DiscussionRenamedPost-old, .DiscussionRenamedPost-new {
font-weight: normal;
font-style: italic;
}
.Post-footer, .Post-actions {
> ul {
list-style-type: none;
padding: 0;
margin: 0;
}
&, a {
color: @muted-color;
}
a {
display: inline-block;
.icon {
display: none;
}
}
}
.Post-footer {
> ul {
> li {
margin-bottom: 5px;
}
}
.icon {
font-size: 14px;
margin-right: 5px;
}
}
.Post-actions {
margin-top: 10px;
.transition(opacity 0.2s);
@media @tablet-up {
margin-bottom: -10px;
opacity: 0;
}
> ul {
> li {
margin-right: 10px;
display: inline-block;
}
}
.Post:hover & {
opacity: 1;
}
}
.PostStream-timeGap {
text-transform: uppercase;
font-weight: bold;
color: @muted-color;
padding: 20px 20px 20px 90px;
background: @control-bg;
font-size: 12px;
@media @phone {
margin: 0 -15px;
padding: 20px 15px;
}
}
.PostPreview {
color: @muted-color;
padding-left: 50px;
line-height: 1.7em;
.Avatar {
float: left;
margin-left: -50px;
.Avatar--size(32px);
}
.username {
color: @text-color;
font-weight: bold;
margin-right: 5px;
}
time {
margin-right: 5px;
text-transform: uppercase;
font-size: 11px;
font-weight: 600;
}
}
@media @phone {
.Post-controls {
margin-top: -6px;
margin-right: -8px;
.Dropdown-toggle {
opacity: 0.5;
}
}
.Post-header {
.Avatar {
.Avatar--size(32px);
vertical-align: middle;
margin-right: 5px;
}
}
.PostUser-badges {
position: absolute;
top: -12px;
left: 6px;
width: 32px;
.Badge {
.Badge--size(20px);
margin-left: -13px;
}
}
.EventPost {
padding-left: 30px;
}
.EventPost-icon {
font-size: 18px;
margin-left: -30px;
margin-top: 2px;
}
}
@media @tablet-up {
.Post {
padding-left: 90px;
.Post-controls {
opacity: 0;
transition: opacity 0.2s;
}
&:hover .Post-controls, .Post-controls.open {
opacity: 1;
}
}
.PostUser-avatar {
margin-left: -90px;
float: left;
.Avatar--size(64px);
}
.PostUser-badges {
float: left;
position: relative;
margin-left: -85px;
margin-top: -3px;
width: 64px;
}
.EventPost-icon {
text-align: right;
margin-left: -90px;
width: 64px;
font-size: 22px;
}
}
.ReplyPlaceholder {
font-size: 15px;
cursor: text;
overflow: hidden;
margin-top: 50px;
border: 2px dashed @control-bg;
color: @muted-color;
border-radius: 10px;
padding: 20px;
.Post-header {
margin: 0;
color: inherit;
}
}
@media @tablet-up {
.ReplyPlaceholder {
margin-left: -20px;
margin-right: -20px;
padding-left: 110px;
border-color: transparent;
transition: border-color 0.2s;
.Post-header {
padding-top: 18px;
}
.Avatar {
margin-top: -18px;
}
&:hover {
border-color: @control-bg;
}
}
}

View File

@ -0,0 +1,83 @@
// ------------------------------------
// Stream
.PostStream {
@media @tablet-up {
margin-top: 10px;
}
}
.PostStream-item {
&:not(:last-child) {
border-bottom: 1px solid @control-bg;
@media @phone {
margin: 0 -15px;
padding: 0 15px;
}
}
}
@keyframes blink {
0% {opacity: 0.5}
50% {opacity: 1}
100% {opacity: 0.5}
}
@-webkit-keyframes blink {
0% {opacity: 0.5}
50% {opacity: 1}
100% {opacity: 0.5}
}
.LoadingPost {
.animation(blink 1s linear);
.animation-iteration-count(infinite);
}
.fakeText {
display: inline-block;
vertical-align: middle;
background: @control-bg;
height: 12px;
width: 100%;
margin-bottom: 20px;
border-radius: @border-radius;
.Post-header & {
height: 16px;
width: 150px;
@media @phone {
margin-bottom: 0;
}
}
}
// .item.highlight .post {
// &:before {
// content: "";
// position: absolute;
// left: -30px;
// top: -5px;
// bottom: -5px;
// width: 5px;
// border-radius: @border-radius;
// background: @fl-primary-color;
// }
// }
@-webkit-keyframes pulsate {
0% {-webkit-transform: scale(1)}
50% {-webkit-transform: scale(1.02)}
100% {-webkit-transform: scale(1)}
}
@keyframes pulsate {
0% {transform: scale(1)}
50% {transform: scale(1.02)}
100% {transform: scale(1)}
}
.pulsate {
.animation(pulsate 1s ease-in-out);
.animation-iteration-count(infinite);
}
.flash {
.animation(pulsate 0.2s ease-in-out);
.animation-iteration-count(1);
}

95
less/forum/Scrubber.less Normal file
View File

@ -0,0 +1,95 @@
.Scrubber {
& a {
margin-left: -5px;
color: @muted-color;
& .fa {
font-size: 14px;
margin-right: 2px;
}
&:hover, &:focus {
text-decoration: none;
color: @link-color;
}
}
}
.Scrubber-scrollbar {
margin: 8px 0 8px 3px;
height: 300px;
min-height: 50px; // JavaScript sets a max-height
position: relative;
}
.Scrubber-before, .Scrubber-after {
border-left: 1px solid @control-bg;
}
.Scrubber-unread {
position: absolute;
border-left: 1px solid lighten(@muted-color, 10%);
width: 100%;
background-image: linear-gradient(to right, @control-bg, fade(@control-bg, 0) 10px, fade(@control-bg, 0));
display: flex;
align-items: center;
color: @muted-color;
text-transform: uppercase;
font-size: 11px;
font-weight: bold;
padding-left: 13px;
overflow: hidden;
}
.Scrubber-handle {
position: relative;
z-index: 1;
background: @body-bg;
width: 100%;
padding: 5px 0;
}
.Scrubber-bar {
height: 100%;
width: 5px;
background: @primary-color;
border-radius: 4px;
float: left;
margin-left: -2px;
transition: background 0.2s;
.disabled & {
background: @control-bg;
}
}
.Scrubber-info {
margin-top: -1.5em;
position: absolute;
top: 50%;
width: 100%;
left: 15px;
& strong {
display: block;
}
}
.Scrubber-description {
color: @muted-color;
}
@media @phone {
.PostStreamScrubber {
.Dropdown-toggle {
font-size: 14px;
}
.Dropdown-menu {
padding: 30px;
font-size: 13px;
}
}
.Scrubber-scrollbar {
height: 40vh !important;
max-height: none !important;
}
}
@media @tablet-up {
.PostStreamScrubber {
margin: 30px 0 0 0;
.Dropdown--expanded();
}
}

Some files were not shown because too many files have changed in this diff Show More