diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2022-10-07 10:14:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-07 10:14:31 +0200 |
commit | 7fb738c8372a700e1b42534cb202005b8c73b946 (patch) | |
tree | 3ec4b6233b492fc86d1003cda0770641e47f2984 /app/javascript | |
parent | 99a43f0282d5115b75a564205ca7d2db31a3a945 (diff) |
Add interaction modal to logged-out web UI (#19306)
Diffstat (limited to 'app/javascript')
12 files changed, 394 insertions, 50 deletions
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 0d3b51f07..381088be7 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -86,6 +86,7 @@ class Status extends ImmutablePureComponent { onToggleHidden: PropTypes.func, onToggleCollapsed: PropTypes.func, onTranslate: PropTypes.func, + onInteractionModal: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, unread: PropTypes.bool, diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 4b384e6e5..9e8cadce2 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -82,6 +82,7 @@ class StatusActionBar extends ImmutablePureComponent { onBookmark: PropTypes.func, onFilter: PropTypes.func, onAddFilter: PropTypes.func, + onInteractionModal: PropTypes.func, withDismiss: PropTypes.bool, withCounters: PropTypes.bool, scrollKey: PropTypes.string, @@ -97,10 +98,12 @@ class StatusActionBar extends ImmutablePureComponent { ] handleReplyClick = () => { - if (me) { + const { signedIn } = this.context.identity; + + if (signedIn) { this.props.onReply(this.props.status, this.context.router.history); } else { - this._openInteractionDialog('reply'); + this.props.onInteractionModal('reply', this.props.status); } } @@ -114,25 +117,25 @@ class StatusActionBar extends ImmutablePureComponent { } handleFavouriteClick = () => { - if (me) { + const { signedIn } = this.context.identity; + + if (signedIn) { this.props.onFavourite(this.props.status); } else { - this._openInteractionDialog('favourite'); + this.props.onInteractionModal('favourite', this.props.status); } } handleReblogClick = e => { - if (me) { + const { signedIn } = this.context.identity; + + if (signedIn) { this.props.onReblog(this.props.status, e); } else { - this._openInteractionDialog('reblog'); + this.props.onInteractionModal('reblog', this.props.status); } } - _openInteractionDialog = type => { - window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - } - handleBookmarkClick = () => { this.props.onBookmark(this.props.status); } diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 9280a6ee3..294105f25 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -237,6 +237,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); }, + onInteractionModal (type, status) { + dispatch(openModal('INTERACTION', { + type, + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index e407a0d55..765b3cc1e 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -96,6 +96,7 @@ class Header extends ImmutablePureComponent { onAddToList: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, + onInteractionModal: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -177,7 +178,7 @@ class Header extends ImmutablePureComponent { } else if (account.getIn(['relationship', 'requested'])) { actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />; } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : undefined} />; + actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />; } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; } diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index f9838442f..f31848f41 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, + onInteractionModal: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -96,6 +97,10 @@ export default class Header extends ImmutablePureComponent { this.props.onChangeLanguages(this.props.account); } + handleInteractionModal = () => { + this.props.onInteractionModal(this.props.account); + } + render () { const { account, hidden, hideTabs } = this.props; @@ -123,6 +128,7 @@ export default class Header extends ImmutablePureComponent { onAddToList={this.handleAddToList} onEditAccountNote={this.handleEditAccountNote} onChangeLanguages={this.handleChangeLanguages} + onInteractionModal={this.handleInteractionModal} domain={this.props.domain} hidden={hidden} /> diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 3d6eb487d..46fa573dd 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -57,6 +57,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onInteractionModal (account) { + dispatch(openModal('INTERACTION', { + type: 'follow', + accountId: account.get('id'), + url: account.get('url'), + })); + }, + onBlock (account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); diff --git a/app/javascript/mastodon/features/interaction_modal/index.js b/app/javascript/mastodon/features/interaction_modal/index.js new file mode 100644 index 000000000..6a7aa9c1c --- /dev/null +++ b/app/javascript/mastodon/features/interaction_modal/index.js @@ -0,0 +1,132 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { registrationsOpen } from 'mastodon/initial_state'; +import { connect } from 'react-redux'; +import Icon from 'mastodon/components/icon'; +import classNames from 'classnames'; + +const mapStateToProps = (state, { accountId }) => ({ + displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), +}); + +class Copypaste extends React.PureComponent { + + static propTypes = { + value: PropTypes.string, + }; + + state = { + copied: false, + }; + + setRef = c => { + this.input = c; + } + + handleInputClick = () => { + this.setState({ copied: false }); + this.input.focus(); + this.input.select(); + this.input.setSelectionRange(0, this.input.value.length); + } + + handleButtonClick = () => { + const { value } = this.props; + navigator.clipboard.writeText(value); + this.input.blur(); + this.setState({ copied: true }); + this.timeout = setTimeout(() => this.setState({ copied: false }), 700); + } + + componentWillUnmount () { + if (this.timeout) clearTimeout(this.timeout); + } + + render () { + const { value } = this.props; + const { copied } = this.state; + + return ( + <div className={classNames('copypaste', { copied })}> + <input + type='text' + ref={this.setRef} + value={value} + readOnly + onClick={this.handleInputClick} + /> + + <button className='button' onClick={this.handleButtonClick}> + {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy' defaultMessage='Copy' />} + </button> + </div> + ); + } + +} + +export default @connect(mapStateToProps) +class InteractionModal extends React.PureComponent { + + static propTypes = { + displayNameHtml: PropTypes.string, + url: PropTypes.string, + type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']), + }; + + render () { + const { url, type, displayNameHtml } = this.props; + + const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />; + + let title, actionDescription, icon; + + switch(type) { + case 'reply': + icon = <Icon id='reply' />; + title = <FormattedMessage id='interaction_modal.title.reply' defaultMessage="Reply to {name}'s post" values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.reply' defaultMessage='With an account on Mastodon, you can respond to this post.' />; + break; + case 'reblog': + icon = <Icon id='retweet' />; + title = <FormattedMessage id='interaction_modal.title.reblog' defaultMessage="Boost {name}'s post" values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.reblog' defaultMessage='With an account on Mastodon, you can boost this post to share it with your own followers.' />; + break; + case 'favourite': + icon = <Icon id='star' />; + title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favourite {name}'s post" values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.' />; + break; + case 'follow': + icon = <Icon id='user-plus' />; + title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />; + actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />; + break; + } + + return ( + <div className='modal-root__modal interaction-modal'> + <div className='interaction-modal__lead'> + <h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3> + <p>{actionDescription} <FormattedMessage id='interaction_modal.preamble' defaultMessage="Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one." /></p> + </div> + + <div className='interaction-modal__choices'> + <div className='interaction-modal__choices__choice'> + <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3> + <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> + <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> + </div> + + <div className='interaction-modal__choices__choice'> + <h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3> + <p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Simply copy and paste this URL into the search bar of your favourite app or the web interface where you are signed in.' /></p> + <Copypaste value={url} /> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js index 0cb42b25a..0beb2e14d 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -43,6 +43,7 @@ class Footer extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -67,26 +68,44 @@ class Footer extends ImmutablePureComponent { }; handleReplyClick = () => { - const { dispatch, askReplyConfirmation, intl } = this.props; - - if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: this._performReply, - })); + const { dispatch, askReplyConfirmation, status, intl } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + })); + } else { + this._performReply(); + } } else { - this._performReply(); + dispatch(openModal('INTERACTION', { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } }; handleFavouriteClick = () => { const { dispatch, status } = this.props; - - if (status.get('favourited')) { - dispatch(unfavourite(status)); + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } } else { - dispatch(favourite(status)); + dispatch(openModal('INTERACTION', { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } }; @@ -97,13 +116,22 @@ class Footer extends ImmutablePureComponent { handleReblogClick = e => { const { dispatch, status } = this.props; - - if (status.get('reblogged')) { - dispatch(unreblog(status)); - } else if ((e && e.shiftKey) || !boostModal) { - this._performReblog(status); + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else if ((e && e.shiftKey) || !boostModal) { + this._performReblog(status); + } else { + dispatch(initBoostModal({ status, onReblog: this._performReblog })); + } } else { - dispatch(initBoostModal({ status, onReblog: this._performReblog })); + dispatch(openModal('INTERACTION', { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } }; diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 50bda69f8..2e240c414 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -194,6 +194,7 @@ class ActionBar extends React.PureComponent { render () { const { status, relationship, intl } = this.props; + const { signedIn, permissions } = this.context.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); @@ -250,7 +251,7 @@ class ActionBar extends React.PureComponent { } } - if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` }); @@ -287,10 +288,10 @@ class ActionBar extends React.PureComponent { <div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> {shareButton} - <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> + <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__action-bar-dropdown'> - <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> + <DropdownMenuContainer size={18} icon='ellipsis-h' disabled={!signedIn} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> </div> </div> ); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 748dc7a92..3d238e7ee 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -180,6 +180,7 @@ class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -228,10 +229,21 @@ class Status extends ImmutablePureComponent { } handleFavouriteClick = (status) => { - if (status.get('favourited')) { - this.props.dispatch(unfavourite(status)); + const { dispatch } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } } else { - this.props.dispatch(favourite(status)); + dispatch(openModal('INTERACTION', { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } } @@ -244,15 +256,25 @@ class Status extends ImmutablePureComponent { } handleReplyClick = (status) => { - let { askReplyConfirmation, dispatch, intl } = this.props; - if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), - })); + const { askReplyConfirmation, dispatch, intl } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), + })); + } else { + dispatch(replyCompose(status, this.context.router.history)); + } } else { - dispatch(replyCompose(status, this.context.router.history)); + dispatch(openModal('INTERACTION', { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } } @@ -261,14 +283,25 @@ class Status extends ImmutablePureComponent { } handleReblogClick = (status, e) => { - if (status.get('reblogged')) { - this.props.dispatch(unreblog(status)); - } else { - if ((e && e.shiftKey) || !boostModal) { - this.handleModalReblog(status); + const { dispatch } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + if (status.get('reblogged')) { + dispatch(unreblog(status)); } else { - this.props.dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); + if ((e && e.shiftKey) || !boostModal) { + this.handleModalReblog(status); + } else { + dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); + } } + } else { + dispatch(openModal('INTERACTION', { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); } } diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index dfa89f2ce..5c273ffa4 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -13,6 +13,7 @@ import AudioModal from './audio_modal'; import ConfirmationModal from './confirmation_modal'; import SubscribedLanguagesModal from 'mastodon/features/subscribed_languages_modal'; import FocalPointModal from './focal_point_modal'; +import InteractionModal from 'mastodon/features/interaction_modal'; import { MuteModal, BlockModal, @@ -41,6 +42,7 @@ const MODAL_COMPONENTS = { 'COMPARE_HISTORY': CompareHistoryModal, 'FILTER': FilterModal, 'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }), + 'INTERACTION': () => Promise.resolve({ default: InteractionModal }), }; export default class ModalRoot extends React.PureComponent { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 491aec339..a3dc4c637 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4899,6 +4899,7 @@ a.status-card.compact:hover { left: 0; width: 100%; height: 100%; + box-sizing: border-box; display: flex; flex-direction: column; align-items: center; @@ -8105,3 +8106,123 @@ noscript { margin: 10px 0; } } + +.interaction-modal { + max-width: 90vw; + width: 600px; + background: $ui-base-color; + border-radius: 8px; + overflow: hidden; + position: relative; + display: block; + padding: 20px; + + h3 { + font-size: 22px; + line-height: 33px; + font-weight: 700; + text-align: center; + } + + &__icon { + color: $highlight-text-color; + margin: 0 5px; + } + + &__lead { + padding: 20px; + text-align: center; + + h3 { + margin-bottom: 15px; + } + + p { + font-size: 17px; + line-height: 22px; + color: $darker-text-color; + } + } + + &__choices { + display: flex; + + &__choice { + flex: 0 0 auto; + width: 50%; + box-sizing: border-box; + padding: 20px; + + h3 { + margin-bottom: 20px; + } + + p { + color: $darker-text-color; + margin-bottom: 20px; + } + + .button { + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } + } + } + + @media screen and (max-width: $no-gap-breakpoint - 1px) { + &__choices { + display: block; + + &__choice { + width: auto; + margin-bottom: 20px; + } + } + } +} + +.copypaste { + display: flex; + align-items: center; + gap: 10px; + + input { + display: block; + font-family: inherit; + background: darken($ui-base-color, 8%); + border: 1px solid $highlight-text-color; + color: $darker-text-color; + border-radius: 4px; + padding: 6px 9px; + line-height: 22px; + font-size: 14px; + transition: border-color 300ms linear; + flex: 1 1 auto; + overflow: hidden; + + &:focus { + outline: 0; + background: darken($ui-base-color, 4%); + } + } + + .button { + flex: 0 0 auto; + transition: background 300ms linear; + } + + &.copied { + input { + border: 1px solid $valid-value-color; + transition: none; + } + + .button { + background: $valid-value-color; + transition: none; + } + } +} |