Skip to content

Commit

Permalink
Tailwind (#1)
Browse files Browse the repository at this point in the history
* Eject login page

* Demo Tailwind

* Update keycloakify and remove account theme boilerplate

* Update keycloakify

* update keycloakify

* Update to Keycloakify v10

* Update Keycloakify

* Update README.md

* Update keycloakify

* update keycloakify

* Update keycloakify

* Update keycloakify

* update readme

* add engines

* Update keycloakify

* Update to Keycloakify 11

* update prettierignore

* Update keycloakify

* Update keycloakify

* Update keycloakify

* update keycloakify

* Update keycloakify

* update keycloakify

* Update keycloakify and fmt

* Update keycloakify

* Update keycloakify

* keycloakify#40

* Update keycloakify

* Update documentation links

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Bump keycloakify

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Update keycloakify

* Updat keycloakify

* Update package.json

---------

Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com>
OualidGardsign and garronej authored Jan 15, 2025
1 parent 9002e0a commit 4357120
Showing 22 changed files with 638 additions and 38 deletions.
12 changes: 0 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -36,18 +36,6 @@ npm run build-keycloak-theme
Note that by default Keycloakify generates multiple .jar files for different versions of Keycloak.
You can customize this behavior, see documentation [here](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).

# Initializing the account theme

```bash
npx keycloakify initialize-account-theme
```

# Initializing the email theme

```bash
npx keycloakify initialize-email-theme
```

# GitHub Actions

The starter comes with a generic GitHub Actions workflow that builds the theme and publishes
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -46,3 +46,4 @@ export default typescriptEslint.config(
},
},
);

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "keycloakify-starter",
"version": "0.0.0",
"version": "0.0.1",
"description": "Starter for Keycloakify 11",
"repository": {
"type": "git",
@@ -17,9 +17,12 @@
"license": "MIT",
"keywords": [],
"dependencies": {
"keycloakify": "^11.8.8",
"keycloakify": "^11.8.7",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.4"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
9 changes: 2 additions & 7 deletions src/kc.gen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This file is auto-generated by the `update-kc-gen` command. Do not edit it manually.
// Hash: 09b09a6c36072d5cf2f8484ab3dc720d28ec8c126df1bafb0b2214a0139848c7
// Hash: a4bb051dacf961c9963f104bb9f9b28088f10d7beadd6cab6df0eb3f56500a51

/* eslint-disable */

@@ -19,12 +19,7 @@ export const kcEnvNames: KcEnvName[] = [];

export const kcEnvDefaults: Record<KcEnvName, string> = {};

/**
* NOTE: Do not import this type except maybe in your entrypoint.
* If you need to import the KcContext import it either from src/login/KcContext.ts or src/account/KcContext.ts.
* Depending on the theme type you are working on.
*/
export type KcContext = import("./login/KcContext").KcContext;
type KcContext = import("./login/KcContext").KcContext;

declare global {
interface Window {
28 changes: 27 additions & 1 deletion src/login/KcPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import "./index.css";
import { Suspense, lazy } from "react";
import type { ClassKey } from "keycloakify/login";
import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n";
import DefaultPage from "keycloakify/login/DefaultPage";
import Template from "keycloakify/login/Template";
//import { twMerge } from "tailwind-merge";

const UserProfileFormFields = lazy(
() => import("keycloakify/login/UserProfileFormFields")
);
const Login = lazy(() => import("./pages/Login"));

const doMakeUserConfirmPassword = true;

@@ -19,6 +23,14 @@ export default function KcPage(props: { kcContext: KcContext }) {
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return (
<Login
{...{ kcContext, i18n, classes }}
Template={Template}
doUseDefaultCss={true}
/>
);
default:
return (
<DefaultPage
@@ -37,4 +49,18 @@ export default function KcPage(props: { kcContext: KcContext }) {
);
}

const classes = {} satisfies { [key in ClassKey]?: string };
const classes = {
/*
This is commended out because the same rules are applied in the index.css file
and applying the tailwind utility classes in the CSS file is recommended over applying them here.
This is because here you're limited in how precisely you can target the DOM elements and manage the specificity.
As you can see here I need to use `!` witch is shorthand for `!important` and this should be avoided if possible.
In the index.css I can simply use `body.kcBodyClass` or `.kcBodyClass.kcBodyClass` instead of just `.kcBodyClass`
to increase the specificity and avoid using `!important`.
*/
//kcBodyClass: twMerge(
// "!bg-[url(./assets/img/background.jpg)] bg-no-repeat bg-center bg-fixed",
// "font-geist"
//),
//kcHeaderWrapperClass: twMerge("text-3xl font-bold underline")
} satisfies { [key in ClassKey]?: string };
Binary file added src/login/assets/fonts/geist/Geist-Black.woff2
Binary file not shown.
Binary file added src/login/assets/fonts/geist/Geist-Bold.woff2
Binary file not shown.
Binary file added src/login/assets/fonts/geist/Geist-Light.woff2
Binary file not shown.
Binary file added src/login/assets/fonts/geist/Geist-Medium.woff2
Binary file not shown.
Binary file added src/login/assets/fonts/geist/Geist-Regular.woff2
Binary file not shown.
Binary file not shown.
Binary file added src/login/assets/fonts/geist/Geist-Thin.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
79 changes: 79 additions & 0 deletions src/login/assets/fonts/geist/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
@font-face {
font-family: "Geist";
src: url("./Geist-Black.woff2") format("woff2");
font-weight: 900;
/* Black */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-Bold.woff2") format("woff2");
font-weight: bold;
/* Bold */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-Light.woff2") format("woff2");
font-weight: 300;
/* Light */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-Medium.woff2") format("woff2");
font-weight: 500;
/* Medium */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-Regular.woff2") format("woff2");
font-weight: 400;
/* Regular */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-SemiBold.woff2") format("woff2");
font-weight: 600;
/* SemiBold */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-Thin.woff2") format("woff2");
font-weight: 100;
/* Thin */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-UltraLight.woff2") format("woff2");
font-weight: 200;
/* UltraLight */
font-style: normal;
}

@font-face {
font-family: "Geist";
src: url("./Geist-UltraBlack.woff2") format("woff2");
font-weight: 950;
/* UltraBlack */
font-style: normal;
}

@font-face {
font-family: "Geist Variable";
src: url("./GeistVariableVF.woff2") format("woff2");
font-weight: 100 950;
/* Range from Thin to UltraBlack */
font-style: normal;
}
Binary file added src/login/assets/img/background.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/login/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import url(./assets/fonts/geist/index.css);

@tailwind base;
@tailwind components;
@tailwind utilities;

body.kcBodyClass {
@apply bg-[url(./assets/img/background.jpg)] bg-no-repeat bg-center bg-fixed;
@apply font-geist;
}

.kcHeaderWrapperClass {
@apply text-3xl font-bold underline;
}
229 changes: 229 additions & 0 deletions src/login/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { useState, useEffect, useReducer } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";

export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;

const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});

const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext;

const { msg, msgStr } = i18n;

const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);

return (
<Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
displayMessage={!messagesPerField.existsError("username", "password")}
headerNode={msg("loginAccountTitle")}
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
infoNode={
<div id="kc-registration-container">
<div id="kc-registration">
<span>
{msg("noAccount")}{" "}
<a tabIndex={8} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
</div>
}
socialProvidersNode={
<>
{realm.password && social?.providers !== undefined && social.providers.length !== 0 && (
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
<hr />
<h2>{msg("identity-provider-login-label")}</h2>
<ul className={kcClsx("kcFormSocialAccountListClass", social.providers.length > 3 && "kcFormSocialAccountListGridClass")}>
{social.providers.map((...[p, , providers]) => (
<li key={p.alias}>
<a
id={`social-${p.alias}`}
className={kcClsx(
"kcFormSocialAccountListButtonClass",
providers.length > 3 && "kcFormSocialAccountGridItem"
)}
type="button"
href={p.loginUrl}
>
{p.iconClasses && <i className={clsx(kcClsx("kcCommonLogoIdP"), p.iconClasses)} aria-hidden="true"></i>}
<span
className={clsx(kcClsx("kcFormSocialAccountNameClass"), p.iconClasses && "kc-social-icon-text")}
dangerouslySetInnerHTML={{ __html: kcSanitize(p.displayName) }}
></span>
</a>
</li>
))}
</ul>
</div>
)}
</>
}
>
<div id="kc-form">
<div id="kc-form-wrapper">
{realm.password && (
<form
id="kc-form-login"
onSubmit={() => {
setIsLoginButtonDisabled(true);
return true;
}}
action={url.loginAction}
method="post"
>
{!usernameHidden && (
<div className={kcClsx("kcFormGroupClass")}>
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
{!realm.loginWithEmailAllowed
? msg("username")
: !realm.registrationEmailAsUsername
? msg("usernameOrEmail")
: msg("email")}
</label>
<input
tabIndex={2}
id="username"
className={kcClsx("kcInputClass")}
name="username"
defaultValue={login.username ?? ""}
type="text"
autoFocus
autoComplete="username"
aria-invalid={messagesPerField.existsError("username", "password")}
/>
{messagesPerField.existsError("username", "password") && (
<span
id="input-error"
className={kcClsx("kcInputErrorMessageClass")}
aria-live="polite"
dangerouslySetInnerHTML={{
__html: kcSanitize(messagesPerField.getFirstError("username", "password"))
}}
/>
)}
</div>
)}

<div className={kcClsx("kcFormGroupClass")}>
<label htmlFor="password" className={kcClsx("kcLabelClass")}>
{msg("password")}
</label>
<PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password">
<input
tabIndex={3}
id="password"
className={kcClsx("kcInputClass")}
name="password"
type="password"
autoComplete="current-password"
aria-invalid={messagesPerField.existsError("username", "password")}
/>
</PasswordWrapper>
{usernameHidden && messagesPerField.existsError("username", "password") && (
<span
id="input-error"
className={kcClsx("kcInputErrorMessageClass")}
aria-live="polite"
dangerouslySetInnerHTML={{
__html: kcSanitize(messagesPerField.getFirstError("username", "password"))
}}
/>
)}
</div>

<div className={kcClsx("kcFormGroupClass", "kcFormSettingClass")}>
<div id="kc-form-options">
{realm.rememberMe && !usernameHidden && (
<div className="checkbox">
<label>
<input
tabIndex={5}
id="rememberMe"
name="rememberMe"
type="checkbox"
defaultChecked={!!login.rememberMe}
/>{" "}
{msg("rememberMe")}
</label>
</div>
)}
</div>
<div className={kcClsx("kcFormOptionsWrapperClass")}>
{realm.resetPasswordAllowed && (
<span>
<a tabIndex={6} href={url.loginResetCredentialsUrl}>
{msg("doForgotPassword")}
</a>
</span>
)}
</div>
</div>

<div id="kc-form-buttons" className={kcClsx("kcFormGroupClass")}>
<input type="hidden" id="id-hidden-input" name="credentialId" value={auth.selectedCredential} />
<input
tabIndex={7}
disabled={isLoginButtonDisabled}
className={clsx(
kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass"),
"rounded-lg"
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</form>
)}
</div>
</div>
</Template>
);
}

function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: string; children: JSX.Element }) {
const { kcClsx, i18n, passwordInputId, children } = props;

const { msgStr } = i18n;

const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);

useEffect(() => {
const passwordInputElement = document.getElementById(passwordInputId);

assert(passwordInputElement instanceof HTMLInputElement);

passwordInputElement.type = isPasswordRevealed ? "text" : "password";
}, [isPasswordRevealed]);

return (
<div className={kcClsx("kcInputGroup")}>
{children}
<button
type="button"
className={kcClsx("kcFormPasswordVisibilityButtonClass")}
aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
aria-controls={passwordInputId}
onClick={toggleIsPasswordRevealed}
>
<i className={kcClsx(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")} aria-hidden />
</button>
</div>
);
}
12 changes: 12 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: {
extend: {
fontFamily: {
geist: ["Geist", "sans-serif"]
}
}
},
plugins: []
};
277 changes: 262 additions & 15 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit 4357120

Please sign in to comment.