0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

feat(ui): ui style foundation update (#583)

* feat(ui): ui style foundation update

ui style foundation update

* fix(ui): remove legacy style

remove legacy style

* refactor(ui): remove errorMessage shrink space logic

remove errorMessage shrink space logic
This commit is contained in:
simeng-li 2022-04-20 15:07:16 +08:00 committed by GitHub
parent 4491eab5b4
commit 93a93b4c8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 263 additions and 310 deletions

View file

@ -1,6 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<symbol width="18" height="18" viewBox="0 0 18 18" id="unchecked">
<circle cx="9" cy="9" r="8" stroke="#D8D8D8" stroke-width="2" fill="transparent"/>
<circle cx="9" cy="9" r="8" stroke="#D8D8D8" stroke-width="2" fill="transparent" />
</symbol>
<symbol width="18" height="18" viewBox="0 0 18 18" id="checked">
<circle cx="9" cy="9" r="9" fill="#6139F6"/>

Before

Width:  |  Height:  |  Size: 790 B

After

Width:  |  Height:  |  Size: 791 B

View file

@ -1,87 +1,55 @@
@use '@/scss/underscore' as _;
/* Foundation */
$color-neutral-100: #111;
$color-neutral-90: #666;
$color-neutral-70: #999;
$color-neutral-50: #aeaeae;
$color-neutral-30: #d8d8d8;
$color-neutral-10: #f4f4f4;
$color-neutral-0: #fff;
$color-primary: #6139f6;
$color-primary-tint-60: #a48dfa;
$color-primary-tint-70: #b09bfa;
$font-family: 'PingFang SC', 'SF Pro Text', sans-serif;
$font-family: 'PingFang SC', 'SF Pro Display', 'Siyuan Heiti', 'Roboto';
$font-family-small: 'PingFang SC', 'SF Pro Text', 'Siyuan Heiti', 'Roboto';
.content {
position: absolute;
inset: 0;
background: var(--color-background);
color: var(--color-font-primary);
background: var(--color-base);
color: var(--color-text);
font: var(--font-body);
}
.universal {
--color-error: #ea0000;
--radius: 8px;
}
.light {
--color-text: #191c1d;
--color-icon: #747778;
--color-caption: #747778;
--color-outline: #78767f;
--color-border: #e0e3e3;
--color-disabled: #c4c7c7;
--color-primary: #5d34f2;
--color-layer: #eff1f1;
--color-error: #ba1b1b;
--color-toast: rgba(25, 28, 29, 80%);
--color-overlay: rgba(25, 28, 29, 16%);
--color-base: #fff;
}
.light,
.dark {
/* Color */
--color-primary: #{$color-primary};
--color-background: #{$color-neutral-0};
--color-secondary-background-active: #{$color-neutral-10};
--color-secondary-background-disabled: #{$color-neutral-10};
--color-border: #{$color-neutral-100};
--color-border-secondary: #{$color-neutral-70};
--color-border-disabled: #{$color-neutral-30};
--color-control-background: #{$color-neutral-10};
--color-control-focus: #{$color-primary-tint-60};
--color-control-action: #{$color-neutral-50};
--color-checkbox-border: #{$color-neutral-30};
--color-divider: #dbdbdb;
--color-dark-background: #{rgba($color-neutral-100, 0.8)};
--color-loading-layer: rgba(0, 0, 0, 8%);
/* Font Color */
--color-font-primary: #{$color-neutral-100};
--color-font-secondary: #444;
--color-font-tertiary: #777;
--color-font-placeholder: #aaa;
--color-font-divider: #bbb;
--color-font-button-text: #{$color-neutral-0};
--color-font-button-text-active: #{rgba($color-neutral-0, 0.4)};
--color-font-secondary-dialog: #{$color-neutral-70};
--color-font-secondary-disabled: #{rgba($color-neutral-100, 0.4)};
--color-font-link: #{$color-primary};
--color-font-link-secondary: #{$color-neutral-70};
--color-font-toast-text: #{$color-neutral-0};
/* ===== Legacy Styling ===== */
--color-heading: #333;
--color-body: #555;
--color-secondary: #888;
--color-placeholder: #aaa;
--color-control-background-disabled: #eaeaea;
/* Shadow */
--shadow-card: 2px 2px 24px rgb(187, 189, 191, 20%);
--shadow-control: 1px 1px 2px rgb(221, 221, 221, 25%);
--color-text: #f7f8f8;
--color-icon: #a9acac;
--color-caption: #a9acac;
--color-outline: #928f9a;
--color-border: #444748;
--color-disabled: #5c5f60;
--color-primary: #7958ff;
--color-layer: linear-gradient(0deg, rgba(202, 190, 255, 14%), rgba(202, 190, 255, 14%)), linear-gradient(0deg, rgba(196, 199, 199, 2%), rgba(196, 199, 199, 2%)), #191c1d;
--color-error: #dd3730;
--color-toast: rgba(247, 248, 248, 80%);
--color-overlay: rgba(247, 248, 248, 80%);
--color-base: #191c1d;
}
.mobile {
--font-title: 600 32px/40px #{$font-family};
--font-heading-2: 500 18px/22px #{$font-family};
--font-heading-2-bold: 600 18px/22px #{$font-family};
--font-control: 500 18px/20px #{$font-family};
--font-button-text: 600 20px/24px #{$font-family};
--font-button-text-small: 500 18px/22px #{$font-family};
--font-body-bold: 500 16px/20px #{$font-family};
--font-body: 400 16px/20px #{$font-family};
--font-body-bold: 600 16px/20px #{$font-family};
--font-body-small: 400 14px/18px #{$font-family};
/* ===== Legacy Styling ===== */
--font-headline: 600 40px/56px #{$font-family};
--font-heading-1: 600 28px/39px #{$font-family};
--font-heading-3: 600 16px/22.4px #{$font-family};
--font-body-small: 500 14px/18px #{$font-family-small};
--font-caption: 400 14px/18px #{$font-family-small};
}

View file

@ -4,6 +4,9 @@ $logo-height: 60px;
.container {
width: 100%;
height: 15vh;
min-height: 92px;
max-height: 148px;
@include _.flex-column;
}
@ -18,6 +21,6 @@ $logo-height: 60px;
}
.headline {
font: var(--font-heading-2-bold);
color: var(--color-font-secondary);
font: var(--font-body);
color: var(--color-text);
}

