diff --git a/src/login/KcPage.tsx b/src/login/KcPage.tsx
index 34b7ac7..80763b7 100644
--- a/src/login/KcPage.tsx
+++ b/src/login/KcPage.tsx
@@ -86,28 +86,13 @@ export default function KcPage(props: { kcContext: KcContext }) {
);
case "register.ftl":
return (
-
-
-
+ UserProfileFormFields={UserProfileFormFields}
+ doMakeUserConfirmPassword={doMakeUserConfirmPassword}
+ />
);
case "error.ftl":
@@ -184,23 +169,13 @@ export default function KcPage(props: { kcContext: KcContext }) {
);
case "login-update-profile.ftl":
return (
-
-
-
+
);
case "login-oauth2-device-verify-user-code.ftl":
return (
diff --git a/src/login/ParticlesBackground.tsx b/src/login/ParticlesBackground.tsx
index a0da0ca..9faa7bb 100644
--- a/src/login/ParticlesBackground.tsx
+++ b/src/login/ParticlesBackground.tsx
@@ -6,7 +6,11 @@ import { type Container, type ISourceOptions } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim"; // if you are going to use `loadSlim`, install the "@tsparticles/slim" package too.
// import { loadBasic } from "@tsparticles/basic"; // if you are going to use `loadBasic`, install the "@tsparticles/basic" package too.
-const ParticlesBackground = () => {
+type ParticlesBackgroundProps = {
+ darkMode?: boolean;
+};
+
+const ParticlesBackground = ({ darkMode = false }: ParticlesBackgroundProps) => {
const [init, setInit] = useState(false);
// this should be run only once per application lifetime
@@ -28,7 +32,85 @@ const ParticlesBackground = () => {
console.log(container);
};
- const options: ISourceOptions = useMemo(
+ const darkOptions: ISourceOptions = useMemo(
+ () => ({
+ background: {
+ color: {
+ value: "#FF00A1"
+ }
+ },
+ fpsLimit: 120,
+ interactivity: {
+ events: {
+ onClick: {
+ enable: false,
+ mode: "push"
+ },
+ onHover: {
+ enable: true,
+ mode: "repulse"
+ }
+ },
+ modes: {
+ push: {
+ quantity: 4
+ },
+ repulse: {
+ distance: 100,
+ duration: 5
+ }
+ }
+ },
+ particles: {
+ color: {
+ value: [
+ "#210014FF",
+ "#00033BFF",
+ "#001F24FF",
+ "#170035FF",
+ "#000333FF",
+ "#210014FF"
+ ]
+ },
+ links: {
+ color: "#ffffff",
+ distance: 150,
+ enable: true,
+ opacity: 0.0,
+ width: 0
+ },
+ move: {
+ direction: "none",
+ enable: true,
+ outModes: {
+ default: "out"
+ },
+ random: true,
+ speed: 1,
+ straight: false
+ },
+ number: {
+ density: {
+ enable: true
+ },
+ value: 400
+ },
+ opacity: {
+ value: 1
+ },
+ shape: {
+ type: "circle"
+ },
+ size: {
+ value: { min: 100, max: 300 }
+ }
+ },
+ detectRetina: true
+ }),
+ []
+ );
+
+ const lightOptions: ISourceOptions = useMemo(
() => ({
background: {
color: {
@@ -119,11 +201,11 @@ const ParticlesBackground = () => {
) {
const {
displayInfo = false,
displayMessage = true,
- displayRequiredFields = false,
headerNode,
infoNode = null,
documentTitle,
@@ -98,11 +97,51 @@ export default function Template(props: TemplateProps
) {
}))
};
+ const clientInfo = (
+
+ {client.attributes.logoUri ? (
+
+ ) : (
+
+ )}
+
+ {client.name}
+
+
+ );
+
+ const showTryAnotherWayLink = (
+
+ );
+
+ const showMessage = displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction);
+
+ const showImutableUsername = auth !== undefined && auth.showUsername && !auth.showResetCredentials;
+
return (
) {
}}
>
-
+
{loading == true ? (
@@ -126,20 +165,31 @@ export default function Template(props: TemplateProps
) {
>
{!isMobile && (
-
+ <>
+ {darkMode ? (
+
+ ) : (
+
+ )}
+ >
)}
) {
}
}}
>
-
-
- {isMobile && (
+
+ {isMobile && (
+
+
{" "}
+
+
+ )}
+
+
+ {headerNode && (
<>
-
{" "}
-
+
+ {headerNode}
+
+ {isMobile && }
>
)}
-
-
-
- {headerNode}
-
- {client.attributes.logoUri && (
-
- )}
-
- {(() => {
- const node =
- auth !== undefined && auth.showUsername && !auth.showResetCredentials ? (
-
-
-
- {auth.attemptedUsername}
-
- {msg("restartLoginTooltip")}
-
-
-
- ) : null;
-
- 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 && (
-
- )}
+
+
+ {auth.attemptedUsername}
+
+ {msg("restartLoginTooltip")}
+
+
+
+ {isMobile && }
+ >
+ )}
+ {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
+ {showMessage && (
+ <>
+ }
+ type={
+ message.type === "error"
+ ? "error"
+ : message.type === "success"
+ ? "success"
+ : message.type === "warning"
+ ? "warning"
+ : "info"
+ }
+ showIcon
+ style={{ margin: 0, width: "100%" }}
+ />
+ {isMobile && }
+ >
+ )}
+
- {displayInfo && {infoNode}
}
+
+ {children}
+ {isMobile && (
+
+ {client.name && {clientInfo}}
+ {auth !== undefined && auth.showTryAnotherWayLink && (
+ {showTryAnotherWayLink}
+ )}
+ {displayInfo && {infoNode}}
+
+ )}
+
+ {!isMobile && (
+
+ {client.name && {clientInfo}}
+ {auth !== undefined && auth.showTryAnotherWayLink && (
+ {showTryAnotherWayLink}
+ )}
+ {displayInfo && {infoNode}}
+
+ )}
+
{!isMobile && (
© 2025
@@ -277,3 +312,27 @@ export default function Template(props: TemplateProps) {
);
}
+
+function FooterCard({ children, darkMode = false }: { children?: ReactNode; darkMode?: boolean }) {
+ const isMobile = useMediaQuery({ maxWidth: 600 });
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/login/UserProfileFormFields.tsx b/src/login/UserProfileFormFields.tsx
index b7fcf39..0784bc0 100644
--- a/src/login/UserProfileFormFields.tsx
+++ b/src/login/UserProfileFormFields.tsx
@@ -11,7 +11,7 @@ import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFo
import type { Attribute } from "keycloakify/login/KcContext";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
-import { Form, FormItemProps, Input, InputProps, Typography, Select, Radio, Checkbox, Button, Space, Divider } from "antd";
+import { Form, FormItemProps, Input, InputProps, Typography, Select, Radio, Checkbox, Button, Space, Divider, Flex } from "antd";
import { EyeOutlined, EyeInvisibleOutlined, PlusOutlined, MinusOutlined } from "@ant-design/icons";
const { Text, Title } = Typography;
@@ -36,68 +36,72 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
const groupNameRef = { current: "" };
return (
- <>
+
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
return (
<>
- 0 ? "error" : undefined}
- help={
- <>
- {attribute.annotations.inputHelperTextBefore !== undefined && (
-
- {advancedMsg(attribute.annotations.inputHelperTextBefore)}
-
- )}
-
- {attribute.annotations.inputHelperTextAfter !== undefined && (
-
- {advancedMsg(attribute.annotations.inputHelperTextAfter)}
-
- )}
- >
- }
- rules={[{ required: true }]}
- name={attribute.name}
- shouldUpdate
- key={attribute.name}
- {...rest}
- >
- {BeforeField !== undefined && (
-
- )}
+
+
+ {advancedMsg(attribute.displayName ?? "")}
+ *
+
+ 0 ? "error" : undefined}
+ rules={[{ required: true }]}
+ name={attribute.name}
+ shouldUpdate
+ key={attribute.name}
+ {...rest}
+ >
+ {BeforeField !== undefined && (
+
+ )}
-
- {AfterField !== undefined && (
-
- )}
-
+ {AfterField !== undefined && (
+
+ )}
+
+
+ {attribute.annotations.inputHelperTextBefore !== undefined && (
+
+ {advancedMsg(attribute.annotations.inputHelperTextBefore)}
+
+ )}
+
+ {attribute.annotations.inputHelperTextAfter !== undefined && (
+
+ {advancedMsg(attribute.annotations.inputHelperTextAfter)}
+
+ )}
+
+
>
);
})}
- >
+
);
}
diff --git a/src/login/custom.css b/src/login/custom.css
index 65ca444..d0293b0 100644
--- a/src/login/custom.css
+++ b/src/login/custom.css
@@ -40,8 +40,20 @@ h2 span {
background: rgb(110, 0, 255);
background: linear-gradient(
45deg,
- rgba(110, 0, 255, 1) 0%,
- rgba(212, 0, 255, 1) 100%
+
+ rgb(255, 0, 153) 0%,
+ rgba(110, 0, 255) 100%
+ );
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.dark > h2, .dark > span {
+ background: rgb(110, 0, 255);
+ background: linear-gradient(
+ 45deg,
+ rgb(255, 0, 115) 0%,
+ rgb(170, 0, 255) 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
@@ -57,8 +69,8 @@ a.ant-typography,
background: rgb(110, 0, 255) !important;
background: linear-gradient(
45deg,
- rgba(110, 0, 255, 1) 0%,
- rgba(212, 0, 255, 1) 100%
+ rgb(170, 0, 255) 0%,
+ rgb(255, 0, 115) 100%
) !important ;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
@@ -192,15 +204,36 @@ a.ant-typography,
background: transparent !important;
}
-input:-webkit-autofill,
-input:-webkit-autofill:hover,
-input:-webkit-autofill:focus,
-input:-webkit-autofill:active,
-input:-internal-autofill-selected {
- -webkit-background-clip: text;
- -webkit-text-fill-color: #7e8500;
- box-shadow: inset 0 0 20px 20px #ffffff !important;
- background-color: green !important
+/* Autofill fix for light mode */
+@media (prefers-color-scheme: light) {
+ input:-webkit-autofill,
+ input:-webkit-autofill:hover,
+ input:-webkit-autofill:focus,
+ input:-webkit-autofill:active,
+ input:-internal-autofill-selected,
+ input:-internal-autofill-previewed {
+ box-shadow: inset 0 0 20px 20px #ffffff00 !important;
+ -webkit-text-fill-color: #000 !important; /* black text in light mode */
+ transition: background-color 5000s ease-in-out 0s;
+ }
+}
+
+/* Autofill fix for dark mode */
+@media (prefers-color-scheme: dark) {
+ input:-webkit-autofill,
+ input:-webkit-autofill:hover,
+ input:-webkit-autofill:focus,
+ input:-webkit-autofill:active,
+ input:-internal-autofill-selected,
+ input:-internal-autofill-previewed {
+ box-shadow: inset 0 0 20px 20px #ffffff00 !important;
+ -webkit-text-fill-color: #fff !important; /* white text in dark mode */
+ transition: background-color 5000s ease-in-out 0s;
+ }
+}
+
+.ant-input-outlined:has(input:-webkit-autofill) {
+ border-color: rgb(250, 173, 20);
}
input[type="password"] {
diff --git a/src/login/fonts.css b/src/login/fonts.css
index 0d56412..22a9afe 100644
--- a/src/login/fonts.css
+++ b/src/login/fonts.css
@@ -247,6 +247,10 @@
format("opentype");
}
+h2 {
+ line-height: 1 !important;
+}
+
p,
span,
h1,
diff --git a/src/login/pages/DeleteAccountConfirm.tsx b/src/login/pages/DeleteAccountConfirm.tsx
index 07e936b..6381d30 100644
--- a/src/login/pages/DeleteAccountConfirm.tsx
+++ b/src/login/pages/DeleteAccountConfirm.tsx
@@ -66,14 +66,14 @@ export default function DeleteAccountConfirm(props: PageProps• {item}
)}
- style={{ margin: "16px 0" }}
+ style={{ margin: "18px 0" }}
/>
{msg("finalDeletionConfirmation")}
-
+