mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 11:29:57 -06:00
color theme tab (#35179)
* added a way to customize theme colors fixes: #33233 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added preview and grouped vars Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added dark mode Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed label Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added empty check Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * use json string in attributes Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * removed use of not exported type Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * output css based on JSON string Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added feature flag Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added a way to customize theme colors fixes: #33233 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * renamed feature to quick theme Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed merge error Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * Restore the Cache tab in Realm Settings (#34311) closes keycloak#17727 Signed-off-by: Christian Janker <christian.janker@gmx.at> Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added a way to customize theme colors fixes: #33233 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * create a zip file instead Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added themes.json to make jar usable Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * use property instead of attribute Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fix the jar file Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * fixed header for preview and some text Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Signed-off-by: Christian Janker <christian.janker@gmx.at> Co-authored-by: Christian Ja <christian.janker@gmx.at>
This commit is contained in:
@@ -65,6 +65,8 @@ public class Profile {
|
||||
|
||||
LOGIN_V1("Legacy Login Theme", Type.DEPRECATED, 1),
|
||||
|
||||
QUICK_THEME("WYSIWYG theme configuration tool", Type.EXPERIMENTAL, 1),
|
||||
|
||||
DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),
|
||||
|
||||
IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),
|
||||
|
||||
@@ -51,7 +51,9 @@ export const Header = () => {
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: indexHref,
|
||||
src: joinPath(environment.resourceUrl, brandImage),
|
||||
src: brandImage.startsWith("/")
|
||||
? joinPath(environment.resourceUrl, brandImage)
|
||||
: brandImage,
|
||||
alt: t("logo"),
|
||||
className: style.brand,
|
||||
}}
|
||||
|
||||
@@ -1344,6 +1344,30 @@ keystorePasswordHelp=Password for the keys
|
||||
clientSettings=Client details
|
||||
deleteClientPolicyConditionConfirm=This action will permanently delete {{condition}}. This cannot be undone.
|
||||
selectATheme=Select a theme
|
||||
themeColors=Theme colors
|
||||
defaults=Reset to defaults
|
||||
themeColorsLight=Theme colors [light]
|
||||
themeColorsDark=Theme colors [dark]
|
||||
themePreviewInfo=In order to preview the theme colors, the current themen needs to be set to the one you want to preview, so we have automatically switched you to the one you want to preview.
|
||||
backgroundImage=Login background image
|
||||
errorColor=Error color
|
||||
successColor=Success color
|
||||
activeColor=Active color
|
||||
primaryColor=Primary color
|
||||
primaryColorHover=Primary color hover
|
||||
secondaryColor=Secondary color
|
||||
linkColor=Link color
|
||||
linkColorHover=Link color hover
|
||||
backgroundColorAccent=Background color accent
|
||||
backgroundColor=Background color
|
||||
backgroundColorNav=Background color navigation
|
||||
backgroundColorHeader=Background color header
|
||||
iconColor=Icon color
|
||||
textColor=Text color
|
||||
lightTextColor=Light text color
|
||||
inputBackgroundColor=Input background color
|
||||
inputTextColor=Input text color
|
||||
defaults=Reset to defaults
|
||||
permissionsList=Permission list
|
||||
attributeGroupHelp=Specifies the user profile group where this attribute will be added. This allows grouping various similar attributes together on different parts of the screen when creating or updating user.
|
||||
createRealm=Create realm
|
||||
@@ -3306,4 +3330,6 @@ organizationsMembersListError=Could not fetch organization members\: {{error}}
|
||||
MANAGED=Managed
|
||||
UNMANAGED=Unmanaged
|
||||
deleteConfirmUsers_one=Delete user {{name}}?
|
||||
deleteConfirmUsers_other=Delete {{count}} users?
|
||||
deleteConfirmUsers_other=Delete {{count}} users?
|
||||
downloadThemeJar=Download theme JAR
|
||||
themeColorInfo=Here you can set the patternfly color variables and create a "theme jar" file that you can download and put in your providers folder to apply the theme to your realm.
|
||||
@@ -102,6 +102,7 @@
|
||||
"flat": "^6.0.1",
|
||||
"i18next": "^24.0.2",
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"keycloak-js": "workspace:*",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.0",
|
||||
|
||||
94
js/apps/admin-ui/public/theme/login.css
Normal file
94
js/apps/admin-ui/public/theme/login.css
Normal file
@@ -0,0 +1,94 @@
|
||||
.pf-v5-c-login__container {
|
||||
grid-template-columns: 34rem;
|
||||
grid-template-areas: "header"
|
||||
"main"
|
||||
}
|
||||
|
||||
.login-pf body {
|
||||
background: var(--keycloak-bg-logo-url) no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.kc-logo-text {
|
||||
background-image: var(--keycloak-logo-url);
|
||||
height: var(--keycloak-logo-height);
|
||||
width: var(--keycloak-logo-width);
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div.kc-logo-text span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kc-login-tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.kc-login-tooltip .kc-tooltip-text {
|
||||
top: -3px;
|
||||
left: 160%;
|
||||
background-color: black;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
|
||||
min-width: 130px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
|
||||
padding: 5px;
|
||||
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
/* Show tooltip */
|
||||
.kc-login-tooltip:hover .kc-tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Arrow for tooltip */
|
||||
.kc-login-tooltip .kc-tooltip-text::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 100%;
|
||||
margin-top: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent black transparent transparent;
|
||||
}
|
||||
|
||||
#kc-recovery-codes-list {
|
||||
columns: 2;
|
||||
}
|
||||
|
||||
#certificate_subjectDN {
|
||||
overflow-wrap: break-word
|
||||
}
|
||||
|
||||
#kc-header-wrapper {
|
||||
font-size: 29px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
line-height: 1.2em;
|
||||
white-space: normal;
|
||||
color: var(--pf-v5-global--Color--light-100) !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: var(--pf-v5-global--spacer--sm);
|
||||
margin-bottom: var(--pf-v5-global--spacer--md);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
div.pf-v5-c-login__main-header {
|
||||
grid-template-columns: 70% 30%;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEnvironment, useHelp } from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
@@ -18,14 +19,15 @@ import { BarsIcon, EllipsisVIcon, HelpIcon } from "@patternfly/react-icons";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHref } from "react-router-dom";
|
||||
import { useEnvironment, useHelp } from "@keycloak/keycloak-ui-shared";
|
||||
import { PageHeaderClearCachesModal } from "./PageHeaderClearCachesModal";
|
||||
import { HelpHeader } from "./components/help-enabler/HelpHeader";
|
||||
import { useAccess } from "./context/access/Access";
|
||||
import { useRealm } from "./context/realm-context/RealmContext";
|
||||
import { useWhoAmI } from "./context/whoami/WhoAmI";
|
||||
import { toDashboard } from "./dashboard/routes/Dashboard";
|
||||
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
|
||||
import { joinPath } from "./utils/joinPath";
|
||||
import useToggle from "./utils/useToggle";
|
||||
import { PageHeaderClearCachesModal } from "./PageHeaderClearCachesModal";
|
||||
import { useAccess } from "./context/access/Access";
|
||||
|
||||
const ManageAccountDropdownItem = () => {
|
||||
const { keycloak } = useEnvironment();
|
||||
@@ -184,9 +186,11 @@ export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const contextLogo = usePreviewLogo();
|
||||
const customLogo = contextLogo?.logo;
|
||||
|
||||
const picture = keycloak.tokenParsed?.picture;
|
||||
const logo = environment.logo ? environment.logo : "/logo.svg";
|
||||
const logo = customLogo || environment.logo || "/logo.svg";
|
||||
const url = useHref(toDashboard({ realm }));
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : url;
|
||||
|
||||
@@ -199,7 +203,11 @@ export const Header = () => {
|
||||
</MastheadToggle>
|
||||
<MastheadBrand href={logoUrl}>
|
||||
<img
|
||||
src={environment.resourceUrl + logo}
|
||||
src={
|
||||
logo.startsWith("/")
|
||||
? joinPath(environment.resourceUrl, logo)
|
||||
: logo
|
||||
}
|
||||
id="masthead-logo"
|
||||
alt={t("logo")}
|
||||
aria-label={t("logo")}
|
||||
|
||||
@@ -253,7 +253,9 @@ export { PoliciesTab } from "./realm-settings/PoliciesTab";
|
||||
export * as RealmSettingsSection from "./realm-settings/RealmSettingsSection";
|
||||
export { RealmSettingsTabs } from "./realm-settings/RealmSettingsTabs";
|
||||
export { RealmSettingsSessionsTab } from "./realm-settings/SessionsTab";
|
||||
export { RealmSettingsThemesTab } from "./realm-settings/ThemesTab";
|
||||
export * as ThemesTab from "./realm-settings/themes/ThemesTab";
|
||||
export { ThemeColors } from "./realm-settings/themes/ThemeColors";
|
||||
export { ThemeSettingsTab } from "./realm-settings/themes/ThemeSettings";
|
||||
export { RealmSettingsTokensTab } from "./realm-settings/TokensTab";
|
||||
export { UserRegistration } from "./realm-settings/UserRegistration";
|
||||
export { RevocationModal } from "./sessions/RevocationModal";
|
||||
|
||||
@@ -42,7 +42,7 @@ import { PartialImportDialog } from "./PartialImport";
|
||||
import { PoliciesTab } from "./PoliciesTab";
|
||||
import ProfilesTab from "./ProfilesTab";
|
||||
import { RealmSettingsSessionsTab } from "./SessionsTab";
|
||||
import { RealmSettingsThemesTab } from "./ThemesTab";
|
||||
import ThemesTab from "./themes/ThemesTab";
|
||||
import { RealmSettingsTokensTab } from "./TokensTab";
|
||||
import { UserRegistration } from "./UserRegistration";
|
||||
import { EventsTab } from "./event-config/EventsTab";
|
||||
@@ -355,7 +355,7 @@ export const RealmSettingsTabs = () => {
|
||||
data-testid="rs-themes-tab"
|
||||
{...themesTab}
|
||||
>
|
||||
<RealmSettingsThemesTab realm={realm!} save={save} />
|
||||
<ThemesTab realm={realm!} save={save} />
|
||||
</Tab>
|
||||
<Tab
|
||||
title={<TabTitleText>{t("keys")}</TabTitleText>}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import type { AppRouteObject } from "../routes";
|
||||
import { AddAttributeRoute } from "./routes/AddAttribute";
|
||||
import { AddClientPolicyRoute } from "./routes/AddClientPolicy";
|
||||
import { AddClientProfileRoute } from "./routes/AddClientProfile";
|
||||
import { NewClientPolicyConditionRoute } from "./routes/AddCondition";
|
||||
import { AddExecutorRoute } from "./routes/AddExecutor";
|
||||
import { AttributeRoute } from "./routes/Attribute";
|
||||
import { ClientPoliciesRoute } from "./routes/ClientPolicies";
|
||||
import { ClientProfileRoute } from "./routes/ClientProfile";
|
||||
import { EditAttributesGroupRoute } from "./routes/EditAttributesGroup";
|
||||
import { EditClientPolicyRoute } from "./routes/EditClientPolicy";
|
||||
import { EditClientPolicyConditionRoute } from "./routes/EditCondition";
|
||||
import { ExecutorRoute } from "./routes/Executor";
|
||||
import { KeyProviderFormRoute } from "./routes/KeyProvider";
|
||||
import { KeysRoute } from "./routes/KeysTab";
|
||||
import { NewAttributesGroupRoute } from "./routes/NewAttributesGroup";
|
||||
import {
|
||||
RealmSettingsRoute,
|
||||
RealmSettingsRouteWithTab,
|
||||
} from "./routes/RealmSettings";
|
||||
import { ClientPoliciesRoute } from "./routes/ClientPolicies";
|
||||
import { AddClientProfileRoute } from "./routes/AddClientProfile";
|
||||
import { ClientProfileRoute } from "./routes/ClientProfile";
|
||||
import { AddExecutorRoute } from "./routes/AddExecutor";
|
||||
import { ExecutorRoute } from "./routes/Executor";
|
||||
import { AddClientPolicyRoute } from "./routes/AddClientPolicy";
|
||||
import { EditClientPolicyRoute } from "./routes/EditClientPolicy";
|
||||
import { NewClientPolicyConditionRoute } from "./routes/AddCondition";
|
||||
import { EditClientPolicyConditionRoute } from "./routes/EditCondition";
|
||||
import { ThemeTabRoute } from "./routes/ThemesTab";
|
||||
import { UserProfileRoute } from "./routes/UserProfile";
|
||||
import { AddAttributeRoute } from "./routes/AddAttribute";
|
||||
import { KeysRoute } from "./routes/KeysTab";
|
||||
import { AttributeRoute } from "./routes/Attribute";
|
||||
import { NewAttributesGroupRoute } from "./routes/NewAttributesGroup";
|
||||
import { EditAttributesGroupRoute } from "./routes/EditAttributesGroup";
|
||||
|
||||
const routes: AppRouteObject[] = [
|
||||
RealmSettingsRoute,
|
||||
@@ -39,6 +40,7 @@ const routes: AppRouteObject[] = [
|
||||
AttributeRoute,
|
||||
NewAttributesGroupRoute,
|
||||
EditAttributesGroupRoute,
|
||||
ThemeTabRoute,
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
26
js/apps/admin-ui/src/realm-settings/routes/ThemesTab.tsx
Normal file
26
js/apps/admin-ui/src/realm-settings/routes/ThemesTab.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { lazy } from "react";
|
||||
import type { Path } from "react-router-dom";
|
||||
import { generateEncodedPath } from "../../utils/generateEncodedPath";
|
||||
import type { AppRouteObject } from "../../routes";
|
||||
|
||||
export type ThemesTabType = "settings" | "lightColors" | "darkColors";
|
||||
|
||||
export type ThemesParams = {
|
||||
realm: string;
|
||||
tab: ThemesTabType;
|
||||
};
|
||||
|
||||
const RealmSettingsSection = lazy(() => import("../RealmSettingsSection"));
|
||||
|
||||
export const ThemeTabRoute: AppRouteObject = {
|
||||
path: "/:realm/realm-settings/themes/:tab",
|
||||
element: <RealmSettingsSection />,
|
||||
breadcrumb: (t) => t("themes"),
|
||||
handle: {
|
||||
access: "view-realm",
|
||||
},
|
||||
};
|
||||
|
||||
export const toThemesTab = (params: ThemesParams): Partial<Path> => ({
|
||||
pathname: generateEncodedPath(ThemeTabRoute.path, params),
|
||||
});
|
||||
69
js/apps/admin-ui/src/realm-settings/themes/ImageUpload.tsx
Normal file
69
js/apps/admin-ui/src/realm-settings/themes/ImageUpload.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared";
|
||||
import { FileUpload } from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
type ImageUploadProps = {
|
||||
name: string;
|
||||
onChange?: (file: string) => void;
|
||||
};
|
||||
|
||||
export const ImageUpload = ({ name, onChange }: ImageUploadProps) => {
|
||||
const [dataUri, setDataUri] = useState("");
|
||||
const [file, setFile] = useState<File>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { control } = useFormContext();
|
||||
|
||||
const fileToDataUri = (file: File) =>
|
||||
new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
resolve(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
if (file) {
|
||||
fileToDataUri(file).then((dataUri) => {
|
||||
setDataUri(dataUri);
|
||||
onChange?.(dataUri);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<>
|
||||
{isLoading && <KeycloakSpinner />}
|
||||
{dataUri && <img src={dataUri} width={200} height={200} />}
|
||||
<FileUpload
|
||||
id={name}
|
||||
type="dataURL"
|
||||
filename={file?.name}
|
||||
dropzoneProps={{
|
||||
accept: {
|
||||
"image/*": [".png", ".gif", ".jpeg", ".jpg", ".svg", ".webp"],
|
||||
},
|
||||
}}
|
||||
onFileInputChange={(_, file) => setFile(file)}
|
||||
onReadStarted={() => setIsLoading(true)}
|
||||
onReadFinished={(_, file) => {
|
||||
setFile(file);
|
||||
field.onChange(file);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onClearClick={() => {
|
||||
setFile(undefined);
|
||||
field.onChange(undefined);
|
||||
setDataUri("");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
23
js/apps/admin-ui/src/realm-settings/themes/LogoContext.tsx
Normal file
23
js/apps/admin-ui/src/realm-settings/themes/LogoContext.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createNamedContext } from "@keycloak/keycloak-ui-shared";
|
||||
import { PropsWithChildren, useContext, useState } from "react";
|
||||
|
||||
type LogoContextProps = {
|
||||
logo: string;
|
||||
setLogo: (logo: string) => void;
|
||||
};
|
||||
|
||||
export const LogoPreviewContext = createNamedContext<
|
||||
LogoContextProps | undefined
|
||||
>("LogoContext", undefined);
|
||||
|
||||
export const usePreviewLogo = () => useContext(LogoPreviewContext);
|
||||
|
||||
export const LogoContext = ({ children }: PropsWithChildren) => {
|
||||
const [logo, setLogo] = useState("");
|
||||
|
||||
return (
|
||||
<LogoPreviewContext.Provider value={{ logo, setLogo }}>
|
||||
{children}
|
||||
</LogoPreviewContext.Provider>
|
||||
);
|
||||
};
|
||||
117
js/apps/admin-ui/src/realm-settings/themes/PatternflyVars.ts
Normal file
117
js/apps/admin-ui/src/realm-settings/themes/PatternflyVars.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// variable is the PF5 variable with the --pf-v5-global-- prefix removed
|
||||
const variables = [
|
||||
{
|
||||
name: "font",
|
||||
defaultValue: '"RedHatText", helvetica, arial, sans-serif',
|
||||
variable: "FontFamily--text",
|
||||
},
|
||||
{
|
||||
name: "errorColor",
|
||||
defaultValue: { light: "#c9190b", dark: "#fe5142" },
|
||||
variable: "danger-color--100",
|
||||
},
|
||||
{
|
||||
name: "successColor",
|
||||
defaultValue: { light: "#3e8635", dark: "#5ba352" },
|
||||
variable: "success-color--100",
|
||||
},
|
||||
{
|
||||
name: "activeColor",
|
||||
defaultValue: { light: "#0066cc", dark: "#1fa7f8" },
|
||||
variable: "active-color--100",
|
||||
},
|
||||
{
|
||||
name: "primaryColor",
|
||||
defaultValue: "#0066cc",
|
||||
variable: { light: "primary-color--100", dark: "primary-color--300" },
|
||||
},
|
||||
{
|
||||
name: "primaryColorHover",
|
||||
defaultValue: "#004080",
|
||||
variable: "primary-color--200",
|
||||
},
|
||||
{
|
||||
name: "secondaryColor",
|
||||
defaultValue: { light: "#0066cc", dark: "#1fa7f8" },
|
||||
variable: "primary-color--100",
|
||||
},
|
||||
{
|
||||
name: "linkColor",
|
||||
defaultValue: { light: "#0066cc", dark: "#1fa7f8" },
|
||||
variable: "link--Color",
|
||||
},
|
||||
{
|
||||
name: "linkColorHover",
|
||||
defaultValue: { light: "#004080", dark: "#73bcf7" },
|
||||
variable: "link--Color--hover",
|
||||
},
|
||||
{
|
||||
name: "backgroundColor",
|
||||
defaultValue: { light: "#ffffff", dark: "#1b1d21" },
|
||||
variable: "BackgroundColor--light-100",
|
||||
},
|
||||
{
|
||||
name: "backgroundColorAccent",
|
||||
defaultValue: "#26292d",
|
||||
variable: { dark: "BackgroundColor--300" },
|
||||
},
|
||||
{
|
||||
name: "backgroundColorNav",
|
||||
defaultValue: { light: "#212427", dark: "#1b1d21" },
|
||||
variable: {
|
||||
light: "BackgroundColor--dark-300",
|
||||
dark: "BackgroundColor--100",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backgroundColorHeader",
|
||||
defaultValue: { light: "#151515", dark: "#030303" },
|
||||
variable: {
|
||||
light: "BackgroundColor--dark-100",
|
||||
dark: "palette--black-1000",
|
||||
},
|
||||
},
|
||||
{ name: "iconColor", defaultValue: "#f0f0f0", variable: "Color--light-200" },
|
||||
{
|
||||
name: "textColor",
|
||||
defaultValue: { light: "#151515", dark: "#e0e0e0" },
|
||||
variable: "Color--100",
|
||||
},
|
||||
{
|
||||
name: "lightTextColor",
|
||||
defaultValue: { light: "#ffffff", dark: "#e0e0e0" },
|
||||
variable: "Color--light-100",
|
||||
},
|
||||
{
|
||||
name: "inputBackgroundColor",
|
||||
defaultValue: "#36373a",
|
||||
variable: { dark: "BackgroundColor--400" },
|
||||
},
|
||||
{
|
||||
name: "inputTextColor",
|
||||
defaultValue: { light: "#151515", dark: "#e0e0e0" },
|
||||
variable: "Color--dark-100",
|
||||
},
|
||||
];
|
||||
|
||||
type Value = { light?: string; dark: string };
|
||||
type ThemeType = keyof Value;
|
||||
|
||||
const convert = (v: string | Value, theme: ThemeType) =>
|
||||
typeof v === "string" ? v : v[theme];
|
||||
|
||||
export const lightTheme = () =>
|
||||
variables
|
||||
.filter((v) => typeof v.defaultValue !== "string")
|
||||
.map((v) => ({
|
||||
name: v.name,
|
||||
defaultValue: convert(v.defaultValue, "light"),
|
||||
variable: convert(v.variable, "light"),
|
||||
}));
|
||||
|
||||
export const darkTheme = () =>
|
||||
variables.map((v) => ({
|
||||
name: v.name,
|
||||
defaultValue: convert(v.defaultValue, "dark"),
|
||||
variable: convert(v.variable, "dark"),
|
||||
}));
|
||||
52
js/apps/admin-ui/src/realm-settings/themes/PreviewWindow.tsx
Normal file
52
js/apps/admin-ui/src/realm-settings/themes/PreviewWindow.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Form,
|
||||
Page,
|
||||
PageSection,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabTitleText,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import { Header } from "../../PageHeader";
|
||||
|
||||
type PreviewWindowProps = {
|
||||
cssVars: Record<string, string>;
|
||||
};
|
||||
|
||||
export const PreviewWindow = ({ cssVars }: PreviewWindowProps) => (
|
||||
<>
|
||||
<style>{`
|
||||
.preview {
|
||||
${Object.entries(cssVars)
|
||||
.map(([key, value]) => `--pf-v5-global--${key}: ${value};`)
|
||||
.join("\n")}
|
||||
}
|
||||
`}</style>
|
||||
<Page className="preview" header={<Header />}>
|
||||
<PageSection
|
||||
variant="light"
|
||||
style={{
|
||||
backgroundColor: cssVars["BackgroundColor--light-100"],
|
||||
}}
|
||||
>
|
||||
<Tabs activeKey={1} className="pf-v5-u-p-lg">
|
||||
<Tab eventKey={0} title={<TabTitleText>Tab One</TabTitleText>} />
|
||||
<Tab eventKey={1} title={<TabTitleText>Tab Two</TabTitleText>} />
|
||||
</Tabs>
|
||||
<Alert title="Error" isInline variant="danger" />
|
||||
<Alert title="Success" isInline variant="success" />
|
||||
<p className="pf-v5-u-p-lg pf-v5-c-content">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</p>
|
||||
<Form>
|
||||
<TextInput id="test" placeholder="Text input" />
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="link">Link button</Button>
|
||||
</Form>
|
||||
</PageSection>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
184
js/apps/admin-ui/src/realm-settings/themes/ThemeColors.tsx
Normal file
184
js/apps/admin-ui/src/realm-settings/themes/ThemeColors.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import { TextControl } from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Flex,
|
||||
FlexItem,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
InputGroupItem,
|
||||
PageSection,
|
||||
Text,
|
||||
TextContent,
|
||||
TextInputProps,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import {
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormContext,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FixedButtonsGroup } from "../../components/form/FixedButtonGroup";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { ImageUpload } from "./ImageUpload";
|
||||
import { usePreviewLogo } from "./LogoContext";
|
||||
import { darkTheme, lightTheme } from "./PatternflyVars";
|
||||
import { PreviewWindow } from "./PreviewWindow";
|
||||
import { ThemeRealmRepresentation } from "./ThemesTab";
|
||||
|
||||
type ThemeType = "light" | "dark";
|
||||
|
||||
type ColorControlProps = TextInputProps & {
|
||||
name: string;
|
||||
label: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const ColorControl = ({ name, color, label, ...props }: ColorControlProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { control, setValue } = useFormContext();
|
||||
const currentValue = useWatch({
|
||||
control,
|
||||
name,
|
||||
});
|
||||
return (
|
||||
<InputGroup>
|
||||
<InputGroupItem isFill>
|
||||
<TextControl {...props} name={name} label={t(label)} />
|
||||
</InputGroupItem>
|
||||
<input
|
||||
type="color"
|
||||
value={currentValue || color}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const switchTheme = (theme: ThemeType) => {
|
||||
if (theme === "light") {
|
||||
document
|
||||
.querySelector('meta[name="color-scheme"]')!
|
||||
.setAttribute("content", "light");
|
||||
document.documentElement.classList.remove("pf-v5-theme-dark");
|
||||
} else {
|
||||
document.documentElement.classList.add("pf-v5-theme-dark");
|
||||
}
|
||||
};
|
||||
|
||||
type ThemeColorsProps = {
|
||||
realm: RealmRepresentation;
|
||||
save: (realm: ThemeRealmRepresentation) => void;
|
||||
theme: "light" | "dark";
|
||||
};
|
||||
|
||||
export const ThemeColors = ({ realm, save, theme }: ThemeColorsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm();
|
||||
const { handleSubmit, watch } = form;
|
||||
const style = watch();
|
||||
const contextLogo = usePreviewLogo();
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const mapping = useMemo(
|
||||
() => (theme === "light" ? lightTheme() : darkTheme()),
|
||||
[],
|
||||
);
|
||||
|
||||
const reset = () => {
|
||||
form.reset({
|
||||
[theme]: mapping.reduce(
|
||||
(acc, m) => ({
|
||||
...acc,
|
||||
[m.variable!]: m.defaultValue,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const setupForm = () => {
|
||||
const values = JSON.parse(realm.attributes?.style || "{}");
|
||||
if (values[theme]) {
|
||||
form.reset(values);
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
const convert = (values: Record<string, File | string>) => {
|
||||
const styles = JSON.parse(realm.attributes?.style || "{}");
|
||||
save({
|
||||
...realm,
|
||||
logo: values.logo as File,
|
||||
bgimage: values.bgimage as File,
|
||||
attributes: {
|
||||
...realm.attributes,
|
||||
style: JSON.stringify({
|
||||
...styles,
|
||||
...values,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setupForm();
|
||||
switchTheme(theme);
|
||||
return () => {
|
||||
switchTheme(mediaQuery.matches ? "dark" : "light");
|
||||
};
|
||||
}, [realm]);
|
||||
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<TextContent className="pf-v5-u-mb-lg">
|
||||
<Text>{t("themeColorInfo")}</Text>
|
||||
</TextContent>
|
||||
{mediaQuery.matches && theme === "light" && (
|
||||
<Alert variant="info" isInline title={t("themePreviewInfo")} />
|
||||
)}
|
||||
<Flex className="pf-v5-u-pt-lg">
|
||||
<FlexItem>
|
||||
<FormAccess isHorizontal role="manage-realm">
|
||||
<FormProvider {...form}>
|
||||
<FormGroup label={t("logo")}>
|
||||
<ImageUpload
|
||||
name="logo"
|
||||
onChange={(logo) => contextLogo?.setLogo(logo)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("backgroundImage")}>
|
||||
<ImageUpload name="bgimage" />
|
||||
</FormGroup>
|
||||
{mapping.map((m) => (
|
||||
<ColorControl
|
||||
key={m.name}
|
||||
color={m.defaultValue!}
|
||||
name={`${theme}.${m.variable!}`}
|
||||
label={m.name}
|
||||
/>
|
||||
))}
|
||||
</FormProvider>
|
||||
</FormAccess>
|
||||
</FlexItem>
|
||||
<FlexItem grow={{ default: "grow" }} style={{ zIndex: 0 }}>
|
||||
<PreviewWindow cssVars={style?.[theme] || {}} />
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<FixedButtonsGroup
|
||||
name="colors"
|
||||
saveText={t("downloadThemeJar")}
|
||||
save={handleSubmit(convert)}
|
||||
reset={setupForm}
|
||||
>
|
||||
<Button type="button" variant="link" onClick={reset}>
|
||||
{t("defaults")}
|
||||
</Button>
|
||||
</FixedButtonsGroup>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
@@ -4,20 +4,17 @@ import { ActionGroup, Button, PageSection } from "@patternfly/react-core";
|
||||
import { useEffect } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormAccess } from "../components/form/FormAccess";
|
||||
import { DefaultSwitchControl } from "../components/SwitchControl";
|
||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import { convertToFormValues } from "../util";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { DefaultSwitchControl } from "../../components/SwitchControl";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
import { convertToFormValues } from "../../util";
|
||||
|
||||
type RealmSettingsThemesTabProps = {
|
||||
type ThemeSettingsTabProps = {
|
||||
realm: RealmRepresentation;
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
};
|
||||
|
||||
export const RealmSettingsThemesTab = ({
|
||||
realm,
|
||||
save,
|
||||
}: RealmSettingsThemesTabProps) => {
|
||||
export const ThemeSettingsTab = ({ realm, save }: ThemeSettingsTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<RealmRepresentation>();
|
||||
192
js/apps/admin-ui/src/realm-settings/themes/ThemesTab.tsx
Normal file
192
js/apps/admin-ui/src/realm-settings/themes/ThemesTab.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import { useEnvironment } from "@keycloak/keycloak-ui-shared";
|
||||
import { Tab, TabTitleText } from "@patternfly/react-core";
|
||||
import JSZip from "jszip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
RoutableTabs,
|
||||
useRoutableTab,
|
||||
} from "../../components/routable-tabs/RoutableTabs";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { joinPath } from "../../utils/joinPath";
|
||||
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";
|
||||
import { ThemesTabType, toThemesTab } from "../routes/ThemesTab";
|
||||
import { LogoContext } from "./LogoContext";
|
||||
import { ThemeColors } from "./ThemeColors";
|
||||
import { ThemeSettingsTab } from "./ThemeSettings";
|
||||
|
||||
type ThemesTabProps = {
|
||||
realm: RealmRepresentation;
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
};
|
||||
|
||||
export type ThemeRealmRepresentation = RealmRepresentation & {
|
||||
logo?: File;
|
||||
bgimage?: File;
|
||||
};
|
||||
|
||||
export default function ThemesTab({ realm, save }: ThemesTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { realm: realmName } = useRealm();
|
||||
const { environment } = useEnvironment();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
|
||||
const saveTheme = async (realm: ThemeRealmRepresentation) => {
|
||||
const zip = new JSZip();
|
||||
|
||||
const styles = JSON.parse(realm.attributes?.style ?? "{}");
|
||||
|
||||
const { logo, bgimage, ...rest } = realm;
|
||||
|
||||
const logoName =
|
||||
"img/logo" + logo?.name?.substring(logo?.name?.lastIndexOf("."));
|
||||
const bgimageName =
|
||||
"img/bgimage" + bgimage?.name?.substring(bgimage?.name?.lastIndexOf("."));
|
||||
|
||||
if (logo) {
|
||||
zip.file(`theme/quick-theme/common/resources/${logoName}`, logo);
|
||||
}
|
||||
if (bgimage) {
|
||||
zip.file(`theme/quick-theme/common/resources/${bgimageName}`, bgimage);
|
||||
}
|
||||
|
||||
zip.file(
|
||||
"theme/quick-theme/admin/theme.properties",
|
||||
`
|
||||
parent=keycloak.v2
|
||||
import=common/quick-theme
|
||||
|
||||
logo=${logoName}
|
||||
styles=css/theme-styles.css
|
||||
`,
|
||||
);
|
||||
|
||||
zip.file(
|
||||
"theme/quick-theme/account/theme.properties",
|
||||
`
|
||||
parent=keycloak.v3
|
||||
import=common/quick-theme
|
||||
|
||||
logo=${logoName}
|
||||
styles=css/theme-styles.css
|
||||
`,
|
||||
);
|
||||
|
||||
zip.file(
|
||||
"theme/quick-theme/login/theme.properties",
|
||||
`
|
||||
parent=keycloak.v2
|
||||
import=common/quick-theme
|
||||
|
||||
styles=css/login.css css/theme-styles.css
|
||||
`,
|
||||
);
|
||||
|
||||
zip.file(
|
||||
"META-INF/keycloak-themes.json",
|
||||
`{
|
||||
"themes": [{
|
||||
"name" : "quick-theme",
|
||||
"types": [ "login", "account", "admin", "common" ]
|
||||
}]
|
||||
}`,
|
||||
);
|
||||
|
||||
const toCss = (obj?: object) =>
|
||||
Object.entries(obj || {})
|
||||
.map(([key, value]) => `--pf-v5-global--${key}: ${value};`)
|
||||
.join("\n");
|
||||
|
||||
const logoCss = (
|
||||
await fetch(joinPath(environment.resourceUrl, "/theme/login.css"))
|
||||
).text();
|
||||
zip.file("theme/quick-theme/common/resources/css/login.css", logoCss);
|
||||
|
||||
zip.file(
|
||||
"theme/quick-theme/common/resources/css/theme-styles.css",
|
||||
`:root {
|
||||
--keycloak-bg-logo-url: url('../${bgimageName}');
|
||||
--keycloak-logo-url: url('../${logoName}');
|
||||
--keycloak-logo-height: 63px;
|
||||
--keycloak-logo-width: 300px;
|
||||
${toCss(styles.light)}
|
||||
}
|
||||
.pf-v5-theme-dark {
|
||||
${toCss(styles.dark)}
|
||||
}
|
||||
`,
|
||||
);
|
||||
save({
|
||||
...rest,
|
||||
attributes: {
|
||||
...rest.attributes,
|
||||
style: JSON.stringify({
|
||||
...styles,
|
||||
logo: logoName,
|
||||
bgimage: bgimageName,
|
||||
}),
|
||||
},
|
||||
});
|
||||
zip.generateAsync({ type: "blob" }).then((content) => {
|
||||
const url = URL.createObjectURL(content);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "quick-theme.jar";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
};
|
||||
|
||||
const param = (tab: ThemesTabType) => ({
|
||||
realm: realmName,
|
||||
tab,
|
||||
});
|
||||
|
||||
const settingsTab = useRoutableTab(toThemesTab(param("settings")));
|
||||
const lightColorsTab = useRoutableTab(toThemesTab(param("lightColors")));
|
||||
const darkColorsTab = useRoutableTab(toThemesTab(param("darkColors")));
|
||||
|
||||
if (!isFeatureEnabled(Feature.QuickTheme)) {
|
||||
return <ThemeSettingsTab realm={realm} save={save} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoutableTabs
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
defaultLocation={toThemesTab({
|
||||
realm: realmName,
|
||||
tab: "settings",
|
||||
})}
|
||||
>
|
||||
<Tab
|
||||
id="themes-settings"
|
||||
title={<TabTitleText>{t("themes")} </TabTitleText>}
|
||||
data-testid="themes-settings-tab"
|
||||
{...settingsTab}
|
||||
>
|
||||
<ThemeSettingsTab realm={realm} save={save} />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="lightColors"
|
||||
title={<TabTitleText>{t("themeColorsLight")}</TabTitleText>}
|
||||
data-testid="lightColors-tab"
|
||||
{...lightColorsTab}
|
||||
>
|
||||
<LogoContext>
|
||||
<ThemeColors realm={realm} save={saveTheme} theme="light" />
|
||||
</LogoContext>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="darkColors"
|
||||
title={<TabTitleText>{t("themeColorsDark")}</TabTitleText>}
|
||||
data-testid="darkColors-tab"
|
||||
{...darkColorsTab}
|
||||
>
|
||||
<LogoContext>
|
||||
<ThemeColors realm={realm} save={saveTheme} theme="dark" />
|
||||
</LogoContext>
|
||||
</Tab>
|
||||
</RoutableTabs>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export enum Feature {
|
||||
DeclarativeUI = "DECLARATIVE_UI",
|
||||
Organizations = "ORGANIZATION",
|
||||
OpenId4VCI = "OID4VC_VCI",
|
||||
QuickTheme = "QUICK_THEME",
|
||||
}
|
||||
|
||||
export default function useIsFeatureEnabled() {
|
||||
|
||||
36
js/pnpm-lock.yaml
generated
36
js/pnpm-lock.yaml
generated
@@ -192,6 +192,9 @@ importers:
|
||||
i18next-http-backend:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
keycloak-js:
|
||||
specifier: workspace:*
|
||||
version: link:../../libs/keycloak-js
|
||||
@@ -3206,6 +3209,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
import-fresh@3.3.0:
|
||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3519,6 +3525,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -3551,6 +3560,9 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
lightningcss-darwin-arm64@1.28.2:
|
||||
resolution: {integrity: sha512-/8cPSqZiusHSS+WQz0W4NuaqFjquys1x+NsdN/XOHb+idGHJSoJ7SoQTVl3DZuAgtPZwFZgRfb/vd1oi8uX6+g==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -3912,6 +3924,9 @@ packages:
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4346,6 +4361,9 @@ packages:
|
||||
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5684,6 +5702,7 @@ snapshots:
|
||||
flat: 6.0.1
|
||||
i18next: 24.0.2(typescript@5.7.2)
|
||||
i18next-http-backend: 3.0.1
|
||||
jszip: 3.10.1
|
||||
keycloak-js: link:libs/keycloak-js
|
||||
lodash-es: 4.17.21
|
||||
monaco-editor: 0.52.0
|
||||
@@ -8207,6 +8226,8 @@ snapshots:
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
import-fresh@3.3.0:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
@@ -8490,6 +8511,13 @@ snapshots:
|
||||
object.assign: 4.1.5
|
||||
object.values: 1.2.0
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -8525,6 +8553,10 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lightningcss-darwin-arm64@1.28.2:
|
||||
optional: true
|
||||
|
||||
@@ -8898,6 +8930,8 @@ snapshots:
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -9384,6 +9418,8 @@ snapshots:
|
||||
functions-have-names: 1.2.3
|
||||
has-property-descriptors: 1.0.2
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
:root {
|
||||
--keycloak-logo-url: url('../img/keycloak-logo-text.png');
|
||||
--keycloak-bg-logo-url: url("../img/keycloak-bg.png");
|
||||
--keycloak-logo-height: 63px;
|
||||
--keycloak-logo-width: 300px;
|
||||
}
|
||||
|
||||
.pf-v5-c-login__container {
|
||||
grid-template-columns: 34rem;
|
||||
grid-template-areas: "header"
|
||||
@@ -5,16 +12,17 @@
|
||||
}
|
||||
|
||||
.login-pf body {
|
||||
background: url("../img/keycloak-bg.png") no-repeat center center fixed;
|
||||
background: var(--keycloak-bg-logo-url) no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.kc-logo-text {
|
||||
background-image: url(../img/keycloak-logo-text.png);
|
||||
background-image: var(--keycloak-logo-url);
|
||||
height: var(--keycloak-logo-height);
|
||||
width: var(--keycloak-logo-width);
|
||||
background-repeat: no-repeat;
|
||||
height: 63px;
|
||||
width: 300px;
|
||||
background-size: contain;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user