1
0
mirror of https://github.com/flarum/core.git synced 2025-07-24 18:21:33 +02: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:
Toby Zerner
2015-07-17 14:47:49 +09:30
parent 0b685b1036
commit 2aa9c2e746
206 changed files with 4337 additions and 8830 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,139 @@
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';
/**
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses
* should implement the `className`, `title`, and `content` methods.
*
* @abstract
*/
export default class Modal extends Component {
constructor(...args) {
super(...args);
/**
* An alert component to show below the header.
*
* @type {Alert}
*/
this.alert = null;
/**
* Whether or not the form is processing.
*
* @type {Boolean}
*/
this.loading = false;
}
view() {
if (this.alert) {
this.alert.props.dismissible = false;
}
return (
<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="Page-titleControl Page-titleControl--text">{this.title()}</h3>
</div>
{alert ? <div className="Modal-alert">{this.alert}</div> : ''}
{this.content()}
</form>
</div>
{LoadingIndicator.component({
className: 'Modal-loading ' + (this.loading ? 'active' : '')
})}
</div>
);
}
/**
* Get the class name to apply to the modal.
*
* @return {String}
* @abstract
*/
className() {
}
/**
* Get the title of the modal dialog.
*
* @return {String}
* @abstract
*/
title() {
}
/**
* Get the content of the modal.
*
* @return {VirtualElement}
* @abstract
*/
content() {
}
/**
* Handle the modal form's submit event.
*
* @param {Event} e
*/
onsubmit() {
}
/**
* Focus on the first input when the modal is ready to be used.
*/
onready() {
this.$('form :input:first').select();
}
/**
* Hide the modal.
*/
hide() {
app.modal.close();
}
/**
* Show an alert describing errors returned from the API, and give focus to
* the first relevant field.
*
* @param {Object} response
*/
handleErrors(response) {
const errors = response && response.errors;
if (errors) {
this.alert(new Alert({
type: 'warning',
message: errors.map((error, k) => [error.detail, k < errors.length - 1 ? m('br') : ''])
}));
}
m.redraw();
if (errors) {
this.$('form [name=' + errors[0].path + ']').select();
} else {
this.$('form :input:first').select();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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