View file

@ -1,17 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import styles from './SocialIconButton.module.scss';
type Props = {
className?: string;
onClick?: () => void;
};
const MoreButton = ({ className, onClick }: Props) => {
return (
<button className={classNames(styles.socialButton, styles.more, className)} onClick={onClick} />
);
};
export default MoreButton;

View file

@ -6,12 +6,8 @@
height: 48px;
border-radius: 50%;
@include _.flex-column;
background: var(--color-secondary-background-active);
background: var(--color-layer);
border: none;
&.more {
background: url('../../assets/icons/more-social-icon.svg') no-repeat center;
}
}
.icon {

View file

@ -1,15 +1,13 @@
@use '@/scss/underscore' as _;
.socialButton {
font: var(--font-control);
border: _.border(var(--color-border));
background: var(--color-background);
color: var(--color-font-primary);
justify-content: flex-start;
border: _.border(var(--color-outline));
background: var(--color-base);
color: var(--color-text);
.icon {
width: _.unit(5);
height: _.unit(5);
width: _.unit(6);
height: _.unit(6);
@include _.image-align-center;
margin-right: _.unit(4);
}

View file

@ -3,7 +3,7 @@ import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
import * as SocialLinkButtonStyles from './SocialLinkButton.module.scss';
import * as socialLinkButtonStyles from './SocialLinkButton.module.scss';
import * as styles from './index.module.scss';
export type Props = {
@ -24,13 +24,18 @@ const SocialLinkButton = ({ isDisabled, className, connector, onClick }: Props)
return (
<button
disabled={isDisabled}
className={classNames(styles.button, SocialLinkButtonStyles.socialButton, className)}
className={classNames(
styles.button,
isDisabled && styles.disabled,
socialLinkButtonStyles.socialButton,
className
)}
type="button"
onClick={() => {
onClick?.(id);
}}
>
{logo && <img src={logo} alt={localName} className={SocialLinkButtonStyles.icon} />}
{logo && <img src={logo} alt={localName} className={socialLinkButtonStyles.icon} />}
{localName}
</button>
);

View file

@ -1,37 +1,38 @@
@use '@/scss/underscore' as _;
.button {
display: flex;
flex-direction: row;
@include _.flex-row;
justify-content: center;
align-items: center;
padding: _.unit(3);
border-radius: _.unit(2);
font: var(--font-button-text);
transition: var(--transition-default-control);
border-radius: var(--radius);
font: var(--font-body-bold);
cursor: pointer;
-webkit-appearance: none;
width: 100%;
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
}
.primary {
border: none;
background: var(--color-primary);
color: var(--color-font-button-text);
color: var(--color-base);
&.disabled {
background: var(--color-layer);
color: var(--color-disabled);
}
}
.secondary {
border: _.border(var(--color-border));
background: var(--color-background);
color: var(--color-font-primary);
border: _.border(var(--color-outline));
background: var(--color-base);
color: var(--color-text);
&.disabled {
color: var(--color-disabled);
}
}
.small {
font: var(--font-button-text-small);
&.secondary {
border: _.border(var(--color-border-secondary));
color: var(--color-font-secondary-dialog);
}
}

View file

@ -5,7 +5,7 @@ import * as styles from './index.module.scss';
export type Props = {
htmlType?: 'button' | 'submit' | 'reset';
size?: 'large' | 'small';
size?: 'regular' | 'small';
isDisabled?: boolean;
className?: string;
children: ReactNode; // TODO: make it i18nKey with optional params
@ -16,7 +16,7 @@ export type Props = {
const Button = ({
htmlType = 'button',
type = 'primary',
size = 'large',
size = 'regular',
isDisabled,
className,
children,
@ -24,7 +24,13 @@ const Button = ({
}: Props) => (
<button
disabled={isDisabled}
className={classNames(styles.button, styles[type], styles[size], className)}
className={classNames(
styles.button,
styles[type],
styles[size],
isDisabled && styles.disabled,
className
)}
type={htmlType}
onClick={onClick}
>

View file

@ -1,21 +1,20 @@
@use '@/scss/underscore' as _;
.container {
background: var(--color-background);
padding: _.unit(6);
border-radius: _.unit(2);
background: var(--color-base);
padding: _.unit(6) _.unit(5);
border-radius: var(--radius);
}
.content {
text-align: center;
font: var(--font-body-medium);
color: var(--color-font-primary);
font: var(--font-body);
color: var(--color-text);
}
.footer {
@include _.flex_row;
margin-top: _.unit(6);
justify-content: space-between;
> * {
flex: 1;

View file

@ -3,15 +3,15 @@
.divider {
@include _.flex-row;
font: var(--font-body-medium);
color: var(--color-font-divider);
font: var(--font-body);
color: var(--color-caption);
margin: _.unit(4) 0;
width: 100%;
.line {
flex: 1;
height: 1px;
background: var(--color-divider);
background: var(--color-border);
&:first-child {
margin-right: _.unit(4);

View file

@ -2,7 +2,7 @@
.container {
padding: _.unit(5);
background: var(--color-background);
background: var(--color-base);
}
.header {
@ -11,3 +11,7 @@
align-items: center;
margin-bottom: _.unit(4);
}
.closeIcon {
fill: var(--color-icon);
}

View file

@ -29,7 +29,7 @@ const Drawer = ({ className, isOpen = false, children, onClose }: Props) => {
>
<div className={styles.container}>
<div className={styles.header}>
<ClearIcon onClick={onClose} />
<ClearIcon className={styles.closeIcon} onClick={onClose} />
</div>
{children}
</div>

View file

@ -1,6 +1,6 @@
@use '@/scss/underscore' as _;
.error {
font: var(--font-body-small);
font: var(--font-caption);
color: var(--color-error);
}

View file

@ -0,0 +1,11 @@
import React, { SVGProps } from 'react';
import More from '@/assets/icons/more-social-icon.svg';
const MoreSocialIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" {...props}>
<use href={`${More}#more`} />
</svg>
);
export default MoreSocialIcon;

View file

@ -4,10 +4,10 @@
position: relative;
@include _.flex-row;
padding: 0 _.unit(4);
border-radius: _.unit(2);
border-radius: var(--radius);
border: _.border();
background: var(--color-control-background);
color: var(--color-font-primary);
background: var(--color-layer);
color: var(--color-text);
> *:not(:first-child) {
@ -15,7 +15,7 @@
}
&.focus {
border: _.border(var(--color-control-focus));
border: _.border(var(--color-primary));
}
&.error {
@ -28,13 +28,13 @@
background: none;
padding: _.unit(3) 0;
caret-color: var(--color-primary);
font: var(--font-control);
transition: var(--transition-default-control);
font: var(--font-body-bold);
&::placeholder {
color: var(--color-placeholder);
color: var(--color-caption);
}
// Overwrite webkit auto-fill style
&:-webkit-autofill {
box-shadow: 0 0 0 30px var(--color-control-background) inset;
transition: background-color 5000s ease-in-out 0s;
@ -47,5 +47,5 @@
}
.actionButton {
fill: var(--color-control-action);
fill: var(--color-icon);
}

View file

@ -1,8 +1,8 @@
@use '@/scss/underscore' as _;
.countryCodeSelector {
color: var(--color-font-primary);
font: var(--font-control);
color: var(--color-text);
font: var(--font-body);
border: none;
background: none;
width: auto;

View file

@ -6,15 +6,15 @@
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-loading-layer);
background-color: var(--color-overlay);
@include _.flex-column;
}
.container {
width: 60px;
height: 60px;
border-radius: 8px;
background-color: var(--color-dark-background);
border-radius: var(--radius);
background-color: var(--color-toast);
@include _.flex-column;
}

View file

@ -0,0 +1,11 @@
@use '@/scss/underscore' as _;
.navBar {
width: 100%;
margin-bottom: _.unit(6);
svg {
margin-left: _.unit(-2);
fill: var(--color-icon);
}
}

View file

@ -0,0 +1,21 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { NavArrowIcon } from '../Icons';
import * as styles from './index.module.scss';
const NavBar = () => {
const navigate = useNavigate();
return (
<div className={styles.navBar}>
<NavArrowIcon
onClick={() => {
navigate(-1);
}}
/>
</div>
);
};
export default NavBar;

View file

@ -4,18 +4,19 @@
@include _.flex-row;
justify-content: space-between;
width: 100%;
max-width: 375px;
max-width: 360px;
margin: 0 auto;
input {
border-radius: _.unit(2);
border: _.border();
background: var(--color-control-background);
background: var(--color-layer);
caret-color: var(--color-primary);
width: _.unit(12);
height: _.unit(12);
width: 44px;
height: 44px;
text-align: center;
font: var(--font-control);
color: var(--color-font-primary);
font: var(--font-body);
color: var(--color-text);
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
@ -24,7 +25,7 @@
}
&:focus {
border: _.border(var(--color-control-focus));
border: _.border(var(--color-primary));
}
&.error {
@ -32,7 +33,7 @@
}
&::placeholder {
color: var(--color-font-placeholder);
color: var(--color-caption);
}
}
}

View file

@ -2,6 +2,7 @@
.terms {
@include _.flex-row;
width: 100%;
input[type='checkbox'] {
appearance: none;
@ -14,11 +15,15 @@
.radioButton {
margin-right: _.unit(2);
transform: scale(0.8);
fill: var(--color-icon);
}
.content {
@include _.text-hint;
.link {
@include _.text-hint;
}
}
.errorMessage {

View file

@ -39,6 +39,7 @@ const TermsOfUse = ({ name, className, termsOfUse, isChecked, error, onChange }:
<div className={styles.content}>
{prefix}
<TextLink
className={styles.link}
text="description.terms_of_use"
href={termsOfUse.contentUrl}
type="secondary"

View file

@ -1,18 +1,17 @@
@use '@/scss/underscore' as _;
.link {
transition: var(--transition-default-control);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
&.primary {
color: var(--color-font-link);
color: var(--color-primary);
font: var(--font-body-bold);
text-decoration: none;
}
&.secondary {
color: var(--color-font-link-secondary);
font: var(--font-body-small);
color: var(--color-caption);
font: var(--font-body);
}
}

View file

@ -7,17 +7,16 @@
right: 0;
transform: translateY(-50%);
@include _.flex-column;
padding: 0 _.unit(8);
pointer-events: none;
.toast {
max-width: 360px;
max-width: 295px;
margin: 0 auto;
padding: _.unit(2) _.unit(4);
font: var(--font-body-medium);
color: var(--color-font-toast-text);
border-radius: _.unit(2);
background: var(--color-dark-background);
min-width: _.unit(25);
font: var(--font-body);
color: var(--color-base);
border-radius: var(--radius);
background: var(--color-toast);
text-align: center;
opacity: 0%;
transition: opacity 0.3s ease-in-out;

View file

@ -3,6 +3,7 @@
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
> * {
@ -11,21 +12,9 @@
.inputField {
margin-bottom: _.unit(4);
&.withError {
margin-bottom: _.unit(2);
}
}
.terms {
margin: _.unit(7) 0 _.unit(6);
&.withError {
margin: _.unit(7) 0 _.unit(2) 0;
}
}
.inputField.withError + .terms {
margin-top: _.unit(9);
margin: _.unit(8) 0 _.unit(4);
}
}

View file

@ -2,7 +2,6 @@
* TODO:
* 1. API redesign handle api error and loading status globally in PageContext
* 2. Input field validation, should move the validation rule to the input field scope
* 3. Forgot password URL
* 4. Read terms of use settings from SignInExperience Settings
*/
@ -36,6 +35,10 @@ type FieldValidations = {
[key in keyof FieldState]?: (state: FieldState) => ErrorType | undefined;
};
type Props = {
className?: string;
};
const defaultState = {
username: '',
password: '',
@ -45,7 +48,7 @@ const defaultState = {
const usernameRegx = /^[A-Z_a-z-][\w-]*$/;
const CreateAccount = () => {
const CreateAccount = ({ className }: Props) => {
const { t, i18n } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const [fieldState, setFieldState] = useState<FieldState>(defaultState);
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
@ -157,9 +160,9 @@ const CreateAccount = () => {
}, [error, i18n, setToast, t]);
return (
<form className={styles.form}>
<form className={classNames(styles.form, className)}>
<Input
className={classNames(styles.inputField, fieldErrors.username && styles.withError)}
className={styles.inputField}
name="username"
autoComplete="username"
placeholder={t('input.username')}
@ -177,7 +180,7 @@ const CreateAccount = () => {
/>
<PasswordInput
forceHidden
className={classNames(styles.inputField, fieldErrors.password && styles.withError)}
className={styles.inputField}
name="password"
autoComplete="current-password"
placeholder={t('input.password')}
@ -192,7 +195,7 @@ const CreateAccount = () => {
/>
<PasswordInput
forceHidden
className={classNames(styles.inputField, fieldErrors.confirmPassword && styles.withError)}
className={styles.inputField}
name="confirm_password"
autoComplete="current-password"
placeholder={t('input.confirm_password')}
@ -207,7 +210,7 @@ const CreateAccount = () => {
/>
<TermsOfUse
name="termsAgreement"
className={classNames(styles.terms, fieldErrors.termsAgreement && styles.withError)}
className={styles.terms}
termsOfUse={{ enabled: true, contentUrl: '/' }}
isChecked={fieldState.termsAgreement}
error={fieldErrors.termsAgreement}

View file

@ -3,6 +3,7 @@
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
> * {
@ -17,7 +18,14 @@
}
}
.link {
font: var(--font-caption);
}
.message {
font: var(--font-caption);
color: var(--color-text);
> span {
color: var(--color-primary);
}

View file

@ -111,6 +111,7 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
renderCountDownMessage
) : (
<TextLink
className={styles.link}
text="description.resend_passcode"
onClick={() => {
void sendPassCode(target);

View file

@ -22,6 +22,7 @@ import * as styles from './index.module.scss';
type Props = {
type: UserFlow;
className?: string;
};
type FieldState = {
@ -41,7 +42,7 @@ const defaultState: FieldState = { email: '', termsAgreement: false };
const emailRegEx = /^\S+@\S+\.\S+$/;
const EmailPasswordless = ({ type }: Props) => {
const EmailPasswordless = ({ type, className }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const [fieldState, setFieldState] = useState<FieldState>(defaultState);
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
@ -89,8 +90,6 @@ const EmailPasswordless = ({ type }: Props) => {
}, [validations, fieldState, asyncSendPasscode]);
useEffect(() => {
console.log(result);
if (result) {
navigate(`/${type}/email/passcode-validation`, { state: { email: fieldState.email } });
}
@ -119,9 +118,9 @@ const EmailPasswordless = ({ type }: Props) => {
}, [error, t, setToast]);
return (
<form className={styles.form}>
<form className={classNames(styles.form, className)}>
<Input
className={classNames(styles.inputField, fieldErrors.email && styles.withError)}
className={styles.inputField}
name="email"
autoComplete="email"
placeholder={t('input.email')}
@ -140,7 +139,7 @@ const EmailPasswordless = ({ type }: Props) => {
<TermsOfUse
name="termsAgreement"
className={classNames(styles.terms, fieldErrors.termsAgreement && styles.withError)}
className={styles.terms}
termsOfUse={{ enabled: true, contentUrl: '/' }}
isChecked={fieldState.termsAgreement}
error={fieldErrors.termsAgreement}

View file

@ -23,6 +23,7 @@ import * as styles from './index.module.scss';
type Props = {
type: UserFlow;
className?: string;
};
type FieldState = {
@ -40,7 +41,7 @@ type FieldValidations = {
const defaultState: FieldState = { phone: '', termsAgreement: false };
const PhonePasswordless = ({ type }: Props) => {
const PhonePasswordless = ({ type, className }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const [fieldState, setFieldState] = useState<FieldState>(defaultState);
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
@ -125,10 +126,10 @@ const PhonePasswordless = ({ type }: Props) => {
}, [error, t, setToast]);
return (
<form className={styles.form}>
<form className={classNames(styles.form, className)}>
<PhoneInput
name="phone"
className={classNames(styles.inputField, fieldErrors.phone && styles.withError)}
className={styles.inputField}
autoComplete="mobile"
placeholder={t('input.phone_number')}
countryCallingCode={phoneNumber.countryCallingCode}
@ -141,7 +142,7 @@ const PhonePasswordless = ({ type }: Props) => {
/>
<TermsOfUse
name="termsAgreement"
className={classNames(styles.terms, fieldErrors.termsAgreement && styles.withError)}
className={styles.terms}
termsOfUse={{ enabled: true, contentUrl: '/' }}
isChecked={fieldState.termsAgreement}
error={fieldErrors.termsAgreement}

View file

@ -3,6 +3,7 @@
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
> * {
@ -11,17 +12,9 @@
.inputField {
margin-bottom: _.unit(11);
&.withError {
margin-bottom: _.unit(9);
}
}
.terms {
margin-bottom: _.unit(6);
&.withError {
margin-bottom: _.unit(5);
}
}
}

View file

@ -12,7 +12,7 @@ import * as styles from './index.module.scss';
type Props = {
signInMethods: LocalSignInMethod[];
type?: 'primary' | 'secondary';
classname?: string;
className?: string;
};
const SignInMethodsKeyMap: {
@ -23,7 +23,7 @@ const SignInMethodsKeyMap: {
sms: 'phone_number',
};
const SignInMethodsLink = ({ signInMethods, type = 'secondary', classname }: Props) => {
const SignInMethodsLink = ({ signInMethods, type = 'secondary', className }: Props) => {
const navigate = useNavigate();
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
@ -43,7 +43,7 @@ const SignInMethodsLink = ({ signInMethods, type = 'secondary', classname }: Pro
);
if (type === 'primary') {
return <div className={classNames(styles.methodsPrimary, classname)}>{signInMethodsLink}</div>;
return <div className={classNames(styles.methodsPrimary, className)}>{signInMethodsLink}</div>;
}
if (signInMethods.length > 1) {
@ -58,13 +58,13 @@ const SignInMethodsLink = ({ signInMethods, type = 'secondary', classname }: Pro
rawText
);
return <div className={classNames(styles.methodsSecondary, classname)}>{textLink}</div>;
return <div className={classNames(styles.methodsSecondary, className)}>{textLink}</div>;
}
const rawText = t('secondary.sign_in_with', { method: signInMethods[0] });
const textLink = reactStringReplace(rawText, signInMethods[0], () => signInMethodsLink[0]);
return <div className={classNames(styles.methodsSecondary, classname)}>{textLink}</div>;
return <div className={classNames(styles.methodsSecondary, className)}>{textLink}</div>;
};
export default SignInMethodsLink;

View file

@ -50,7 +50,8 @@ describe('SecondarySocialSignIn', () => {
<SecondarySocialSignIn connectors={socialConnectors} />
</MemoryRouter>
);
expect(container.querySelectorAll('button')).toHaveLength(4);
expect(container.querySelectorAll('button')).toHaveLength(3);
expect(container.querySelector('svg')).not.toBeNull();
});
it('invoke web social signIn', async () => {

View file

@ -2,8 +2,8 @@ import { ConnectorMetadata } from '@logto/schemas';
import classNames from 'classnames';
import React, { useMemo } from 'react';
import MoreButton from '@/components/Button/MoreButton';
import SocialIconButton from '@/components/Button/SocialIconButton';
import MoreSocialIcon from '@/components/Icons/MoreSocialIcon';
import useSocial from '@/hooks/use-social';
import * as styles from './index.module.scss';
@ -38,7 +38,9 @@ const SecondarySocialSignIn = ({ className, connectors, showMoreConnectors }: Pr
}}
/>
))}
{isOverSize && <MoreButton className={styles.socialButton} onClick={showMoreConnectors} />}
{isOverSize && (
<MoreSocialIcon className={styles.socialButton} onClick={showMoreConnectors} />
)}
</div>
);
};

View file

@ -3,23 +3,28 @@
.socialIconList {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-row;
justify-content: center;
.socialButton {
margin-right: _.unit(10);
margin-right: _.unit(8);
&:last-child {
margin-right: 0;
}
}
.moreButton {
border-radius: 50%;
}
}
.socialLinkList {
width: 100%;
max-width: 360px;
@include _.flex-column;
margin: 0 auto;
@include _.flex-column;
.socialLinkButton {
margin-bottom: _.unit(4);

View file

@ -3,34 +3,18 @@
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
> * {
width: 100%;
}
.inputField:first-child {
.inputField {
margin-bottom: _.unit(4);
&.withError {
margin-bottom: _.unit(2);
}
}
.textLink {
margin-top: _.unit(3);
text-align: right;
}
.inputField.withError + .textLink {
margin-top: _.unit(1);
}
.terms {
margin: _.unit(6) 0;
&.withError {
margin: _.unit(5) 0 _.unit(2) 0;
}
}
}

View file

@ -19,7 +19,6 @@ describe('<UsernameSignin>', () => {
expect(container.querySelector('input[name="username"]')).not.toBeNull();
expect(container.querySelector('input[name="password"]')).not.toBeNull();
expect(queryByText('action.sign_in')).not.toBeNull();
expect(queryByText('description.forgot_password')).not.toBeNull();
expect(queryByText('description.agree_with_terms')).not.toBeNull();
});

View file

@ -2,12 +2,11 @@
* TODO:
* 1. API redesign handle api error and loading status globally in PageContext
* 2. Input field validation, should move the validation rule to the input field scope
* 3. Forgot password URL
* 4. Read terms of use settings from SignInExperience Settings
*/
import classNames from 'classnames';
import React, { FC, useState, useCallback, useEffect, useContext, useMemo } from 'react';
import React, { useState, useCallback, useEffect, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { signInBasic } from '@/apis/sign-in';
@ -16,7 +15,6 @@ import { ErrorType } from '@/components/ErrorMessage';
import Input from '@/components/Input';
import PasswordInput from '@/components/Input/PasswordInput';
import TermsOfUse from '@/components/TermsOfUse';
import TextLink from '@/components/TextLink';
import PageContext from '@/hooks/page-context';
import useApi from '@/hooks/use-api';
@ -36,13 +34,17 @@ type FieldValidations = {
[key in keyof FieldState]?: (state: FieldState) => ErrorType | undefined;
};
type Props = {
className?: string;
};
const defaultState: FieldState = {
username: '',
password: '',
termsAgreement: false,
};
const UsernameSignin: FC = () => {
const UsernameSignin = ({ className }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const [fieldState, setFieldState] = useState<FieldState>(defaultState);
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
@ -130,9 +132,9 @@ const UsernameSignin: FC = () => {
}, [error, t, setToast]);
return (
<form className={styles.form}>
<form className={classNames(styles.form, className)}>
<Input
className={classNames(styles.inputField, fieldErrors.username && styles.withError)}
className={styles.inputField}
name="username"
autoComplete="username"
placeholder={t('input.username')}
@ -149,7 +151,7 @@ const UsernameSignin: FC = () => {
}}
/>
<PasswordInput
className={classNames(styles.inputField, fieldErrors.password && styles.withError)}
className={styles.inputField}
name="password"
autoComplete="current-password"
placeholder={t('input.password')}
@ -162,16 +164,10 @@ const UsernameSignin: FC = () => {
}
}}
/>
<TextLink
className={styles.textLink}
type="secondary"
text="description.forgot_password"
href="/passcode"
/>
<TermsOfUse
name="termsAgreement"
className={classNames(styles.terms, fieldErrors.termsAgreement && styles.withError)}
className={styles.terms}
termsOfUse={{ enabled: true, contentUrl: '/' }}
isChecked={fieldState.termsAgreement}
error={fieldErrors.termsAgreement}

View file

@ -6,15 +6,6 @@
@include _.flex-column;
}
.navBar {
width: 100%;
margin-bottom: _.unit(6);
svg {
margin-left: _.unit(-2);
}
}
.title {
width: 100%;
@include _.title;
@ -23,7 +14,7 @@
.detail {
width: 100%;
margin-bottom: _.unit(9);
margin-bottom: _.unit(6);
font: var(--font-body);
color: var(--color-font-tertiary);
color: var(--color-caption);
}

View file

@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { NavArrowIcon } from '@/components/Icons';
import NavBar from '@/components/NavBar';
import PasscodeValidation from '@/containers/PasscodeValidation';
import { UserFlow } from '@/types';
@ -46,13 +46,7 @@ const Passcode = () => {
return (
<div className={styles.wrapper}>
<div className={styles.navBar}>
<NavArrowIcon
onClick={() => {
navigate(-1);
}}
/>
</div>
<NavBar />
<div className={styles.title}>{t('action.enter_passcode')}</div>
<div className={styles.detail}>{t('description.enter_passcode', { address: target })}</div>
<PasscodeValidation type={type} method={method} target={target} />

View file

@ -6,17 +6,8 @@
@include _.flex-column;
}
.navBar {
width: 100%;
margin-bottom: _.unit(6);
svg {
margin-left: _.unit(-2);
}
}
.title {
width: 100%;
@include _.title;
margin-bottom: _.unit(9);
margin-bottom: _.unit(6);
}

View file

@ -2,7 +2,7 @@ import React, { useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { NavArrowIcon } from '@/components/Icons';
import NavBar from '@/components/NavBar';
import CreateAccount from '@/containers/CreateAccount';
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
@ -37,13 +37,7 @@ const Register = () => {
return (
<div className={styles.wrapper}>
<div className={styles.navBar}>
<NavArrowIcon
onClick={() => {
navigate(-1);
}}
/>
</div>
<NavBar />
<div className={styles.title}>{t('action.create_account')}</div>
{registerForm}
</div>

View file

@ -6,17 +6,9 @@
@include _.flex-column;
}
.navBar {
width: 100%;
margin-bottom: _.unit(6);
svg {
margin-left: _.unit(-2);
}
}
.title {
width: 100%;
@include _.title;
margin-bottom: _.unit(9);
margin-bottom: _.unit(6);
}

View file

@ -2,7 +2,7 @@ import React, { useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { NavArrowIcon } from '@/components/Icons';
import NavBar from '@/components/NavBar';
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
import UsernameSignin from '@/containers/UsernameSignin';
@ -37,13 +37,7 @@ const SecondarySignIn = () => {
return (
<div className={styles.wrapper}>
<div className={styles.navBar}>
<NavArrowIcon
onClick={() => {
navigate(-1);
}}
/>
</div>
<NavBar />
<div className={styles.title}>{t('action.sign_in')}</div>
{signInForm}
</div>

View file

@ -6,7 +6,7 @@
@include _.flex-column;
.header {
margin-bottom: _.unit(10);
margin-bottom: _.unit(12);
}

View file

@ -17,7 +17,12 @@ const SignIn = () => {
logo="https://avatars.githubusercontent.com/u/84981374?s=400&u=6c44c3642f2fe15a59a56cdcb0358c0bd8b92f57&v=4"
/>
<UsernameSignin />
<TextLink className={styles.createAccount} href="/register" text="action.create_account" />
<TextLink
className={styles.createAccount}
type="secondary"
href="/register"
text="action.create_account"
/>
</div>
);
};

View file

@ -20,13 +20,13 @@
}
@mixin text-hint {
font: var(--font-body-small);
color: var(--color-font-link-secondary);
font: var(--font-caption);
color: var(--color-caption);
}
@mixin title {
font: var(--font-title);
color: var(--color-font-primary);
color: var(--color-text);
}
@function border($color: transparent, $width: 1) {

View file

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