Compare commits

3 Commits
Author SHA1 Message Date
Zeusina 76f56c1571 feat: add basic glassmorphism 2026-07-01 18:30:14 +00:00
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
25 changed files with 2804 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));
}
+15 -16
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(
@@ -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: "",
+194
View File
@@ -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}
</>
);
}
+26
View File
@@ -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>
);
}
+209
View File
@@ -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 1020px 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);
}
+2
View File
@@ -0,0 +1,2 @@
export { GlassAmbientBackground } from "./GlassAmbientBackground";
export { GlassCard } from "./GlassCard";
+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>
); );
} }
+11 -11
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>{" "}
|{" "} |{" "}
<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>
+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>
+655 -767
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