Compare commits
3
Commits
11ce5b59b3
...
76f56c1571
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f56c1571 | ||
|
|
1a928923c4 | ||
|
|
4d98eaccbd |
@@ -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",
|
"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",
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
+15
-16
@@ -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(
|
||||||
@@ -100,14 +100,14 @@ const classes = {
|
|||||||
kcBodyClass: "",
|
kcBodyClass: "",
|
||||||
|
|
||||||
kcLoginClass:
|
kcLoginClass:
|
||||||
"relative z-[1] min-h-screen flex flex-col items-center pt-[15vh] px-4 pb-8 sm:px-[18px] mac-slide-up",
|
"relative z-[1] min-h-screen flex flex-col items-center justify-center px-4 pb-8 sm:px-[18px] mac-slide-up",
|
||||||
|
|
||||||
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-foreground/90 glass-text-stable",
|
||||||
|
|
||||||
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]",
|
"glass relative w-full sm:w-[460px] rounded-xl 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: "",
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
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 { CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
||||||
|
import { GlassAmbientBackground, GlassCard } from "./glass";
|
||||||
|
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 (
|
||||||
|
<GlassAmbientBackground>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<GlassCard 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>}
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
</GlassAmbientBackground>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animated ambient background for the glassmorphism login theme.
|
||||||
|
* Uses only CSS transforms/opacity for GPU-composited motion.
|
||||||
|
* Respects `prefers-reduced-motion` (animation stops) and is aria-hidden.
|
||||||
|
*/
|
||||||
|
export function GlassAmbientBackground({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="fixed inset-0 -z-10 overflow-hidden pointer-events-none"
|
||||||
|
>
|
||||||
|
{/* Slow, soft color blobs */}
|
||||||
|
<div
|
||||||
|
className="glass-ambient-blob"
|
||||||
|
style={{
|
||||||
|
width: "52vw",
|
||||||
|
height: "52vw",
|
||||||
|
left: "-12vw",
|
||||||
|
top: "-12vh",
|
||||||
|
background: "var(--glass-ambient-1)",
|
||||||
|
animationDuration: "24s",
|
||||||
|
animationDelay: "-5s"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="glass-ambient-blob"
|
||||||
|
style={{
|
||||||
|
width: "44vw",
|
||||||
|
height: "44vw",
|
||||||
|
right: "-8vw",
|
||||||
|
top: "8vh",
|
||||||
|
background: "var(--glass-ambient-2)",
|
||||||
|
animationDuration: "28s",
|
||||||
|
animationDelay: "-12s"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="glass-ambient-blob"
|
||||||
|
style={{
|
||||||
|
width: "38vw",
|
||||||
|
height: "38vw",
|
||||||
|
left: "18vw",
|
||||||
|
bottom: "-12vh",
|
||||||
|
background: "var(--glass-ambient-3)",
|
||||||
|
animationDuration: "32s",
|
||||||
|
animationDelay: "-8s"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="glass-ambient-blob"
|
||||||
|
style={{
|
||||||
|
width: "34vw",
|
||||||
|
height: "34vw",
|
||||||
|
right: "14vw",
|
||||||
|
bottom: "4vh",
|
||||||
|
background: "var(--glass-ambient-4)",
|
||||||
|
animationDuration: "26s",
|
||||||
|
animationDelay: "-18s"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Sharper mesh layer so the glass has defined shapes to refract */}
|
||||||
|
<div className="glass-ambient-mesh" />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Glass card wrapper.
|
||||||
|
* Applies the base `.glass` utility and keeps the same flex/gap contract
|
||||||
|
* as the shadcn Card so upstream changes around the Card component are easy
|
||||||
|
* to merge: only this isolated component needs to be updated.
|
||||||
|
*/
|
||||||
|
export function GlassCard({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"glass relative flex flex-col gap-6 p-6 sm:p-8 text-card-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Glassmorphism / Liquid Glass theme tokens
|
||||||
|
*
|
||||||
|
* Isolated module: all glass-specific tokens and utilities live here.
|
||||||
|
* Changing upstream Keycloakify or shadcn files should not affect this file,
|
||||||
|
* and changes here are intentionally scoped to the login theme.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Blur radius — keep between 10–20px for GPU/UX balance */
|
||||||
|
--glass-blur: 14px;
|
||||||
|
--glass-blur-heavy: 20px;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Light theme glass tokens.
|
||||||
|
* Higher opacity than typical Dribbble glass so the card is readable
|
||||||
|
* and clearly separated from the animated background.
|
||||||
|
*/
|
||||||
|
--glass-bg: hsla(0 0% 100% / 0.42);
|
||||||
|
--glass-bg-solid: hsla(0 0% 100% / 0.82);
|
||||||
|
--glass-bg-scrim: hsla(0 0% 100% / 0.62);
|
||||||
|
--glass-border: hsla(0 0% 100% / 0.58);
|
||||||
|
--glass-border-highlight: hsla(0 0% 100% / 0.85);
|
||||||
|
--glass-shadow: 0 14px 44px hsla(220 40% 16% / 0.18);
|
||||||
|
--glass-shadow-elevated: 0 24px 64px hsla(220 40% 12% / 0.24);
|
||||||
|
|
||||||
|
/* Ambient background blobs — saturated and fairly opaque so the glass has something to refract */
|
||||||
|
--glass-ambient-1: hsla(250 85% 62% / 0.55);
|
||||||
|
--glass-ambient-2: hsla(320 80% 58% / 0.5);
|
||||||
|
--glass-ambient-3: hsla(190 90% 52% / 0.48);
|
||||||
|
--glass-ambient-4: hsla(40 95% 58% / 0.42);
|
||||||
|
|
||||||
|
/* Text stability on busy backgrounds */
|
||||||
|
--glass-text-shadow: 0 1px 2px hsla(0 0% 0% / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
/* Dark glass needs to be noticeably darker/tinted or it disappears on a dark page */
|
||||||
|
--glass-bg: hsla(220 20% 14% / 0.62);
|
||||||
|
--glass-bg-solid: hsla(220 20% 10% / 0.9);
|
||||||
|
--glass-bg-scrim: hsla(220 20% 8% / 0.78);
|
||||||
|
--glass-border: hsla(0 0% 100% / 0.18);
|
||||||
|
--glass-border-highlight: hsla(0 0% 100% / 0.32);
|
||||||
|
--glass-shadow: 0 14px 44px hsla(0 0% 0% / 0.45);
|
||||||
|
--glass-shadow-elevated: 0 24px 64px hsla(0 0% 0% / 0.55);
|
||||||
|
|
||||||
|
/* Dark-mode blobs: still vivid but not overwhelming */
|
||||||
|
--glass-ambient-1: hsla(250 85% 58% / 0.4);
|
||||||
|
--glass-ambient-2: hsla(320 80% 55% / 0.36);
|
||||||
|
--glass-ambient-3: hsla(190 90% 50% / 0.36);
|
||||||
|
--glass-ambient-4: hsla(40 95% 55% / 0.3);
|
||||||
|
|
||||||
|
--glass-text-shadow: 0 1px 3px hsla(0 0% 0% / 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced transparency: make glass almost opaque and drop the blur */
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
:root {
|
||||||
|
--glass-bg: hsla(0 0% 100% / 0.95);
|
||||||
|
--glass-bg-solid: hsla(0 0% 100% / 0.99);
|
||||||
|
--glass-bg-scrim: hsla(0 0% 100% / 0.99);
|
||||||
|
--glass-border: hsla(0 0% 0% / 0.14);
|
||||||
|
--glass-border-highlight: hsla(0 0% 0% / 0.1);
|
||||||
|
--glass-blur: 2px;
|
||||||
|
--glass-blur-heavy: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--glass-bg: hsla(220 20% 8% / 0.97);
|
||||||
|
--glass-bg-solid: hsla(220 20% 6% / 0.99);
|
||||||
|
--glass-bg-scrim: hsla(220 20% 6% / 0.99);
|
||||||
|
--glass-border: hsla(0 0% 100% / 0.14);
|
||||||
|
--glass-border-highlight: hsla(0 0% 100% / 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base glass layer — use for cards, panels, overlays */
|
||||||
|
.glass {
|
||||||
|
/* Tinted fill plus a subtle top-left sheen to mimic physical glass */
|
||||||
|
background: linear-gradient(
|
||||||
|
145deg,
|
||||||
|
hsla(0 0% 100% / 0.22) 0%,
|
||||||
|
hsla(0 0% 100% / 0.06) 55%,
|
||||||
|
hsla(0 0% 100% / 0.14) 100%
|
||||||
|
),
|
||||||
|
var(--glass-bg);
|
||||||
|
backdrop-filter: blur(var(--glass-blur)) saturate(145%);
|
||||||
|
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(145%);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 0 var(--glass-border-highlight),
|
||||||
|
inset 0 0 0 1px hsla(0 0% 100% / 0.04),
|
||||||
|
var(--glass-shadow);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* More opaque glass — used behind dense text or form sections */
|
||||||
|
.glass-solid {
|
||||||
|
background: linear-gradient(
|
||||||
|
145deg,
|
||||||
|
hsla(0 0% 100% / 0.18) 0%,
|
||||||
|
hsla(0 0% 100% / 0.04) 55%,
|
||||||
|
hsla(0 0% 100% / 0.1) 100%
|
||||||
|
),
|
||||||
|
var(--glass-bg-solid);
|
||||||
|
backdrop-filter: blur(var(--glass-blur-heavy)) saturate(125%);
|
||||||
|
-webkit-backdrop-filter: blur(var(--glass-blur-heavy)) saturate(125%);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 0 var(--glass-border-highlight),
|
||||||
|
inset 0 0 0 1px hsla(0 0% 100% / 0.04),
|
||||||
|
var(--glass-shadow);
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrim gradient for text protection on glass surfaces */
|
||||||
|
.glass-scrim {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--glass-bg-scrim) 0%,
|
||||||
|
color-mix(in oklab, var(--glass-bg-scrim) 85%, transparent) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for browsers without backdrop-filter support */
|
||||||
|
@supports not (backdrop-filter: blur(12px)) {
|
||||||
|
.glass {
|
||||||
|
background: var(--glass-bg-solid);
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-solid {
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient background blobs — keep some definition so the glass can refract them */
|
||||||
|
.glass-ambient-blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(44px);
|
||||||
|
opacity: 0.85;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
animation: glass-float 20s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.glass-ambient-blob {
|
||||||
|
animation: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glass-float {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, 0, 0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate3d(30px, -40px, 0) scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra mesh layer behind the card: sharper color bands for stronger refraction */
|
||||||
|
.glass-ambient-mesh {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 20% 30%,
|
||||||
|
var(--glass-ambient-1) 0%,
|
||||||
|
transparent 45%
|
||||||
|
),
|
||||||
|
radial-gradient(circle at 80% 20%, var(--glass-ambient-2) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 60% 80%, var(--glass-ambient-3) 0%, transparent 45%),
|
||||||
|
radial-gradient(circle at 30% 75%, var(--glass-ambient-4) 0%, transparent 40%);
|
||||||
|
filter: blur(28px);
|
||||||
|
will-change: transform;
|
||||||
|
animation: glass-mesh-drift 30s ease-in-out infinite alternate;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.glass-ambient-mesh {
|
||||||
|
animation: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glass-mesh-drift {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1.08) translate3d(-20px, 10px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text stability helper */
|
||||||
|
.glass-text-stable {
|
||||||
|
text-shadow: var(--glass-text-shadow);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { GlassAmbientBackground } from "./GlassAmbientBackground";
|
||||||
|
export { GlassCard } from "./GlassCard";
|
||||||
+14
-7
@@ -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: "Сессия: ",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>{" "}
|
||||||
|{" "}
|
|{" "}
|
||||||
|
<Button variant="link" size="sm" asChild>
|
||||||
<a href={ShortCodeLink}>{msg("UseShortCode")}</a>
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+655
-767
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -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
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user