diff --git a/app/javascript/images/screen_federation.svg b/app/javascript/images/screen_federation.svg
new file mode 100644
index 000000000..7019a7356
--- /dev/null
+++ b/app/javascript/images/screen_federation.svg
@@ -0,0 +1 @@
+
diff --git a/app/javascript/images/screen_hello.svg b/app/javascript/images/screen_hello.svg
new file mode 100644
index 000000000..7bcdd0afd
--- /dev/null
+++ b/app/javascript/images/screen_hello.svg
@@ -0,0 +1 @@
+
diff --git a/app/javascript/images/screen_interactions.svg b/app/javascript/images/screen_interactions.svg
new file mode 100644
index 000000000..41873371a
--- /dev/null
+++ b/app/javascript/images/screen_interactions.svg
@@ -0,0 +1 @@
+
diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js
index a161c50ef..a1dd3a731 100644
--- a/app/javascript/mastodon/actions/onboarding.js
+++ b/app/javascript/mastodon/actions/onboarding.js
@@ -1,14 +1,8 @@
-import { openModal } from './modal';
import { changeSetting, saveSettings } from './settings';
-export function showOnboardingOnce() {
- return (dispatch, getState) => {
- const alreadySeen = getState().getIn(['settings', 'onboarded']);
+export const INTRODUCTION_VERSION = 20181216044202;
- if (!alreadySeen) {
- dispatch(openModal('ONBOARDING'));
- dispatch(changeSetting(['onboarded'], true));
- dispatch(saveSettings());
- }
- };
+export const closeOnboarding = () => dispatch => {
+ dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
+ dispatch(saveSettings());
};
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index b2b0265aa..2912540a0 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -1,11 +1,12 @@
import React from 'react';
-import { Provider } from 'react-redux';
+import { Provider, connect } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
-import { showOnboardingOnce } from '../actions/onboarding';
+import { INTRODUCTION_VERSION } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
+import Introduction from '../features/introduction';
import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
@@ -18,11 +19,39 @@ addLocaleData(localeData);
export const store = configureStore();
const hydrateAction = hydrateStore(initialState);
-store.dispatch(hydrateAction);
-// load custom emojis
+store.dispatch(hydrateAction);
store.dispatch(fetchCustomEmojis());
+const mapStateToProps = state => ({
+ showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
+});
+
+@connect(mapStateToProps)
+class MastodonMount extends React.PureComponent {
+
+ static propTypes = {
+ showIntroduction: PropTypes.bool,
+ };
+
+ render () {
+ const { showIntroduction } = this.props;
+
+ if (showIntroduction) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+ }
+
+}
+
export default class Mastodon extends React.PureComponent {
static propTypes = {
@@ -31,14 +60,6 @@ export default class Mastodon extends React.PureComponent {
componentDidMount() {
this.disconnect = store.dispatch(connectUserStream());
-
- // Desktop notifications
- // Ask after 1 minute
- if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
- window.setTimeout(() => Notification.requestPermission(), 60 * 1000);
- }
-
- store.dispatch(showOnboardingOnce());
}
componentWillUnmount () {
@@ -54,11 +75,7 @@ export default class Mastodon extends React.PureComponent {
return (
-
-
-
-
-
+
);
diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js
new file mode 100644
index 000000000..6e0617f72
--- /dev/null
+++ b/app/javascript/mastodon/features/introduction/index.js
@@ -0,0 +1,196 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactSwipeableViews from 'react-swipeable-views';
+import classNames from 'classnames';
+import { connect } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import { closeOnboarding } from '../../actions/onboarding';
+import screenHello from '../../../images/screen_hello.svg';
+import screenFederation from '../../../images/screen_federation.svg';
+import screenInteractions from '../../../images/screen_interactions.svg';
+import logoTransparent from '../../../images/logo_transparent.svg';
+
+const FrameWelcome = ({ domain, onNext }) => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+FrameWelcome.propTypes = {
+ domain: PropTypes.string.isRequired,
+ onNext: PropTypes.func.isRequired,
+};
+
+const FrameFederation = ({ onNext }) => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+FrameFederation.propTypes = {
+ onNext: PropTypes.func.isRequired,
+};
+
+const FrameInteractions = ({ onNext }) => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+FrameInteractions.propTypes = {
+ onNext: PropTypes.func.isRequired,
+};
+
+@connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
+export default class Introduction extends React.PureComponent {
+
+ static propTypes = {
+ domain: PropTypes.string.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ };
+
+ state = {
+ currentIndex: 0,
+ };
+
+ componentWillMount () {
+ this.pages = [
+ ,
+ ,
+ ,
+ ];
+ }
+
+ componentDidMount() {
+ window.addEventListener('keyup', this.handleKeyUp);
+ }
+
+ componentWillUnmount() {
+ window.addEventListener('keyup', this.handleKeyUp);
+ }
+
+ handleDot = (e) => {
+ const i = Number(e.currentTarget.getAttribute('data-index'));
+ e.preventDefault();
+ this.setState({ currentIndex: i });
+ }
+
+ handlePrev = () => {
+ this.setState(({ currentIndex }) => ({
+ currentIndex: Math.max(0, currentIndex - 1),
+ }));
+ }
+
+ handleNext = () => {
+ const { pages } = this;
+
+ this.setState(({ currentIndex }) => ({
+ currentIndex: Math.min(currentIndex + 1, pages.length - 1),
+ }));
+ }
+
+ handleSwipe = (index) => {
+ this.setState({ currentIndex: index });
+ }
+
+ handleFinish = () => {
+ this.props.dispatch(closeOnboarding());
+ }
+
+ handleKeyUp = ({ key }) => {
+ switch (key) {
+ case 'ArrowLeft':
+ this.handlePrev();
+ break;
+ case 'ArrowRight':
+ this.handleNext();
+ break;
+ }
+ }
+
+ render () {
+ const { currentIndex } = this.state;
+ const { pages } = this;
+
+ return (
+
+
+ {pages.map((page, i) => (
+ {page}
+ ))}
+
+
+
+ {pages.map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index b3b1ea862..cc2ab6c8c 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -11,7 +11,6 @@ import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal';
import {
- OnboardingModal,
MuteModal,
ReportModal,
EmbedModal,
@@ -21,7 +20,6 @@ import {
const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
- 'ONBOARDING': OnboardingModal,
'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
deleted file mode 100644
index 4a5b249c9..000000000
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ /dev/null
@@ -1,324 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ReactSwipeableViews from 'react-swipeable-views';
-import classNames from 'classnames';
-import Permalink from '../../../components/permalink';
-import ComposeForm from '../../compose/components/compose_form';
-import Search from '../../compose/components/search';
-import NavigationBar from '../../compose/components/navigation_bar';
-import ColumnHeader from './column_header';
-import { List as ImmutableList } from 'immutable';
-import { me } from '../../../initial_state';
-
-const noop = () => { };
-
-const messages = defineMessages({
- home_title: { id: 'column.home', defaultMessage: 'Home' },
- notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
- local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
- federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' },
-});
-
-const PageOne = ({ acct, domain }) => (
-
-
-
-
-
-
-
-
-
-
- @{acct}@{domain}
-
-
-
-
-
-
-);
-
-PageOne.propTypes = {
- acct: PropTypes.string.isRequired,
- domain: PropTypes.string.isRequired,
-};
-
-const PageTwo = ({ myAccount }) => (
-
-);
-
-PageTwo.propTypes = {
- myAccount: ImmutablePropTypes.map.isRequired,
-};
-
-const PageThree = ({ myAccount }) => (
-
-
-
-
#illustration, introductions: #introductions }} />
-
-
-);
-
-PageThree.propTypes = {
- myAccount: ImmutablePropTypes.map.isRequired,
-};
-
-const PageFour = ({ domain, intl }) => (
-
-);
-
-PageFour.propTypes = {
- domain: PropTypes.string.isRequired,
- intl: PropTypes.object.isRequired,
-};
-
-const PageSix = ({ admin, domain }) => {
- let adminSection = '';
-
- if (admin) {
- adminSection = (
-
- @{admin.get('acct')} }} />
-
- }} />
-
- );
- }
-
- return (
-
-
- {adminSection}
-
GitHub }} />
-
}} />
-
-
- );
-};
-
-PageSix.propTypes = {
- admin: ImmutablePropTypes.map,
- domain: PropTypes.string.isRequired,
-};
-
-const mapStateToProps = state => ({
- myAccount: state.getIn(['accounts', me]),
- admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
- domain: state.getIn(['meta', 'domain']),
-});
-
-export default @connect(mapStateToProps)
-@injectIntl
-class OnboardingModal extends React.PureComponent {
-
- static propTypes = {
- onClose: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- myAccount: ImmutablePropTypes.map.isRequired,
- domain: PropTypes.string.isRequired,
- admin: ImmutablePropTypes.map,
- };
-
- state = {
- currentIndex: 0,
- };
-
- componentWillMount() {
- const { myAccount, admin, domain, intl } = this.props;
- this.pages = [
- ,
- ,
- ,
- ,
- ,
- ];
- };
-
- componentDidMount() {
- window.addEventListener('keyup', this.handleKeyUp);
- }
-
- componentWillUnmount() {
- window.addEventListener('keyup', this.handleKeyUp);
- }
-
- handleSkip = (e) => {
- e.preventDefault();
- this.props.onClose();
- }
-
- handleDot = (e) => {
- const i = Number(e.currentTarget.getAttribute('data-index'));
- e.preventDefault();
- this.setState({ currentIndex: i });
- }
-
- handlePrev = () => {
- this.setState(({ currentIndex }) => ({
- currentIndex: Math.max(0, currentIndex - 1),
- }));
- }
-
- handleNext = () => {
- const { pages } = this;
- this.setState(({ currentIndex }) => ({
- currentIndex: Math.min(currentIndex + 1, pages.length - 1),
- }));
- }
-
- handleSwipe = (index) => {
- this.setState({ currentIndex: index });
- }
-
- handleKeyUp = ({ key }) => {
- switch (key) {
- case 'ArrowLeft':
- this.handlePrev();
- break;
- case 'ArrowRight':
- this.handleNext();
- break;
- }
- }
-
- handleClose = () => {
- this.props.onClose();
- }
-
- render () {
- const { pages } = this;
- const { currentIndex } = this.state;
- const hasMore = currentIndex < pages.length - 1;
-
- const nextOrDoneBtn = hasMore ? (
-
- ) : (
-
- );
-
- return (
-
-
- {pages.map((page, i) => {
- const className = classNames('onboarding-modal__page__wrapper', `onboarding-modal__page__wrapper-${i}`, {
- 'onboarding-modal__page__wrapper--active': i === currentIndex,
- });
-
- return (
- {page}
- );
- })}
-
-
-
-
-
-
-
-
- {pages.map((_, i) => {
- const className = classNames('onboarding-modal__dot', {
- active: i === currentIndex,
- });
-
- return (
-
- );
- })}
-
-
-
- {nextOrDoneBtn}
-
-
-
- );
- }
-
-}
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 662375a76..e11235a81 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -294,6 +294,7 @@ class UI extends React.PureComponent {
componentWillMount () {
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
+
document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false);
document.addEventListener('drop', this.handleDrop, false);
@@ -304,8 +305,13 @@ class UI extends React.PureComponent {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
+ if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
+ window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
+ }
+
this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications());
+
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
}
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 2a15c052f..235fd2a07 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -102,10 +102,6 @@ export function Mutes () {
return import(/* webpackChunkName: "features/mutes" */'../../mutes');
}
-export function OnboardingModal () {
- return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
-}
-
export function MuteModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
}
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 0990a4f25..4bce74187 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -16,6 +16,7 @@
@import 'mastodon/stream_entries';
@import 'mastodon/boost';
@import 'mastodon/components';
+@import 'mastodon/introduction';
@import 'mastodon/modal';
@import 'mastodon/emoji_picker';
@import 'mastodon/about';
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 1c1b8c506..d2b3baaf0 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3835,25 +3835,6 @@ a.status-card.compact:hover {
flex-direction: column;
}
-.onboarding-modal__pager {
- height: 80vh;
- width: 80vw;
- max-width: 520px;
- max-height: 470px;
-
- .react-swipeable-view-container > div {
- width: 100%;
- height: 100%;
- box-sizing: border-box;
- display: none;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- display: flex;
- user-select: text;
- }
-}
-
.error-modal__body {
height: 80vh;
width: 80vw;
@@ -3887,22 +3868,6 @@ a.status-card.compact:hover {
text-align: center;
}
-@media screen and (max-width: 550px) {
- .onboarding-modal {
- width: 100%;
- height: 100%;
- border-radius: 0;
- }
-
- .onboarding-modal__pager {
- width: 100%;
- height: auto;
- max-width: none;
- max-height: none;
- flex: 1 1 auto;
- }
-}
-
.onboarding-modal__paginator,
.error-modal__footer {
flex: 0 0 auto;
@@ -3951,124 +3916,6 @@ a.status-card.compact:hover {
justify-content: center;
}
-.onboarding-modal__dots {
- flex: 1 1 auto;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.onboarding-modal__dot {
- width: 14px;
- height: 14px;
- border-radius: 14px;
- background: darken($ui-secondary-color, 16%);
- margin: 0 3px;
- cursor: pointer;
-
- &:hover {
- background: darken($ui-secondary-color, 18%);
- }
-
- &.active {
- cursor: default;
- background: darken($ui-secondary-color, 24%);
- }
-}
-
-.onboarding-modal__page__wrapper {
- pointer-events: none;
- padding: 25px;
- padding-bottom: 0;
-
- &.onboarding-modal__page__wrapper--active {
- pointer-events: auto;
- }
-}
-
-.onboarding-modal__page {
- cursor: default;
- line-height: 21px;
-
- h1 {
- font-size: 18px;
- font-weight: 500;
- color: $inverted-text-color;
- margin-bottom: 20px;
- }
-
- a {
- color: $highlight-text-color;
-
- &:hover,
- &:focus,
- &:active {
- color: lighten($highlight-text-color, 4%);
- }
- }
-
- .navigation-bar a {
- color: inherit;
- }
-
- p {
- font-size: 16px;
- color: $lighter-text-color;
- margin-top: 10px;
- margin-bottom: 10px;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- strong {
- font-weight: 500;
- background: $ui-base-color;
- color: $secondary-text-color;
- border-radius: 4px;
- font-size: 14px;
- padding: 3px 6px;
-
- @each $lang in $cjk-langs {
- &:lang(#{$lang}) {
- font-weight: 700;
- }
- }
- }
- }
-}
-
-.onboarding-modal__page__wrapper-0 {
- background: url('../images/elephant_ui_greeting.svg') no-repeat left bottom / auto 250px;
- height: 100%;
- padding: 0;
-}
-
-.onboarding-modal__page-one {
- &__lead {
- padding: 65px;
- padding-top: 45px;
- padding-bottom: 0;
- margin-bottom: 10px;
-
- h1 {
- font-size: 26px;
- line-height: 36px;
- margin-bottom: 8px;
- }
-
- p {
- margin-bottom: 0;
- }
- }
-
- &__extra {
- padding-right: 65px;
- padding-left: 185px;
- text-align: center;
- }
-}
-
.display-case {
text-align: center;
font-size: 15px;
@@ -4091,92 +3938,6 @@ a.status-card.compact:hover {
}
}
-.onboarding-modal__page-two,
-.onboarding-modal__page-three,
-.onboarding-modal__page-four,
-.onboarding-modal__page-five {
- p {
- text-align: left;
- }
-
- .figure {
- background: darken($ui-base-color, 8%);
- color: $secondary-text-color;
- margin-bottom: 20px;
- border-radius: 4px;
- padding: 10px;
- text-align: center;
- font-size: 14px;
- box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.3);
-
- .onboarding-modal__image {
- border-radius: 4px;
- margin-bottom: 10px;
- }
-
- &.non-interactive {
- pointer-events: none;
- text-align: left;
- }
- }
-}
-
-.onboarding-modal__page-four__columns {
- .row {
- display: flex;
- margin-bottom: 20px;
-
- & > div {
- flex: 1 1 0;
- margin: 0 10px;
-
- &:first-child {
- margin-left: 0;
- }
-
- &:last-child {
- margin-right: 0;
- }
-
- p {
- text-align: center;
- }
- }
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- .column-header {
- color: $primary-text-color;
- }
-}
-
-@media screen and (max-width: 320px) and (max-height: 600px) {
- .onboarding-modal__page p {
- font-size: 14px;
- line-height: 20px;
- }
-
- .onboarding-modal__page-two .figure,
- .onboarding-modal__page-three .figure,
- .onboarding-modal__page-four .figure,
- .onboarding-modal__page-five .figure {
- font-size: 12px;
- margin-bottom: 10px;
- }
-
- .onboarding-modal__page-four__columns .row {
- margin-bottom: 10px;
- }
-
- .onboarding-modal__page-four__columns .column-header {
- padding: 5px;
- font-size: 12px;
- }
-}
-
.onboard-sliders {
display: inline-block;
max-width: 30px;
diff --git a/app/javascript/styles/mastodon/introduction.scss b/app/javascript/styles/mastodon/introduction.scss
new file mode 100644
index 000000000..222d8f60e
--- /dev/null
+++ b/app/javascript/styles/mastodon/introduction.scss
@@ -0,0 +1,153 @@
+.introduction {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ @media screen and (max-width: 920px) {
+ background: darken($ui-base-color, 8%);
+ display: block !important;
+ }
+
+ &__pager {
+ background: darken($ui-base-color, 8%);
+ box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+ overflow: hidden;
+ }
+
+ &__pager,
+ &__frame {
+ border-radius: 10px;
+ width: 50vw;
+ min-width: 920px;
+
+ @media screen and (max-width: 920px) {
+ min-width: 0;
+ width: 100%;
+ border-radius: 0;
+ box-shadow: none;
+ }
+ }
+
+ &__frame-wrapper {
+ opacity: 0;
+ transition: opacity 500ms linear;
+
+ &.active {
+ opacity: 1;
+ transition: opacity 50ms linear;
+ }
+ }
+
+ &__frame {
+ overflow: hidden;
+ }
+
+ &__illustration {
+ height: 50vh;
+
+ @media screen and (max-width: 630px) {
+ height: auto;
+ }
+
+ img {
+ object-fit: cover;
+ display: block;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ &__text {
+ border-top: 2px solid $ui-highlight-color;
+
+ &--columnized {
+ display: flex;
+
+ & > div {
+ flex: 1 1 33.33%;
+ text-align: center;
+ padding: 25px;
+ padding-bottom: 30px;
+ }
+
+ @media screen and (max-width: 630px) {
+ display: block;
+ padding: 15px 0;
+ padding-bottom: 20px;
+
+ & > div {
+ padding: 10px 25px;
+ }
+ }
+ }
+
+ h3 {
+ font-size: 24px;
+ line-height: 1.5;
+ font-weight: 700;
+ margin-bottom: 10px;
+ }
+
+ p {
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+ color: $darker-text-color;
+
+ code {
+ display: inline-block;
+ background: darken($ui-base-color, 8%);
+ font-size: 15px;
+ border: 1px solid lighten($ui-base-color, 8%);
+ border-radius: 2px;
+ padding: 1px 3px;
+ }
+ }
+
+ &--centered {
+ padding: 25px;
+ padding-bottom: 30px;
+ text-align: center;
+ }
+ }
+
+ &__dots {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 25px;
+
+ @media screen and (max-width: 630px) {
+ display: none;
+ }
+ }
+
+ &__dot {
+ width: 14px;
+ height: 14px;
+ border-radius: 14px;
+ border: 1px solid $ui-highlight-color;
+ background: transparent;
+ margin: 0 3px;
+ cursor: pointer;
+
+ &:hover {
+ background: lighten($ui-base-color, 8%);
+ }
+
+ &.active {
+ cursor: default;
+ background: $ui-highlight-color;
+ }
+ }
+
+ &__action {
+ padding: 25px;
+ padding-top: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}