Redesign the landing page, mount public timeline on it (#4122)

* Redesign the landing page, mount public timeline on it

* Adjust the standalone mounted component to the lacking of router

* Adjust auth layout pages to new design

* Fix tests

* Standalone public timeline polling every 5 seconds

* Remove now obsolete translations

* Add responsive design for new landing page

* Address reviews

* Add floating clouds behind frontpage form

* Use access token from public page when available

* Fix mentions and hashtags links, cursor on status content in standalone mode

* Add footer link to source code

* Fix errors on pages that don't embed the component, use classnames

* Fix tests

* Change anonymous autoPlayGif default to false

* When gif autoplay is disabled, hover to play

* Add option to hide the timeline preview

* Slightly improve alt layout

* Add elephant friend to new frontpage

* Display "back to mastodon" in place of "login" when logged in on frontpage

* Change polling time to 3s
This commit is contained in:
Eugen Rochko 2017-07-11 15:27:59 +02:00 committed by GitHub
parent 8784bd79d0
commit e19eefe219
68 changed files with 959 additions and 658 deletions

View File

@ -4,7 +4,10 @@ class AboutController < ApplicationController
before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more, :terms]
def show; end
def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def more; end
@ -15,6 +18,7 @@ class AboutController < ApplicationController
def new_user
User.new.tap(&:build_account)
end
helper_method :new_user
def set_instance_presenter
@ -24,4 +28,11 @@ class AboutController < ApplicationController
def set_body_classes
@body_classes = 'about-body'
end
def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end

View File

@ -11,8 +11,15 @@ module Admin
site_terms
open_registrations
closed_registrations_message
open_deletion
timeline_preview
).freeze
BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
).freeze
BOOLEAN_SETTINGS = %w(open_registrations).freeze
def edit
@settings = Setting.all_as_records

View File

@ -15,12 +15,16 @@ class HomeController < ApplicationController
end
def set_initial_state_json
state = InitialStatePresenter.new(settings: Web::Setting.find_by(user: current_user)&.data || {},
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username))
serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer)
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
}
end
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent {
size: PropTypes.number.isRequired,
direction: PropTypes.string,
ariaLabel: PropTypes.string,
disabled: PropTypes.bool,
};
static defaultProps = {
@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent {
}
render () {
const { icon, items, size, direction, ariaLabel } = this.props;
const { expanded } = this.state;
const { icon, items, size, direction, ariaLabel, disabled } = this.props;
const { expanded } = this.state;
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
if (disabled) {
return (
<div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</div>
);
}
const dropdownItems = expanded && (
<ul className='dropdown__content-list'>
@ -80,8 +91,8 @@ export default class DropdownMenu extends React.PureComponent {
return (
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
<i className={`fa fa-fw fa-${icon} dropdown__icon`} aria-hidden />
<DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>
<DropdownContent className={directionClass}>

View File

@ -11,18 +11,44 @@ const messages = defineMessages({
class Item extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
autoPlayGif: PropTypes.bool,
};
static defaultProps = {
autoPlayGif: false,
};
handleMouseEnter = (e) => {
if (this.hoverToPlay()) {
e.target.play();
}
}
handleMouseLeave = (e) => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
}
hoverToPlay () {
const { attachment, autoPlayGif } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv';
}
handleClick = (e) => {
const { index, onClick } = this.props;
if (e.button === 0) {
if (this.context.router && e.button === 0) {
e.preventDefault();
onClick(index);
}
@ -116,6 +142,8 @@ class Item extends React.PureComponent {
role='application'
src={attachment.get('url')}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
loop
muted
@ -144,7 +172,11 @@ export default class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
autoPlayGif: PropTypes.bool,
};
static defaultProps = {
autoPlayGif: false,
};
state = {

View File

@ -15,7 +15,7 @@ export default class Permalink extends React.PureComponent {
};
handleClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(this.props.to);
}
@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
const { href, children, className, ...other } = this.props;
return (
<a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
{children}
</a>
);

View File

@ -140,12 +140,16 @@ export default class Status extends ImmutablePureComponent {
}
handleClick = () => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
handleAccountClick = (e) => {
if (e.button === 0) {
if (this.context.router && e.button === 0) {
const id = Number(e.currentTarget.getAttribute('data-id'));
e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
@ -236,7 +240,7 @@ export default class Status extends ImmutablePureComponent {
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
{statusAvatar}
</div>

View File

@ -40,7 +40,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onBlock: PropTypes.func,
onReport: PropTypes.func,
onMuteConversation: PropTypes.func,
me: PropTypes.number.isRequired,
me: PropTypes.number,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@ -97,6 +97,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
const { status, me, intl, withDismiss } = this.props;
const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
let menu = [];
let reblogIcon = 'retweet';
@ -137,12 +138,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<div className='status__action-bar-dropdown'>
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
<DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div>
</div>
);

View File

@ -6,6 +6,7 @@ import emojify from '../emoji';
import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
export default class StatusContent extends React.PureComponent {
@ -43,10 +44,11 @@ export default class StatusContent extends React.PureComponent {
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
link.setAttribute('title', link.href);
}
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
}
@ -59,7 +61,7 @@ export default class StatusContent extends React.PureComponent {
}
onMentionClick = (mention, e) => {
if (e.button === 0) {
if (this.context.router && e.button === 0) {
e.preventDefault();
this.context.router.history.push(`/accounts/${mention.get('id')}`);
}
@ -68,7 +70,7 @@ export default class StatusContent extends React.PureComponent {
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (e.button === 0) {
if (this.context.router && e.button === 0) {
e.preventDefault();
this.context.router.history.push(`/timelines/tag/${hashtag}`);
}
@ -120,6 +122,9 @@ export default class StatusContent extends React.PureComponent {
const content = { __html: emojify(status.get('content')) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
@ -141,7 +146,7 @@ export default class StatusContent extends React.PureComponent {
}
return (
<div className='status__content status__content--with-action' ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
@ -157,7 +162,7 @@ export default class StatusContent extends React.PureComponent {
return (
<div
ref={this.setRef}
className='status__content status__content--with-action'
className={classNames}
style={directionStyle}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}

View File

@ -14,6 +14,10 @@ const messages = defineMessages({
@injectIntl
export default class VideoPlayer extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
width: PropTypes.number,
@ -119,11 +123,15 @@ export default class VideoPlayer extends React.PureComponent {
</div>
);
let expandButton = (
<div className='status__video-player-expand'>
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
let expandButton = '';
if (this.context.router) {
expandButton = (
<div className='status__video-player-expand'>
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
}
let muteButton = '';
@ -138,7 +146,7 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -146,7 +154,7 @@ export default class VideoPlayer extends React.PureComponent {
);
} else {
return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState));
}
export default class TimelineContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<PublicTimeline />
</Provider>
</IntlProvider>
);
}
}

View File

@ -0,0 +1,76 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../../ui/containers/status_list_container';
import {
refreshPublicTimeline,
expandPublicTimeline,
} from '../../../actions/timelines';
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
});
@connect()
@injectIntl
export default class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
componentDidMount () {
const { dispatch } = this.props;
dispatch(refreshPublicTimeline());
this.polling = setInterval(() => {
dispatch(refreshPublicTimeline());
}, 3000);
}
componentWillUnmount () {
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
}
}
handleLoadMore = () => {
this.props.dispatch(expandPublicTimeline());
}
render () {
const { intl } = this.props;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='globe'
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
/>
<StatusListContainer
timelineId='public'
loadMore={this.handleLoadMore}
scrollKey='standalone_public_timeline'
trackScroll={false}
/>
</Column>
);
}
}

View File

@ -4,6 +4,9 @@ import { delegate } from 'rails-ujs';
import emojify from '../mastodon/emoji';
import { getLocale } from '../mastodon/locales';
import loadPolyfills from '../mastodon/load_polyfills';
import TimelineContainer from '../mastodon/containers/timeline_container';
import React from 'react';
import ReactDOM from 'react-dom';
require.context('../images/', true);
@ -36,6 +39,13 @@ function loaded() {
const datetime = new Date(content.getAttribute('datetime'));
content.textContent = relativeFormat.format(datetime);;
});
const mountNode = document.getElementById('mastodon-timeline');
if (mountNode !== null) {
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<TimelineContainer {...props} />, mountNode);
}
}
function main() {

View File

@ -116,10 +116,6 @@
.wrapper {
padding: 20px;
}
.features-list {
display: block;
}
}
}
@ -301,80 +297,438 @@
}
}
.features-list {
.features-list__row {
display: flex;
margin-bottom: 20px;
padding: 10px 0;
justify-content: space-between;
.features-list__column {
flex: 1 1 0;
&:first-child {
padding-top: 0;
}
ul {
list-style: none;
.visual {
flex: 0 0 auto;
display: flex;
align-items: center;
margin-left: 15px;
.fa {
display: block;
color: $ui-primary-color;
font-size: 48px;
}
}
li {
margin: 0;
.text {
font-size: 16px;
line-height: 30px;
color: lighten($ui-base-color, 26%);
h6 {
font-weight: 500;
color: $ui-primary-color;
}
}
}
.screenshot-with-signup {
display: flex;
margin-bottom: 20px;
.landing-page {
.header-wrapper {
padding-top: 15px;
background: $ui-base-color;
background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color);
position: relative;
.mascot {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
img {
display: block;
.mascot-container {
max-width: 800px;
margin: 0 auto;
max-width: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
}
.mascot {
position: absolute;
bottom: -14px;
width: auto;
height: auto;
left: 60px;
z-index: 3;
}
}
.simple_form,
.closed-registrations-message {
width: 300px;
flex: 0 0 auto;
background: rgba(darken($ui-base-color, 7%), 0.5);
padding: 14px;
border-radius: 4px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
p,
li {
font: inherit;
font-weight: inherit;
margin-bottom: 0;
}
.actions {
margin-bottom: 0;
.header {
line-height: 30px;
overflow: hidden;
.container {
display: flex;
justify-content: space-between;
}
.info {
text-align: center;
.hero {
margin-top: 50px;
align-items: center;
position: relative;
.floats {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
img {
position: absolute;
transition: all 0.1s linear;
animation-name: floating;
animation-duration: 1.7s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: linear;
z-index: 2;
}
.float-1 {
height: 170px;
right: -120px;
bottom: 0;
}
.float-2 {
height: 100px;
right: 210px;
bottom: 0;
animation-delay: 0.2s;
}
.float-3 {
height: 140px;
right: 110px;
top: -30px;
animation-delay: 0.1s;
}
}
.simple_form,
.closed-registrations-message {
background: darken($ui-base-color, 4%);
width: 280px;
padding: 15px 20px;
border-radius: 4px 4px 0 0;
line-height: initial;
position: relative;
z-index: 4;
.actions {
margin-bottom: 0;
button,
.button,
.block-button {
margin-bottom: 0;
}
}
}
.heading {
position: relative;
z-index: 4;
padding-bottom: 150px;
}
.closed-registrations-message {
min-height: 330px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
ul {
list-style: none;
margin: 0;
li {
display: inline-block;
vertical-align: bottom;
margin: 0;
&:first-child a {
padding-left: 0;
}
&:last-child a {
padding-right: 0;
}
}
}
.links {
position: relative;
z-index: 4;
a {
color: $ui-secondary-color;
display: flex;
justify-content: center;
align-items: center;
color: $ui-primary-color;
text-decoration: none;
padding: 12px 16px;
line-height: 32px;
font-family: 'mastodon-font-display', sans-serif;
font-weight: 500;
font-size: 14px;
&:hover {
color: $ui-secondary-color;
}
}
.brand {
a {
padding-left: 0;
color: $white;
}
img {
width: 32px;
height: 32px;
margin-right: 10px;
}
}
}
}
@media screen and (max-width: 625px) {
.mascot {
display: none;
.container {
width: 100%;
box-sizing: border-box;
max-width: 800px;
margin: 0 auto;
}
.wrapper {
max-width: 800px;
margin: 0 auto;
padding: 0;
}
.learn-more-cta {
background: darken($ui-base-color, 4%);
padding: 50px 0;
}
h3 {
font-family: 'mastodon-font-display', sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $ui-primary-color;
}
p {
font-size: 16px;
line-height: 30px;
color: lighten($ui-base-color, 26%);
}
.features {
padding: 50px 0;
.container {
display: flex;
}
}
#mastodon-timeline {
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: $primary-text-color;
width: 330px;
margin-right: 30px;
flex: 0 0 auto;
background: $ui-base-color;
overflow: hidden;
box-shadow: 0 0 6px rgba($black, 0.1);
.column {
padding: 0;
border-radius: 4px;
overflow: hidden;
height: 100%;
}
.simple_form,
.closed-registrations-message {
flex: auto;
.scrollable {
height: 400px;
}
p {
font-size: inherit;
line-height: inherit;
font-weight: inherit;
color: $primary-text-color;
a {
color: $ui-secondary-color;
text-decoration: none;
}
}
}
.about-mastodon {
max-width: 675px;
p {
margin-bottom: 20px;
}
.features-list {
margin-top: 20px;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 500;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: $ui-primary-color;
}
h1 {
font-family: 'mastodon-font-display', sans-serif;
font-size: 26px;
line-height: 30px;
margin-bottom: 0;
font-weight: 500;
color: $ui-secondary-color;
small {
font-family: 'mastodon-font-sans-serif', sans-serif;
display: block;
font-size: 18px;
font-weight: 400;
color: lighten($ui-base-color, 26%);
}
}
.footer-links {
padding-bottom: 50px;
text-align: right;
color: lighten($ui-base-color, 26%);
p {
font-size: 14px;
}
a {
color: inherit;
text-decoration: underline;
}
}
@media screen and (max-width: 800px) {
.container {
padding: 0 20px;
}
.header-wrapper .mascot {
left: 20px;
}
}
@media screen and (max-width: 689px) {
.header-wrapper .mascot {
display: none;
}
}
@media screen and (max-width: 675px) {
.header-wrapper {
padding-top: 0;
}
.header .container,
.features .container {
display: block;
}
.links {
padding-top: 15px;
background: darken($ui-base-color, 4%);
}
.header {
padding-top: 0;
.hero {
margin-top: 30px;
padding: 0;
.heading {
padding-bottom: 20px;
}
}
.floats {
display: none;
}
.heading,
.nav {
text-align: center;
}
.heading h1 {
padding: 30px 0;
}
.hero {
.simple_form,
.closed-registrations-message {
background: darken($ui-base-color, 8%);
width: 100%;
border-radius: 0;
box-sizing: border-box;
}
}
}
#mastodon-timeline {
height: 70vh;
width: 100%;
margin-bottom: 50px;
}
}
}
.closed-registrations-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
@keyframes floating {
from {
transform: translate(0, 0);
}
65% {
transform: translate(0, 4px);
}
to {
transform: translate(0, -0);
}
}

View File

@ -1,6 +1,6 @@
body {
font-family: 'mastodon-font-sans-serif', sans-serif;
background: $ui-base-color url('../images/background-photo.jpg');
background: $ui-base-color;
background-size: cover;
background-attachment: fixed;
font-size: 13px;
@ -22,6 +22,11 @@ body {
background: $ui-base-color;
}
&.about-body {
background: darken($ui-base-color, 8%);
padding-bottom: 0;
}
&.embed {
background: transparent;
margin: 0;

File diff suppressed because one or more lines are too long

View File

@ -42,8 +42,38 @@
cursor: default;
}
&.button-alternative {
font-size: 16px;
line-height: 36px;
height: auto;
color: $ui-base-color;
background: $ui-primary-color;
text-transform: none;
padding: 4px 16px;
&:active,
&:focus,
&:hover {
background-color: lighten($ui-primary-color, 4%);
}
}
&.button-secondary {
//
font-size: 16px;
line-height: 36px;
height: auto;
color: $ui-primary-color;
text-transform: none;
background: transparent;
padding: 3px 15px;
border: 1px solid $ui-primary-color;
&:active,
&:focus,
&:hover {
border-color: lighten($ui-primary-color, 4%);
color: lighten($ui-primary-color, 4%);
}
}
&.button--block {

View File

@ -10,52 +10,36 @@
}
.logo-container {
max-width: 400px;
margin: 100px auto;
margin-bottom: 0;
cursor: default;
margin-bottom: 50px;
@media screen and (max-width: 360px) {
margin: 30px auto;
}
h1 {
display: block;
text-align: center;
color: $primary-text-color;
font-size: 48px;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
img {
display: block;
margin: 20px auto;
width: 180px;
height: 180px;
width: 32px;
height: 32px;
margin-right: 10px;
}
a {
color: inherit;
display: flex;
justify-content: center;
align-items: center;
color: $primary-text-color;
text-decoration: none;
outline: 0;
img {
opacity: 0.8;
transition: opacity 0.8s ease;
}
&:hover {
img {
opacity: 1;
transition-duration: 0.2s;
}
}
}
small {
display: block;
font-size: 12px;
font-weight: 400;
font-family: 'mastodon-font-monospace', monospace;
padding: 12px 16px;
line-height: 32px;
font-family: 'mastodon-font-display', sans-serif;
font-weight: 500;
font-size: 14px;
}
}
}

View File

@ -7,3 +7,11 @@
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'mastodon-font-display';
src: local('Montserrat'),
url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}

View File

@ -24,6 +24,20 @@ code {
p.hint {
margin-bottom: 15px;
color: lighten($ui-base-color, 32%);
&.subtle-hint {
text-align: center;
font-size: 12px;
line-height: 18px;
margin-top: 15px;
margin-bottom: 0;
color: lighten($ui-base-color, 26%);
a {
color: $ui-primary-color;
}
}
}
strong {
@ -197,8 +211,6 @@ code {
&:active,
&:focus {
position: relative;
top: 1px;
background-color: darken($ui-highlight-color, 5%);
}
@ -219,6 +231,27 @@ code {
select {
font-size: 16px;
}
.input-with-append {
position: relative;
.input input {
padding-right: 127px;
}
.append {
position: absolute;
right: 0;
top: 0;
padding: 7px 4px;
padding-bottom: 9px;
font-size: 16px;
color: lighten($ui-base-color, 26%);
font-family: inherit;
pointer-events: none;
cursor: default;
}
}
}
.flash-message {
@ -240,7 +273,7 @@ code {
text-align: center;
a {
color: $primary-text-color;
color: $ui-primary-color;
text-decoration: none;
&:hover {

View File

@ -5,6 +5,7 @@ class InstancePresenter
:closed_registrations_message,
:site_contact_email,
:open_registrations,
:site_title,
:site_description,
:site_extended_description,
:site_terms,

View File

@ -5,32 +5,41 @@ class InitialStateSerializer < ActiveModel::Serializer
:media_attachments, :settings