* Add account media gallery view to web UI * Link media view from account dropdown * Adjust linkmaster
@@ -37,6 +37,14 @@ export const ACCOUNT_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_TIMELINE_EXPAND_REQUEST' | |||
export const ACCOUNT_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_TIMELINE_EXPAND_SUCCESS'; | |||
export const ACCOUNT_TIMELINE_EXPAND_FAIL = 'ACCOUNT_TIMELINE_EXPAND_FAIL'; | |||
export const ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST'; | |||
export const ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS'; | |||
export const ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL = 'ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL'; | |||
export const ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST'; | |||
export const ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS'; | |||
export const ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL'; | |||
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; | |||
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; | |||
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL'; | |||
@@ -96,17 +104,17 @@ export function fetchAccountTimeline(id, replace = false) { | |||
const ids = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()); | |||
const newestId = ids.size > 0 ? ids.first() : null; | |||
let params = ''; | |||
let params = {}; | |||
let skipLoading = false; | |||
if (newestId !== null && !replace) { | |||
params = `?since_id=${newestId}`; | |||
params.since_id = newestId; | |||
skipLoading = true; | |||
} | |||
dispatch(fetchAccountTimelineRequest(id, skipLoading)); | |||
api(getState).get(`/api/v1/accounts/${id}/statuses${params}`).then(response => { | |||
api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => { | |||
dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading)); | |||
}).catch(error => { | |||
dispatch(fetchAccountTimelineFail(id, error, skipLoading)); | |||
@@ -114,6 +122,29 @@ export function fetchAccountTimeline(id, replace = false) { | |||
}; | |||
}; | |||
export function fetchAccountMediaTimeline(id, replace = false) { | |||
return (dispatch, getState) => { | |||
const ids = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()); | |||
const newestId = ids.size > 0 ? ids.first() : null; | |||
let params = { only_media: 'true', limit: 12 }; | |||
let skipLoading = false; | |||
if (newestId !== null && !replace) { | |||
params.since_id = newestId; | |||
skipLoading = true; | |||
} | |||
dispatch(fetchAccountMediaTimelineRequest(id, skipLoading)); | |||
api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => { | |||
dispatch(fetchAccountMediaTimelineSuccess(id, response.data, replace, skipLoading)); | |||
}).catch(error => { | |||
dispatch(fetchAccountMediaTimelineFail(id, error, skipLoading)); | |||
}); | |||
}; | |||
}; | |||
export function expandAccountTimeline(id) { | |||
return (dispatch, getState) => { | |||
const lastId = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()).last(); | |||
@@ -134,6 +165,27 @@ export function expandAccountTimeline(id) { | |||
}; | |||
}; | |||
export function expandAccountMediaTimeline(id) { | |||
return (dispatch, getState) => { | |||
const lastId = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()).last(); | |||
dispatch(expandAccountMediaTimelineRequest(id)); | |||
api(getState).get(`/api/v1/accounts/${id}/statuses`, { | |||
params: { | |||
limit: 12, | |||
only_media: 'true', | |||
max_id: lastId | |||
} | |||
}).then(response => { | |||
const next = getLinks(response).refs.find(link => link.rel === 'next'); | |||
dispatch(expandAccountMediaTimelineSuccess(id, response.data, next)); | |||
}).catch(error => { | |||
dispatch(expandAccountMediaTimelineFail(id, error)); | |||
}); | |||
}; | |||
}; | |||
export function fetchAccountRequest(id) { | |||
return { | |||
type: ACCOUNT_FETCH_REQUEST, | |||
@@ -251,6 +303,34 @@ export function fetchAccountTimelineFail(id, error, skipLoading) { | |||
}; | |||
}; | |||
export function fetchAccountMediaTimelineRequest(id, skipLoading) { | |||
return { | |||
type: ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST, | |||
id, | |||
skipLoading | |||
}; | |||
}; | |||
export function fetchAccountMediaTimelineSuccess(id, statuses, replace, skipLoading) { | |||
return { | |||
type: ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, | |||
id, | |||
statuses, | |||
replace, | |||
skipLoading | |||
}; | |||
}; | |||
export function fetchAccountMediaTimelineFail(id, error, skipLoading) { | |||
return { | |||
type: ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL, | |||
id, | |||
error, | |||
skipLoading, | |||
skipAlert: error.response.status === 404 | |||
}; | |||
}; | |||
export function expandAccountTimelineRequest(id) { | |||
return { | |||
type: ACCOUNT_TIMELINE_EXPAND_REQUEST, | |||
@@ -275,6 +355,30 @@ export function expandAccountTimelineFail(id, error) { | |||
}; | |||
}; | |||
export function expandAccountMediaTimelineRequest(id) { | |||
return { | |||
type: ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST, | |||
id | |||
}; | |||
}; | |||
export function expandAccountMediaTimelineSuccess(id, statuses, next) { | |||
return { | |||
type: ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, | |||
id, | |||
statuses, | |||
next | |||
}; | |||
}; | |||
export function expandAccountMediaTimelineFail(id, error) { | |||
return { | |||
type: ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL, | |||
id, | |||
error | |||
}; | |||
}; | |||
export function blockAccount(id) { | |||
return (dispatch, getState) => { | |||
dispatch(blockAccountRequest(id)); | |||
@@ -4,6 +4,10 @@ import PropTypes from 'prop-types'; | |||
class DropdownMenu extends React.PureComponent { | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
static propTypes = { | |||
icon: PropTypes.string.isRequired, | |||
items: PropTypes.array.isRequired, | |||
@@ -26,13 +30,17 @@ class DropdownMenu extends React.PureComponent { | |||
handleClick = (e) => { | |||
const i = Number(e.currentTarget.getAttribute('data-index')); | |||
const { action } = this.props.items[i]; | |||
const { action, to } = this.props.items[i]; | |||
e.preventDefault(); | |||
if (typeof action === 'function') { | |||
e.preventDefault(); | |||
action(); | |||
this.dropdown.hide(); | |||
} else if (to) { | |||
this.context.router.push(to); | |||
} | |||
this.dropdown.hide(); | |||
} | |||
renderItem = (item, i) => { | |||
@@ -26,6 +26,7 @@ import GettingStarted from '../features/getting_started'; | |||
import PublicTimeline from '../features/public_timeline'; | |||
import CommunityTimeline from '../features/community_timeline'; | |||
import AccountTimeline from '../features/account_timeline'; | |||
import AccountGallery from '../features/account_gallery'; | |||
import HomeTimeline from '../features/home_timeline'; | |||
import Compose from '../features/compose'; | |||
import Followers from '../features/followers'; | |||
@@ -204,6 +205,7 @@ class Mastodon extends React.PureComponent { | |||
<Route path='accounts/:accountId' component={AccountTimeline} /> | |||
<Route path='accounts/:accountId/followers' component={Followers} /> | |||
<Route path='accounts/:accountId/following' component={Following} /> | |||
<Route path='accounts/:accountId/media' component={AccountGallery} /> | |||
<Route path='follow_requests' component={FollowRequests} /> | |||
<Route path='blocks' component={Blocks} /> | |||
@@ -15,6 +15,7 @@ const messages = defineMessages({ | |||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, | |||
follow: { id: 'account.follow', defaultMessage: 'Follow' }, | |||
report: { id: 'account.report', defaultMessage: 'Report @{name}' }, | |||
media: { id: 'account.media', defaultMessage: 'Media' }, | |||
disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' }, | |||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, | |||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, | |||
@@ -43,6 +44,8 @@ class ActionBar extends React.PureComponent { | |||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); | |||
menu.push(null); | |||
menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); | |||
menu.push(null); | |||
if (account.get('id') === me) { | |||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); | |||
@@ -0,0 +1,39 @@ | |||
import React from 'react'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import Permalink from '../../../components/permalink'; | |||
class MediaItem extends ImmutablePureComponent { | |||
static propTypes = { | |||
media: ImmutablePropTypes.map.isRequired | |||
}; | |||
render () { | |||
const { media } = this.props; | |||
const status = media.get('status'); | |||
let content, style; | |||
if (media.get('type') === 'gifv') { | |||
content = <span className='media-gallery__gifv__label'>GIF</span>; | |||
} | |||
if (!status.get('sensitive')) { | |||
style = { backgroundImage: `url(${media.get('preview_url')})` }; | |||
} | |||
return ( | |||
<div className='account-gallery__item'> | |||
<Permalink | |||
to={`/statuses/${status.get('id')}`} | |||
href={status.get('url')} | |||
style={style}> | |||
{content} | |||
</Permalink> | |||
</div> | |||
); | |||
} | |||
} | |||
export default MediaItem; |
@@ -0,0 +1,99 @@ | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import { | |||
fetchAccount, | |||
fetchAccountMediaTimeline, | |||
expandAccountMediaTimeline | |||
} from '../../actions/accounts'; | |||
import LoadingIndicator from '../../components/loading_indicator'; | |||
import Column from '../ui/components/column'; | |||
import ColumnBackButton from '../../components/column_back_button'; | |||
import Immutable from 'immutable'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import { getAccountGallery } from '../../selectors'; | |||
import MediaItem from './components/media_item'; | |||
import HeaderContainer from '../account_timeline/containers/header_container'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { ScrollContainer } from 'react-router-scroll'; | |||
const mapStateToProps = (state, props) => ({ | |||
medias: getAccountGallery(state, Number(props.params.accountId)), | |||
isLoading: state.getIn(['timelines', 'accounts_media_timelines', Number(props.params.accountId), 'isLoading']), | |||
hasMore: !!state.getIn(['timelines', 'accounts_media_timelines', Number(props.params.accountId), 'next']), | |||
autoPlayGif: state.getIn(['meta', 'auto_play_gif']), | |||
}); | |||
class AccountGallery extends ImmutablePureComponent { | |||
static propTypes = { | |||
params: PropTypes.object.isRequired, | |||
dispatch: PropTypes.func.isRequired, | |||
medias: ImmutablePropTypes.list.isRequired, | |||
isLoading: PropTypes.bool, | |||
hasMore: PropTypes.bool, | |||
autoPlayGif: PropTypes.bool, | |||
}; | |||
componentDidMount () { | |||
this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); | |||
this.props.dispatch(fetchAccountMediaTimeline(Number(this.props.params.accountId))); | |||
} | |||
componentWillReceiveProps (nextProps) { | |||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { | |||
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); | |||
this.props.dispatch(fetchAccountMediaTimeline(Number(this.props.params.accountId))); | |||
} | |||
} | |||
handleScroll = (e) => { | |||
const { scrollTop, scrollHeight, clientHeight } = e.target; | |||
if (scrollTop === scrollHeight - clientHeight) { | |||
this.props.dispatch(expandAccountMediaTimeline(Number(this.props.params.accountId))); | |||
} | |||
} | |||
render () { | |||
const { medias, autoPlayGif, isLoading } = this.props; | |||
if (!medias && isLoading) { | |||
return ( | |||
<Column> | |||
<LoadingIndicator /> | |||
</Column> | |||
); | |||
} | |||
return ( | |||
<Column> | |||
<ColumnBackButton /> | |||
<ScrollContainer scrollKey='account_gallery'> | |||
<div className='scrollable' onScroll={this.handleScroll}> | |||
<HeaderContainer accountId={this.props.params.accountId} /> | |||
<div className='account-section-headline'> | |||
<FormattedMessage id='account.media' defaultMessage='Media' /> | |||
</div> | |||
<div className='account-gallery__container'> | |||
{medias.map(media => | |||
<MediaItem | |||
key={media.get('id')} | |||
media={media} | |||
autoPlayGif={autoPlayGif} | |||
/> | |||
)} | |||
</div> | |||
</div> | |||
</ScrollContainer> | |||
</Column> | |||
); | |||
} | |||
} | |||
export default connect(mapStateToProps)(AccountGallery); |
@@ -28,16 +28,11 @@ class Blocks extends ImmutablePureComponent { | |||
intl: PropTypes.object.isRequired | |||
}; | |||
constructor (props, context) { | |||
super(props, context); | |||
this.handleScroll = this.handleScroll.bind(this); | |||
} | |||
componentWillMount () { | |||
this.props.dispatch(fetchBlocks()); | |||
} | |||
handleScroll (e) { | |||
handleScroll = (e) => { | |||
const { scrollTop, scrollHeight, clientHeight } = e.target; | |||
if (scrollTop === scrollHeight - clientHeight) { | |||
@@ -26,7 +26,7 @@ class Favourites extends ImmutablePureComponent { | |||
this.props.dispatch(fetchFavourites(Number(this.props.params.statusId))); | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
componentWillReceiveProps (nextProps) { | |||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { | |||
this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId))); | |||
} | |||
@@ -21,16 +21,11 @@ const mapStateToProps = state => ({ | |||
class Mutes extends ImmutablePureComponent { | |||
constructor (props, context) { | |||
super(props, context); | |||
this.handleScroll = this.handleScroll.bind(this); | |||
} | |||
componentWillMount () { | |||
this.props.dispatch(fetchMutes()); | |||
} | |||
handleScroll (e) { | |||
handleScroll = (e) => { | |||
const { scrollTop, scrollHeight, clientHeight } = e.target; | |||
if (scrollTop === scrollHeight - clientHeight) { | |||
@@ -7,7 +7,7 @@ | |||
"account.followers": "フォロワー", | |||
"account.follows": "フォロー", | |||
"account.follows_you": "フォローされています", | |||
"account.media": "Media", | |||
"account.media": "メディア", | |||
"account.mention": "返信", | |||
"account.mute": "ミュート", | |||
"account.posts": "投稿", | |||
@@ -23,6 +23,8 @@ import { | |||
import { | |||
ACCOUNT_TIMELINE_FETCH_SUCCESS, | |||
ACCOUNT_TIMELINE_EXPAND_SUCCESS, | |||
ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, | |||
ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, | |||
ACCOUNT_BLOCK_SUCCESS | |||
} from '../actions/accounts'; | |||
import { | |||
@@ -113,6 +115,8 @@ export default function statuses(state = initialState, action) { | |||
case TIMELINE_EXPAND_SUCCESS: | |||
case ACCOUNT_TIMELINE_FETCH_SUCCESS: | |||
case ACCOUNT_TIMELINE_EXPAND_SUCCESS: | |||
case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS: | |||
case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS: | |||
case CONTEXT_FETCH_SUCCESS: | |||
case NOTIFICATIONS_REFRESH_SUCCESS: | |||
case NOTIFICATIONS_EXPAND_SUCCESS: | |||
@@ -24,6 +24,12 @@ import { | |||
ACCOUNT_TIMELINE_EXPAND_REQUEST, | |||
ACCOUNT_TIMELINE_EXPAND_SUCCESS, | |||
ACCOUNT_TIMELINE_EXPAND_FAIL, | |||
ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST, | |||
ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, | |||
ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL, | |||
ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST, | |||
ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, | |||
ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL, | |||
ACCOUNT_BLOCK_SUCCESS, | |||
ACCOUNT_MUTE_SUCCESS | |||
} from '../actions/accounts'; | |||
@@ -79,6 +85,7 @@ const initialState = Immutable.Map({ | |||
}), | |||
accounts_timelines: Immutable.Map(), | |||
accounts_media_timelines: Immutable.Map(), | |||
ancestors: Immutable.Map(), | |||
descendants: Immutable.Map() | |||
}); | |||
@@ -148,6 +155,20 @@ const normalizeAccountTimeline = (state, accountId, statuses, replace = false) = | |||
.update('items', Immutable.List(), list => (replace ? ids : ids.concat(list)))); | |||
}; | |||
const normalizeAccountMediaTimeline = (state, accountId, statuses, next) => { | |||
let ids = Immutable.List(); | |||
statuses.forEach((status, i) => { | |||
state = normalizeStatus(state, status); | |||
ids = ids.set(i, status.get('id')); | |||
}); | |||
return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map | |||
.set('isLoading', false) | |||
.set('next', next) | |||
.update('items', Immutable.List(), list => ids.concat(list))); | |||
}; | |||
const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { | |||
let moreIds = Immutable.List([]); | |||
@@ -162,6 +183,20 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { | |||
.update('items', list => list.concat(moreIds))); | |||
}; | |||
const appendNormalizedAccountMediaTimeline = (state, accountId, statuses, next) => { | |||
let moreIds = Immutable.List([]); | |||
statuses.forEach((status, i) => { | |||
state = normalizeStatus(state, status); | |||
moreIds = moreIds.set(i, status.get('id')); | |||
}); | |||
return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map | |||
.set('isLoading', false) | |||
.set('next', next) | |||
.update('items', list => list.concat(moreIds))); | |||
}; | |||
const updateTimeline = (state, timeline, status, references) => { | |||
const top = state.getIn([timeline, 'top']); | |||
@@ -205,6 +240,7 @@ const deleteStatus = (state, id, accountId, references, reblogOf) => { | |||
// Remove references from account timelines | |||
state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); | |||
state = state.updateIn(['accounts_media_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); | |||
// Remove references from context | |||
state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { | |||
@@ -302,6 +338,16 @@ export default function timelines(state = initialState, action) { | |||
return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); | |||
case ACCOUNT_TIMELINE_EXPAND_SUCCESS: | |||
return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); | |||
case ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST: | |||
case ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST: | |||
return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true)); | |||
case ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL: | |||
case ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL: | |||
return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false)); | |||
case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS: | |||
return normalizeAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); | |||
case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS: | |||
return appendNormalizedAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); | |||
case ACCOUNT_BLOCK_SUCCESS: | |||
case ACCOUNT_MUTE_SUCCESS: | |||
return filterTimelines(state, action.relationship, action.statuses); | |||
@@ -74,3 +74,17 @@ export const makeGetNotification = () => { | |||
return base.set('account', account); | |||
}); | |||
}; | |||
export const getAccountGallery = createSelector([ | |||
(state, id) => state.getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()), | |||
state => state.get('statuses'), | |||
], (statusIds, statuses) => { | |||
let medias = Immutable.List(); | |||
statusIds.forEach(statusId => { | |||
const status = statuses.get(statusId); | |||
medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status))); | |||
}); | |||
return medias; | |||
}); |
@@ -3427,3 +3427,55 @@ button.icon-button.active i.fa-retweet { | |||
transform: translate(-50%, -50%); | |||
} | |||
/* End Video Player */ | |||
.account-gallery__container { | |||
margin: -2px; | |||
padding: 4px; | |||
} | |||
.account-gallery__item { | |||
float: left; | |||
width: 96px; | |||
height: 96px; | |||
margin: 2px; | |||
a { | |||
display: block; | |||
width: 100%; | |||
height: 100%; | |||
background-color: $base-overlay-background; | |||
background-size: cover; | |||
background-position: center; | |||
position: relative; | |||
} | |||
} | |||
.account-section-headline { | |||
color: lighten($ui-base-color, 26%); | |||
background: lighten($ui-base-color, 2%); | |||
border-bottom: 1px solid lighten($ui-base-color, 4%); | |||
padding: 15px 10px; | |||
font-size: 14px; | |||
font-weight: 500; | |||
position: relative; | |||
cursor: default; | |||
&::before, | |||
&::after { | |||
display: block; | |||
content: ""; | |||
position: absolute; | |||
bottom: 0; | |||
left: 18px; | |||
width: 0; | |||
height: 0; | |||
border-style: solid; | |||
border-width: 0 10px 10px; | |||
border-color: transparent transparent lighten($ui-base-color, 4%); | |||
} | |||
&::after { | |||
bottom: -1px; | |||
border-color: transparent transparent $ui-base-color; | |||
} | |||
} |