feat: integrate shadcn/ui with Tailwind v4
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
-13012
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -17,11 +17,19 @@
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"@radix-ui/react-label": "^2.1.11",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"keycloakify": "^11.15.9",
|
||||
"lucide-react": "^1.23.0",
|
||||
"radix-ui": "^1.6.1",
|
||||
"react": "^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": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { ClassKey } from "keycloakify/login";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/login/DefaultPage";
|
||||
import Template from "keycloakify/login/Template";
|
||||
import Template from "./Template";
|
||||
import "./tailwind.css";
|
||||
|
||||
const UserProfileFormFields = lazy(
|
||||
|
||||
@@ -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
@@ -11,17 +11,21 @@ const { useI18n, ofTypeI18n } = i18nBuilder
|
||||
"identity-provider-login-label": "Or sign in with",
|
||||
loginChooseAuthenticator: "Select authentication method",
|
||||
"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-help-text": "Sign in by entering your password.",
|
||||
"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-help-text": "Use a passkey to sign in.",
|
||||
"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-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-help-text":
|
||||
"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",
|
||||
doQrCodeLogin: "Sign In with QR Code",
|
||||
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",
|
||||
successQrCodeLoginMessage: "Click on the QR code to continue",
|
||||
doShortCodeLogin: "Short Code",
|
||||
UseShortCode: "Use Short Code",
|
||||
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:
|
||||
"Open the login screen on another device. Select QR Code, and click Use Short Code. Enter the code shown below",
|
||||
doShortCode: "Short code"
|
||||
@@ -63,7 +69,8 @@ const { useI18n, ofTypeI18n } = i18nBuilder
|
||||
"ext-qr-code-login-display-name": "QR Код",
|
||||
"ext-qr-code-login-help-text":
|
||||
"Войдите, отсканировав 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":
|
||||
"Войдите, введя свое имя пользователя и пароль, или отсканируйте QR-код для использования другого устройства.",
|
||||
qrLoginSessionLabel: "Сессия: ",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
export default function QrLoginCanceled(
|
||||
props: PageProps<Extract<KcContext, { pageId: "qr-login-canceled.ftl" }>, I18n>
|
||||
) {
|
||||
export default function QrLoginCanceled(props: PageProps<Extract<KcContext, { pageId: "qr-login-canceled.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { msg } = i18n;
|
||||
|
||||
@@ -17,7 +16,11 @@ export default function QrLoginCanceled(
|
||||
displayInfo={false}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useEffect, useRef, useState } from "react";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function QrLoginScan(
|
||||
props: PageProps<Extract<KcContext, { pageId: "qr-login-scan.ftl" }>, I18n>
|
||||
) {
|
||||
export default function QrLoginScan(props: PageProps<Extract<KcContext, { pageId: "qr-login-scan.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { url, QRauthImage, QRauthExecId, QRauthToken, tabId, alignment, ShortCode, ShortCodeLink, refreshRate = 15 } = kcContext;
|
||||
|
||||
@@ -73,8 +72,9 @@ export default function QrLoginScan(
|
||||
|
||||
{ShortCode && (
|
||||
<p id="com-hadleyso-qr-auth-short-zone" className="kcQrShortCodeZone">
|
||||
<a
|
||||
href="#"
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
id="com-hadleyso-qr-auth-short-start"
|
||||
onClick={event => {
|
||||
setShowShortCode(true);
|
||||
@@ -82,9 +82,11 @@ export default function QrLoginScan(
|
||||
}}
|
||||
>
|
||||
{msg("CannotScan")}
|
||||
</a>{" "}
|
||||
</Button>{" "}
|
||||
|{" "}
|
||||
<Button variant="link" size="sm" asChild>
|
||||
<a href={ShortCodeLink}>{msg("UseShortCode")}</a>
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -105,11 +107,9 @@ export default function QrLoginScan(
|
||||
method="post"
|
||||
>
|
||||
<input type="hidden" name="authenticationExecution" value={QRauthExecId ?? ""} />
|
||||
<input
|
||||
type="submit"
|
||||
value={msgStr("doLogIn")}
|
||||
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonPrimaryClass ?? ""} ${classes?.kcButtonLargeClass ?? ""}`}
|
||||
/>
|
||||
<Button type="submit" size="lg">
|
||||
{msgStr("doLogIn")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Template>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
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(
|
||||
props: PageProps<Extract<KcContext, { pageId: "qr-login-short-start.ftl" }>, I18n>
|
||||
) {
|
||||
export default function QrLoginShortStart(props: PageProps<Extract<KcContext, { pageId: "qr-login-short-start.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
@@ -21,11 +22,11 @@ export default function QrLoginShortStart(
|
||||
<div className={classes?.kcFormGroupClass}>
|
||||
<p className="kcQrShortCodeInfoClass">{msg("doShortCodeInfo")}</p>
|
||||
|
||||
<label htmlFor="shortCode" className={classes?.kcLabelClass}>
|
||||
<Label htmlFor="shortCode" className={classes?.kcLabelClass}>
|
||||
{msg("doShortCode")}
|
||||
</label>
|
||||
</Label>
|
||||
|
||||
<input
|
||||
<Input
|
||||
id="shortCode"
|
||||
name="shortCode"
|
||||
type="number"
|
||||
@@ -41,11 +42,7 @@ export default function QrLoginShortStart(
|
||||
</div>
|
||||
|
||||
<div className={classes?.kcFormGroupClass}>
|
||||
<input
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonPrimaryClass ?? ""}`}
|
||||
/>
|
||||
<Button type="submit">{msgStr("doSubmit")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
export default function QrLoginSuccess(
|
||||
props: PageProps<Extract<KcContext, { pageId: "qr-login-success.ftl" }>, I18n>
|
||||
) {
|
||||
export default function QrLoginSuccess(props: PageProps<Extract<KcContext, { pageId: "qr-login-success.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { msg } = i18n;
|
||||
|
||||
@@ -18,7 +17,9 @@ export default function QrLoginSuccess(
|
||||
headerNode={msg("successQrCodeLoginTitle")}
|
||||
>
|
||||
<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>
|
||||
</Template>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
export default function QrLoginVerify(
|
||||
props: PageProps<Extract<KcContext, { pageId: "qr-login-verify.ftl" }>, I18n>
|
||||
) {
|
||||
export default function QrLoginVerify(props: PageProps<Extract<KcContext, { pageId: "qr-login-verify.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { ua_device, ua_os, ua_agent, local_localized, tabId, approveURL, rejectURL } = kcContext;
|
||||
|
||||
@@ -20,11 +20,13 @@ export default function QrLoginVerify(
|
||||
headerNode={msg("doQrCodeVerify")}
|
||||
>
|
||||
<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">
|
||||
You are authorizing a session on a <b>{ua_device}</b> running <b>{ua_os}</b> / <b>{ua_agent}</b> in
|
||||
locale <b>{local_localized}</b>.
|
||||
You are authorizing a session on a <b>{ua_device}</b> running <b>{ua_os}</b> / <b>{ua_agent}</b> in locale{" "}
|
||||
<b>{local_localized}</b>.
|
||||
</p>
|
||||
|
||||
{tabId && (
|
||||
@@ -34,20 +36,12 @@ export default function QrLoginVerify(
|
||||
)}
|
||||
|
||||
<div className="kcQrVerifyButtonsClass">
|
||||
<a
|
||||
href={approveURL}
|
||||
type="button"
|
||||
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonPrimaryClass ?? ""} ${classes?.kcButtonLargeClass ?? ""}`}
|
||||
>
|
||||
{msg("doAccept")}
|
||||
</a>
|
||||
<a
|
||||
href={rejectURL}
|
||||
type="button"
|
||||
className={`${classes?.kcButtonClass ?? ""} ${classes?.kcButtonDefaultClass ?? ""} ${classes?.kcButtonLargeClass ?? ""}`}
|
||||
>
|
||||
{msg("doDecline")}
|
||||
</a>
|
||||
<Button asChild size="lg">
|
||||
<a href={approveURL}>{msg("doAccept")}</a>
|
||||
</Button>
|
||||
<Button variant="secondary" asChild size="lg">
|
||||
<a href={rejectURL}>{msg("doDecline")}</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Template>
|
||||
|
||||
+67
-6
@@ -1,9 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@theme {
|
||||
@theme inline {
|
||||
--font-sans: "SF Pro Text", "SF Pro Display", "Helvetica Neue", "Segoe UI", system-ui,
|
||||
sans-serif;
|
||||
|
||||
/* macOS theme tokens */
|
||||
--color-mac-text-primary: #17202b;
|
||||
--color-mac-text-secondary: #4d5b70;
|
||||
--color-mac-primary: #2c73ff;
|
||||
@@ -27,9 +29,58 @@
|
||||
--color-mac-orb-b: rgba(114, 89, 255, 0.22);
|
||||
--color-mac-orb-c: rgba(30, 182, 146, 0.24);
|
||||
--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 {
|
||||
: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,
|
||||
*::after {
|
||||
@@ -224,7 +275,9 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.14s ease, box-shadow 0.14s ease;
|
||||
transition:
|
||||
transform 0.14s ease,
|
||||
box-shadow 0.14s ease;
|
||||
}
|
||||
|
||||
.kcQrCodeContainer:hover {
|
||||
@@ -315,7 +368,8 @@
|
||||
letter-spacing: 0.15em;
|
||||
font-size: 1.25rem;
|
||||
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"] {
|
||||
@@ -421,7 +475,8 @@
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
backdrop-filter: blur(10px);
|
||||
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-weight: 600;
|
||||
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");
|
||||
}
|
||||
|
||||
.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");
|
||||
}
|
||||
|
||||
.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");
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -18,7 +18,13 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path alias */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
||||
+7
-1
@@ -2,6 +2,7 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { keycloakify } from "keycloakify/vite-plugin";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -15,5 +16,10 @@ export default defineConfig({
|
||||
}
|
||||
}),
|
||||
tailwindcss()
|
||||
]
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user