[web] Create a first implementation of the CIAM version of the email confirmation page (#29432)

* Move `main#main-content.content.content-alt` into a React component (`ContentLayout`)

* Create the CIAM variant of `RegistrationConfirmEmailForm`

* `bin/run web npm run extract-translations`

* Use `CiamLayout` in the Storybook demo

* Add `SplitTestProvider` in tests

* Fix Storybook: Wrap `RegistrationConfirmEmailForm` Story in `onboarding-confirm-email`

* Refactor SCSS files:

- only imports in all.scss
- split .storybook-layout and .storybook-enabled
- extract ciam-spacing.scss

GitOrigin-RevId: f4a214a0978423a1621dd8f60bf459af7b8f877e
This commit is contained in:
Antoine Clausse
2025-11-04 12:54:04 +01:00
committed by Copybot
parent a4d9d5789a
commit 2ebc411db4
12 changed files with 237 additions and 125 deletions

View File

@@ -12,6 +12,11 @@ export const defaultSplitTestsArgTypes = {
},
options: ['enabled'],
},
uniaccessphase1: {
description: 'Enable CIAM designs',
control: { type: 'select' as const },
options: ['default', 'enabled'],
},
}
export const withSplitTests = <ArgTypes = typeof defaultSplitTestsArgTypes,>(

View File

@@ -2086,6 +2086,7 @@
"us_gov_banner_government_purchasing": "",
"us_gov_banner_small_business_reseller": "",
"usage_metrics": "",
"use_a_different_email": "",
"use_a_different_password": "",
"use_saml_metadata_to_configure_sso_with_idp": "",
"use_your_own_machine": "",
@@ -2114,6 +2115,7 @@
"vat": "",
"vat_number": "",
"verify_email_address_before_enabling_managed_users": "",
"verify_your_email_address": "",
"view": "",
"view_all": "",
"view_audit_logs_group_subtext": "",

View File

@@ -2,7 +2,7 @@ import { postJSON } from '@/infrastructure/fetch-json'
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import Notification from '@/shared/components/notification'
import getMeta from '@/utils/meta'
import { FormEvent, MouseEventHandler, ReactNode, useState } from 'react'
import { FormEvent, MouseEventHandler, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import LoadingSpinner from '@/shared/components/loading-spinner'
import MaterialIcon from '@/shared/components/material-icon'
@@ -27,8 +27,9 @@ type ConfirmEmailFormProps = {
onSuccessfulConfirmation?: () => void
interstitial: boolean
isModal?: boolean
onCancel?: () => void
onCancel?: MouseEventHandler<HTMLButtonElement>
outerError?: string
isCiam?: boolean
}
export function ConfirmEmailForm({
@@ -43,6 +44,7 @@ export function ConfirmEmailForm({
isModal,
onCancel,
outerError,
isCiam,
}: ConfirmEmailFormProps) {
const { t } = useTranslation()
const [confirmationCode, setConfirmationCode] = useState('')
@@ -163,20 +165,6 @@ export function ConfirmEmailForm({
)
}
let intro: ReactNode | null = (
<h5 className="h5">{t('confirm_your_email')}</h5>
)
if (isModal)
intro = outerErrorDisplay ? (
<div className="mt-4" />
) : (
<h3 className="h5">{outerErrorDisplay ? null : t('we_sent_code')}</h3>
)
if (interstitial)
intro = (
<h1 className="h3 interstitial-header">{t('confirm_your_email')}</h1>
)
return (
<form
onSubmit={submitHandler}
@@ -196,7 +184,12 @@ export function ConfirmEmailForm({
/>
)}
{intro}
<Title
isModal={isModal}
interstitial={interstitial}
isCiam={isCiam}
outerErrorDisplay={outerErrorDisplay}
/>
<OLFormLabel htmlFor="one-time-code">
{isModal
@@ -260,6 +253,30 @@ export function ConfirmEmailForm({
)
}
function Title({
isModal,
interstitial,
outerErrorDisplay,
isCiam,
}: {
isModal?: boolean
interstitial: boolean
isCiam?: boolean
outerErrorDisplay: string | null
}) {
const { t } = useTranslation()
if (isCiam) return <h1>{t('verify_your_email_address')}</h1>
if (isModal)
return outerErrorDisplay ? (
<div className="mt-4" />
) : (
<h3 className="h5">{outerErrorDisplay ? null : t('we_sent_code')}</h3>
)
if (interstitial)
return <h1 className="h3 interstitial-header">{t('confirm_your_email')}</h1>
return <h5 className="h5">{t('confirm_your_email')}</h5>
}
function ConfirmEmailSuccessfullForm({
successMessage,
successButtonText,

View File

@@ -0,0 +1,26 @@
import React, { FC, ReactNode } from 'react'
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
type Props = { children: ReactNode }
const CiamLayout: FC<Props> = ({ children }: Props) => (
<div className="ciam-layout ciam-enabled">
<a
href="/"
aria-label="Overleaf"
className="brand"
style={{ backgroundImage: `url("${overleafLogo}")` }}
/>
<div className="ciam-container">
<main className="ciam-card" id="main-content">
{children}
</main>
</div>
<footer>
<a href="https://www.overleaf.com/legal#Privacy">Privacy</a>
<a href="https://www.overleaf.com/legal#Terms">Terms</a>
</footer>
</div>
)
export default CiamLayout

View File

@@ -0,0 +1,16 @@
import React, { FC, ReactNode } from 'react'
type Props = { children: ReactNode; isMain?: boolean; alt?: boolean }
const ContentLayout: FC<Props> = ({ children, isMain, alt }: Props) => {
const className = alt ? 'content content-alt' : 'content'
return isMain ? (
<main className={className} id="main-content">
{children}
</main>
) : (
<div className={className}>{children}</div>
)
}
export default ContentLayout

View File

@@ -5,7 +5,7 @@ import OLPageContentCard from '@/shared/components/ol/ol-page-content-card'
import OLRow from '@/shared/components/ol/ol-row'
import OLCol from '@/shared/components/ol/ol-col'
import OLButton from '@/shared/components/ol/ol-button'
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
import CiamLayout from '@/shared/components/layouts/ciam-layout'
const lorem = (n: number) => {
const quacks = ['quack', 'quack', 'quack', 'quak']
@@ -249,27 +249,13 @@ export const CompleteRegistration = () => (
)
export const Ciam = () => (
<div className="ciam-layout">
<a
href="/"
aria-label="Overleaf"
className="brand"
style={{ backgroundImage: `url("${overleafLogo}")` }}
/>
<div className="ciam-container">
<main className="ciam-card" id="main-content">
<h1>Create your Overleaf account</h1>
<p>{lorem(20)}</p>
<hr />
<p>{lorem(20)}</p>
<OLButton>Button</OLButton>
</main>
</div>
<footer>
<a href="https://www.overleaf.com/legal#Privacy">Privacy</a>
<a href="https://www.overleaf.com/legal#Terms">Terms</a>
</footer>
</div>
<CiamLayout>
<h1>Create your Overleaf account</h1>
<p>{lorem(20)}</p>
<hr />
<p>{lorem(20)}</p>
<OLButton>Button</OLButton>
</CiamLayout>
)
export default {

View File

@@ -1,70 +1,5 @@
@import 'ciam-variables';
@import 'ciam-colors';
@import 'ciam-layout';
@import 'ciam-mixins';
.ciam-layout {
padding: var(--ciam-spacing-350);
display: flex;
min-height: 100%;
flex-direction: column;
font-family: var(--ciam-font-family-sans), sans-serif;
color: var(--ciam-color-text-primary);
font-size: var(--ciam-font-size-400);
line-height: 1.5;
@include ciam-body-md-regular;
.ciam-container {
flex: 1 1 auto;
}
.brand {
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
height: 64px;
width: 130px;
margin: var(--ciam-spacing-350) auto;
display: block;
@include media-breakpoint-up(sm) {
margin: var(--ciam-spacing-350) var(--ciam-spacing-800);
}
}
h1 {
@include ciam-heading-sm-semibold;
}
.ciam-card {
box-shadow:
0 4px 6px -4px rgb(0 0 0 / 10%),
0 1px 29px -3px rgb(0 0 0 / 16%);
padding: var(--ciam-spacing-800) var(--ciam-spacing-400);
border-radius: var(--ciam-border-radius-400);
max-width: 460px;
margin: var(--ciam-spacing-400) auto;
@include media-breakpoint-up(sm) {
padding: var(--ciam-spacing-1300);
}
}
footer {
display: flex;
gap: var(--ciam-spacing-600);
text-transform: uppercase;
justify-content: center;
margin: var(--ciam-spacing-350) auto;
@include media-breakpoint-up(sm) {
margin: var(--ciam-spacing-350) var(--ciam-spacing-800);
justify-content: start;
}
a {
text-decoration: none;
@include ciam-body-sm-regular;
}
}
}
@import 'ciam-spacing';
@import 'ciam-variables';

View File

@@ -1,4 +1,4 @@
.ciam-layout {
.ciam-enabled {
--ciam-color-neutral-50: #fafafa;
--ciam-color-neutral-100: #f2f2f2;
--ciam-color-neutral-200: #e6e6e6;

View File

@@ -0,0 +1,72 @@
@import 'ciam-mixins';
.ciam-layout {
padding: var(--ciam-spacing-350);
display: flex;
min-height: 100vh;
flex-direction: column;
@include ciam-body-md-regular;
}
.ciam-enabled {
font-family: var(--ciam-font-family-sans), sans-serif;
color: var(--ciam-color-text-primary);
font-size: var(--ciam-font-size-400);
line-height: 1.5;
.ciam-container {
flex: 1 1 auto;
}
.brand {
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
height: 64px;
width: 130px;
margin: var(--ciam-spacing-350) auto;
display: block;
@include media-breakpoint-up(sm) {
margin: var(--ciam-spacing-350) var(--ciam-spacing-800);
}
}
h1 {
@include ciam-heading-sm-semibold;
}
.ciam-card {
box-shadow:
0 4px 6px -4px rgb(0 0 0 / 10%),
0 1px 29px -3px rgb(0 0 0 / 16%);
padding: var(--ciam-spacing-800) var(--ciam-spacing-400);
border-radius: var(--ciam-border-radius-400);
max-width: 460px;
margin: var(--ciam-spacing-400) auto;
@include media-breakpoint-up(sm) {
padding: var(--ciam-spacing-1300);
}
}
footer {
display: flex;
gap: var(--ciam-spacing-600);
text-transform: uppercase;
justify-content: center;
margin: var(--ciam-spacing-350) auto;
@include media-breakpoint-up(sm) {
margin: var(--ciam-spacing-350) var(--ciam-spacing-800);
justify-content: start;
}
a {
text-decoration: none;
@include ciam-body-sm-regular;
}
}
}

View File

@@ -0,0 +1,66 @@
.ciam-enabled {
--ciam-font-weight-regular: 400;
--ciam-font-weight-medium: 500;
--ciam-font-weight-semibold: 600;
--ciam-font-weight-bold: 700;
--ciam-font-family-sans: inter, sans-serif;
--ciam-spacing-50: 2px;
--ciam-spacing-100: 4px;
--ciam-spacing-150: 6px;
--ciam-spacing-200: 8px;
--ciam-spacing-250: 10px;
--ciam-spacing-300: 12px;
--ciam-spacing-350: 14px;
--ciam-spacing-400: 16px;
--ciam-spacing-500: 20px;
--ciam-spacing-600: 24px;
--ciam-spacing-700: 28px;
--ciam-spacing-800: 32px;
--ciam-spacing-900: 36px;
--ciam-spacing-1000: 40px;
--ciam-spacing-1100: 44px;
--ciam-spacing-1200: 48px;
--ciam-spacing-1300: 52px;
--ciam-spacing-1400: 56px;
--ciam-spacing-1500: 60px;
--ciam-spacing-1600: 64px;
--ciam-spacing-1700: 68px;
--ciam-spacing-1800: 72px;
--ciam-spacing-1900: 76px;
--ciam-spacing-2000: 80px;
--ciam-spacing-2100: 84px;
--ciam-spacing-2200: 88px;
--ciam-spacing-2300: 92px;
--ciam-spacing-2400: 96px;
--ciam-base-unit: 4px;
--ciam-font-size-300: 12px;
--ciam-font-size-350: 14px;
--ciam-font-size-400: 16px;
--ciam-font-size-450: 18px;
--ciam-font-size-500: 20px;
--ciam-font-size-600: 24px;
--ciam-font-size-700: 28px;
--ciam-font-size-800: 32px;
--ciam-font-size-1000: 40px;
--ciam-font-size-1400: 56px;
--ciam-font-size-1800: 72px;
--ciam-font-line-height-400: 16px;
--ciam-font-line-height-500: 20px;
--ciam-font-line-height-600: 24px;
--ciam-font-line-height-700: 28px;
--ciam-font-line-height-800: 32px;
--ciam-font-line-height-900: 36px;
--ciam-font-line-height-1000: 40px;
--ciam-font-line-height-1200: 48px;
--ciam-font-line-height-1600: 64px;
--ciam-font-line-height-1800: 72px;
--ciam-border-width-25: 1px;
--ciam-border-width-50: 2px;
--ciam-border-radius-50: 2px;
--ciam-border-radius-100: 4px;
--ciam-border-radius-200: 8px;
--ciam-border-radius-300: 12px;
--ciam-border-radius-400: 16px;
--ciam-border-radius-600: 24px;
--ciam-border-radius-full: 9999px;
}

View File

@@ -1,24 +1,9 @@
@import 'ciam-mixins';
@import 'ciam-colors';
// TODO: Replace `fuchsia` by the correct colors.
.ciam-layout {
// Spacings
--ciam-spacing-200: 8px;
--ciam-spacing-250: 10px;
--ciam-spacing-350: 12px;
--ciam-spacing-400: 16px;
--ciam-spacing-600: 24px; // TODO: confirm this variable name (couldn't find in design system)
--ciam-spacing-800: 32px; // TODO: confirm this variable name (couldn't find in design system)
--ciam-spacing-1300: 52px;
.ciam-enabled {
// Base variables
--ciam-color-text-secondary: var(--ciam-color-neutral-800);
--ciam-color-text-primary: var(--ciam-color-neutral-900);
--ciam-border-radius-200: var(--ciam-spacing-300);
--ciam-border-radius-400: var(--ciam-spacing-400);
--ciam-font-family-sans: 'Inter', sans-serif;
// Links
// used in services/web/frontend/stylesheets/base/links.scss

View File

@@ -2617,6 +2617,7 @@
"us_gov_banner_government_purchasing": "<0>Get __appName__ for US federal government. </0>Move faster through procurement with our tailored purchasing options. Talk to our government team.",
"us_gov_banner_small_business_reseller": "<0>Easy procurement for US federal government. </0>We partner with small business resellers to help you buy Overleaf organizational plans. Talk to our government team.",
"usage_metrics": "Usage metrics",
"use_a_different_email": "Use a <0>different email</0>.",
"use_a_different_password": "Please use a different password",
"use_saml_metadata_to_configure_sso_with_idp": "Use the Overleaf SAML metadata to configure SSO with your Identity Provider.",
"use_your_own_machine": "Use your own machine, with your own setup",
@@ -2652,6 +2653,7 @@
"vat": "VAT",
"vat_number": "VAT Number",
"verify_email_address_before_enabling_managed_users": "You need to verify your email address before enabling managed users.",
"verify_your_email_address": "Verify your email address",
"view": "View",
"view_all": "View all",
"view_audit_logs_group_subtext": "View and download audit logs for your group subscription",