mirror of
https://github.com/flarum/framework.git
synced 2025-04-13 14:23:58 +08:00
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:
parent
0b685b1036
commit
2aa9c2e746
@ -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',
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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> : '';
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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'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 "DELETE" 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>
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -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), '.'];
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
];
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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())
|
||||
})
|
||||
);
|
||||
|
@ -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)
|
||||
})
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -15,6 +15,6 @@ export default class NotificationsPage extends Component {
|
||||
}
|
||||
|
||||
view() {
|
||||
return <div>{NotificationList.component()}</div>;
|
||||
return <div className="NotificationsPage">{NotificationList.component()}</div>;
|
||||
}
|
||||
}
|
||||
|
@ -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()}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
));
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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())
|
||||
})
|
||||
);
|
||||
|
@ -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>
|
||||
];
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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)
|
||||
})
|
||||
);
|
||||
|
@ -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
|
||||
})}
|
||||
|
@ -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)]}
|
||||
|
@ -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'
|
||||
})
|
||||
);
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,6 @@ export default class History {
|
||||
home() {
|
||||
this.stack.splice(1);
|
||||
|
||||
m.route(this.stack[0].url);
|
||||
m.route('/');
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
@ -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).
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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> : ''
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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'})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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'.
|
||||
*
|
||||
|
@ -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(' ')}</div>;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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'})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import Component from 'flarum/Component';
|
||||
*/
|
||||
class Separator extends Component {
|
||||
view() {
|
||||
return <li className="divider"/>;
|
||||
return <li className="Dropdown-separator"/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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() : '';
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
' '
|
||||
];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -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(' ');
|
||||
|
@ -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;
|
||||
|
76
framework/core/less/forum/ActivityPage.less
Normal file
76
framework/core/less/forum/ActivityPage.less
Normal 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;
|
||||
}
|
||||
}
|
34
framework/core/less/forum/AvatarEditor.less
Normal file
34
framework/core/less/forum/AvatarEditor.less
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
19
framework/core/less/forum/DiscussionHero.less
Normal file
19
framework/core/less/forum/DiscussionHero.less
Normal 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;
|
||||
}
|
22
framework/core/less/forum/DiscussionList.less
Normal file
22
framework/core/less/forum/DiscussionList.less
Normal 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;
|
||||
}
|
||||
}
|
253
framework/core/less/forum/DiscussionListItem.less
Normal file
253
framework/core/less/forum/DiscussionListItem.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
145
framework/core/less/forum/DiscussionPage.less
Normal file
145
framework/core/less/forum/DiscussionPage.less
Normal file
@ -0,0 +1,145 @@
|
||||
.DiscussionPage-nav {
|
||||
> ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media @phone {
|
||||
.DiscussionPage-nav {
|
||||
margin: 0 -15px;
|
||||
border-bottom: 1px solid @control-bg;
|
||||
|
||||
> ul > li {
|
||||
margin: 15px;
|
||||
display: inline-block;
|
||||
|
||||
&.item-controls, &.item-scrubber {
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media @tablet-up {
|
||||
.DiscussionPage-nav {
|
||||
float: right;
|
||||
|
||||
&, > ul {
|
||||
width: 150px;
|
||||
}
|
||||
> ul {
|
||||
position: fixed;
|
||||
margin-top: 30px;
|
||||
z-index: 1;
|
||||
|
||||
> li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.ButtonGroup, .Button {
|
||||
width: 100%;
|
||||
}
|
||||
.ButtonGroup:not(.itemCount1) {
|
||||
.SplitDropdown-button {
|
||||
width: 77%;
|
||||
}
|
||||
.Dropdown-toggle {
|
||||
width: 22%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media @tablet-up {
|
||||
.DiscussionPage-stream {
|
||||
margin-right: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Discussions Pane
|
||||
|
||||
@media @phone {
|
||||
.DiscussionPage-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media @tablet-up {
|
||||
.DiscussionPage-list {
|
||||
left: -@pane-width;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
z-index: @zindex-pane;
|
||||
overflow: auto;
|
||||
top: @header-height;
|
||||
bottom: 0;
|
||||
width: @pane-width;
|
||||
background: @body-bg;
|
||||
padding-bottom: 40px;
|
||||
border-top: 1px solid @control-bg;
|
||||
.box-shadow(2px 2px 6px -2px @shadow-color);
|
||||
.transition(left 0.2s);
|
||||
|
||||
.paneShowing & {
|
||||
left: 0;
|
||||
}
|
||||
.DiscussionListItem {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&.active {
|
||||
background: @control-bg;
|
||||
}
|
||||
}
|
||||
.DiscussionListItem-controls {
|
||||
top: 5px;
|
||||
}
|
||||
.DiscussionListItem-content {
|
||||
padding-left: 52px + 15px;
|
||||
padding-right: 65px + 15px;
|
||||
}
|
||||
.DiscussionListItem-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
.DiscussionListItem-relevantPosts {
|
||||
margin-left: -52px;
|
||||
margin-right: -65px;
|
||||
}
|
||||
.DiscussionListItem-count {
|
||||
margin-top: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media @desktop-hd {
|
||||
.DiscussionPage-list {
|
||||
.panePinned & {
|
||||
left: 0;
|
||||
z-index: @zindex-composer - 1;
|
||||
.transition(none);
|
||||
}
|
||||
}
|
||||
// When the pane is pinned, move the other page content inwards
|
||||
.App-content, .App-footer {
|
||||
.hasPane.panePinned & {
|
||||
margin-left: @pane-width;
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.App-header .container {
|
||||
transition: width 0.2s;
|
||||
|
||||
.hasPane.panePinned & {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
47
framework/core/less/forum/Hero.less
Normal file
47
framework/core/less/forum/Hero.less
Normal file
@ -0,0 +1,47 @@
|
||||
.Hero {
|
||||
margin-top: -1px;
|
||||
background: @hero-bg;
|
||||
text-align: center;
|
||||
color: @hero-color;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.container {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.Hero-close {
|
||||
float: right;
|
||||
margin-top: -10px;
|
||||
color: inherit;
|
||||
}
|
||||
.Hero-subtitle {
|
||||
margin: 8px 0 0;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
@media @phone {
|
||||
.Hero-close {
|
||||
margin-right: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media @tablet-up {
|
||||
.Hero {
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
.container {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
.Hero-subtitle {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
37
framework/core/less/forum/IndexPage.less
Normal file
37
framework/core/less/forum/IndexPage.less
Normal 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;
|
||||
}
|
||||
}
|
49
framework/core/less/forum/NotificationGrid.less
Normal file
49
framework/core/less/forum/NotificationGrid.less
Normal 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%);
|
||||
}
|
||||
}
|
||||
}
|
121
framework/core/less/forum/NotificationList.less
Normal file
121
framework/core/less/forum/NotificationList.less
Normal 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;
|
||||
}
|
||||
}
|
39
framework/core/less/forum/NotificationsDropdown.less
Normal file
39
framework/core/less/forum/NotificationsDropdown.less
Normal file
@ -0,0 +1,39 @@
|
||||
.NotificationsDropdown {
|
||||
.Dropdown-menu {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.NotificationList-content {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
& .Dropdown-toggle .Button-label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
@media @tablet-up {
|
||||
.NotificationsDropdown {
|
||||
.Dropdown-menu {
|
||||
width: 400px;
|
||||
}
|
||||
.Dropdown-toggle {
|
||||
.Button--icon();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.NotificationsDropdown-button.unread .Button-icon {
|
||||
display: inline-block;
|
||||
border-radius: 12px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
padding: 2px 0;
|
||||
font-weight: bold;
|
||||
margin: -2px 0;
|
||||
background: @primary-color;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
398
framework/core/less/forum/Post.less
Normal file
398
framework/core/less/forum/Post.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
83
framework/core/less/forum/PostStream.less
Normal file
83
framework/core/less/forum/PostStream.less
Normal file
@ -0,0 +1,83 @@
|
||||
// ------------------------------------
|
||||
// Stream
|
||||
|
||||
.PostStream {
|
||||
@media @tablet-up {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
.PostStream-item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid @control-bg;
|
||||
|
||||
@media @phone {
|
||||
margin: 0 -15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {opacity: 0.5}
|
||||
50% {opacity: 1}
|
||||
100% {opacity: 0.5}
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
0% {opacity: 0.5}
|
||||
50% {opacity: 1}
|
||||
100% {opacity: 0.5}
|
||||
}
|
||||
.LoadingPost {
|
||||
.animation(blink 1s linear);
|
||||
.animation-iteration-count(infinite);
|
||||
}
|
||||
.fakeText {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background: @control-bg;
|
||||
height: 12px;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
border-radius: @border-radius;
|
||||
|
||||
.Post-header & {
|
||||
height: 16px;
|
||||
width: 150px;
|
||||
|
||||
@media @phone {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// .item.highlight .post {
|
||||
// &:before {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// left: -30px;
|
||||
// top: -5px;
|
||||
// bottom: -5px;
|
||||
// width: 5px;
|
||||
// border-radius: @border-radius;
|
||||
// background: @fl-primary-color;
|
||||
// }
|
||||
// }
|
||||
@-webkit-keyframes pulsate {
|
||||
0% {-webkit-transform: scale(1)}
|
||||
50% {-webkit-transform: scale(1.02)}
|
||||
100% {-webkit-transform: scale(1)}
|
||||
}
|
||||
@keyframes pulsate {
|
||||
0% {transform: scale(1)}
|
||||
50% {transform: scale(1.02)}
|
||||
100% {transform: scale(1)}
|
||||
}
|
||||
.pulsate {
|
||||
.animation(pulsate 1s ease-in-out);
|
||||
.animation-iteration-count(infinite);
|
||||
}
|
||||
.flash {
|
||||
.animation(pulsate 0.2s ease-in-out);
|
||||
.animation-iteration-count(1);
|
||||
}
|
95
framework/core/less/forum/Scrubber.less
Normal file
95
framework/core/less/forum/Scrubber.less
Normal 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
Loading…
x
Reference in New Issue
Block a user