From e23ac4cc278572d2131f0def115986e2e8f431a7 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 4 Aug 2025 01:33:44 +0100 Subject: [PATCH] Improved design --- src/login/Template.tsx | 321 ++++++++------- src/login/custom.css | 17 +- src/login/fonts.css | 6 +- src/login/pages/Code.tsx | 56 +-- src/login/pages/DeleteAccountConfirm.tsx | 7 +- src/login/pages/DeleteCredential.tsx | 59 +-- src/login/pages/Error.tsx | 21 +- src/login/pages/FrontchannelLogout.tsx | 4 +- src/login/pages/IdpReviewUserProfile.tsx | 13 +- src/login/pages/Info.tsx | 42 +- src/login/pages/Login.tsx | 22 +- src/login/pages/LoginConfigTotp.tsx | 385 +++++++++--------- src/login/pages/LoginIdpLinkConfirm.tsx | 5 +- src/login/pages/LoginIdpLinkEmail.tsx | 2 +- .../pages/LoginOauth2DeviceVerifyUserCode.tsx | 12 +- src/login/pages/LoginOauthGrant.tsx | 212 +++++----- src/login/pages/LoginOtp.tsx | 6 +- src/login/pages/LoginPageExpired.tsx | 12 +- src/login/pages/LoginPassword.tsx | 30 +- .../pages/LoginRecoveryAuthnCodeConfig.tsx | 116 +++--- .../pages/LoginRecoveryAuthnCodeInput.tsx | 19 +- src/login/pages/LoginResetOtp.tsx | 10 +- src/login/pages/LoginResetPassword.tsx | 22 +- src/login/pages/LoginUpdatePassword.tsx | 12 +- src/login/pages/LoginUsername.tsx | 23 +- src/login/pages/LoginVerifyEmail.tsx | 4 +- src/login/pages/LogoutConfirm.tsx | 17 +- src/login/pages/Register.tsx | 118 +++--- src/login/pages/SamlPostForm.tsx | 2 +- src/login/pages/SelectAuthenticator.tsx | 2 +- src/login/pages/Terms.tsx | 11 +- src/login/pages/UpdateEmail.tsx | 7 +- src/login/pages/WebauthnAuthenticate.tsx | 194 ++++----- src/login/pages/WebauthnError.tsx | 2 +- src/login/pages/WebauthnRegister.tsx | 93 +++-- 35 files changed, 928 insertions(+), 956 deletions(-) diff --git a/src/login/Template.tsx b/src/login/Template.tsx index b4e599a..f4d7876 100644 --- a/src/login/Template.tsx +++ b/src/login/Template.tsx @@ -26,11 +26,11 @@ export default function Template(props: TemplateProps) { i18n, doUseDefaultCss, children -} = props; + } = props; const { msg, msgStr, currentLanguage, enabledLanguages } = i18n; - const { realm, auth, url, message, isAppInitiatedAction } = kcContext; + const { realm, auth, url, message, isAppInitiatedAction, client } = kcContext; const isMobile = useMediaQuery({ maxWidth: 600 }); @@ -53,26 +53,26 @@ export default function Template(props: TemplateProps) { useEffect(() => { setDarkMode(windowQuery.matches ? true : false); document.fonts.ready.then(() => { - // Wait for all images - const images = Array.from(document.images); - if (images.length === 0) { - setLoading(false); - return; - } - let loaded = 0; - function checkDone() { - loaded++; - if (loaded === images.length) setLoading(false); - } - images.forEach(img => { - if (img.complete) { - checkDone(); - } else { - img.addEventListener("load", checkDone); - img.addEventListener("error", checkDone); + // Wait for all images + const images = Array.from(document.images); + if (images.length === 0) { + setLoading(false); + return; } + let loaded = 0; + function checkDone() { + loaded++; + if (loaded === images.length) setLoading(false); + } + images.forEach(img => { + if (img.complete) { + checkDone(); + } else { + img.addEventListener("load", checkDone); + img.addEventListener("error", checkDone); + } + }); }); - }); }, []); useEffect(() => { @@ -105,138 +105,179 @@ export default function Template(props: TemplateProps) { colorPrimary: "rgba(212, 0, 255, 1)", colorLink: "#6E00FF", colorLinkHover: "#b175ff", - borderRadius: 15 + borderRadius: 20 }, algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm }} > - - {loading == true ? (
- -
) : null} + + {loading == true ? ( +
+ +
+ ) : null} - - {!isMobile && Logo } -
-
- {isMobile && <>Logo } - - - - -
- {(() => { - const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( - - {headerNode} - - ) : ( - <> -
- - - {auth.attemptedUsername} - - -
- - - ); - - if (displayRequiredFields) { - return ( - <> - - {node} -
- - * {msg("requiredFields")} - -
- - ); - } - - return node; - })()} -
- - {/* App-initiated actions should not see warning messages about the need to complete the action during login. */} - {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && ( - } - type={ - message.type === "error" - ? "error" - : message.type === "success" - ? "success" - : message.type === "warning" - ? "warning" - : "info" - } - showIcon - style={{ marginBottom: "24px" }} - /> - )} -
-
- {children} - {auth !== undefined && auth.showTryAnotherWayLink && ( -
- -
- -
-
- )} - {displayInfo &&
{infoNode}
} -
- - {enabledLanguages.length > 1 && ( -
- - - - -
+ + {!isMobile && ( + Logo )} -
+ + +
+ {isMobile && ( + <> + Logo{" "} + + + )} + +
+ {(() => { + const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( + + {client.attributes.logoUri && ( + {client.name + )} + + {headerNode} + + + ) : ( + <> +
+ + + {auth.attemptedUsername} + + +
+ + + ); + + if (displayRequiredFields) { + return ( + <> + {node} +
+ + * {msg("requiredFields")} + +
+ + ); + } + + return node; + })()} +
+ + {/* App-initiated actions should not see warning messages about the need to complete the action during login. */} + {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && ( + } + type={ + message.type === "error" + ? "error" + : message.type === "success" + ? "success" + : message.type === "warning" + ? "warning" + : "info" + } + showIcon + style={{ marginBottom: "26px" }} + /> + )} +
+ + {children} + {auth !== undefined && auth.showTryAnotherWayLink && ( +
+ +
+ +
+
+ )} + + {displayInfo &&
{infoNode}
} +
+
+ {!isMobile && ( + + © 2025 + {enabledLanguages.length > 1 && ( + <> + | + + + + {currentLanguage.label} + + + + + + )} + + )} + +
); diff --git a/src/login/custom.css b/src/login/custom.css index 35c784e..65ca444 100644 --- a/src/login/custom.css +++ b/src/login/custom.css @@ -195,11 +195,16 @@ a.ant-typography, input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, -input:-webkit-autofill:active { +input:-webkit-autofill:active, +input:-internal-autofill-selected { -webkit-background-clip: text; - -webkit-text-fill-color: #ffffff; - transition: background-color 5000s ease-in-out 0s; - box-shadow: inset 0 0 20px 20px #23232329; + -webkit-text-fill-color: #7e8500; + box-shadow: inset 0 0 20px 20px #ffffff !important; + background-color: green !important +} + +input[type="password"] { + letter-spacing: 5px; } .ant-form-item:last-child { @@ -215,4 +220,8 @@ input:-webkit-autofill:active { .ant-alert { margin-bottom: 5px; +} + +.ant-form-item { + margin-bottom: 15px; } \ No newline at end of file diff --git a/src/login/fonts.css b/src/login/fonts.css index 6744804..0d56412 100644 --- a/src/login/fonts.css +++ b/src/login/fonts.css @@ -282,9 +282,9 @@ h2 span * { font-family: "Grold-Rounded-Slim" !important; text-transform: uppercase; font-weight: 700; - font-size: 40px; - line-height: 0.9px; - letter-spacing: 0.02em; + font-size: 36px; + line-height: 0.9px !important; + letter-spacing: 0.01em; } h1 span { diff --git a/src/login/pages/Code.tsx b/src/login/pages/Code.tsx index 262cf65..dd3f582 100644 --- a/src/login/pages/Code.tsx +++ b/src/login/pages/Code.tsx @@ -19,34 +19,36 @@ export default function Code(props: PageProps - {code.success ? ( - <> - - - {msg("copyCodeInstruction")} - +
+ {code.success ? ( + <> + + + {msg("copyCodeInstruction")} + - - {code.code} - - - - ) : ( - code.error && ( - - } - /> - ) - )} + + {code.code} + + + + ) : ( + code.error && ( + + } + /> + ) + )} +
); } diff --git a/src/login/pages/DeleteAccountConfirm.tsx b/src/login/pages/DeleteAccountConfirm.tsx index d7e9688..07e936b 100644 --- a/src/login/pages/DeleteAccountConfirm.tsx +++ b/src/login/pages/DeleteAccountConfirm.tsx @@ -1,4 +1,4 @@ -import { Form, Button, Typography, Alert, Divider, List, Flex, FormProps } from "antd"; +import { Form, Button, Typography, Alert, List, Flex, FormProps } from "antd"; import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { KcContext } from "../KcContext"; import type { I18n } from "../i18n"; @@ -53,7 +53,7 @@ export default function DeleteAccountConfirm(props: PageProps -
+ {msg("deletingImplies")} @@ -72,9 +72,8 @@ export default function DeleteAccountConfirm(props: PageProps {msg("finalDeletionConfirmation")} - - + +
+ + + + - - - + + + +
); } diff --git a/src/login/pages/Error.tsx b/src/login/pages/Error.tsx index 3c079f0..9ba1fda 100644 --- a/src/login/pages/Error.tsx +++ b/src/login/pages/Error.tsx @@ -2,9 +2,7 @@ import type { PageProps } from "keycloakify/login/pages/PageProps"; import { kcSanitize } from "keycloakify/lib/kcSanitize"; import type { KcContext } from "../KcContext"; import type { I18n } from "../i18n"; -import { Alert, Button, Typography, Space, Divider } from "antd"; - -const { Title } = Typography; +import { Alert, Button, Space} from "antd"; export default function Error(props: PageProps, I18n>) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; @@ -18,19 +16,18 @@ export default function Error(props: PageProps{msg("errorTitle")}} + headerNode={msg("errorTitle")} > - - } type="error" showIcon /> - - {!skipLink && client !== undefined && client.baseUrl !== undefined && ( - <> - +
+ + } type="error" showIcon /> + + {!skipLink && client !== undefined && client.baseUrl !== undefined && ( - - )} + )} +
); } diff --git a/src/login/pages/FrontchannelLogout.tsx b/src/login/pages/FrontchannelLogout.tsx index dac6367..efc881b 100644 --- a/src/login/pages/FrontchannelLogout.tsx +++ b/src/login/pages/FrontchannelLogout.tsx @@ -24,9 +24,9 @@ export default function FrontchannelLogout(props: PageProps{msg("frontchannel-logout.title")}} + headerNode={msg("frontchannel-logout.title")} > - + {msg("frontchannel-logout.message")} , I18n> & { UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; @@ -17,6 +18,7 @@ export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) { const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, doMakeUserConfirmPassword } = props; const { msg, msgStr } = i18n; const { url, messagesPerField } = kcContext; + const isMobile = useMediaQuery({ maxWidth: 600 }); const [isFormSubmittable, setIsFormSubmittable] = useState(false); return ( @@ -26,11 +28,13 @@ export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) { doUseDefaultCss={doUseDefaultCss} classes={classes} displayMessage={messagesPerField.exists("global")} - displayRequiredFields headerNode={msg("loginIdpReviewProfileTitle")} >
-
+
- - + - + ); } if (actionUri) { return ( <> - - @@ -52,8 +48,7 @@ export default function Info(props: PageProps - - @@ -64,19 +59,16 @@ export default function Info(props: PageProps{messageHeader}} - > - - - - - - {renderActionLink()} + ); } diff --git a/src/login/pages/Login.tsx b/src/login/pages/Login.tsx index 4bcaf5e..fbbb59c 100644 --- a/src/login/pages/Login.tsx +++ b/src/login/pages/Login.tsx @@ -68,22 +68,7 @@ export default function Login(props: PageProps - {client.attributes.logoUri ? ( - - {client.name - {msg("loginAccountTitle")} - - ) : ( - {msg("loginAccountTitle")} - )} - - } + headerNode={msg("loginAccountTitle")} displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled} infoNode={ @@ -123,6 +108,7 @@ export default function Login(props: PageProps @@ -154,7 +140,7 @@ export default function Login(props: PageProps - + {realm.rememberMe && !usernameHidden && ( @@ -171,7 +157,6 @@ export default function Login(props: PageProps )} - - {isAppInitiatedAction ? ( + ]} + /> + + + {msg("authenticatorCode")} * + + } + name="totp" + validateStatus={messagesPerField.existsError("totp") ? "error" : undefined} + help={ + messagesPerField.existsError("totp") && ( + + ) + } + > + } + aria-invalid={messagesPerField.existsError("totp")} + /> + + + + {msg("loginTotpDeviceName")} {totp.otpCredentials.length >= 1 && *} + + } + name="userLabel" + validateStatus={messagesPerField.existsError("userLabel") ? "error" : undefined} + help={ + messagesPerField.existsError("userLabel") && ( + + ) + } + > + } + aria-invalid={messagesPerField.existsError("userLabel")} + /> + +
+
+ + + + + - ) : null} - - + {isAppInitiatedAction ? ( + + ) : null} + + +
); diff --git a/src/login/pages/LoginIdpLinkConfirm.tsx b/src/login/pages/LoginIdpLinkConfirm.tsx index c886a0e..b68044f 100644 --- a/src/login/pages/LoginIdpLinkConfirm.tsx +++ b/src/login/pages/LoginIdpLinkConfirm.tsx @@ -1,4 +1,4 @@ -import { Button, Divider, Form, Space } from "antd"; +import { Button, Form, Space } from "antd"; import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { KcContext } from "../KcContext"; import type { I18n } from "../i18n"; @@ -10,8 +10,7 @@ export default function LoginIdpLinkConfirm(props: PageProps - - +
diff --git a/src/login/pages/LoginResetOtp.tsx b/src/login/pages/LoginResetOtp.tsx index 26b4eb5..59c9a7e 100644 --- a/src/login/pages/LoginResetOtp.tsx +++ b/src/login/pages/LoginResetOtp.tsx @@ -3,7 +3,7 @@ import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { KcContext } from "../KcContext"; import type { I18n } from "../i18n"; -const { Text, Title } = Typography; +const { Text } = Typography; export default function LoginResetOtp(props: PageProps, I18n>) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; @@ -17,9 +17,9 @@ export default function LoginResetOtp(props: PageProps{msg("doLogIn")}} + headerNode={msg("doLogIn")} > - + {msg("otp-reset-description")} @@ -32,12 +32,10 @@ export default function LoginResetOtp(props: PageProps - + + ) : ( - + ); const handleSubmit = (): void => { @@ -41,11 +43,10 @@ export default function LoginResetPassword(props: PageProps - {displayInfo && displayMessage && } - id="kc-reset-password-form" layout="vertical" @@ -54,7 +55,10 @@ export default function LoginResetPassword(props: PageProps + {displayInfo && displayMessage && } + - - + + - + {msg("backToLogin")} diff --git a/src/login/pages/LoginUpdatePassword.tsx b/src/login/pages/LoginUpdatePassword.tsx index 12acc3a..85e590b 100644 --- a/src/login/pages/LoginUpdatePassword.tsx +++ b/src/login/pages/LoginUpdatePassword.tsx @@ -1,12 +1,10 @@ import { useState } from "react"; -import { Button, Typography, Form, Input, Checkbox, Divider, Flex } from "antd"; +import { Button, Form, Input, Checkbox, Flex } from "antd"; import { EyeOutlined, EyeInvisibleOutlined } from "@ant-design/icons"; import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { KcContext } from "../KcContext"; import type { I18n } from "../i18n"; -const { Title } = Typography; - type FieldType = { "password-new": string; "password-confirm": string; @@ -56,9 +54,9 @@ export default function LoginUpdatePassword(props: PageProps{msg("updatePasswordTitle")}} + headerNode={msg("updatePasswordTitle")} > -
+ {msg("logoutOtherSessions")} - - diff --git a/src/login/pages/LoginUsername.tsx b/src/login/pages/LoginUsername.tsx index 9e49dc1..f3a7131 100644 --- a/src/login/pages/LoginUsername.tsx +++ b/src/login/pages/LoginUsername.tsx @@ -64,22 +64,7 @@ export default function LoginUsername(props: PageProps - {client.attributes.logoUri ? ( - - {client.name - {msg("loginAccountTitle")} - - ) : ( - {msg("loginAccountTitle")} - )} - - } + headerNode={msg("loginAccountTitle")} displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled} infoNode={ @@ -124,6 +109,7 @@ export default function LoginUsername(props: PageProps {!usernameHidden && ( )} - + {realm.rememberMe && !usernameHidden && ( @@ -152,8 +138,6 @@ export default function LoginUsername(props: PageProps - -