Upgraded UI to use floating design
Some checks failed
ci / test (push) Has been cancelled
ci / Check if version upgrade (push) Has been cancelled
ci / create_github_release (push) Has been cancelled

This commit is contained in:
Tom Butcher 2025-08-03 21:33:47 +01:00
parent a16c83967d
commit 6950f5f258
5 changed files with 81 additions and 43 deletions

View File

@ -6,11 +6,11 @@ import { useSetClassName } from "keycloakify/tools/useSetClassName";
import { useInitialize } from "keycloakify/login/Template.useInitialize"; import { useInitialize } from "keycloakify/login/Template.useInitialize";
import type { I18n } from "./i18n"; import type { I18n } from "./i18n";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import { Layout, Typography, Dropdown, Button, Alert, Space, Divider, ConfigProvider, Flex, theme } from "antd"; import { Layout, Typography, Dropdown, Button, Alert, Space, Divider, Card, ConfigProvider, Flex, theme, Spin } from "antd";
import { GlobalOutlined } from "@ant-design/icons"; import { GlobalOutlined } from "@ant-design/icons";
import ParticlesBackground from "./ParticlesBackground"; import ParticlesBackground from "./ParticlesBackground";
const { Sider, Content } = Layout; const { Content } = Layout;
const { Title, Text } = Typography; const { Title, Text } = Typography;
export default function Template(props: TemplateProps<KcContext, I18n>) { export default function Template(props: TemplateProps<KcContext, I18n>) {
@ -35,6 +35,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const isMobile = useMediaQuery({ maxWidth: 600 }); const isMobile = useMediaQuery({ maxWidth: 600 });
const [darkMode, setDarkMode] = useState(false); const [darkMode, setDarkMode] = useState(false);
const [loading, setLoading] = useState(true);
const windowQuery = window.matchMedia("(prefers-color-scheme:dark)"); const windowQuery = window.matchMedia("(prefers-color-scheme:dark)");
const darkModeChange = useCallback((event: MediaQueryListEvent) => { const darkModeChange = useCallback((event: MediaQueryListEvent) => {
@ -50,8 +51,28 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, [windowQuery, darkModeChange]); }, [windowQuery, darkModeChange]);
useEffect(() => { useEffect(() => {
console.log(windowQuery.matches ? true : false);
setDarkMode(windowQuery.matches ? true : false); 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);
}
});
});
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -90,23 +111,27 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
> >
<Layout style={{ minHeight: "var(--unit-100vh)", maxHeight: "var(--unit-100vh)" }}> <Layout style={{ minHeight: "var(--unit-100vh)", maxHeight: "var(--unit-100vh)" }}>
<ParticlesBackground /> <ParticlesBackground />
<Sider
width={isMobile ? "100%" : "500px"} {loading == true ? (<div className="loadingOverlay" style={{backgroundColor: darkMode ? '#000000' : '#ffffff'}}>
<Spin/>
</div>) : null}
<Content
style={{ style={{
backgroundColor: darkMode ? "#141414" : "#fff", background: "#f5f5f5",
boxShadow: "2px 0 8px rgba(0,0,0,0.1)",
padding: "40px 40px",
paddingRight: "20px",
display: "flex", display: "flex",
flexDirection: "column", alignItems: "center",
justifyContent: "space-between", justifyContent: "center"
zIndex: 1
}} }}
> ><Card style={{ borderRadius: isMobile ? "0px" : "20px", width: isMobile ? "100vw" : "500px", zIndex: 1, height: isMobile ? "100vh" : "unset", padding: "40px", boxShadow: "0px 5px 15px 5px rgb(0 0 0 / 25%)", paddingRight: "20px"
<Flex style={{ height: "100%" }} vertical> }} variant="borderless" styles={{body: {
<div style={{ marginRight: "20px" }}> padding: 0
<img src="https://cdn.tombutcher.work/logos/logo-auth.png" alt="Logo" style={{ width: "220px" }} /> }}}><div style={{ height: "100%" }}>
<Divider style={{ margin: "24px 0" }} /> <div style={{height: "100%", marginRight: "20px"}}>
<img src="https://cdn.tombutcher.work/logos/logo-auth-horizontal.png" alt="Logo" style={{ width: "100%", margin: "0" }} /> <Divider style={{ margin: "24px 0" }} />
<div style={{ marginBottom: "24px" }}> <div style={{ marginBottom: "24px" }}>
{(() => { {(() => {
@ -173,7 +198,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
/> />
)} )}
</div> </div>
<div style={{ overflowY: "auto", paddingRight: "20px", height: "100%" }}> <div style={{ overflowY: "auto", height: isMobile ? "100%" : "unset", maxHeight: isMobile ? "100%" : "calc(100vh / 2.25)", paddingRight: "20px" }}>
{children} {children}
{auth !== undefined && auth.showTryAnotherWayLink && ( {auth !== undefined && auth.showTryAnotherWayLink && (
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post"> <form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
@ -195,7 +220,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
</div> </div>
{enabledLanguages.length > 1 && ( {enabledLanguages.length > 1 && (
<div style={{ paddingRight: "20px" }}> <div style={{ marginRight: "20px"}}>
<Divider style={{ margin: "24px 0" }} /> <Divider style={{ margin: "24px 0" }} />
<Dropdown menu={languageItems} trigger={["click"]}> <Dropdown menu={languageItems} trigger={["click"]}>
<Button style={{ width: "100%", textAlign: "left" }}> <Button style={{ width: "100%", textAlign: "left" }}>
@ -207,17 +232,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
</Dropdown> </Dropdown>
</div> </div>
)} )}
</Flex> </div></Card></Content>
</Sider>
<Content
style={{
background: "#f5f5f5",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
></Content>
</Layout> </Layout>
</ConfigProvider> </ConfigProvider>
); );

View File

@ -54,14 +54,14 @@ a.ant-typography,
.ant-btn-color-link.ant-btn-variant-link:hover, .ant-btn-color-link.ant-btn-variant-link:hover,
.ant-typography code { .ant-typography code {
color: black; color: black;
background: rgb(110, 0, 255); background: rgb(110, 0, 255) !important;
background: linear-gradient( background: linear-gradient(
45deg, 45deg,
rgba(110, 0, 255, 1) 0%, rgba(110, 0, 255, 1) 0%,
rgba(212, 0, 255, 1) 100% rgba(212, 0, 255, 1) 100%
); ) !important ;
-webkit-background-clip: text; -webkit-background-clip: text !important;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent !important;
} }
.ant-checkbox-checked .ant-checkbox-inner { .ant-checkbox-checked .ant-checkbox-inner {
@ -202,3 +202,14 @@ input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
box-shadow: inset 0 0 20px 20px #23232329; box-shadow: inset 0 0 20px 20px #23232329;
} }
.ant-form-item:last-child {
margin-bottom: 0 !important;
}
.loadingOverlay {
width: 100vw;
height: var(--unit-100vh);
z-index: 2;
position: fixed;
}

View File

@ -282,7 +282,7 @@ h2 span * {
font-family: "Grold-Rounded-Slim" !important; font-family: "Grold-Rounded-Slim" !important;
text-transform: uppercase; text-transform: uppercase;
font-weight: 700; font-weight: 700;
font-size: 40px; font-size: 38px;
line-height: 0.9px; line-height: 0.9px;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }

View File

@ -77,7 +77,7 @@ export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pa
showIcon showIcon
></Alert> ></Alert>
)} )}
{oauth.clientScopesRequested.length > 0 && (
<List <List
dataSource={oauth.clientScopesRequested} dataSource={oauth.clientScopesRequested}
bordered={true} bordered={true}
@ -101,7 +101,7 @@ export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pa
</List.Item> </List.Item>
)} )}
/> />
)}
{(client.attributes.policyUri || client.attributes.tosUri) && ( {(client.attributes.policyUri || client.attributes.tosUri) && (
<> <>
<Space direction="vertical" style={{ marginTop: "12px" }}> <Space direction="vertical" style={{ marginTop: "12px" }}>

View File

@ -6,7 +6,7 @@ import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { useScript } from "keycloakify/login/pages/WebauthnAuthenticate.useScript"; import { useScript } from "keycloakify/login/pages/WebauthnAuthenticate.useScript";
const { Title, Text, Paragraph } = Typography; const { Title, Text, Link } = Typography;
// Define interfaces for better TypeScript support // Define interfaces for better TypeScript support
interface Authenticator { interface Authenticator {
@ -36,6 +36,17 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
i18n i18n
}); });
React.useEffect(() => {
if (isLoading) return;
const timer = setTimeout(() => {
const btn = document.getElementById(authButtonId) as HTMLButtonElement | null;
if (btn && !btn.disabled) {
btn.click();
}
}, 500);
return () => clearTimeout(timer);
}, [isLoading]);
// Function to determine the appropriate icon based on transport // Function to determine the appropriate icon based on transport
const getAuthenticatorIcon = (iconClass: string): React.ReactNode => { const getAuthenticatorIcon = (iconClass: string): React.ReactNode => {
if (iconClass.includes("mobile")) return <MobileOutlined />; if (iconClass.includes("mobile")) return <MobileOutlined />;
@ -50,9 +61,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
doUseDefaultCss={false} doUseDefaultCss={false}
displayInfo={realm.registrationAllowed && !registrationDisabled} displayInfo={realm.registrationAllowed && !registrationDisabled}
infoNode={ infoNode={
<Paragraph> <Space>
{msg("noAccount")} <a href={url.registrationUrl}>{msg("doRegister")}</a> <Text>{msg("noAccount")}</Text>
</Paragraph> <Link href={url.registrationUrl}>{msg("doRegister")}</Link>
</Space>
} }
headerNode={ headerNode={
<> <>
@ -66,7 +78,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
<Title level={3}>{msg("webauthn-login-title")}</Title> <Title level={3}>{msg("webauthn-login-title")}</Title>
</Space> </Space>
) : ( ) : (
<Title level={3}>{msg("webauthn-login-title")}</Title> null
)} )}
</> </>
} }