From 2ebc411db4fe513b8c985e536e86761d7b6fa6ac Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Tue, 4 Nov 2025 12:54:04 +0100 Subject: [PATCH] [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 --- .../web/.storybook/utils/with-split-tests.tsx | 5 ++ .../web/frontend/extracted-translations.json | 2 + .../components/emails/confirm-email-form.tsx | 51 ++++++++----- .../shared/components/layouts/ciam-layout.tsx | 26 +++++++ .../components/layouts/content-layout.tsx | 16 ++++ .../frontend/stories/page-layouts.stories.tsx | 30 ++------ .../web/frontend/stylesheets/ciam/all.scss | 73 +------------------ .../stylesheets/ciam/ciam-colors.scss | 2 +- .../stylesheets/ciam/ciam-layout.scss | 72 ++++++++++++++++++ .../stylesheets/ciam/ciam-spacing.scss | 66 +++++++++++++++++ .../stylesheets/ciam/ciam-variables.scss | 17 +---- services/web/locales/en.json | 2 + 12 files changed, 237 insertions(+), 125 deletions(-) create mode 100644 services/web/frontend/js/shared/components/layouts/ciam-layout.tsx create mode 100644 services/web/frontend/js/shared/components/layouts/content-layout.tsx create mode 100644 services/web/frontend/stylesheets/ciam/ciam-layout.scss create mode 100644 services/web/frontend/stylesheets/ciam/ciam-spacing.scss diff --git a/services/web/.storybook/utils/with-split-tests.tsx b/services/web/.storybook/utils/with-split-tests.tsx index 083b3d49ca..da25e74cfb 100644 --- a/services/web/.storybook/utils/with-split-tests.tsx +++ b/services/web/.storybook/utils/with-split-tests.tsx @@ -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 = ( diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index eec2d01531..d32a4dd8f2 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx index 4af5964d82..8383d98795 100644 --- a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx +++ b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx @@ -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 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 = ( -
{t('confirm_your_email')}
- ) - if (isModal) - intro = outerErrorDisplay ? ( -
- ) : ( -

{outerErrorDisplay ? null : t('we_sent_code')}

- ) - if (interstitial) - intro = ( -

{t('confirm_your_email')}

- ) - return (
)} - {intro} + <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, diff --git a/services/web/frontend/js/shared/components/layouts/ciam-layout.tsx b/services/web/frontend/js/shared/components/layouts/ciam-layout.tsx new file mode 100644 index 0000000000..435bc2f400 --- /dev/null +++ b/services/web/frontend/js/shared/components/layouts/ciam-layout.tsx @@ -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 diff --git a/services/web/frontend/js/shared/components/layouts/content-layout.tsx b/services/web/frontend/js/shared/components/layouts/content-layout.tsx new file mode 100644 index 0000000000..b77ce98585 --- /dev/null +++ b/services/web/frontend/js/shared/components/layouts/content-layout.tsx @@ -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 diff --git a/services/web/frontend/stories/page-layouts.stories.tsx b/services/web/frontend/stories/page-layouts.stories.tsx index d482fc641e..18df4004a4 100644 --- a/services/web/frontend/stories/page-layouts.stories.tsx +++ b/services/web/frontend/stories/page-layouts.stories.tsx @@ -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 { diff --git a/services/web/frontend/stylesheets/ciam/all.scss b/services/web/frontend/stylesheets/ciam/all.scss index cc91d31b92..8c890eb90d 100644 --- a/services/web/frontend/stylesheets/ciam/all.scss +++ b/services/web/frontend/stylesheets/ciam/all.scss @@ -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'; diff --git a/services/web/frontend/stylesheets/ciam/ciam-colors.scss b/services/web/frontend/stylesheets/ciam/ciam-colors.scss index 622ff3cf5f..a48203a34b 100644 --- a/services/web/frontend/stylesheets/ciam/ciam-colors.scss +++ b/services/web/frontend/stylesheets/ciam/ciam-colors.scss @@ -1,4 +1,4 @@ -.ciam-layout { +.ciam-enabled { --ciam-color-neutral-50: #fafafa; --ciam-color-neutral-100: #f2f2f2; --ciam-color-neutral-200: #e6e6e6; diff --git a/services/web/frontend/stylesheets/ciam/ciam-layout.scss b/services/web/frontend/stylesheets/ciam/ciam-layout.scss new file mode 100644 index 0000000000..6934702b47 --- /dev/null +++ b/services/web/frontend/stylesheets/ciam/ciam-layout.scss @@ -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; + } + } +} diff --git a/services/web/frontend/stylesheets/ciam/ciam-spacing.scss b/services/web/frontend/stylesheets/ciam/ciam-spacing.scss new file mode 100644 index 0000000000..5002f6badf --- /dev/null +++ b/services/web/frontend/stylesheets/ciam/ciam-spacing.scss @@ -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; +} diff --git a/services/web/frontend/stylesheets/ciam/ciam-variables.scss b/services/web/frontend/stylesheets/ciam/ciam-variables.scss index 110d25aba8..00806c2a99 100644 --- a/services/web/frontend/stylesheets/ciam/ciam-variables.scss +++ b/services/web/frontend/stylesheets/ciam/ciam-variables.scss @@ -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 diff --git a/services/web/locales/en.json b/services/web/locales/en.json index afac928e0e..0ce7013ff4 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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",