2 Commits
Author SHA1 Message Date
Zeusina 1a928923c4 feat: use shadcn default styles 2026-07-01 18:08:20 +00:00
Zeusina 4d98eaccbd feat: integrate shadcn/ui with Tailwind v4 2026-07-01 14:38:20 +00:00
21 changed files with 2452 additions and 13993 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));
}
+14 -15
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(
@@ -104,10 +104,10 @@ const classes = {
kcHeaderWrapperClass: "max-w-[600px] mx-auto", kcHeaderWrapperClass: "max-w-[600px] mx-auto",
kcHeaderClass: kcHeaderClass:
"mb-6 text-center text-[0.93rem] tracking-[0.01em] text-mac-text-secondary", "mb-6 text-center text-[0.93rem] tracking-[0.01em] text-muted-foreground",
kcFormCardClass: kcFormCardClass:
"relative w-full sm:w-[460px] rounded-3xl border border-mac-card-stroke bg-transparent px-4 py-5 sm:px-7 sm:py-[30px]", "relative w-full sm:w-[460px] rounded-xl border border-border bg-card px-4 py-5 sm:px-7 sm:py-[30px]",
kcFormHeaderClass: "relative grid grid-cols-[1fr_auto] gap-3 items-baseline mb-1", kcFormHeaderClass: "relative grid grid-cols-[1fr_auto] gap-3 items-baseline mb-1",
kcLocaleMainClass: "m-0 col-start-2 row-start-1 flex items-center", kcLocaleMainClass: "m-0 col-start-2 row-start-1 flex items-center",
@@ -121,12 +121,12 @@ const classes = {
kcFormGroupErrorClass: "", kcFormGroupErrorClass: "",
kcFormGroupHeader: "", kcFormGroupHeader: "",
kcLabelWrapperClass: "", kcLabelWrapperClass: "",
kcLabelClass: "block mb-1.5 text-[0.84rem] text-mac-text-secondary font-medium", kcLabelClass: "block mb-1.5 text-[0.84rem] text-foreground font-medium",
kcInputClass: "", kcInputClass: "",
kcInputLargeClass: "", kcInputLargeClass: "",
kcTextareaClass: "", kcTextareaClass: "",
kcInputErrorMessageClass: "block mt-1.5 text-[0.82rem] text-mac-error", kcInputErrorMessageClass: "block mt-1.5 text-[0.82rem] text-destructive",
kcButtonClass: "", kcButtonClass: "",
kcButtonBlockClass: "", kcButtonBlockClass: "",
@@ -144,7 +144,7 @@ const classes = {
kcFormOptionsWrapperClass: "flex items-center justify-end min-h-[44px]", kcFormOptionsWrapperClass: "flex items-center justify-end min-h-[44px]",
kcAlertClass: kcAlertClass:
"flex items-center gap-2.5 rounded-xl mb-3.5 border border-white/45 backdrop-blur-md px-3 py-2.5", "flex items-center gap-2.5 rounded-md mb-3.5 border border-border px-3 py-2.5",
kcAlertTitleClass: "", kcAlertTitleClass: "",
kcLocaleClass: "", kcLocaleClass: "",
@@ -153,24 +153,24 @@ const classes = {
kcLocaleListClass: "block list-none m-0", kcLocaleListClass: "block list-none m-0",
kcLocaleListItemClass: "list-none", kcLocaleListItemClass: "list-none",
kcLocaleItemClass: kcLocaleItemClass:
"block px-3.5 py-2 text-[0.88rem] font-medium text-mac-text-primary transition-colors duration-120", "block px-3.5 py-2 text-[0.88rem] font-medium text-foreground transition-colors duration-120",
kcSelectAuthListClass: "grid gap-3", kcSelectAuthListClass: "grid gap-3",
kcSelectAuthListItemClass: kcSelectAuthListItemClass:
"flex items-center gap-3 p-3 rounded-xl border border-white/45 bg-white/40 backdrop-blur-md transition-all duration-140 hover:border-white/70 hover:bg-white/50 hover:-translate-y-0.5 hover:shadow-lg", "flex items-center gap-3 p-3 rounded-md border border-border bg-background transition-all duration-140 hover:border-input hover:bg-muted",
kcSelectAuthListItemIconClass: kcSelectAuthListItemIconClass:
"shrink-0 w-10 h-10 rounded-full grid place-items-center bg-[rgba(70,98,138,0.12)] text-[#244672]", "shrink-0 w-10 h-10 rounded-full grid place-items-center bg-muted text-foreground",
kcSelectAuthListItemIconPropertyClass: "", kcSelectAuthListItemIconPropertyClass: "",
kcSelectAuthListItemArrowClass: "", kcSelectAuthListItemArrowClass: "",
kcSelectAuthListItemArrowIconClass: "flex-1 min-w-0 flex flex-col gap-0.5", kcSelectAuthListItemArrowIconClass: "flex-1 min-w-0 flex flex-col gap-0.5",
kcSelectAuthListItemBodyClass: "", kcSelectAuthListItemBodyClass: "",
kcSelectAuthListItemHeadingClass: kcSelectAuthListItemHeadingClass:
"text-[0.96rem] font-semibold text-mac-text-primary leading-tight", "text-[0.96rem] font-semibold text-foreground leading-tight",
kcSelectAuthListItemDescriptionClass: kcSelectAuthListItemDescriptionClass:
"text-[0.82rem] font-medium text-mac-text-secondary leading-snug flex flex-wrap gap-1", "text-[0.82rem] font-medium text-muted-foreground leading-snug flex flex-wrap gap-1",
kcSelectAuthListItemFillClass: "hidden", kcSelectAuthListItemFillClass: "hidden",
kcSelectAuthListItemTitle: kcSelectAuthListItemTitle:
"m-0 mb-1 text-[0.9rem] font-semibold text-mac-text-secondary", "m-0 mb-1 text-[0.9rem] font-semibold text-muted-foreground",
kcFormSocialAccountSectionClass: "", kcFormSocialAccountSectionClass: "",
kcFormSocialAccountListClass: "list-none m-0 p-0 grid gap-2", kcFormSocialAccountListClass: "list-none m-0 p-0 grid gap-2",
@@ -178,7 +178,7 @@ const classes = {
kcFormSocialAccountGridItem: "", kcFormSocialAccountGridItem: "",
kcFormSocialAccountLinkClass: "", kcFormSocialAccountLinkClass: "",
kcFormSocialAccountListButtonClass: kcFormSocialAccountListButtonClass:
"flex items-center gap-2.5 w-full min-h-10 rounded-xl border border-white/45 bg-white/40 backdrop-blur-md px-3 py-2 text-[rgba(35,50,70,0.96)] no-underline transition-all duration-140", "flex items-center gap-2.5 w-full min-h-10 rounded-md border border-border bg-background px-3 py-2 text-foreground no-underline transition-all duration-140 hover:bg-muted",
kcFormSocialAccountNameClass: "text-[0.95rem] font-semibold leading-tight", kcFormSocialAccountNameClass: "text-[0.95rem] font-semibold leading-tight",
kcCommonLogoIdP: "", kcCommonLogoIdP: "",
@@ -187,8 +187,7 @@ const classes = {
kcLoginOTPListItemHeaderClass: "flex items-center w-full", kcLoginOTPListItemHeaderClass: "flex items-center w-full",
kcLoginOTPListItemIconClass: "hidden", kcLoginOTPListItemIconClass: "hidden",
kcLoginOTPListItemIconBodyClass: "hidden", kcLoginOTPListItemIconBodyClass: "hidden",
kcLoginOTPListItemTitleClass: kcLoginOTPListItemTitleClass: "text-[0.98rem] font-semibold text-foreground",
"text-[0.98rem] font-semibold text-[rgba(34,51,75,0.95)]",
kcInputWrapperClass: "grid gap-2", kcInputWrapperClass: "grid gap-2",
kcInputGroup: "", kcInputGroup: "",
+191
View File
@@ -0,0 +1,191 @@
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"), "w-full sm:w-[460px]")}>
<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"
)}
>
<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="w-full">
<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="w-full">
<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="mb-4 w-full">
<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>
+615 -768
View File
File diff suppressed because it is too large Load Diff
+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