0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

style(ui): button & confirm modal (#817)

* style(ui):  button & confirm modal

button and confirm modal desktop styling

* fix(ui): fix typo

* fix(ui): fix image alt

fix image alt
This commit is contained in:
simeng-li 2022-05-15 11:03:19 +08:00 committed by GitHub
parent 1173ad1b8c
commit 9e9e8cdb50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 337 additions and 114 deletions

View file

@ -44,6 +44,7 @@ const translation = {
nav_back: 'Back',
},
description: {
reminder: 'Reminder',
not_found: '404 Not Found',
loading: 'Loading...',
redirecting: 'Redirecting...',

View file

@ -46,6 +46,7 @@ const translation = {
nav_back: '返回',
},
description: {
reminder: '提示',
not_found: '404 页面不存在',
loading: '读取中...',
redirecting: '页面跳转中...',

View file

@ -2,8 +2,6 @@
.socialButton {
width: 48px;
height: 48px;
border-radius: 50%;
@include _.flex-column;
background: var(--color-layer);
@ -11,7 +9,29 @@
}
.icon {
width: 28px;
height: 28px;
@include _.image-align-center;
}
:global(body.mobile) {
.socialButton {
width: 48px;
height: 48px;
}
.icon {
width: 32px;
height: 32px;
}
}
:global(body.desktop) {
.socialButton {
width: 28px;
height: 28px;
}
.icon {
width: 16px;
height: 16px;
}
}

View file

@ -1,14 +1,8 @@
@use '@/scss/underscore' as _;
.socialButton {
border: _.border(var(--color-outline));
background: transparent;
color: var(--color-text);
.icon {
width: _.unit(6);
height: _.unit(6);
@include _.image-align-center;
margin-right: _.unit(4);
}
.icon {
width: _.unit(6);
height: _.unit(6);
@include _.image-align-center;
margin-right: _.unit(4);
}

View file

@ -28,8 +28,9 @@ const SocialLinkButton = ({ isDisabled, className, connector, onClick }: Props)
disabled={isDisabled}
className={classNames(
styles.button,
styles.secondary,
styles.large,
isDisabled && styles.disabled,
socialLinkButtonStyles.socialButton,
className
)}
type="button"
@ -37,7 +38,7 @@ const SocialLinkButton = ({ isDisabled, className, connector, onClick }: Props)
onClick?.(target);
}}
>
{logo && <img src={logo} alt={localName} className={socialLinkButtonStyles.icon} />}
{logo && <img src={logo} alt={target} className={socialLinkButtonStyles.icon} />}
{localName}
</button>
);

View file

@ -7,20 +7,24 @@
border-radius: var(--radius);
font: var(--font-body-bold);
cursor: pointer;
width: 100%;
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.2s ease-in-out;
white-space: nowrap;
}
.large {
width: 100%;
}
.small {
min-width: 44px;
}
.primary {
border: none;
background: var(--color-primary);
color: var(--color-primary-button-text);
&.disabled {
background: var(--color-layer);
color: var(--color-disabled);
}
}
.secondary {
@ -29,10 +33,75 @@
color: var(--color-text);
&.disabled {
color: var(--color-disabled);
border-color: var(--color-border);
color: var(--color-text-disabled);
}
}
.small {
font: var(--font-button-text-small);
.outline {
border: _.border(var(--color-primary));
background: transparent;
color: var(--color-primary);
&.disabled {
border-color: var(--color-border);
color: var(--color-text-disabled);
}
&:focus-visible {
outline: 3px solid var(--color-focused-variant);
}
&:active {
background: var(--color-pressed-variant);
}
&:not(:disabled):not(:active):hover {
background: var(--color-hover-variant);
}
}
:global(body.mobile) {
.primary {
&.disabled {
background: var(--color-layer);
color: var(--color-text-disabled);
}
}
}
:global(body.desktop) {
.primary {
&.disabled {
background: var(--color-inverse-on-surface);
color: var(--color-text-disabled);
}
&:focus-visible {
outline: 3px solid var(--color-focused-variant);
}
&:active {
background: var(--color-primary-pressed);
}
&:not(:disabled):not(:active):hover {
background: var(--color-primary-hover);
}
}
.secondary {
&:focus-visible {
outline: 3px solid var(--color-focused);
}
&:active {
background: var(--color-pressed);
}
&:not(:disabled):not(:active):hover {
background: var(--color-hover);
}
}
}

View file

@ -5,18 +5,18 @@ import * as styles from './index.module.scss';
export type Props = {
htmlType?: 'button' | 'submit' | 'reset';
size?: 'regular' | 'small';
isDisabled?: boolean;
className?: string;
children: ReactNode; // TODO: make it i18nKey with optional params
type?: 'primary' | 'secondary';
type?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'large';
onClick?: React.MouseEventHandler;
};
const Button = ({
htmlType = 'button',
type = 'primary',
size = 'regular',
size = 'large',
isDisabled,
className,
children,

View file

@ -8,7 +8,7 @@ type Props = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>;
const Checkbox = ({ disabled, ...rest }: Props, ref: Ref<HTMLInputElement>) => {
return (
<div className={styles.checkbox}>
<input type="checkbox" disabled={disabled} {...rest} ref={ref} />
<input type="checkbox" disabled={disabled} {...rest} ref={ref} readOnly />
<CheckBoxIcon className={styles.icon} />
</div>
);

View file

@ -0,0 +1,62 @@
import classNames from 'classnames';
import React from 'react';
import { isMobile } from 'react-device-detect';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import Button from '@/components/Button';
import * as modalStyles from '../../scss/modal.module.scss';
import { CloseIcon } from '../Icons';
import * as styles from './Acmodal.module.scss';
import { ModalProps } from './type';
const AcModal = ({
className,
isOpen = false,
children,
cancelText = 'action.cancel',
confirmText = 'action.confirm',
onConfirm,
onClose,
}: ModalProps) => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
return (
<ReactModal
role="dialog"
isOpen={isOpen}
className={classNames(styles.modal, className)}
overlayClassName={classNames(modalStyles.overlay, styles.overlay)}
appElement={document.querySelector('main') ?? undefined}
onAfterOpen={() => {
document.body.classList.add('static');
}}
onAfterClose={() => {
document.body.classList.remove('static');
}}
>
<div className={styles.container}>
<div className={styles.header}>
{t('description.reminder')}
<CloseIcon onClick={onClose} />
</div>
<div className={styles.content}>{children}</div>
<div className={styles.footer}>
<Button
type={isMobile ? 'secondary' : 'outline'}
size={isMobile ? 'large' : 'small'}
onClick={onClose}
>
{t(cancelText)}
</Button>
<Button size={isMobile ? 'large' : 'small'} onClick={onConfirm ?? onClose}>
{t(confirmText)}
</Button>
</div>
</div>
</ReactModal>
);
};
export default AcModal;

View file

@ -0,0 +1,47 @@
@use '@/scss/underscore' as _;
.overlay {
z-index: 100;
}
.modal {
position: absolute;
width: 600px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
outline: none;
}
.container {
background: var(--color-dialogue);
border-radius: var(--radius);
padding: _.unit(6);
}
.header {
font: var(--font-header);
color: var(--color-text);
@include _.flex-row;
justify-content: space-between;
margin-bottom: _.unit(4);
}
.content {
font: var(--font-body);
color: var(--color-text);
margin-bottom: _.unit(6);
}
.footer {
@include _.flex_row;
justify-content: flex-end;
> * {
flex-shrink: 1;
}
> button:first-child {
margin-right: _.unit(4);
}
}

View file

@ -4,9 +4,18 @@
z-index: 100;
}
.modal {
position: absolute;
left: 20px;
right: 20px;
top: 50%;
transform: translate(0, -50%);
outline: none;
}
.container {
background: var(--color-dialogue);
padding: _.unit(5);
background: var(--color-dialogue);
border-radius: var(--radius);
}

View file

@ -0,0 +1,50 @@
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import Button from '@/components/Button';
import * as modalStyles from '../../scss/modal.module.scss';
import * as styles from './MobileModal.module.scss';
import { ModalProps } from './type';
const MobileModal = ({
className,
isOpen = false,
children,
cancelText = 'action.cancel',
confirmText = 'action.confirm',
onConfirm,
onClose,
}: ModalProps) => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
return (
<ReactModal
role="dialog"
isOpen={isOpen}
className={classNames(styles.modal, className)}
overlayClassName={classNames(modalStyles.overlay, styles.overlay)}
appElement={document.querySelector('main') ?? undefined}
onAfterOpen={() => {
document.body.classList.add('static');
}}
onAfterClose={() => {
document.body.classList.remove('static');
}}
>
<div className={styles.container}>
<div className={styles.content}>{children}</div>
<div className={styles.footer}>
<Button type="secondary" onClick={onClose}>
{t(cancelText)}
</Button>
<Button onClick={onConfirm ?? onClose}>{t(confirmText)}</Button>
</div>
</div>
</ReactModal>
);
};
export default MobileModal;

View file

@ -1,61 +1,12 @@
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import { TFuncKey, useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import React from 'react';
import { isMobile } from 'react-device-detect';
import Button from '@/components/Button';
import AcModal from './AcModal';
import MobileModal from './MobileModal';
import { ModalProps } from './type';
import * as modalStyles from '../../scss/modal.module.scss';
import * as styles from './index.module.scss';
type Props = {
className?: string;
isOpen?: boolean;
children: ReactNode;
cancelText?: TFuncKey<'translation', 'main_flow'>;
confirmText?: TFuncKey<'translation', 'main_flow'>;
onConfirm?: () => void;
onClose: () => void;
};
const ConfirmModal = ({
className,
isOpen = false,
children,
cancelText = 'action.cancel',
confirmText = 'action.confirm',
onConfirm,
onClose,
}: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
return (
<ReactModal
role="dialog"
isOpen={isOpen}
className={classNames(modalStyles.modal, className)}
overlayClassName={classNames(modalStyles.overlay, styles.overlay)}
appElement={document.querySelector('main') ?? undefined}
onAfterOpen={() => {
document.body.classList.add('static');
}}
onAfterClose={() => {
document.body.classList.remove('static');
}}
>
<div className={styles.container}>
<div className={styles.content}>{children}</div>
<div className={styles.footer}>
<Button type="secondary" size="small" onClick={onClose}>
{t(cancelText)}
</Button>
<Button size="small" onClick={onConfirm ?? onClose}>
{t(confirmText)}
</Button>
</div>
</div>
</ReactModal>
);
const ConfirmModal = (props: ModalProps) => {
return isMobile ? <MobileModal {...props} /> : <AcModal {...props} />;
};
export default ConfirmModal;

View file

@ -0,0 +1,12 @@
import { ReactNode } from 'react';
import { TFuncKey } from 'react-i18next';
export type ModalProps = {
className?: string;
isOpen?: boolean;
children: ReactNode;
cancelText?: TFuncKey<'translation', 'main_flow'>;
confirmText?: TFuncKey<'translation', 'main_flow'>;
onConfirm?: () => void;
onClose: () => void;
};

View file

@ -42,7 +42,7 @@ const SecondarySocialSignIn = ({ className }: Props) => {
))}
{isOverSize && (
<MoreSocialIcon
className={styles.socialButton}
className={styles.moreButton}
onClick={() => {
setShowModal(true);
}}

View file

@ -3,33 +3,48 @@
.socialIconList {
@include _.flex-row;
justify-content: center;
}
.socialButton {
margin-right: _.unit(8);
.socialButton {
margin-right: _.unit(8);
&:last-child {
margin-right: 0;
}
&:last-child {
margin-right: 0;
}
}
.moreButton {
border-radius: 50%;
}
.moreButton {
border-radius: 50%;
}
.socialLinkList {
@include _.flex-column;
}
.socialLinkButton {
margin-bottom: _.unit(4);
}
.socialLinkButton {
margin-bottom: _.unit(4);
}
.expandIcon {
width: 20px;
height: 20px;
.expandIcon {
width: 20px;
height: 20px;
&.expanded {
transform: rotate(180deg);
}
&.expanded {
transform: rotate(180deg);
}
}
:global(body.mobile) {
.moreButton {
width: 48px;
height: 48px;
}
}
:global(body.desktop) {
.moreButton {
width: 28px;
height: 28px;
}
}

View file

@ -1,12 +1,3 @@
.modal {
position: absolute;
left: 20px;
right: 20px;
top: 50%;
transform: translate(0, -50%);
outline: none;
}
.overlay {
position: fixed;
background: var(--color-overlay);