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 0b685b1036
commit 2aa9c2e746
206 changed files with 4337 additions and 8830 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ export default class ChangeEmailModal extends Modal {
}
className() {
return 'modal-sm change-email-modal';
return 'ChangeEmailModal Modal--small';
}
title() {
@ -36,11 +36,11 @@ export default class ChangeEmailModal extends Modal {
const emailProviderName = this.email().split('@')[1];
return (
<div className="modal-body">
<div class="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>
<div class="form-group">
<a href={'http://' + emailProviderName} className="btn btn-primary btn-block">Go to {emailProviderName}</a>
<div className="Modal-body">
<div class="Form Form--centered">
<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">
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">Go to {emailProviderName}</a>
</div>
</div>
</div>
@ -48,17 +48,17 @@ export default class ChangeEmailModal extends Modal {
}
return (
<div className="modal-body">
<div class="form-centered">
<div class="form-group">
<input type="email" name="email" className="form-control"
<div className="Modal-body">
<div class="Form Form--centered">
<div class="Form-group">
<input type="email" name="email" className="FormControl"
placeholder={app.session.user.email()}
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading}/>
</div>
<div class="form-group">
<button type="submit" className="btn btn-primary btn-block" disabled={this.loading}>Save Changes</button>
<div class="Form-group">
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>Save Changes</button>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -62,7 +62,7 @@ class Composer extends Component {
view() {
const classes = {
'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;
@ -76,10 +76,10 @@ class Composer extends Component {
};
return (
<div className={'composer ' + classList(classes)}>
<div className="composer-handle"/>
<ul className="composer-controls">{listItems(this.controlItems().toArray())}</ul>
<div className="composer-content" onclick={showIfMinimized}>
<div className={'Composer ' + classList(classes)}>
<div className="Composer-handle" config={this.configHandle.bind(this)}/>
<ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul>
<div className="Composer-content" onclick={showIfMinimized}>
{this.component ? this.component.render() : ''}
</div>
</div>
@ -113,20 +113,8 @@ class Composer extends Component {
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 = {};
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();
$(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.
*
@ -185,15 +195,17 @@ class Composer extends Component {
* of any flexible elements inside the composer's body.
*/
updateHeight() {
// TODO: update this in a way that is independent of the TextEditor being
// present.
const height = this.computedHeight();
const $flexible = this.$('.flexible-height');
const $flexible = this.$('.TextEditor-flexible');
this.$().height(height);
if ($flexible.length) {
const headerHeight = $flexible.offset().top - this.$().offset().top;
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);
}
@ -209,7 +221,7 @@ class Composer extends Component {
this.position !== Composer.PositionEnum.MINIMIZED;
const paddingBottom = visible
? this.computedHeight() - parseInt($('#page').css('padding-bottom'), 10)
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
: 0;
$('#content').css({paddingBottom});
}
@ -431,7 +443,7 @@ class Composer extends Component {
icon: 'minus minimize',
title: 'Minimize',
onclick: this.minimize.bind(this),
wrapperClass: 'back-control'
itemClassName: 'App-backControl'
}));
items.add('fullScreen', ComposerButton.component({

View File

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

View File

@ -8,6 +8,6 @@ export default class ComposerButton extends Button {
static 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() {
return 'modal-sm delete-account-modal';
return 'DeleteAccountModal Modal--small';
}
title() {
@ -28,24 +28,24 @@ export default class DeleteAccountModal extends Modal {
content() {
return (
<div className="modal-body">
<div className="form-centered">
<div className="help-text">
<div className="Modal-body">
<div className="Form Form--centered">
<div className="helpText">
<p>Hold up! If you delete your account, there&#39;s no going back. Keep in mind:</p>
<ul>
<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>
</ul>
</div>
<div className="form-group">
<input className="form-control"
<div className="Form-group">
<input className="FormControl"
name="confirm"
placeholder="Type &quot;DELETE&quot; to proceed"
placeholder="Type 'DELETE' to proceed"
oninput={m.withAttr('value', this.confirmation)}/>
</div>
<div className="form-group">
<div className="Form-group">
<button type="submit"
className="btn btn-primary btn-block"
className="Button Button--primary Button--block"
disabled={this.loading || this.confirmation() !== 'DELETE'}>
Delete Account
</button>

View File

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

View File

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

View File

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

View File

@ -45,28 +45,27 @@ export default class DiscussionListItem extends Component {
const isUnread = discussion.isUnread();
const showUnread = !this.showRepliesCount() && isUnread;
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();
return this.subtree.retain() || (
<div className={'discussion-list-item' + (this.active() ? ' active' : '')}>
<div className={'DiscussionListItem ' + (this.active() ? 'active' : '')}>
{controls.length ? Dropdown.component({
icon: 'ellipsis-v',
children: controls,
className: 'contextual-controls',
buttonClassName: 'btn btn-default btn-naked btn-controls slidable-underneath slidable-underneath-right',
menuClassName: 'dropdown-menu-right'
className: 'DiscussionListItem-controls',
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--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)}>
{icon('check', {className: 'icon'})}
{icon('check')}
</a>
<div className={'discussion-summary slidable-slider' + (isUnread ? ' unread' : '')}>
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
<a href={startUser ? app.route.user(startUser) : '#'}
className="author"
className="DiscussionListItem-author"
title={'Started by ' + (startUser ? startUser.username() : '[deleted]') + ' ' + humanTime(discussion.startTime())}
config={function(element) {
$(element).tooltip({placement: 'right'});
@ -75,23 +74,25 @@ export default class DiscussionListItem extends Component {
{avatar(startUser, {title: ''})}
</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)}
config={m.route}
className="main">
<h3 className="title">{highlight(discussion.title(), this.props.params.q)}</h3>
<ul className="info">{listItems(this.infoItems().toArray())}</ul>
className="DiscussionListItem-main">
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.props.params.q)}</h3>
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
</a>
<span className="count"
<span className="DiscussionListItem-count"
onclick={this.markAsRead.bind(this)}
title={showUnread ? 'Mark as Read' : ''}>
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
</span>
{relevantPosts && relevantPosts.length
? <div className="relevant-posts">
? <div className="DiscussionListItem-relevantPosts">
{relevantPosts.map(post => PostPreview.component({post, highlight: this.props.params.q}))}
</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
// reveal controls.
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());
}
}

View File

@ -92,25 +92,27 @@ export default class DiscussionPage extends mixin(Component, evented) {
const discussion = this.discussion;
return (
<div>
<div className="DiscussionPage">
{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()}
</div>
: ''}
<div className="discussion-area">
<div className="DiscussionPage-discussion">
{discussion
? [
DiscussionHero.component({discussion}),
<div className="container">
<nav className="discussion-nav">
<nav className="DiscussionPage-nav">
<ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav>
{this.stream.render()}
<div className="DiscussionPage-stream">
{this.stream.render()}
</div>
</div>
]
: LoadingIndicator.component({className: 'loading-indicator-block'})}
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</div>
</div>
);
@ -119,10 +121,10 @@ export default class DiscussionPage extends mixin(Component, evented) {
config(isInitialized, context) {
if (isInitialized) return;
context.retain = true;
// context.retain = true;
$('body').addClass('discussion-page');
context.onunload = () => $('body').removeClass('discussion-page');
$('#app').addClass('App--discussion');
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)
.map(record => app.store.getById('posts', record.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
@ -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
// we will make sure it is visible in the viewport – if it is not we will
// scroll the list down to it.
const $discussion = $list.find('.discussion-list-item.active');
const $discussion = $list.find('.DiscussionListItem.active');
if ($discussion.length) {
const listTop = $list.offset().top;
const listBottom = listTop + $list.outerHeight();
@ -265,15 +267,15 @@ export default class DiscussionPage extends mixin(Component, evented) {
SplitDropdown.component({
children: DiscussionControls.controls(this.discussion, this).toArray(),
icon: 'ellipsis-v',
className: 'primary-control',
buttonClassName: 'btn btn-primary'
className: 'App-primaryControl',
buttonClassName: 'Button--primary'
})
);
items.add('scrubber',
PostStreamScrubber.component({
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 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 Button from 'flarum/components/Button';
import LinkButton from 'flarum/components/LinkButton';
/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
@ -28,13 +28,12 @@ export default class DiscussionsSearchSource {
const results = this.results[query] || [];
return [
<li className="dropdown-header">Discussions</li>,
<li className="Dropdown-header">Discussions</li>,
<li>
{Button.component({
{LinkButton.component({
icon: 'search',
children: 'Search all discussions for "' + query + '"',
href: app.route('index', {q: query}),
config: m.route
href: app.route('index', {q: query})
})}
</li>,
results.map(discussion => {
@ -42,10 +41,10 @@ export default class DiscussionsSearchSource {
const post = relevantPosts && relevantPosts[0];
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}>
<div className="title">{highlight(discussion.title(), query)}</div>
{post ? <div className="excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''}
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{post ? <div className="DiscussionSearchResult-excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''}
</a>
</li>
);

View File

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

View File

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

View File

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

View File

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

View File

@ -29,40 +29,44 @@ export default class ForgotPasswordModal extends Modal {
}
className() {
return 'modal-sm forgot-password';
return 'ForgotPasswordModal Modal--small';
}
title() {
return 'Forgot Password';
}
body() {
content() {
if (this.success) {
const emailProviderName = this.email().split('@')[1];
return (
<div className="form-centered">
<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-group">
<a href={'http://' + emailProviderName} className="btn btn-primary btn-block">Go to {emailProviderName}</a>
<div className="Modal-body">
<div className="Form Form--centered">
<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>
<div className="Form-group">
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">Go to {emailProviderName}</a>
</div>
</div>
</div>
);
}
return (
<div className="form-centered">
<p className="help-text">Enter your email address and we will send you a link to reset your password.</p>
<div className="form-group">
<input className="form-control" name="email" type="email" placeholder="Email"
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="form-group">
<button type="submit" className="btn btn-primary btn-block" disabled={this.loading}>
Recover Password
</button>
<div className="Modal-body">
<div className="Form Form--centered">
<p className="helpText">Enter your email address and we will send you a link to reset your password.</p>
<div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder="Email"
value={this.email()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="Form-group">
<button type="submit" className="Button Button--primary Button--block" disabled={this.loading}>
Recover Password
</button>
</div>
</div>
</div>
);
@ -92,7 +96,7 @@ export default class ForgotPasswordModal extends Modal {
},
response => {
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 {
view() {
return (
<ul className="header-controls">
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);

View File

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

View File

@ -59,16 +59,16 @@ export default class IndexPage extends Component {
view() {
return (
<div className="index-area">
<div className="IndexPage">
{this.hero()}
<div className="container">
<nav className="side-nav index-nav" config={affixSidebar}>
<nav className="IndexPage-nav sideNav" config={affixSidebar}>
<ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav>
<div className="offset-content index-results">
<div className="index-toolbar">
<ul className="index-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="index-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
<div className="IndexPage-results sideNavOffset">
<div className="IndexPage-toolbar">
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
</div>
{app.cache.discussionList.render()}
</div>
@ -80,10 +80,10 @@ export default class IndexPage extends Component {
config(isInitialized, context) {
if (isInitialized) return;
$('body').addClass('index-page');
$('#app').addClass('App--index');
context.onunload = () => {
$('body').removeClass('index-page');
$('.global-page').css('min-height', '');
$('#app').removeClass('App--index')
.css('min-height', '');
};
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
// previous hero. Maintain the same scroll position relative to the bottom
// 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;
$('.global-page').css('min-height', $(window).height() + heroHeight);
$('#app').css('min-height', $(window).height() + heroHeight);
$(window).scrollTop(scrollTop - (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
// scroll down to that discussion so that it's in view.
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) {
const indexTop = $('#header').outerHeight();
@ -141,8 +141,8 @@ export default class IndexPage extends Component {
Button.component({
children: 'Start a Discussion',
icon: 'edit',
className: 'btn btn-primary new-discussion',
itemClassName: 'primary-control',
className: 'Button Button--primary IndexPage-newDiscussion',
itemClassName: 'App-primaryControl',
onclick: this.newDiscussion.bind(this)
})
);
@ -150,7 +150,8 @@ export default class IndexPage extends Component {
items.add('nav',
SelectDropdown.component({
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;
}
@ -222,12 +214,21 @@ export default class IndexPage extends Component {
actionItems() {
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) {
items.add('markAllAsRead',
Button.component({
title: 'Mark All as Read',
icon: 'check',
className: 'btn btn-default btn-icon',
className: 'Button Button--icon',
onclick: this.markAllAsRead.bind(this)
})
);

View File

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

View File

@ -31,50 +31,49 @@ export default class LogInModal extends Modal {
}
className() {
return 'modal-sm login-modal';
return 'LogInModal Modal--small';
}
title() {
return 'Log In';
}
body() {
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() {
content() {
return [
<p className="forgot-password-link">
<a href="javascript:;" onclick={this.forgotPassword.bind(this)}>Forgot password?</a>
</p>,
<p className="sign-up-link">
Don't have an account?
<a href="javascript:;" onclick={this.signUp.bind(this)}>Sign Up</a>
</p>
<div className="Modal-body">
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" 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="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() {
const email = this.email();
const props = email.indexOf('@') !== -1 ? {email} : null;
const props = email.indexOf('@') !== -1 ? {email} : undefined;
app.modal.show(new ForgotPasswordModal(props));
}
@ -101,7 +100,7 @@ export default class LogInModal extends Modal {
app.modal.show(new SignUpModal(props));
}
focus() {
onready() {
this.$('[name=' + (this.email() ? 'password' : 'email') + ']').select();
}
@ -123,17 +122,17 @@ export default class LogInModal extends Modal {
if (response && response.code === 'confirm_email') {
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 {
this.alert = Alert.component({
type: 'warning',
message: 'Your login details were incorrect.'
type: 'error',
children: 'Your login details were incorrect.'
});
}
m.redraw();
this.focus();
this.onready();
}
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ export default class PostEdited extends Component {
const title = 'Edited ' + (editUser ? 'by ' + editUser.username() + ' ' : '') + humanTime(post.editTime());
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
// input so that the user can quickly copy the URL.
const selectPermalink = function() {
setTimeout(() => $(this).parent().find('.permalink').select());
setTimeout(() => $(this).parent().find('.PostMeta-permalink').select());
m.redraw.strategy('none');
};
return (
<div className="dropdown post-meta">
<a href="javascript:;" data-toggle="dropdown" className="dropdown-toggle" onclick={selectPermalink}>
<div className="Dropdown PostMeta">
<a className="Dropdown-toggle" onclick={selectPermalink} data-toggle="dropdown">
{humanTime(time)}
</a>
<div className="dropdown-menu">
<span className="number">Post #{post.number()}</span>
<div className="Dropdown-menu dropdown-menu">
<span className="PostMeta-number">Post #{post.number()}</span>{' '}
{fullTime(time)}
{touch
? <a href="btn btn-default permalink" href={permalink}>{permalink}</a>
: <input className="form-control permalink" value="permalink" onclick={e => e.stopPropagation()} />}
? <a href="Button PostMeta-permalink" href={permalink}>{permalink}</a>
: <input className="FormControl PostMeta-permalink" value={permalink} onclick={e => e.stopPropagation()} />}
</div>
</div>
);

View File

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

View File

@ -79,7 +79,7 @@ class PostStream extends mixin(Component, evented) {
m.redraw(true);
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));
});
@ -204,7 +204,7 @@ class PostStream extends mixin(Component, evented) {
if (dt > 1000 * 60 * 60 * 24 * 4) {
content = [
<div className="time-gap">
<div className="PostStream-timeGap">
<span>{moment.duration(dt).humanize()} later</span>
</div>,
content
@ -218,7 +218,7 @@ class PostStream extends mixin(Component, evented) {
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.composingReplyTo(this.discussion)
? (
<div className="post-stream-item" key="reply">
<div className="PostStream-item" key="reply">
{ReplyPlaceholder.component({discussion: this.discussion})}
</div>
) : ''
@ -265,7 +265,7 @@ class PostStream extends mixin(Component, evented) {
const loadAheadDistance = 500;
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) {
this.loadPrevious();
@ -273,7 +273,7 @@ class PostStream extends mixin(Component, evented) {
}
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) {
this.loadNext();
@ -334,7 +334,7 @@ class PostStream extends mixin(Component, evented) {
if (start < this.visibleStart || end > this.visibleEnd) return;
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();
};
@ -386,7 +386,7 @@ class PostStream extends mixin(Component, evented) {
* @return {Promise}
*/
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;
}
@ -431,7 +431,7 @@ class PostStream extends mixin(Component, evented) {
let startNumber;
let endNumber;
this.$('.post-stream-item').each(function() {
this.$('.PostStream-item').each(function() {
const $item = $(this);
const top = $item.offset().top;
const height = $item.outerHeight(true);
@ -472,7 +472,7 @@ class PostStream extends mixin(Component, evented) {
* @return {jQuery.Deferred}
*/
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));
}
@ -487,7 +487,7 @@ class PostStream extends mixin(Component, evented) {
* @return {jQuery.Deferred}
*/
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);
}

View File

@ -72,9 +72,9 @@ export default class PostStreamScrubber extends Component {
const unreadPercent = Math.min(this.count() - this.index, unreadCount) / this.count();
const viewing = [
<span className="index">{retain || formatNumber(this.visibleIndex())}</span>,
<span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
' of ',
<span className="count">{formatNumber(this.count())}</span>,
<span className="Scrubber-count">{formatNumber(this.count())}</span>,
' posts '
];
@ -95,34 +95,34 @@ export default class PostStreamScrubber extends Component {
}
return (
<div className={'post-stream-scrubber dropdown ' + (this.disabled() ? 'disabled ' : '') + (this.props.className || '')}>
<a href="javascript:;" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
<div className={'PostStreamScrubber Dropdown ' + (this.disabled() ? 'disabled ' : '') + (this.props.className || '')}>
<button className="Button Dropdown-toggle" data-toggle="dropdown">
{viewing} {icon('sort')}
</a>
</button>
<div className="dropdown-menu">
<div className="scrubber">
<a href="javascript:;" className="scrubber-first" onclick={this.goToFirst.bind(this)}>
<div className="Dropdown-menu dropdown-menu">
<div className="Scrubber">
<a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
{icon('angle-double-up')} Original Post
</a>
<div className="scrubber-scrollbar">
<div className="scrubber-before"/>
<div className="scrubber-handle">
<div className="scrubber-bar"/>
<div className="scrubber-info">
<div className="Scrubber-scrollbar">
<div className="Scrubber-before"/>
<div className="Scrubber-handle">
<div className="Scrubber-bar"/>
<div className="Scrubber-info">
<strong>{viewing}</strong>
<span class="description">{retain || this.description}</span>
<span class="Scrubber-description">{retain || this.description}</span>
</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
</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
</a>
</div>
@ -208,7 +208,7 @@ export default class PostStreamScrubber extends Component {
// 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,
// 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 visible = 0;
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
// that position.
this.$('.scrubber-scrollbar')
this.$('.Scrubber-scrollbar')
.bind('click', this.onclick.bind(this))
// 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.indexStart = 0;
this.$('.scrubber-handle')
this.$('.Scrubber-handle')
.css('cursor', 'move')
.bind('mousedown touchstart', this.onmousedown.bind(this))
@ -331,8 +331,8 @@ export default class PostStreamScrubber extends Component {
const visible = this.visible || 1;
const $scrubber = this.$();
$scrubber.find('.index').text(formatNumber(this.visibleIndex()));
$scrubber.find('.description').text(this.description);
$scrubber.find('.Scrubber-index').text(formatNumber(this.visibleIndex()));
$scrubber.find('.Scrubber-description').text(this.description);
$scrubber.toggleClass('disabled', this.disabled());
const heights = {};
@ -342,7 +342,7 @@ export default class PostStreamScrubber extends Component {
const func = animate ? 'animate' : 'css';
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');
// 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
// percentage per post, then we need to adjust the 'before' percentage to
// 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 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
// the sidebar and doesn't overlap the footer.
const scrubber = this.$();
const scrollbar = this.$('.scrubber-scrollbar');
const scrollbar = this.$('.Scrubber-scrollbar');
scrollbar.css('max-height', $(window).height() -
scrubber.offset().top + $(window).scrollTop() -
parseInt($('.global-page').css('padding-bottom'), 10) -
parseInt($('#app').css('padding-bottom'), 10) -
(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
// the index at which the drag was started, and then scroll there.
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 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
// 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();
let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100;
// 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
// 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-
// content component to jump to that index.

View File

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

View File

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

View File

@ -27,7 +27,7 @@ export default class ReplyComposer extends ComposerBody {
items.add('title', (
<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>
));

View File

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

View File

@ -75,25 +75,25 @@ export default class Search extends Component {
}
return (
<div className={'search dropdown ' + classList({
<div className={'Search Dropdown ' + classList({
open: this.value() && this.hasFocus,
active: !!currentSearch,
loading: !!this.loadingSources
})}>
<div className="search-input">
<input className="form-control"
<div className="Search-input">
<input className="FormControl"
placeholder="Search Forum"
value={this.value()}
oninput={m.withAttr('value', this.value)}
onfocus={() => this.hasFocus = true}
onblur={() => this.hasFocus = false}/>
{this.loadingSources
? LoadingIndicator.component({size: 'tiny', className: 'btn btn-icon btn-link'})
? LoadingIndicator.component({size: 'tiny', className: 'Button Button--icon Button--link'})
: 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>
<ul className="dropdown-menu dropdown-menu-right search-results">
<ul className="Dropdown-menu Search-results">
{this.sources.map(source => source.view(this.value()))}
</ul>
</div>
@ -108,12 +108,12 @@ export default class Search extends Component {
const search = this;
this.$('.search-results')
this.$('.Search-results')
.on('mousedown', e => e.preventDefault())
.on('click', () => this.$('input').blur())
// 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.selectableItems().index(this)
);
@ -169,7 +169,7 @@ export default class Search extends Component {
search.searched.push(query);
m.redraw();
}, 500);
}, 250);
});
}
@ -215,7 +215,7 @@ export default class Search extends Component {
* @return {jQuery}
*/
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) {
const $items = this.selectableItems();
let $item = $items.filter(`[data-index=${index}]`);
let $item = $items.filter(`[data-index="${index}"]`);
if (!$item.length) {
$item = $items.eq(index);

View File

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

View File

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

View File

@ -45,39 +45,50 @@ export default class SignUpModal extends Modal {
}
className() {
return 'modal-sm signup-modal' + (this.welcomeUser ? ' signup-modal-success' : '');
return 'Modal--small SignUpModal' + (this.welcomeUser ? ' SignUpModal--success' : '');
}
title() {
return 'Sign Up';
}
content() {
return [
<div className="Modal-body">
{this.body()}
</div>,
<div className="Modal-footer">
{this.footer()}
</div>
];
}
body() {
const body = [(
<div className="form-centered">
<div className="form-group">
<input className="form-control" name="username" placeholder="Username"
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="username" placeholder="Username"
value={this.username()}
onchange={m.withAttr('value', this.email)}
disabled={this.loading} />
</div>
<div className="form-group">
<input className="form-control" name="email" type="email" placeholder="Email"
<div className="Form-group">
<input className="FormControl" name="email" type="email" placeholder="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"
<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="btn btn-primary btn-block"
<div className="Form-group">
<button className="Button Button--primary Button--block"
type="submit"
disabled={this.loading}>
Sign Up
@ -96,17 +107,17 @@ export default class SignUpModal extends Modal {
};
body.push(
<div className="signup-welcome" style={{background: user.color()}} config={fadeIn}>
<div className="darken-overlay"/>
<div className="SignUpModal-welcome" style={{background: user.color()}} config={fadeIn}>
<div className="darkenBackground"/>
<div className="container">
{avatar(user)}
<h3>Welcome, {user.username()}!</h3>
{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><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>
@ -118,9 +129,9 @@ export default class SignUpModal extends Modal {
footer() {
return [
<p className="log-in-link">
Already have an account?
<a href="javascript:;" onclick={this.logIn.bind(this)}>Log In</a>
<p className="SignUpModal-logIn">
Already have an account?{' '}
<a onclick={this.logIn.bind(this)}>Log In</a>
</p>
];
}

View File

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

View File

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

View File

@ -29,27 +29,27 @@ export default class UserBio extends Component {
let content;
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 {
let subContent;
if (this.loading) {
subContent = <p className="placeholder">Saving</p>;
subContent = <p className="UserBio-placeholder">Saving</p>;
} else {
const bioHtml = user.bioHtml();
if (bioHtml) {
subContent = m.trust(bioHtml);
} 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 (
<div className={'bio ' + classList({
<div className={'UserBio ' + classList({
editable: this.isEditable(),
editing: this.editing
})}

View File

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

View File

@ -33,24 +33,24 @@ export default class UserPage extends Component {
view() {
return (
<div>
<div className="UserPage">
{this.user ? [
UserCard.component({
user: this.user,
className: 'hero user-hero',
className: 'Hero UserHero',
editable: this.user.canEdit(),
controlsButtonClassName: 'btn btn-default'
controlsButtonClassName: 'Button'
}),
<div className="container">
<nav className="side-nav user-nav" config={affixSidebar}>
<nav className="sideNav UserPage-nav" config={affixSidebar}>
<ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav>
<div className="offset-content user-content">
<div className="sideNavOffset UserPage-content">
{this.content()}
</div>
</div>
] : [
LoadingIndicator.component({className: 'loading-indicator-block'})
LoadingIndicator.component({className: 'LoadingIndicator--block'})
]}
</div>
);
@ -59,8 +59,8 @@ export default class UserPage extends Component {
config(isInitialized, context) {
if (isInitialized) return;
$('body').addClass('user-page');
context.onunload = () => $('body').removeClass('user-page');
$('#app').addClass('App--user');
context.onunload = () => $('#app').removeClass('App--user');
}
/**
@ -117,7 +117,8 @@ export default class UserPage extends Component {
items.add('nav',
SelectDropdown.component({
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',
LinkButton.component({
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'
})
);
@ -152,7 +153,7 @@ export default class UserPage extends Component {
items.add('posts',
LinkButton.component({
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'
})
);

View File

@ -22,9 +22,9 @@ export default class UsersSearchResults {
if (!results.length) return '';
return [
<li className="dropdown-header">Users</li>,
<li className="Dropdown-header">Users</li>,
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}>
{avatar(user)}
{highlight(user.username(), query)}

View File

@ -20,15 +20,15 @@ export default class WelcomeHero extends Component {
};
return (
<header className="hero welcome-hero">
<header className="Hero WelcomeHero">
<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')}
</button>
<div className="container-narrow">
<h2>{app.forum.attribute('welcomeTitle')}</h2>
<div className="subtitle">{m.trust(app.forum.attribute('welcomeMessage'))}</div>
<div className="containerNarrow">
<h2 className="Hero-title">{app.forum.attribute('welcomeTitle')}</h2>
<div className="Hero-subtitle">{m.trust(app.forum.attribute('welcomeMessage'))}</div>
</div>
</div>
</header>

View File

@ -12,7 +12,7 @@ import FooterPrimary from 'flarum/components/FooterPrimary';
import FooterSecondary from 'flarum/components/FooterSecondary';
import Composer from 'flarum/components/Composer';
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
@ -23,18 +23,18 @@ import Alerts from 'flarum/components/Alerts';
export default function boot(app) {
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-primary'), HeaderPrimary.component());
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
m.mount(document.getElementById('footer-primary'), FooterPrimary.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.composer = m.mount(document.getElementById('composer'), Composer.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(document.getElementById('content'), '/', mapRoutes(app.routes));
@ -47,15 +47,22 @@ export default function boot(app) {
if (e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault();
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
// 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
// touch devices.
$(() => FastClick.attach(document.body));
$('#app').affix({
offset: {top: offsetTop}
});
app.booted = true;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@ export default function affixSidebar(element, isInitialized) {
if (isInitialized) return;
const $sidebar = $(element);
const $header = $('.global-header');
const $footer = $('.global-footer');
const $header = $('#header');
const $footer = $('#footer');
// Don't affix the sidebar if it is taller than the viewport (otherwise
// 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)');
};
$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) {
// Update the references to the elements underneath the slider, provided
// they're not disabled.
$underneathLeft = $element.find('.slidable-underneath-left:not(.disabled)');
$underneathRight = $element.find('.slidable-underneath-right:not(.disabled)');
$underneathLeft = $element.find('.Slidable-underneath--left:not(.disabled)');
$underneathRight = $element.find('.Slidable-underneath--right:not(.disabled)');
startX = e.originalEvent.targetTouches[0].clientX;
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
// them depending on the slider's position. We also make the controls
// icon get a bit bigger the further they slide.
const toggle = ($underneath, active) => {
const toggle = ($underneath, side) => {
if ($underneath.length) {
const active = side === 'left' ? pos > 0 : pos < 0;
if (active && $underneath.hasClass('elastic')) {
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));
$underneath.find('.icon').css('transform', 'scale(' + scale + ')');
} else {
pos = Math.min(0, pos);
pos = Math[side === 'left' ? 'min' : 'max'](0, pos);
}
};
toggle($underneathLeft, pos > 0);
toggle($underneathRight, pos < 0);
toggle($underneathLeft, 'left');
toggle($underneathRight, 'right');
$(this).css('transform', 'translate(' + pos + 'px, 0)');
$(this).css('background-position-x', pos + 'px');

View File

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

View File

@ -10,7 +10,7 @@ import extract from 'flarum/utils/extract';
* The alert may have the following special props:
*
* - `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.
* - `dismissible` Whether or not the alert can be 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 type = extract(attrs, 'type');
attrs.className = 'alert alert-' + type + ' ' + (attrs.className || '');
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const children = extract(attrs, 'children');
const controls = extract(attrs, 'controls') || [];
@ -37,17 +37,17 @@ export default class Alert extends Component {
if (dismissible || dismissible === undefined) {
dismissControl.push(Button.component({
icon: 'times',
className: 'btn btn-link btn-icon dismiss',
className: 'Button Button--link Button--icon Alert-dismiss',
onclick: ondismiss
}));
}
return (
<div {...attrs}>
<span className="alert-body">
<span className="Alert-body">
{children}
</span>
<ul className="alert-controls">
<ul className="Alert-controls">
{listItems(controls.concat(dismissControl))}
</ul>
</div>

View File

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

View File

@ -9,7 +9,7 @@ import extract from 'flarum/utils/extract';
* A badge may have the following special props:
*
* - `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.
*
* 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 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,
// and then one is added/removed, Mithril will correctly redraw the series
@ -29,7 +30,7 @@ export default class Badge extends Component {
return (
<span {...attrs}>
{iconName ? icon(iconName, {className: 'icon'}) : ''}
{iconName ? icon(iconName, {className: 'Badge-icon'}) : ''}
</span>
);
}

View File

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

View File

@ -27,7 +27,7 @@ export default class Checkbox extends Component {
}
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.props.disabled) className += ' disabled';
@ -37,7 +37,7 @@ export default class Checkbox extends Component {
checked={this.props.state}
disabled={this.props.disabled}
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
<div className="checkbox-display">
<div className="Checkbox-display">
{this.getDisplay()}
</div>
{this.props.children}

View File

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

View File

@ -18,6 +18,14 @@ export default class LinkButton extends Button {
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'.
*

View File

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

View File

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

View File

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

View File

@ -14,11 +14,11 @@ export default class Select extends Component {
const {options, onchange, value} = this.props;
return (
<span className="select">
<select className="form-control" onchange={m.withAttr('value', onchange.bind(this))} value={value}>
<span className="Select">
<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>)}
</select>
{icon('sort', {className: 'caret'})}
{icon('sort', {className: 'Select-caret'})}
</span>
);
}

View File

@ -10,16 +10,18 @@ export default class SelectDropdown extends Dropdown {
static initProps(props) {
super.initProps(props);
props.className += ' select-dropdown';
props.className += ' Dropdown--select';
}
getButtonContent() {
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 [
<span className="label">{label}</span>,
icon('sort', {className: 'caret'})
<span className="Button-label">{label}</span>, ' ',
icon('sort', {className: 'Button-caret'})
];
}
}

View File

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

View File

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

View File

@ -6,7 +6,7 @@
* @return {Object}
*/
export default function avatar(user, attrs = {}) {
attrs.className = 'avatar ' + (attrs.className || '');
attrs.className = 'Avatar ' + (attrs.className || '');
let content = '';
// 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 classList from 'flarum/utils/classList';
function isSeparator(item) {
return item && item.component === Separator;
@ -28,10 +29,20 @@ function withoutUnnecessarySeparators(items) {
export default function listItems(items) {
return withoutUnnecessarySeparators(items).map(item => {
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;
return isListItem
? item
: <li className={(item.itemName ? 'item-' + item.itemName : '') + ' ' + (className || '')}>{item}</li>;
return [
isListItem
? item
: <li className={classList([
(item.itemName ? 'item-' + item.itemName : ''),
className,
(active ? 'active' : '')
])}>
{item}
</li>,
' '
];
});
};
}

View File

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

View File

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

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

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

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

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