Make UI for authentication flow match the rest of the dashboard (#8211)

- Changes all authentication screens to look similar to the rest of the dashboard
- Greatly simplifies HTML structure

# Important Notes
- This is being added before the official design is ready, *but* it should be useful anyway, because it greatly simplifies the HTML, which should make it easier to implement the new design
- The auth screens have a larger border-radius than all other elements in the app. This is intentional, to make them look like they continue naturally from the fully rounded submit buttons.
- Basic testing done:
- Logging in should still work
- Signing up should still work
- Setting username should still work
- Changing password should still work
- Forgot password should still work
- Password reset should still work
This commit is contained in:
somebody1234 2023-11-09 20:48:41 +10:00 committed by GitHub
parent 210c1907a8
commit ce1ef7df03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 779 additions and 1130 deletions

View File

@ -18,9 +18,9 @@ const REGISTRATION_QUERY_PARAMS = {
email: 'email',
} as const
// ============================
// === Confirm Registration ===
// ============================
// ===========================
// === ConfirmRegistration ===
// ===========================
/** An empty component redirecting users based on the backend response to user registration. */
export default function ConfirmRegistration() {

View File

@ -0,0 +1,101 @@
/** @file Styled input element. */
import * as React from 'react'
// =================
// === Constants ===
// =================
/** The delay to wait before checking input validity. */
const DEBOUNCE_MS = 1000
// =======================
// === ControlledInput ===
// =======================
/** Props for an {@link Input}. */
export interface ControlledInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string
error?: string
validate?: boolean
setValue: (value: string) => void
shouldReportValidityRef?: React.MutableRefObject<boolean>
}
/** A component for authentication from inputs, with preset styles. */
export default function ControlledInput(props: ControlledInputProps) {
const {
setValue,
error,
validate = false,
shouldReportValidityRef,
onChange,
onBlur,
...passThrough
} = props
const [reportTimeoutHandle, setReportTimeoutHandle] = React.useState<number | null>(null)
const [hasReportedValidity, setHasReportedValidity] = React.useState(false)
const [wasJustBlurred, setWasJustBlurred] = React.useState(false)
return (
<input
{...passThrough}
onChange={event => {
onChange?.(event)
setValue(event.target.value)
setWasJustBlurred(false)
if (validate) {
if (reportTimeoutHandle != null) {
window.clearTimeout(reportTimeoutHandle)
}
const currentTarget = event.currentTarget
if (error != null) {
currentTarget.setCustomValidity('')
currentTarget.setCustomValidity(
currentTarget.checkValidity() ||
shouldReportValidityRef?.current === false
? ''
: error
)
}
if (hasReportedValidity) {
if (
shouldReportValidityRef?.current === false ||
currentTarget.checkValidity()
) {
setHasReportedValidity(false)
}
} else {
setReportTimeoutHandle(
window.setTimeout(() => {
if (
shouldReportValidityRef?.current !== false &&
!currentTarget.reportValidity()
) {
setHasReportedValidity(true)
}
}, DEBOUNCE_MS)
)
}
}
}}
onBlur={
validate
? event => {
onBlur?.(event)
if (wasJustBlurred) {
setHasReportedValidity(false)
} else {
const currentTarget = event.currentTarget
if (shouldReportValidityRef?.current !== false) {
if (!currentTarget.reportValidity()) {
event.preventDefault()
}
}
setWasJustBlurred(true)
}
}
: onBlur
}
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full border w-full py-2"
/>
)
}

View File

@ -1,14 +1,13 @@
/** @file Registration confirmation page for when a user clicks the confirmation link set to their
* email address. */
/** @file Page to enter offlin mode and redirect to dashboard. */
import * as React from 'react'
import * as app from '../../components/app'
import * as authProvider from '../providers/auth'
import * as hooks from '../../hooks'
// ============================
// === Confirm Registration ===
// ============================
// ========================
// === EnterOfflineMode ===
// ========================
/** An empty component redirecting users based on the backend response to user registration. */
export default function EnterOfflineMode() {

View File

@ -1,4 +1,4 @@
/** @file Styled wrapper around FontAwesome icons. */
/** @file A fixed-size container for a {@link fontawesome.FontAwesomeIcon FontAwesomeIcon}. */
import * as React from 'react'
import * as fontawesome from '@fortawesome/react-fontawesome'
@ -16,13 +16,9 @@ export interface FontAwesomeIconProps {
/** A fixed-size container for a {@link fontawesome.FontAwesomeIcon FontAwesomeIcon}. */
export default function FontAwesomeIcon(props: FontAwesomeIconProps) {
return (
<span
className={
'absolute left-0 top-0 flex items-center justify-center h-full w-10 ' +
'text-blue-500'
}
>
<fontawesome.FontAwesomeIcon icon={props.icon} />
</span>
<fontawesome.FontAwesomeIcon
className="absolute left-0 top-0 text-blue-500 px-3 h-full w-4"
icon={props.icon}
/>
)
}

View File

@ -1,7 +1,6 @@
/** @file Container responsible for rendering and interactions in first half of forgot password
* flow. */
import * as React from 'react'
import * as router from 'react-router-dom'
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
import AtIcon from 'enso-assets/at.svg'
@ -9,10 +8,10 @@ import GoBackIcon from 'enso-assets/go_back.svg'
import * as app from '../../components/app'
import * as auth from '../providers/auth'
import SvgMask from './svgMask'
import Input from './input'
import SvgIcon from './svgIcon'
import Link from './link'
import SubmitButton from './submitButton'
// ======================
// === ForgotPassword ===
@ -25,72 +24,29 @@ export default function ForgotPassword() {
const [email, setEmail] = React.useState('')
return (
<div className="min-h-screen flex flex-col items-center justify-center">
<div className="flex flex-col bg-white shadow-md p-8 rounded-md w-full max-w-md">
<div className="font-medium self-center text-xl uppercase text-gray-800">
Forgot Your Password?
</div>
<div className="mt-10">
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
<form
className="flex flex-col gap-6 bg-frame-selected rounded-2xl shadow-md p-8 w-full max-w-md"
onSubmit={async event => {
event.preventDefault()
await forgotPassword(email)
}}
>
<div className="flex flex-col mb-6">
<label
htmlFor="email"
className="mb-1 text-xs tracking-wide text-gray-600"
>
E-Mail Address:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={AtIcon} />
</SvgIcon>
<div className="font-medium self-center text-xl">Forgot Your Password?</div>
<Input
id="email"
required
validate
type="email"
name="email"
autoComplete="email"
placeholder="E-Mail Address"
label="Email"
icon={AtIcon}
placeholder="Enter your email"
value={email}
setValue={setEmail}
/>
</div>
</div>
<div className="flex w-full">
<button
type="submit"
className={
'flex items-center justify-center focus:outline-none text-white text-sm ' +
'bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
'duration-150 ease-in'
}
>
<span className="mr-2 uppercase">Send link</span>
<span>
<SvgMask src={ArrowRightIcon} />
</span>
</button>
</div>
<SubmitButton text="Send link" icon={ArrowRightIcon} />
</form>
</div>
<div className="flex justify-center items-center mt-6">
<router.Link
to={app.LOGIN_PATH}
className={
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs ' +
'text-center'
}
>
<span>
<SvgMask src={GoBackIcon} />
</span>
<span className="ml-2">Go back to login</span>
</router.Link>
</div>
</div>
<Link to={app.LOGIN_PATH} icon={GoBackIcon} text="Go back to login" />
</div>
)
}

View File

@ -1,101 +1,37 @@
/** @file Styled input element. */
/** @file A styled input that includes an icon. */
import * as React from 'react'
// =================
// === Constants ===
// =================
/** The delay to wait before checking input validity. */
const DEBOUNCE_MS = 1000
import type * as controlledInput from './controlledInput'
import ControlledInput from './controlledInput'
import SvgIcon from './svgIcon'
// =============
// === Input ===
// =============
/** Props for an {@link Input}. */
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string
error?: string
validate?: boolean
setValue: (value: string) => void
shouldReportValidityRef?: React.MutableRefObject<boolean>
/** Props for a {@link Input}. */
export interface InputProps extends controlledInput.ControlledInputProps {
label: string | null
icon: string
footer?: React.ReactNode
}
/** A component for authentication from inputs, with preset styles. */
/** A styled input that includes an icon. */
export default function Input(props: InputProps) {
const {
setValue,
error,
validate = false,
shouldReportValidityRef,
onChange,
onBlur,
...passThrough
} = props
const [reportTimeoutHandle, setReportTimeoutHandle] = React.useState<number | null>(null)
const [hasReportedValidity, setHasReportedValidity] = React.useState(false)
const [wasJustBlurred, setWasJustBlurred] = React.useState(false)
return (
<input
{...passThrough}
onChange={event => {
onChange?.(event)
setValue(event.target.value)
setWasJustBlurred(false)
if (validate) {
if (reportTimeoutHandle != null) {
window.clearTimeout(reportTimeoutHandle)
}
const currentTarget = event.currentTarget
if (error != null) {
currentTarget.setCustomValidity('')
currentTarget.setCustomValidity(
currentTarget.checkValidity() ||
shouldReportValidityRef?.current === false
? ''
: error
const { label, icon, footer, ...passthrough } = props
const input = (
<div className="relative">
<SvgIcon src={icon} />
<ControlledInput {...passthrough} />
</div>
)
}
if (hasReportedValidity) {
if (
shouldReportValidityRef?.current === false ||
currentTarget.checkValidity()
) {
setHasReportedValidity(false)
}
} else {
setReportTimeoutHandle(
window.setTimeout(() => {
if (
shouldReportValidityRef?.current !== false &&
!currentTarget.reportValidity()
) {
setHasReportedValidity(true)
}
}, DEBOUNCE_MS)
)
}
}
}}
onBlur={
validate
? event => {
onBlur?.(event)
if (wasJustBlurred) {
setHasReportedValidity(false)
} else {
const currentTarget = event.currentTarget
if (shouldReportValidityRef?.current !== false) {
if (!currentTarget.reportValidity()) {
event.preventDefault()
}
}
setWasJustBlurred(true)
}
}
: onBlur
}
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-lg border border-gray-400 w-full py-2 focus:outline-none focus:border-blue-400"
/>
return label != null || footer != null ? (
<label className="flex flex-col gap-1">
{label}
{input}
{footer}
</label>
) : (
input
)
}

View File

@ -0,0 +1,30 @@
/** @file A styled colored link with an icon. */
import * as React from 'react'
import * as router from 'react-router-dom'
import SvgMask from './svgMask'
// ============
// === Link ===
// ============
/** Props for a {@link Link}. */
export interface LinkProps {
to: string
icon: string
text: string
}
/** A styled colored link with an icon. */
export default function Link(props: LinkProps) {
const { to, icon, text } = props
return (
<router.Link
to={to}
className="flex gap-2 items-center font-bold text-blue-500 hover:text-blue-700 text-xs text-center"
>
<SvgMask src={icon} />
{text}
</router.Link>
)
}

View File

@ -15,8 +15,8 @@ import * as validation from '../../dashboard/validation'
import * as app from '../../components/app'
import FontAwesomeIcon from './fontAwesomeIcon'
import Input from './input'
import SvgIcon from './svgIcon'
import SvgMask from './svgMask'
import Link from './link'
import SubmitButton from './submitButton'
// =================
// === Constants ===
@ -43,11 +43,10 @@ export default function Login() {
const shouldReportValidityRef = React.useRef(true)
return (
<div className="min-h-screen flex flex-col items-center justify-center">
<div className="flex flex-col bg-white shadow-md p-8 rounded-md w-full max-w-md">
<div className="font-medium self-center text-xl uppercase text-gray-800">
Login To Your Account
</div>
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
<div className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md">
<div className="font-medium self-center text-xl">Login to your account</div>
<div className="flex flex-col gap-6">
<button
onMouseDown={() => {
shouldReportValidityRef.current = false
@ -56,10 +55,10 @@ export default function Login() {
event.preventDefault()
await signInWithGoogle()
}}
className="relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200"
className="relative rounded-full bg-cloud/10 hover:bg-gray-200 py-2"
>
<FontAwesomeIcon icon={fontawesomeIcons.faGoogle} />
<span>Sign Up or Login with Google</span>
Sign up or login with Google
</button>
<button
onMouseDown={() => {
@ -69,20 +68,19 @@ export default function Login() {
event.preventDefault()
await signInWithGitHub()
}}
className="relative mt-6 border rounded-md py-2 text-sm text-gray-800 bg-gray-100 hover:bg-gray-200"
className="relative rounded-full bg-cloud/10 hover:bg-gray-200 py-2"
>
<FontAwesomeIcon icon={fontawesomeIcons.faGithub} />
<span>Sign Up or Login with Github</span>
Sign up or login with GitHub
</button>
<div className="relative mt-10 h-px bg-gray-300">
<div className="absolute left-0 top-0 flex justify-center w-full -mt-2">
<span className="bg-white px-4 text-xs text-gray-500 uppercase">
Or Login With Email
</span>
</div>
<div className="flex items-center gap-2">
<div className="grow border-t border-primary/30 h-0" />
<span className="text-xs self-center text-primary/60">or login with email</span>
<div className="grow border-t border-primary/30 h-0" />
</div>
<div className="mt-10">
<form
className="flex flex-col gap-6"
onSubmit={async event => {
event.preventDefault()
setIsSubmitting(true)
@ -91,109 +89,53 @@ export default function Login() {
setIsSubmitting(false)
}}
>
<div className="flex flex-col mb-6">
<label
htmlFor="email"
className="mb-1 text-xs tracking-wide text-gray-600"
>
E-Mail Address:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={AtIcon} />
</SvgIcon>
<Input
required
validate
id="email"
type="email"
name="email"
autoComplete="email"
placeholder="E-Mail Address"
label="Email"
icon={AtIcon}
placeholder="Enter your email"
value={email}
setValue={setEmail}
shouldReportValidityRef={shouldReportValidityRef}
/>
</div>
</div>
<div className="flex flex-col mb-6">
<label
htmlFor="password"
className="mb-1 text-xs tracking-wide text-gray-600"
>
Password:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="password"
type="password"
name="password"
autoComplete="current-password"
placeholder="Password"
label="Password"
icon={LockIcon}
placeholder="Enter your password"
pattern={validation.PASSWORD_PATTERN}
error={validation.PASSWORD_ERROR}
value={password}
setValue={setPassword}
shouldReportValidityRef={shouldReportValidityRef}
/>
</div>
</div>
<div className="flex items-center mb-6 -mt-4">
<div className="flex ml-auto">
footer={
<router.Link
to={app.FORGOT_PASSWORD_PATH}
className="inline-flex text-xs text-blue-500 hover:text-blue-700"
className="text-xs text-blue-500 hover:text-blue-700 text-end"
>
Forgot Your Password?
</router.Link>
</div>
</div>
<div className="flex w-full">
<button
disabled={isSubmitting}
type="submit"
className={
'flex items-center justify-center focus:outline-none text-white ' +
'text-sm bg-blue-600 hover:bg-blue-700 rounded py-2 w-full ' +
'transition duration-150 ease-in disabled:opacity-50'
}
>
<span className="mr-2 uppercase">Login</span>
<SvgMask src={ArrowRightIcon} />
</button>
</div>
/>
<SubmitButton disabled={isSubmitting} text="Login" icon={ArrowRightIcon} />
</form>
</div>
<div className="flex justify-center items-center mt-6">
<router.Link
<Link
to={app.REGISTRATION_PATH}
className={
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 ' +
'text-xs text-center'
}
>
<SvgMask src={CreateAccountIcon} />
<span className="ml-2">You don&apos;t have an account?</span>
</router.Link>
</div>
<div className="flex justify-center items-center mt-6">
<router.Link
icon={CreateAccountIcon}
text="Don't have an account?"
/>
<Link
to={app.ENTER_OFFLINE_MODE_PATH}
className={
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 ' +
'text-xs text-center'
}
>
<SvgMask src={ArrowRightIcon} />
<span className="ml-2">Continue without creating an account</span>
</router.Link>
</div>
</div>
icon={ArrowRightIcon}
text="Continue without creating an account"
/>
</div>
)
}

View File

@ -13,8 +13,8 @@ import * as validation from '../../dashboard/validation'
import * as app from '../../components/app'
import Input from './input'
import SvgIcon from './svgIcon'
import SvgMask from './svgMask'
import Link from './link'
import SubmitButton from './submitButton'
// =================
// === Constants ===
@ -39,18 +39,9 @@ export default function Registration() {
const { organizationId } = parseUrlSearchParams(location.search)
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8">
<div
className={
'rounded-md bg-white w-full max-w-sm sm:max-w-md border border-gray-200 ' +
'shadow-md px-4 py-6 sm:p-8'
}
>
<div className="font-medium self-center text-xl uppercase text-gray-800">
Create new account
</div>
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
<form
className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md"
onSubmit={async event => {
event.preventDefault()
setIsSubmitting(true)
@ -58,111 +49,47 @@ export default function Registration() {
setIsSubmitting(false)
}}
>
<div className="flex flex-col mb-4">
<label htmlFor="email" className="mb-1 text-xs tracking-wide text-gray-600">
E-Mail Address:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={AtIcon} />
</SvgIcon>
<div className="font-medium self-center text-xl">Create a new account</div>
<Input
required
validate
id="email"
type="email"
name="email"
autoComplete="email"
placeholder="E-Mail Address"
label="Email"
icon={AtIcon}
placeholder="Enter your email"
value={email}
setValue={setEmail}
/>
</div>
</div>
<div className="flex flex-col mb-4">
<label
htmlFor="password"
className="mb-1 text-xs tracking-wide text-gray-600"
>
Password:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="password"
type="password"
name="password"
autoComplete="new-password"
placeholder="Password"
label="Password"
icon={LockIcon}
placeholder="Enter your password"
pattern={validation.PASSWORD_PATTERN}
error={validation.PASSWORD_ERROR}
value={password}
setValue={setPassword}
/>
</div>
</div>
<div className="flex flex-col mb-4">
<label
htmlFor="password_confirmation"
className="mb-1 text-xs tracking-wide text-gray-600"
>
Confirm Password:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="password_confirmation"
type="password"
name="password_confirmation"
autoComplete="new-password"
placeholder="Confirm Password"
label="Confirm password"
icon={LockIcon}
placeholder="Confirm your password"
pattern={string.regexEscape(password)}
error={validation.CONFIRM_PASSWORD_ERROR}
value={confirmPassword}
setValue={setConfirmPassword}
/>
</div>
</div>
<div className="flex w-full mt-6">
<button
disabled={isSubmitting}
type="submit"
className={
'flex items-center justify-center focus:outline-none text-white text-sm ' +
'bg-indigo-600 hover:bg-indigo-700 rounded py-2 w-full transition ' +
'duration-150 ease-in disabled:opacity-50'
}
>
<span className="mr-2 uppercase">Register</span>
<span>
<SvgMask src={CreateAccountIcon} />
</span>
</button>
</div>
<SubmitButton disabled={isSubmitting} text="Register" icon={CreateAccountIcon} />
</form>
</div>
<div className="flex justify-center items-center mt-6">
<router.Link
to={app.LOGIN_PATH}
className={
'inline-flex items-center font-bold text-indigo-500 hover:text-indigo-700 ' +
'text-sm text-center'
}
>
<span>
<SvgMask src={GoBackIcon} />
</span>
<span className="ml-2">Already have an account?</span>
</router.Link>
</div>
<Link to={app.LOGIN_PATH} icon={GoBackIcon} text="Already have an account?" />
</div>
)
}

View File

@ -15,8 +15,8 @@ import * as string from '../../string'
import * as validation from '../../dashboard/validation'
import Input from './input'
import SvgIcon from './svgIcon'
import SvgMask from './svgMask'
import Link from './link'
import SubmitButton from './submitButton'
// =================
// === Constants ===
@ -53,148 +53,64 @@ export default function ResetPassword() {
}
return (
<div className="min-h-screen flex flex-col items-center justify-center">
<div className={'flex flex-col bg-white shadow-md p-8 rounded-md w-full ' + 'max-w-md'}>
<div className="font-medium self-center text-xl uppercase text-gray-800">
Reset Your Password
</div>
<div className="mt-10">
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
<form
className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md"
onSubmit={async event => {
event.preventDefault()
await onSubmit()
}}
>
<div className="flex flex-col mb-6">
<label
htmlFor="email"
className="mb-1 text-xs tracking-wide text-gray-600"
>
E-Mail Address:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={AtIcon} />
</SvgIcon>
<div className="font-medium self-center text-xl">Reset your password</div>
<Input
required
id="email"
type="email"
name="email"
autoComplete="email"
placeholder="E-Mail Address"
label="Email"
icon={AtIcon}
placeholder="Enter your email"
value={email}
setValue={setEmail}
/>
</div>
</div>
<div className="flex flex-col mb-6">
<label
htmlFor="code"
className="mb-1 text-xs tracking-wide text-gray-600"
>
Confirmation Code:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
id="code"
type="text"
name="code"
autoComplete="one-time-code"
placeholder="Confirmation Code"
label="Confirmation code"
icon={LockIcon}
placeholder="Enter the confirmation code"
value={code}
setValue={setCode}
/>
</div>
</div>
<div className="flex flex-col mb-6">
<label
htmlFor="new_password"
className="mb-1 text-xs tracking-wide text-gray-600"
>
New Password:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="new_password"
type="password"
name="new_password"
autoComplete="new-password"
placeholder="New Password"
label="New password"
icon={LockIcon}
placeholder="Enter your new password"
pattern={validation.PASSWORD_PATTERN}
error={validation.PASSWORD_ERROR}
value={newPassword}
setValue={setNewPassword}
/>
</div>
</div>
<div className="flex flex-col mb-6">
<label
htmlFor="new_password_confirm"
className="mb-1 text-xs tracking-wide text-gray-600"
>
Confirm New Password:
</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="new_password_confirm"
type="password"
name="new_password_confirm"
autoComplete="new-password"
placeholder="Confirm New Password"
label="Confirm new password"
icon={LockIcon}
placeholder="Confirm your new password"
pattern={string.regexEscape(newPassword)}
error={validation.CONFIRM_PASSWORD_ERROR}
value={newPasswordConfirm}
setValue={setNewPasswordConfirm}
/>
</div>
</div>
<div className="flex w-full">
<button
type="submit"
className={
'flex items-center justify-center focus:outline-none text-white text-sm ' +
'bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
'duration-150 ease-in'
}
>
<span className="mr-2 uppercase">Reset</span>
<span>
<SvgMask src={ArrowRightIcon} />
</span>
</button>
</div>
<SubmitButton text="Reset" icon={ArrowRightIcon} />
</form>
</div>
<div className="flex justify-center items-center mt-6">
<router.Link
to={app.LOGIN_PATH}
className={
'inline-flex items-center font-bold text-blue-500 hover:text-blue-700 text-xs ' +
'text-center'
}
>
<span>
<SvgMask src={GoBackIcon} />
</span>
<span className="ml-2">Go back to login</span>
</router.Link>
</div>
</div>
<Link to={app.LOGIN_PATH} icon={GoBackIcon} text="Go back to login" />
</div>
)
}

View File

@ -7,10 +7,9 @@ import AtIcon from 'enso-assets/at.svg'
import * as auth from '../providers/auth'
import * as backendProvider from '../../providers/backend'
import SvgMask from './svgMask'
import Input from './input'
import SvgIcon from './svgIcon'
import SubmitButton from './submitButton'
// ===================
// === SetUsername ===
@ -25,59 +24,29 @@ export default function SetUsername() {
const [username, setUsername] = React.useState('')
return (
<div className="min-h-screen flex flex-col items-center justify-center">
<div
data-testid="set-username-panel"
className={
'flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full ' +
'max-w-md'
}
>
<div className="font-medium self-center text-xl uppercase text-gray-800">
Set your username
</div>
<div className="mt-10">
<div className="flex flex-col gap-6 text-primary text-sm items-center justify-center min-h-screen">
<form
data-testid="set-username-panel"
className="flex flex-col gap-6 bg-frame-selected rounded-4xl shadow-md p-8 w-full max-w-md"
onSubmit={async event => {
event.preventDefault()
await authSetUsername(backend, username, email)
}}
>
<div className="flex flex-col mb-6">
<div className="relative">
<SvgIcon>
<SvgMask src={AtIcon} />
</SvgIcon>
<div className="font-medium self-center text-xl">Set your username</div>
<Input
id="username"
type="text"
name="username"
autoComplete="off"
placeholder="Username"
label={null}
icon={AtIcon}
placeholder="Enter your username"
value={username}
setValue={setUsername}
/>
</div>
</div>
<div className="flex w-full">
<button
type="submit"
className={
'flex items-center justify-center focus:outline-none text-white text-sm ' +
'bg-blue-600 hover:bg-blue-700 rounded py-2 w-full transition ' +
'duration-150 ease-in'
}
>
<span className="mr-2 uppercase">Set username</span>
<span>
<SvgMask src={ArrowRightIcon} />
</span>
</button>
</div>
<SubmitButton text="Set username" icon={ArrowRightIcon} />
</form>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,30 @@
/** @file A styled submit button. */
import * as React from 'react'
import SvgMask from './svgMask'
// ====================
// === SubmitButton ===
// ====================
/** Props for a {@link SubmitButton}. */
export interface SubmitButtonProps {
disabled?: boolean
text: string
icon: string
}
/** A styled submit button. */
export default function SubmitButton(props: SubmitButtonProps) {
const { disabled = false, text, icon } = props
return (
<button
disabled={disabled}
type="submit"
className="flex gap-2 items-center justify-center focus:outline-none text-white bg-blue-600 hover:bg-blue-700 rounded-full py-2 w-full transition duration-150 ease-in disabled:opacity-50"
>
{text}
<SvgMask src={icon} />
</button>
)
}

View File

@ -1,20 +1,23 @@
/** @file Styled wrapper around SVG images. */
import * as React from 'react'
import SvgMask from './svgMask'
// ===============
// === SvgIcon ===
// ===============
/** Props for a {@link SvgIcon}. */
export interface SvgIconProps extends React.PropsWithChildren {}
export interface SvgIconProps {
src: string
}
/** A fixed-size container for a SVG image. */
export default function SvgIcon(props: SvgIconProps) {
const { children } = props
const { src } = props
return (
<div className="inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 text-gray-400">
{children}
<SvgMask src={src} />
</div>
)
}

View File

@ -1317,7 +1317,7 @@ export default function AssetsTable(props: AssetsTableProps) {
<div className="flex flex-col w-min min-w-full h-full">
{backend.type !== backendModule.BackendType.local && (
<div className="sticky top-0 h-0">
<div className="block sticky right-0 ml-auto w-29 p-2 z-1">
<div className="block sticky right-0 ml-auto w-29 px-2 pt-2.25 pb-1.75 z-1">
<div className="inline-flex gap-3">
{columnModule.EXTRA_COLUMNS.map(column => (
<Button

View File

@ -1,4 +1,4 @@
/** @file Managing the logic and displaying the UI for the password change function. */
/** @file A modal for changing the user's password. */
import * as React from 'react'
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
@ -9,14 +9,13 @@ import * as modalProvider from '../../providers/modal'
import * as string from '../../string'
import * as validation from '../validation'
import Input from './input'
import Input from '../../authentication/components/input'
import Modal from './modal'
import SvgIcon from './svgIcon'
import SvgMask from '../../authentication/components/svgMask'
import SubmitButton from '../../authentication/components/submitButton'
// ==========================
// === ResetPasswordModal ===
// ==========================
// ===========================
// === ChangePasswordModal ===
// ===========================
/** A modal for changing the user's password. */
export default function ChangePasswordModal() {
@ -30,17 +29,9 @@ export default function ChangePasswordModal() {
return (
<Modal centered className="bg-dim">
<div
data-testid="change-password-modal"
className="flex flex-col bg-frame-selected backdrop-blur-3xl rounded-2xl p-8 w-full max-w-md"
onClick={event => {
event.stopPropagation()
}}
>
<div className="self-center text-xl">Change Your Password</div>
<div className="mt-10">
<form
className="flex flex-col gap-6"
data-testid="change-password-modal"
className="flex flex-col gap-6 bg-frame-selected backdrop-blur-3xl rounded-2xl p-8 w-full max-w-md"
onSubmit={async event => {
event.preventDefault()
setIsSubmitting(true)
@ -50,13 +41,11 @@ export default function ChangePasswordModal() {
unsetModal()
}
}}
onClick={event => {
event.stopPropagation()
}}
>
<div className="flex flex-col gap-1">
<label htmlFor="old_password">Old Password:</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<div className="self-center text-xl">Change your password</div>
<Input
autoFocus
required
@ -65,70 +54,45 @@ export default function ChangePasswordModal() {
type="password"
name="old_password"
autoComplete="current-password"
placeholder="Old Password"
label="Old password"
icon={LockIcon}
placeholder="Enter your old password"
pattern={validation.PASSWORD_PATTERN}
error={validation.PASSWORD_ERROR}
value={oldPassword}
setValue={setOldPassword}
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-2xl w-full py-2 focus:outline-none focus:border-blue-400"
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="new_password">New Password:</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="new_password"
type="password"
name="new_password"
autoComplete="new-password"
placeholder="New Password"
label="New password"
icon={LockIcon}
placeholder="Enter your new password"
pattern={validation.PASSWORD_PATTERN}
error={validation.PASSWORD_ERROR}
value={newPassword}
setValue={setNewPassword}
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="confirm_new_password">Confirm New Password:</label>
<div className="relative">
<SvgIcon>
<SvgMask src={LockIcon} />
</SvgIcon>
<Input
required
validate
id="confirm_new_password"
type="password"
name="confirm_new_password"
autoComplete="new-password"
placeholder="Confirm New Password"
label="Confirm new password"
icon={LockIcon}
placeholder="Confirm your new password"
pattern={string.regexEscape(newPassword)}
error={validation.CONFIRM_PASSWORD_ERROR}
value={confirmNewPassword}
setValue={setConfirmNewPassword}
className="text-sm placeholder-gray-500 pl-10 pr-4 rounded-full w-full py-2"
/>
</div>
</div>
<button
disabled={isSubmitting}
type="submit"
className="flex items-center justify-center text-white text-sm bg-cloud rounded-full gap-2 h-10 disabled:opacity-50"
>
Reset
<SvgMask src={ArrowRightIcon} />
</button>
<SubmitButton disabled={isSubmitting} text="Reset" icon={ArrowRightIcon} />
</form>
</div>
</div>
</Modal>
)
}

View File

@ -777,9 +777,7 @@ export default function Chat(props: ChatProps) {
/>
))}
</div>
<div className="rounded-2xl bg-white p-1 mx-2 my-1">
<form onSubmit={sendCurrentMessage}>
<div>
<form className="rounded-2xl bg-white p-1 mx-2 my-1" onSubmit={sendCurrentMessage}>
<textarea
ref={messageInputRef}
rows={1}
@ -839,9 +837,7 @@ export default function Chat(props: ChatProps) {
Reply!
</button>
</div>
</div>
</form>
</div>
{!isPaidUser && (
<button
className="text-left leading-5 rounded-2xl bg-call-to-action text-white p-2 mx-2 my-1"

View File

@ -38,20 +38,18 @@ export default function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
return (
<Modal centered className="bg-dim">
<div
<form
data-testid="confirm-delete-modal"
ref={element => {
element?.focus()
}}
tabIndex={-1}
className="relative rounded-2xl pointer-events-auto before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
className="relative flex flex-col gap-2 rounded-2xl w-96 px-4 p-2 pointer-events-auto before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
onKeyDown={event => {
if (event.key !== 'Escape') {
event.stopPropagation()
}
}}
>
<form
onClick={event => {
event.stopPropagation()
}}
@ -61,10 +59,9 @@ export default function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
// delete an important asset.
onSubmit()
}}
className="relative flex flex-col rounded-2xl gap-2 w-96 px-4 p-2"
>
<div>Are you sure you want to delete {description}?</div>
<div className="flex gap-2">
<div className="relative">Are you sure you want to delete {description}?</div>
<div className="relative flex gap-2">
<button
type="submit"
className="hover:cursor-pointer inline-block text-white bg-delete rounded-full px-4 py-1"
@ -80,7 +77,6 @@ export default function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
</button>
</div>
</form>
</div>
</Modal>
)
}

View File

@ -1,80 +0,0 @@
/** @file Input element with default event handlers. */
import * as React from 'react'
// =================
// === Constants ===
// =================
/** The delay to wait before checking input validity. */
const DEBOUNCE_MS = 1000
// =============
// === Input ===
// =============
/** Props for an `<input>` HTML element. */
type InputAttributes = JSX.IntrinsicElements['input']
/** Props for an {@link Input}. */
export interface InputProps extends InputAttributes {
value: string
error?: string
validate?: boolean
setValue: (value: string) => void
}
/** A component for authentication form inputs, with preset styles. */
export default function Input(props: InputProps) {
const { setValue, error, validate = false, onChange, onBlur, ...passthroughProps } = props
const [reportTimeoutHandle, setReportTimeoutHandle] = React.useState<number | null>(null)
const [hasReportedValidity, setHasReportedValidity] = React.useState(false)
const [wasJustBlurred, setWasJustBlurred] = React.useState(false)
return (
<input
{...passthroughProps}
onChange={event => {
onChange?.(event)
setValue(event.target.value)
setWasJustBlurred(false)
if (validate) {
if (reportTimeoutHandle != null) {
window.clearTimeout(reportTimeoutHandle)
}
const currentTarget = event.currentTarget
if (error != null) {
currentTarget.setCustomValidity('')
currentTarget.setCustomValidity(currentTarget.checkValidity() ? '' : error)
}
if (hasReportedValidity) {
if (currentTarget.checkValidity()) {
setHasReportedValidity(false)
}
} else {
setReportTimeoutHandle(
window.setTimeout(() => {
if (!currentTarget.reportValidity()) {
setHasReportedValidity(true)
}
}, DEBOUNCE_MS)
)
}
}
}}
onBlur={event => {
onBlur?.(event)
if (validate) {
if (wasJustBlurred) {
setHasReportedValidity(false)
} else {
const currentTarget = event.currentTarget
setTimeout(() => {
currentTarget.reportValidity()
}, 0)
}
}
setWasJustBlurred(true)
getSelection()?.empty()
}}
/>
)
}

View File

@ -117,19 +117,11 @@ export default function ManageLabelsModal<
}}
>
<div className="absolute bg-frame-selected backdrop-blur-3xl rounded-2xl h-full w-full" />
<div className="relative flex flex-col rounded-2xl gap-2 p-2">
<div>
<h2 className="text-sm font-bold">Labels</h2>
{/* Space reserved for other tabs. */}
</div>
<form
className="flex gap-1"
className="relative flex flex-col gap-1 rounded-2xl gap-2 p-2"
onSubmit={async event => {
event.preventDefault()
setLabels(oldLabels => [
...oldLabels,
backendModule.LabelName(query),
])
setLabels(oldLabels => [...oldLabels, backendModule.LabelName(query)])
try {
if (color != null) {
await doCreateLabel(query, color)
@ -143,6 +135,10 @@ export default function ManageLabelsModal<
unsetModal()
}}
>
<div>
<h2 className="text-sm font-bold">Labels</h2>
{/* Space reserved for other tabs. */}
</div>
<div
className={`flex items-center grow rounded-full border border-black-a10 gap-2 px-1 ${
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
@ -154,8 +150,7 @@ export default function ManageLabelsModal<
!canSelectColor || color == null
? {}
: {
backgroundColor:
backendModule.lChColorToCssColor(color),
backgroundColor: backendModule.lChColorToCssColor(color),
}
}
>
@ -176,7 +171,6 @@ export default function ManageLabelsModal<
>
<div className="h-6 py-0.5">Create</div>
</button>
</form>
{canSelectColor && (
<div className="flex gap-1">
<div className="grow flex items-center gap-1">
@ -201,7 +195,7 @@ export default function ManageLabelsModal<
</div>
))}
</div>
</div>
</form>
</div>
</Modal>
)

View File

@ -40,16 +40,14 @@ export default function NewDataConnectorModal(props: NewDataConnectorModalProps)
return (
<Modal centered className="bg-dim">
<div
<form
tabIndex={-1}
className="relative rounded-2xl pointer-events-auto before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
className="relative flex flex-col gap-2 rounded-2xl w-96 p-4 pt-2 pointer-events-auto before:inset-0 before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
onKeyDown={event => {
if (event.key !== 'Escape') {
event.stopPropagation()
}
}}
>
<form
onClick={event => {
event.stopPropagation()
}}
@ -59,10 +57,9 @@ export default function NewDataConnectorModal(props: NewDataConnectorModalProps)
// delete an important asset.
onSubmit()
}}
className="relative flex flex-col rounded-2xl gap-2 w-96 px-4 py-2"
>
<h1 className="text-sm font-semibold">New Data Connector</h1>
<div className="flex">
<h1 className="relative text-sm font-semibold">New Data Connector</h1>
<div className="relative flex">
<div className="w-12 h-6 py-1">Name</div>
<input
autoFocus
@ -73,7 +70,7 @@ export default function NewDataConnectorModal(props: NewDataConnectorModalProps)
}}
/>
</div>
<div className="flex">
<div className="relative flex">
<div className="w-12 h-6 py-1">Value</div>
<input
placeholder="Enter the value of the data connector"
@ -83,7 +80,7 @@ export default function NewDataConnectorModal(props: NewDataConnectorModalProps)
}}
/>
</div>
<div className="flex gap-2">
<div className="relative flex gap-2">
<button
disabled={!canSubmit}
type="submit"
@ -100,7 +97,6 @@ export default function NewDataConnectorModal(props: NewDataConnectorModalProps)
</button>
</div>
</form>
</div>
</Modal>
)
}

View File

@ -47,22 +47,19 @@ export default function NewLabelModal(props: NewLabelModalProps) {
return (
<Modal className="absolute bg-dim">
<div
<form
data-testid="new-label-modal"
tabIndex={-1}
style={{
left: position.left + window.scrollX,
top: position.top + window.scrollY,
}}
className="relative rounded-2xl pointer-events-auto w-80"
className="relative flex flex-col gap-2 rounded-2xl pointer-events-auto w-80 p-4 pt-2 before:inset-0 before:absolute before:rounded-2xl before:bg-frame-selected before:backdrop-blur-3xl before:w-full before:h-full"
onKeyDown={event => {
if (event.key !== 'Escape') {
event.stopPropagation()
}
}}
>
<div className="absolute rounded-2xl bg-frame-selected backdrop-blur-3xl w-full h-full" />
<form
onClick={event => {
event.stopPropagation()
}}
@ -72,10 +69,9 @@ export default function NewLabelModal(props: NewLabelModalProps) {
// delete an important asset.
onSubmit()
}}
className="relative flex flex-col rounded-2xl gap-2 w-80 px-4 py-2"
>
<h1 className="text-sm font-semibold">New Label</h1>
<label className="flex">
<h1 className="relative text-sm font-semibold">New Label</h1>
<label className="relative flex">
<div className="w-12 h-6 py-1">Name</div>
<input
autoFocus
@ -99,7 +95,7 @@ export default function NewLabelModal(props: NewLabelModalProps) {
/>
</label>
<label
className="flex"
className="relative flex"
onClick={event => {
event.preventDefault()
}}
@ -109,7 +105,7 @@ export default function NewLabelModal(props: NewLabelModalProps) {
<ColorPicker setColor={setColor} />
</div>
</label>
<div className="flex gap-2">
<div className="relative flex gap-2">
<button
disabled={!canSubmit}
type="submit"
@ -126,7 +122,6 @@ export default function NewLabelModal(props: NewLabelModalProps) {
</button>
</div>
</form>
</div>
</Modal>
)
}

View File

@ -1,20 +0,0 @@
/** @file Styled wrapper around SVG images. */
import * as React from 'react'
// ===============
// === SvgIcon ===
// ===============
/** Props for a {@link SvgIcon}. */
export interface SvgIconProps extends React.PropsWithChildren {}
/** A fixed-size container for a SVG image. */
export default function SvgIcon(props: SvgIconProps) {
const { children } = props
return (
<div className="inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 text-gray-400">
{children}
</div>
)
}

View File

@ -69,6 +69,9 @@ export const theme = {
xl: '1.1875rem',
'4xl': '2.375rem',
},
borderRadius: {
'4xl': '2rem',
},
lineHeight: {
'144.5': '144.5%',
'170': '170%',

View File

@ -20,37 +20,37 @@ export const VALID_EMAIL = 'email@example.com'
/** Find an email input (if any) on the current page. */
export function locateEmailInput(page: test.Locator | test.Page) {
return page.getByLabel('E-Mail Address:')
return page.getByLabel('Email')
}
/** Find a password input (if any) on the current page. */
export function locatePasswordInput(page: test.Locator | test.Page) {
return page.getByLabel('Password:', { exact: true })
return page.getByPlaceholder('Enter your password')
}
/** Find a "confirm password" input (if any) on the current page. */
export function locateConfirmPasswordInput(page: test.Locator | test.Page) {
return page.getByLabel('Confirm Password:')
return page.getByLabel('Confirm password')
}
/** Find an "old password" input (if any) on the current page. */
export function locateOldPasswordInput(page: test.Locator | test.Page) {
return page.getByLabel('Old Password:')
return page.getByLabel('Old password')
}
/** Find a "new password" input (if any) on the current page. */
export function locateNewPasswordInput(page: test.Locator | test.Page) {
return page.getByLabel('New Password:', { exact: true })
return page.getByPlaceholder('Enter your new password')
}
/** Find a "confirm new password" input (if any) on the current page. */
export function locateConfirmNewPasswordInput(page: test.Locator | test.Page) {
return page.getByLabel('Confirm New Password:')
return page.getByPlaceholder('Confirm your new password')
}
/** Find a "username" input (if any) on the current page. */
export function locateUsernameInput(page: test.Locator | test.Page) {
return page.getByPlaceholder('Username')
return page.getByPlaceholder('Enter your username')
}
/** Find a "name" input for a "new label" modal (if any) on the current page. */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB