feat: integrate shadcn/ui with Tailwind v4

This commit is contained in:
2026-07-01 14:38:20 +00:00
parent 11ce5b59b3
commit 4d98eaccbd
21 changed files with 1896 additions and 13217 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/login/tailwind.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"utils": "@/lib/utils"
}
}
-13012
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -17,11 +17,19 @@
"license": "MIT", "license": "MIT",
"keywords": [], "keywords": [],
"dependencies": { "dependencies": {
"@radix-ui/react-label": "^2.1.11",
"@radix-ui/react-slot": "^1.3.0",
"@tailwindcss/vite": "^4.3.1", "@tailwindcss/vite": "^4.3.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"keycloakify": "^11.15.9", "keycloakify": "^11.15.9",
"lucide-react": "^1.23.0",
"radix-ui": "^1.6.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwindcss": "^4.3.1" "tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.1",
"tw-animate-css": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.15.0", "@eslint/js": "^9.15.0",
+63
View File
@@ -0,0 +1,63 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current"
}
},
defaultVariants: {
variant: "default"
}
}
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
);
}
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
className
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };
+62
View File
@@ -0,0 +1,62 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
+86
View File
@@ -0,0 +1,86 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent
};
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
);
}
export { Input };
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react";
import { Label as LabelPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
);
}
export { Label };
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+1 -1
View File
@@ -3,7 +3,7 @@ import type { ClassKey } from "keycloakify/login";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n"; import { useI18n } from "./i18n";
import DefaultPage from "keycloakify/login/DefaultPage"; import DefaultPage from "keycloakify/login/DefaultPage";
import Template from "keycloakify/login/Template"; import Template from "./Template";
import "./tailwind.css"; import "./tailwind.css";
const UserProfileFormFields = lazy( const UserProfileFormFields = lazy(
+196
View File
@@ -0,0 +1,196 @@
import { useEffect } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { useSetClassName } from "keycloakify/tools/useSetClassName";
import { useInitialize } from "keycloakify/login/Template.useInitialize";
import type { TemplateProps } from "keycloakify/login/TemplateProps";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
export default function Template(props: TemplateProps<KcContext, I18n>) {
const {
kcContext,
i18n,
doUseDefaultCss,
classes,
children,
displayInfo = false,
displayMessage = true,
displayRequiredFields = false,
headerNode,
socialProvidersNode = null,
infoNode = null,
documentTitle,
bodyClassName
} = props;
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
useEffect(() => {
document.title = documentTitle ?? msgStr("loginTitle", realm.displayName || realm.name);
}, [documentTitle, msgStr, realm.displayName, realm.name]);
useSetClassName({
qualifiedName: "html",
className: kcClsx("kcHtmlClass")
});
useSetClassName({
qualifiedName: "body",
className: bodyClassName ?? kcClsx("kcBodyClass")
});
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
if (!isReadyToRender) {
return null;
}
const header = (() => {
const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
<h1 id="kc-page-title">{headerNode}</h1>
) : (
<div id="kc-username" className={kcClsx("kcFormGroupClass")}>
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
<div className="kc-login-tooltip">
<i className={kcClsx("kcResetFlowIcon")} />
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
);
if (displayRequiredFields) {
return (
<div className={kcClsx("kcContentWrapperClass")}>
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle">
<span className="required">*</span>
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">{node}</div>
</div>
);
}
return node;
})();
return (
<div className={kcClsx("kcLoginClass")}>
<div id="kc-header" className={kcClsx("kcHeaderClass")}>
<div id="kc-header-wrapper" className={kcClsx("kcHeaderWrapperClass")}>
{msg("loginTitleHtml", realm.displayNameHtml || realm.name)}
</div>
</div>
<Card
className={clsx(
kcClsx("kcFormCardClass"),
"relative w-full sm:w-[460px] rounded-3xl border border-mac-card-stroke bg-mac-card-bg/80 backdrop-blur-xl px-4 py-5 sm:px-7 sm:py-[30px] shadow-2xl"
)}
>
<CardHeader className={clsx(kcClsx("kcFormHeaderClass"), "space-y-0 p-0 pb-4")}>
{enabledLanguages.length > 1 && (
<div className={kcClsx("kcLocaleMainClass")} id="kc-locale">
<div id="kc-locale-wrapper" className={kcClsx("kcLocaleWrapperClass")}>
<div id="kc-locale-dropdown" className={clsx("menu-button-links", kcClsx("kcLocaleDropDownClass"))}>
<button
tabIndex={1}
id="kc-current-locale-link"
aria-label={msgStr("languages")}
aria-haspopup="true"
aria-expanded="false"
aria-controls="language-switch1"
>
{currentLanguage.label}
</button>
<ul
role="menu"
tabIndex={-1}
aria-labelledby="kc-current-locale-link"
aria-activedescendant=""
id="language-switch1"
className={kcClsx("kcLocaleListClass")}
>
{enabledLanguages.map(({ languageTag, label, href }, i: number) => (
<li key={languageTag} className={kcClsx("kcLocaleListItemClass")} role="none">
<a role="menuitem" id={`language-${i + 1}`} className={kcClsx("kcLocaleItemClass")} href={href}>
{label}
</a>
</li>
))}
</ul>
</div>
</div>
</div>
)}
{header}
</CardHeader>
<CardContent id="kc-content" className="p-0">
<div id="kc-content-wrapper">
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
<Alert
className={clsx(
`alert-${message.type}`,
kcClsx("kcAlertClass"),
`pf-m-${message.type === "error" ? "danger" : message.type}`,
"mb-4 border-white/45 bg-white/40 backdrop-blur-md"
)}
>
<AlertDescription
dangerouslySetInnerHTML={{
__html: kcSanitize(message.summary)
}}
/>
</Alert>
)}
{children}
{auth !== undefined && auth.showTryAnotherWayLink && (
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
<div className={kcClsx("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" />
<a
href="#"
id="try-another-way"
onClick={event => {
(document.forms.namedItem("kc-select-try-another-way-form") as HTMLFormElement).requestSubmit();
event.preventDefault();
return false;
}}
>
{msg("doTryAnotherWay")}
</a>
</div>
</form>
)}
{socialProvidersNode}
{displayInfo && (
<div id="kc-info" className={kcClsx("kcSignUpClass")}>
<div id="kc-info-wrapper" className={kcClsx("kcInfoAreaWrapperClass")}>
{infoNode}
</div>
</div>
)}
</div>
</CardContent>
{infoNode && !displayInfo && <CardFooter className="p-0 pt-4">{infoNode}</CardFooter>}
</Card>
</div>
);
}
+14 -7
View File
@@ -11,17 +11,21 @@ const { useI18n, ofTypeI18n } = i18nBuilder
"identity-provider-login-label": "Or sign in with", "identity-provider-login-label": "Or sign in with",
loginChooseAuthenticator: "Select authentication method", loginChooseAuthenticator: "Select authentication method",
"auth-username-password-form-display-name": "Username and password", "auth-username-password-form-display-name": "Username and password",
"auth-username-password-form-help-text": "Sign in by entering your username and password.", "auth-username-password-form-help-text":
"Sign in by entering your username and password.",
"password-display-name": "Password", "password-display-name": "Password",
"password-help-text": "Sign in by entering your password.", "password-help-text": "Sign in by entering your password.",
"otp-display-name": "Authenticator application", "otp-display-name": "Authenticator application",
"otp-help-text": "Enter the verification code from your authenticator application.", "otp-help-text":
"Enter the verification code from your authenticator application.",
"webauthn-display-name": "Passkey", "webauthn-display-name": "Passkey",
"webauthn-help-text": "Use a passkey to sign in.", "webauthn-help-text": "Use a passkey to sign in.",
"webauthn-passwordless-display-name": "Passkey", "webauthn-passwordless-display-name": "Passkey",
"webauthn-passwordless-help-text": "Use a passkey to sign in without a password.", "webauthn-passwordless-help-text":
"Use a passkey to sign in without a password.",
"ext-qr-code-login-display-name": "QR Code", "ext-qr-code-login-display-name": "QR Code",
"ext-qr-code-login-help-text": "Sign in with another device by scanning a QR code", "ext-qr-code-login-help-text":
"Sign in with another device by scanning a QR code",
"auth-username-password-form-qr-code-display-name": "Username and password", "auth-username-password-form-qr-code-display-name": "Username and password",
"auth-username-password-form-qr-code-help-text": "auth-username-password-form-qr-code-help-text":
"Sign in by entering your username and password, or scan a QR code to use another device.", "Sign in by entering your username and password, or scan a QR code to use another device.",
@@ -33,13 +37,15 @@ const { useI18n, ofTypeI18n } = i18nBuilder
"webauthn-doAuthenticate": "Sign in with Passkey", "webauthn-doAuthenticate": "Sign in with Passkey",
doQrCodeLogin: "Sign In with QR Code", doQrCodeLogin: "Sign In with QR Code",
doQrCodeVerify: "Confirm this sign in", doQrCodeVerify: "Confirm this sign in",
doQrCodeWarning: "Do not scan or approve QR codes sent by other people. If you are unsure, cancel the sign in.", doQrCodeWarning:
"Do not scan or approve QR codes sent by other people. If you are unsure, cancel the sign in.",
successQrCodeLoginTitle: "Success", successQrCodeLoginTitle: "Success",
successQrCodeLoginMessage: "Click on the QR code to continue", successQrCodeLoginMessage: "Click on the QR code to continue",
doShortCodeLogin: "Short Code", doShortCodeLogin: "Short Code",
UseShortCode: "Use Short Code", UseShortCode: "Use Short Code",
CannotScan: "Cannot Scan", CannotScan: "Cannot Scan",
doShortCodeInfo: "Authorize another device, enter the device short code. Do not use codes sent by other people", doShortCodeInfo:
"Authorize another device, enter the device short code. Do not use codes sent by other people",
doShortCodeSteps: doShortCodeSteps:
"Open the login screen on another device. Select QR Code, and click Use Short Code. Enter the code shown below", "Open the login screen on another device. Select QR Code, and click Use Short Code. Enter the code shown below",
doShortCode: "Short code" doShortCode: "Short code"
@@ -63,7 +69,8 @@ const { useI18n, ofTypeI18n } = i18nBuilder
"ext-qr-code-login-display-name": "QR Код", "ext-qr-code-login-display-name": "QR Код",
"ext-qr-code-login-help-text": "ext-qr-code-login-help-text":
"Войдите, отсканировав QR-код с другого устройства", "Войдите, отсканировав QR-код с другого устройства",
"auth-username-password-form-qr-code-display-name": "Имя пользователя и пароль", "auth-username-password-form-qr-code-display-name":
"Имя пользователя и пароль",
"auth-username-password-form-qr-code-help-text": "auth-username-password-form-qr-code-help-text":
"Войдите, введя свое имя пользователя и пароль, или отсканируйте QR-код для использования другого устройства.", "Войдите, введя свое имя пользователя и пароль, или отсканируйте QR-код для использования другого устройства.",
qrLoginSessionLabel: "Сессия: ", qrLoginSessionLabel: "Сессия: ",
+7 -4
View File
@@ -1,10 +1,9 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { Alert, AlertTitle } from "@/components/ui/alert";
export default function QrLoginCanceled( export default function QrLoginCanceled(props: PageProps<Extract<KcContext, { pageId: "qr-login-canceled.ftl" }>, I18n>) {
props: PageProps<Extract<KcContext, { pageId: "qr-login-canceled.ftl" }>, I18n>
) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msg } = i18n; const { msg } = i18n;
@@ -17,7 +16,11 @@ export default function QrLoginCanceled(
displayInfo={false} displayInfo={false}
headerNode={msg("consentDenied")} headerNode={msg("consentDenied")}
> >
<div id="kc-qr-canceled-message" className={classes?.kcFormClass} /> <div id="kc-qr-canceled-message" className={classes?.kcFormClass}>
<Alert variant="destructive" className="border-white/45 bg-white/40 backdrop-blur-md">
<AlertTitle>{msg("consentDenied")}</AlertTitle>
</Alert>
</div>
</Template> </Template>
); );
} }
+12 -12
View File
@@ -2,10 +2,9 @@ import { useEffect, useRef, useState } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { Button } from "@/components/ui/button";
export default function QrLoginScan( export default function QrLoginScan(props: PageProps<Extract<KcContext, { pageId: "qr-login-scan.ftl" }>, I18n>) {
props: PageProps<Extract<KcContext, { pageId: "qr-login-scan.ftl" }>, I18n>
) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { url, QRauthImage, QRauthExecId, QRauthToken, tabId, alignment, ShortCode, ShortCodeLink, refreshRate = 15 } = kcContext; const { url, QRauthImage, QRauthExecId, QRauthToken, tabId, alignment, ShortCode, ShortCodeLink, refreshRate = 15 } = kcContext;
@@ -73,8 +72,9 @@ export default function QrLoginScan(
{ShortCode && ( {ShortCode && (
<p id="com-hadleyso-qr-auth-short-zone" className="kcQrShortCodeZone"> <p id="com-hadleyso-qr-auth-short-zone" className="kcQrShortCodeZone">
<a <Button
href="#" variant="link"
size="sm"
id="com-hadleyso-qr-auth-short-start" id="com-hadleyso-qr-auth-short-start"
onClick={event => { onClick={event => {
setShowShortCode(true); setShowShortCode(true);
@@ -82,9 +82,11 @@ export default function QrLoginScan(
}} }}
> >
{msg("CannotScan")} {msg("CannotScan")}
</a>{" "} </Button>{" "}
|{" "} |{" "}
<a href={ShortCodeLink}>{msg("UseShortCode")}</a> <Button variant="link" size="sm" asChild>
<a href={ShortCodeLink}>{msg("UseShortCode")}</a>
</Button>
</p> </p>
)} )}
@@ -105,11 +107,9 @@ export default function QrLoginScan(
method="post" method="post"
> >
<input type="hidden" name="authenticationExecution" value={QRauthExecId ?? ""} /> <input type="hidden" name="authenticationExecution" value={QRauthExecId ?? ""} />
<input <Button type="submit" size="lg">
type="submit" {msgStr("doLogIn")}
value={msgStr("doLogIn")} </Button>
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonPrimaryClass ?? ""} ${classes?.kcButtonLargeClass ?? ""}`}
/>
</form> </form>
</div> </div>
</Template> </Template>
+8 -11
View File
@@ -1,10 +1,11 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function QrLoginShortStart( export default function QrLoginShortStart(props: PageProps<Extract<KcContext, { pageId: "qr-login-short-start.ftl" }>, I18n>) {
props: PageProps<Extract<KcContext, { pageId: "qr-login-short-start.ftl" }>, I18n>
) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
@@ -21,11 +22,11 @@ export default function QrLoginShortStart(
<div className={classes?.kcFormGroupClass}> <div className={classes?.kcFormGroupClass}>
<p className="kcQrShortCodeInfoClass">{msg("doShortCodeInfo")}</p> <p className="kcQrShortCodeInfoClass">{msg("doShortCodeInfo")}</p>
<label htmlFor="shortCode" className={classes?.kcLabelClass}> <Label htmlFor="shortCode" className={classes?.kcLabelClass}>
{msg("doShortCode")} {msg("doShortCode")}
</label> </Label>
<input <Input
id="shortCode" id="shortCode"
name="shortCode" name="shortCode"
type="number" type="number"
@@ -41,11 +42,7 @@ export default function QrLoginShortStart(
</div> </div>
<div className={classes?.kcFormGroupClass}> <div className={classes?.kcFormGroupClass}>
<input <Button type="submit">{msgStr("doSubmit")}</Button>
type="submit"
value={msgStr("doSubmit")}
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonPrimaryClass ?? ""}`}
/>
</div> </div>
</form> </form>
</Template> </Template>
+5 -4
View File
@@ -1,10 +1,9 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { Alert, AlertTitle } from "@/components/ui/alert";
export default function QrLoginSuccess( export default function QrLoginSuccess(props: PageProps<Extract<KcContext, { pageId: "qr-login-success.ftl" }>, I18n>) {
props: PageProps<Extract<KcContext, { pageId: "qr-login-success.ftl" }>, I18n>
) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msg } = i18n; const { msg } = i18n;
@@ -18,7 +17,9 @@ export default function QrLoginSuccess(
headerNode={msg("successQrCodeLoginTitle")} headerNode={msg("successQrCodeLoginTitle")}
> >
<div id="kc-qr-success-message" className={classes?.kcFormClass}> <div id="kc-qr-success-message" className={classes?.kcFormClass}>
<p className="kcQrSuccessMessageClass">{msg("successQrCodeLoginMessage")}</p> <Alert className="kcQrSuccessMessageClass border-white/45 bg-white/40 backdrop-blur-md">
<AlertTitle>{msg("successQrCodeLoginMessage")}</AlertTitle>
</Alert>
</div> </div>
</Template> </Template>
); );
+14 -20
View File
@@ -1,10 +1,10 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import { Button } from "@/components/ui/button";
import { Alert, AlertTitle } from "@/components/ui/alert";
export default function QrLoginVerify( export default function QrLoginVerify(props: PageProps<Extract<KcContext, { pageId: "qr-login-verify.ftl" }>, I18n>) {
props: PageProps<Extract<KcContext, { pageId: "qr-login-verify.ftl" }>, I18n>
) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { ua_device, ua_os, ua_agent, local_localized, tabId, approveURL, rejectURL } = kcContext; const { ua_device, ua_os, ua_agent, local_localized, tabId, approveURL, rejectURL } = kcContext;
@@ -20,11 +20,13 @@ export default function QrLoginVerify(
headerNode={msg("doQrCodeVerify")} headerNode={msg("doQrCodeVerify")}
> >
<div id="kc-qr-verify-form" className={classes?.kcFormClass}> <div id="kc-qr-verify-form" className={classes?.kcFormClass}>
<p className="kcQrWarningClass">{msg("doQrCodeWarning")}</p> <Alert variant="destructive" className="kcQrWarningClass mb-4">
<AlertTitle>{msg("doQrCodeWarning")}</AlertTitle>
</Alert>
<p className="kcQrDeviceInfoClass"> <p className="kcQrDeviceInfoClass">
You are authorizing a session on a <b>{ua_device}</b> running <b>{ua_os}</b> / <b>{ua_agent}</b> in You are authorizing a session on a <b>{ua_device}</b> running <b>{ua_os}</b> / <b>{ua_agent}</b> in locale{" "}
locale <b>{local_localized}</b>. <b>{local_localized}</b>.
</p> </p>
{tabId && ( {tabId && (
@@ -34,20 +36,12 @@ export default function QrLoginVerify(
)} )}
<div className="kcQrVerifyButtonsClass"> <div className="kcQrVerifyButtonsClass">
<a <Button asChild size="lg">
href={approveURL} <a href={approveURL}>{msg("doAccept")}</a>
type="button" </Button>
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonPrimaryClass ?? ""} ${classes?.kcButtonLargeClass ?? ""}`} <Button variant="secondary" asChild size="lg">
> <a href={rejectURL}>{msg("doDecline")}</a>
{msg("doAccept")} </Button>
</a>
<a
href={rejectURL}
type="button"
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonDefaultClass ?? ""} ${classes?.kcButtonLargeClass ?? ""}`}
>
{msg("doDecline")}
</a>
</div> </div>
</div> </div>
</Template> </Template>
+67 -6
View File
@@ -1,9 +1,11 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css";
@theme { @theme inline {
--font-sans: "SF Pro Text", "SF Pro Display", "Helvetica Neue", "Segoe UI", system-ui, --font-sans: "SF Pro Text", "SF Pro Display", "Helvetica Neue", "Segoe UI", system-ui,
sans-serif; sans-serif;
/* macOS theme tokens */
--color-mac-text-primary: #17202b; --color-mac-text-primary: #17202b;
--color-mac-text-secondary: #4d5b70; --color-mac-text-secondary: #4d5b70;
--color-mac-primary: #2c73ff; --color-mac-primary: #2c73ff;
@@ -27,9 +29,58 @@
--color-mac-orb-b: rgba(114, 89, 255, 0.22); --color-mac-orb-b: rgba(114, 89, 255, 0.22);
--color-mac-orb-c: rgba(30, 182, 146, 0.24); --color-mac-orb-c: rgba(30, 182, 146, 0.24);
--color-mac-focus: rgba(63, 133, 255, 0.45); --color-mac-focus: rgba(63, 133, 255, 0.45);
/* shadcn/ui semantic tokens */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
} }
@layer base { @layer base {
:root {
--background: hsl(213 100% 98%);
--foreground: hsl(213 29% 13%);
--card: hsl(0 0% 100% / 0.85);
--card-foreground: hsl(213 29% 13%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(213 29% 13%);
--primary: hsl(217 100% 58%);
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(213 50% 95%);
--secondary-foreground: hsl(213 29% 13%);
--muted: hsl(213 40% 92%);
--muted-foreground: hsl(218 18% 37%);
--accent: hsl(217 100% 95%);
--accent-foreground: hsl(213 29% 13%);
--destructive: hsl(350 66% 43%);
--destructive-foreground: hsl(0 0% 100%);
--border: hsl(213 40% 85%);
--input: hsl(213 40% 85%);
--ring: hsl(217 100% 58%);
--radius: 0.625rem;
}
*, *,
*::before, *::before,
*::after { *::after {
@@ -224,7 +275,9 @@
display: grid; display: grid;
place-items: center; place-items: center;
cursor: pointer; cursor: pointer;
transition: transform 0.14s ease, box-shadow 0.14s ease; transition:
transform 0.14s ease,
box-shadow 0.14s ease;
} }
.kcQrCodeContainer:hover { .kcQrCodeContainer:hover {
@@ -315,7 +368,8 @@
letter-spacing: 0.15em; letter-spacing: 0.15em;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
} }
#com-hadleyso-qr-auth input[type="submit"] { #com-hadleyso-qr-auth input[type="submit"] {
@@ -421,7 +475,8 @@
background: rgba(255, 255, 255, 0.45); background: rgba(255, 255, 255, 0.45);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
color: var(--color-mac-text-primary); color: var(--color-mac-text-primary);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 1.15rem; font-size: 1.15rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@@ -931,11 +986,17 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23244672'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23244672'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z'/%3E%3C/svg%3E");
} }
.kcSelectAuthListItemClass:has([data-kc-msg="auth-username-password-form-display-name"]) .kcSelectAuthListItemIconClass i.kcAuthenticatorDefaultClass { .kcSelectAuthListItemClass:has(
[data-kc-msg="auth-username-password-form-display-name"]
)
.kcSelectAuthListItemIconClass
i.kcAuthenticatorDefaultClass {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23244672'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23244672'%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E");
} }
.kcSelectAuthListItemClass:has([data-kc-msg="ext-qr-code-login-display-name"]) .kcSelectAuthListItemIconClass i.kcAuthenticatorDefaultClass { .kcSelectAuthListItemClass:has([data-kc-msg="ext-qr-code-login-display-name"])
.kcSelectAuthListItemIconClass
i.kcAuthenticatorDefaultClass {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23244672'%3E%3Cg%3E%3Crect fill='none' height='24' width='24'/%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Cpath d='M3,11h8V3H3V11z M5,5h4v4H5V5z'/%3E%3Cpath d='M3,21h8v-8H3V21z M5,15h4v4H5V15z'/%3E%3Cpath d='M13,3v8h8V3H13z M19,9h-4V5h4V9z'/%3E%3Crect height='2' width='2' x='19' y='19'/%3E%3Crect height='2' width='2' x='13' y='13'/%3E%3Crect height='2' width='2' x='15' y='15'/%3E%3Crect height='2' width='2' x='13' y='17'/%3E%3Crect height='2' width='2' x='15' y='19'/%3E%3Crect height='2' width='2' x='17' y='17'/%3E%3Crect height='2' width='2' x='17' y='13'/%3E%3Crect height='2' width='2' x='19' y='15'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23244672'%3E%3Cg%3E%3Crect fill='none' height='24' width='24'/%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Cpath d='M3,11h8V3H3V11z M5,5h4v4H5V5z'/%3E%3Cpath d='M3,21h8v-8H3V21z M5,15h4v4H5V15z'/%3E%3Cpath d='M13,3v8h8V3H13z M19,9h-4V5h4V9z'/%3E%3Crect height='2' width='2' x='19' y='19'/%3E%3Crect height='2' width='2' x='13' y='13'/%3E%3Crect height='2' width='2' x='15' y='15'/%3E%3Crect height='2' width='2' x='13' y='17'/%3E%3Crect height='2' width='2' x='15' y='19'/%3E%3Crect height='2' width='2' x='17' y='17'/%3E%3Crect height='2' width='2' x='17' y='13'/%3E%3Crect height='2' width='2' x='19' y='15'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
} }
+7 -1
View File
@@ -18,7 +18,13 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
/* Path alias */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
+7 -1
View File
@@ -2,6 +2,7 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { keycloakify } from "keycloakify/vite-plugin"; import { keycloakify } from "keycloakify/vite-plugin";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import path from "path";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@@ -15,5 +16,10 @@ export default defineConfig({
} }
}), }),
tailwindcss() tailwindcss()
] ],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src")
}
}
}); });
+1269 -137
View File
File diff suppressed because it is too large Load Diff