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 type { I18n } from "./i18n";
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 ParticlesBackground from "./ParticlesBackground";
const { Sider, Content } = Layout;
const { Content } = Layout;
const { Title, Text } = Typography;
export default function Template(props: TemplateProps<KcContext, I18n>) {
@ -26,7 +26,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
i18n,
doUseDefaultCss,
children
} = props;
} = props;
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
@ -35,6 +35,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const isMobile = useMediaQuery({ maxWidth: 600 });
const [darkMode, setDarkMode] = useState(false);
const [loading, setLoading] = useState(true);
const windowQuery = window.matchMedia("(prefers-color-scheme:dark)");
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
@ -50,8 +51,28 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, [windowQuery, darkModeChange]);
useEffect(() => {
console.log(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(() => {
@ -90,23 +111,27 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
>
<Layout style={{ minHeight: "var(--unit-100vh)", maxHeight: "var(--unit-100vh)" }}>
<ParticlesBackground />
<Sider
width={isMobile ? "100%" : "500px"}
{loading == true ? (<div className="loadingOverlay" style={{backgroundColor: darkMode ? '#000000' : '#ffffff'}}>
<Spin/>
</div>) : null}
<Content
style={{
backgroundColor: darkMode ? "#141414" : "#fff",
boxShadow: "2px 0 8px rgba(0,0,0,0.1)",
padding: "40px 40px",
paddingRight: "20px",
background: "#f5f5f5",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
zIndex: 1
alignItems: "center",
justifyContent: "center"
}}
>
<Flex style={{ height: "100%" }} vertical>
<div style={{ marginRight: "20px" }}>
<img src="https://cdn.tombutcher.work/logos/logo-auth.png" alt="Logo" style={{ width: "220px" }} />
<Divider style={{ margin: "24px 0" }} />
><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"
}} variant="borderless" styles={{body: {
padding: 0
}}}><div style={{ height: "100%" }}>
<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" }}>
{(() => {
@ -173,7 +198,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
/>
)}
</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}
{auth !== undefined && auth.showTryAnotherWayLink && (
<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>
{enabledLanguages.length > 1 && (
<div style={{ paddingRight: "20px" }}>
<div style={{ marginRight: "20px"}}>
<Divider style={{ margin: "24px 0" }} />
<Dropdown menu={languageItems} trigger={["click"]}>
<Button style={{ width: "100%", textAlign: "left" }}>
@ -207,17 +232,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
</Dropdown>
</div>
)}
</Flex>
</Sider>
<Content
style={{
background: "#f5f5f5",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
></Content>
</div></Card></Content>
</Layout>
</ConfigProvider>
);

View File

@ -54,14 +54,14 @@ a.ant-typography,
.ant-btn-color-link.ant-btn-variant-link:hover,
.ant-typography code {
color: black;
background: rgb(110, 0, 255);
background: rgb(110, 0, 255) !important;
background: linear-gradient(
45deg,
rgba(110, 0, 255, 1) 0%,
rgba(212, 0, 255, 1) 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
) !important ;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
}
.ant-checkbox-checked .ant-checkbox-inner {
@ -202,3 +202,14 @@ input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s;
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;
text-transform: uppercase;
font-weight: 700;
font-size: 40px;
font-size: 38px;
line-height: 0.9px;
letter-spacing: 0.02em;
}

View File

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

View File

@ -6,7 +6,7 @@ import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
import { useScript } from "keycloakify/login/pages/WebauthnAuthenticate.useScript";
const { Title, Text, Paragraph } = Typography;
const { Title, Text, Link } = Typography;
// Define interfaces for better TypeScript support
interface Authenticator {
@ -36,6 +36,17 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
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
const getAuthenticatorIcon = (iconClass: string): React.ReactNode => {
if (iconClass.includes("mobile")) return <MobileOutlined />;
@ -50,9 +61,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
doUseDefaultCss={false}
displayInfo={realm.registrationAllowed && !registrationDisabled}
infoNode={
<Paragraph>
{msg("noAccount")} <a href={url.registrationUrl}>{msg("doRegister")}</a>
</Paragraph>
<Space>
<Text>{msg("noAccount")}</Text>
<Link href={url.registrationUrl}>{msg("doRegister")}</Link>
</Space>
}
headerNode={
<>
@ -66,7 +78,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
<Title level={3}>{msg("webauthn-login-title")}</Title>
</Space>
) : (
<Title level={3}>{msg("webauthn-login-title")}</Title>
null
)}
</>
}