Compare commits
10
Commits
bb49401545
...
main
Generated
+26
@@ -13,6 +13,7 @@
|
||||
"@dagrejs/dagre": "^1.1.8",
|
||||
"@keycloak/keycloak-admin-client": "26.7.0",
|
||||
"@keycloakify/email-native": "~260007.0.0",
|
||||
"@keycloakify/keycloak-account-ui": "~260700.0.3",
|
||||
"@keycloakify/keycloak-admin-ui": "~260700.0.2",
|
||||
"@keycloakify/keycloak-ui-shared": "~260700.0.2",
|
||||
"@patternfly/patternfly": "^5.4.2",
|
||||
@@ -1246,6 +1247,31 @@
|
||||
"integrity": "sha512-hFUiOwwpc84eXPAct44G/pTBkYnDQF1jIObME+ZUrb7XidmFJ/TdH9q49LKpd9sIivSyOEJa1qLtrggP+c+BJw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@keycloakify/keycloak-account-ui": {
|
||||
"version": "260700.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@keycloakify/keycloak-account-ui/-/keycloak-account-ui-260700.0.3.tgz",
|
||||
"integrity": "sha512-K7jveFTb4BYYs33KI5VL+wF4H6/QSmQWT9NnqJsZbdIy7vnaFwZPUGRzJ2giQ9FHxGqG6R63W/mgmrnyzwdDUA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tsafe": "^1.8.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@keycloakify/keycloak-ui-shared": "~260700.0.2",
|
||||
"@patternfly/patternfly": "^5.4.2",
|
||||
"@patternfly/react-core": "^5.4.14",
|
||||
"@patternfly/react-icons": "^5.4.2",
|
||||
"@patternfly/react-table": "^5.4.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^18.3.18",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next-fetch-backend": "^7.0.0",
|
||||
"lodash-es": "^4.18.1",
|
||||
"react": "^18.3.1",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-i18next": "^16.5.1",
|
||||
"react-router-dom": "^6.30.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@keycloakify/keycloak-admin-ui": {
|
||||
"version": "260700.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@keycloakify/keycloak-admin-ui/-/keycloak-admin-ui-260700.0.2.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@keycloakify/keycloak-admin-ui": "~260700.0.2",
|
||||
"@keycloakify/keycloak-account-ui": "~260700.0.3",
|
||||
"@dagrejs/dagre": "^1.1.8",
|
||||
"@keycloak/keycloak-admin-client": "26.7.0",
|
||||
"@keycloakify/keycloak-ui-shared": "~260700.0.2",
|
||||
|
||||
@@ -5,3 +5,6 @@
|
||||
|
||||
# === @keycloakify/keycloak-admin-ui v260700.0.2 ===
|
||||
/admin/early-color-scheme.js
|
||||
|
||||
# === @keycloakify/keycloak-account-ui v260700.0.3 ===
|
||||
/account/early-color-scheme.js
|
||||
|
||||
+259
-15
@@ -1,9 +1,34 @@
|
||||
# This file is managed by Keycloakify, do not edit it manually.
|
||||
|
||||
# === Owned files start ===
|
||||
# account/colorScheme.ts
|
||||
# account/KcAccountUi.tsx
|
||||
# account/KcContext.ts
|
||||
# account/KcPage.tsx
|
||||
# admin/App.tsx
|
||||
# admin/colorScheme.ts
|
||||
# admin/index.css
|
||||
# admin/KcAdminUi.tsx
|
||||
# admin/page-nav.css
|
||||
# admin/PageHeader.tsx
|
||||
# admin/PageNav.tsx
|
||||
# account/assets/logo.svg
|
||||
# account/personal-info/PersonalInfo.tsx
|
||||
# account/root/Header.tsx
|
||||
# account/root/PageNav.tsx
|
||||
# account/root/Root.tsx
|
||||
# admin/assets/logo.svg
|
||||
# admin/dashboard/dashboard.css
|
||||
# admin/dashboard/Dashboard.tsx
|
||||
# admin/i18n/messages_en.properties
|
||||
# admin/i18n/messages_zh_Hans.properties
|
||||
# admin/i18n/messages_zh_Hant.properties
|
||||
# email/html/template.ftl
|
||||
# email/messages/messages_en.properties
|
||||
# email/messages/messages_zh_CN.properties
|
||||
# email/messages/messages_zh_TW.properties
|
||||
# account/components/page/Page.tsx
|
||||
# admin/components/view-header/ViewHeader.tsx
|
||||
# === Owned files end =====
|
||||
|
||||
# === @keycloakify/email-native v260007.0.0 ===
|
||||
@@ -30,7 +55,6 @@
|
||||
/email/messages/messages_da.properties
|
||||
/email/messages/messages_de.properties
|
||||
/email/messages/messages_el.properties
|
||||
/email/messages/messages_en.properties
|
||||
/email/messages/messages_es.properties
|
||||
/email/messages/messages_fa.properties
|
||||
/email/messages/messages_fi.properties
|
||||
@@ -51,8 +75,6 @@
|
||||
/email/messages/messages_th.properties
|
||||
/email/messages/messages_tr.properties
|
||||
/email/messages/messages_uk.properties
|
||||
/email/messages/messages_zh_CN.properties
|
||||
/email/messages/messages_zh_TW.properties
|
||||
/email/text/email-test.ftl
|
||||
/email/text/email-update-confirmation.ftl
|
||||
/email/text/email-verification-with-code.ftl
|
||||
@@ -72,22 +94,16 @@
|
||||
|
||||
# === @keycloakify/keycloak-admin-ui v260700.0.2 ===
|
||||
/admin/admin-client.tsx
|
||||
/admin/App.tsx
|
||||
/admin/banners.module.css
|
||||
/admin/Banners.tsx
|
||||
/admin/colorScheme.ts
|
||||
/admin/environment-types.ts
|
||||
/admin/environment.ts
|
||||
/admin/ForbiddenSection.tsx
|
||||
/admin/help-urls.ts
|
||||
/admin/i18next.d.ts
|
||||
/admin/KcAdminUi.tsx
|
||||
/admin/KcContext.ts
|
||||
/admin/KcPage.tsx
|
||||
/admin/page-nav.css
|
||||
/admin/PageHeader.tsx
|
||||
/admin/PageHeaderClearCachesModal.tsx
|
||||
/admin/PageNav.tsx
|
||||
/admin/PageNotFoundSection.tsx
|
||||
/admin/Root.tsx
|
||||
/admin/routes.tsx
|
||||
@@ -124,8 +140,6 @@
|
||||
/admin/clients/utils.ts
|
||||
/admin/components/SwitchControl.tsx
|
||||
/admin/context/RecentRealms.tsx
|
||||
/admin/dashboard/dashboard.css
|
||||
/admin/dashboard/Dashboard.tsx
|
||||
/admin/dashboard/ProviderInfo.tsx
|
||||
/admin/dashboard/routes.ts
|
||||
/admin/events/AdminEvents.tsx
|
||||
@@ -152,7 +166,6 @@
|
||||
/admin/i18n/messages_cs.properties
|
||||
/admin/i18n/messages_de.properties
|
||||
/admin/i18n/messages_el.properties
|
||||
/admin/i18n/messages_en.properties
|
||||
/admin/i18n/messages_es.properties
|
||||
/admin/i18n/messages_eu.properties
|
||||
/admin/i18n/messages_fr.properties
|
||||
@@ -175,8 +188,6 @@
|
||||
/admin/i18n/messages_tr.properties
|
||||
/admin/i18n/messages_uk.properties
|
||||
/admin/i18n/messages_vi.properties
|
||||
/admin/i18n/messages_zh_Hans.properties
|
||||
/admin/i18n/messages_zh_Hant.properties
|
||||
/admin/identity-providers/IdentityProvidersSection.tsx
|
||||
/admin/identity-providers/ManageOrderDialog.tsx
|
||||
/admin/identity-providers/OIDCConfigurationRepresentation.ts
|
||||
@@ -515,7 +526,6 @@
|
||||
/admin/components/users/UserDataTableAttributeSearchForm.tsx
|
||||
/admin/components/users/UserDataTableToolbarItems.tsx
|
||||
/admin/components/users/UserSelect.tsx
|
||||
/admin/components/view-header/ViewHeader.tsx
|
||||
/admin/components/wizard-section-header/wizard-section-header.css
|
||||
/admin/components/wizard-section-header/WizardSectionHeader.tsx
|
||||
/admin/context/access/Access.tsx
|
||||
@@ -831,3 +841,237 @@
|
||||
/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx
|
||||
/shared/keycloak-ui-shared/controls/table/PaginatingTableToolbar.tsx
|
||||
/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx
|
||||
|
||||
# === @keycloakify/keycloak-account-ui v260700.0.3 ===
|
||||
/account/api.ts
|
||||
/account/environment.ts
|
||||
/account/i18n-type.ts
|
||||
/account/i18next.d.ts
|
||||
/account/index.ts
|
||||
/account/routes.tsx
|
||||
/account/account-security/AccountRow.tsx
|
||||
/account/account-security/DeviceActivity.tsx
|
||||
/account/account-security/LinkedAccounts.tsx
|
||||
/account/account-security/LinkedAccountsToolbar.tsx
|
||||
/account/account-security/SigningIn.tsx
|
||||
/account/api/constants.ts
|
||||
/account/api/methods.ts
|
||||
/account/api/parse-links.ts
|
||||
/account/api/parse-response.ts
|
||||
/account/api/representations.ts
|
||||
/account/api/request.ts
|
||||
/account/applications/Applications.tsx
|
||||
/account/assets/content.ts
|
||||
/account/assets/favicon.svg
|
||||
/account/content/ContentComponent.tsx
|
||||
/account/content/fetchContent.ts
|
||||
/account/groups/Groups.tsx
|
||||
/account/i18n/i18n.ts
|
||||
/account/i18n/index.ts
|
||||
/account/i18n/messages_ar.properties
|
||||
/account/i18n/messages_az.properties
|
||||
/account/i18n/messages_ca.properties
|
||||
/account/i18n/messages_cs.properties
|
||||
/account/i18n/messages_da.properties
|
||||
/account/i18n/messages_de.properties
|
||||
/account/i18n/messages_el.properties
|
||||
/account/i18n/messages_en.properties
|
||||
/account/i18n/messages_es.properties
|
||||
/account/i18n/messages_eu.properties
|
||||
/account/i18n/messages_fa.properties
|
||||
/account/i18n/messages_fi.properties
|
||||
/account/i18n/messages_fr.properties
|
||||
/account/i18n/messages_hr.properties
|
||||
/account/i18n/messages_hu.properties
|
||||
/account/i18n/messages_hy.properties
|
||||
/account/i18n/messages_id.properties
|
||||
/account/i18n/messages_it.properties
|
||||
/account/i18n/messages_ja.properties
|
||||
/account/i18n/messages_ka.properties
|
||||
/account/i18n/messages_kk.properties
|
||||
/account/i18n/messages_ky.properties
|
||||
/account/i18n/messages_lt.properties
|
||||
/account/i18n/messages_lv.properties
|
||||
/account/i18n/messages_nl.properties
|
||||
/account/i18n/messages_no.properties
|
||||
/account/i18n/messages_pl.properties
|
||||
/account/i18n/messages_pt_BR.properties
|
||||
/account/i18n/messages_pt.properties
|
||||
/account/i18n/messages_ro.properties
|
||||
/account/i18n/messages_ru.properties
|
||||
/account/i18n/messages_sk.properties
|
||||
/account/i18n/messages_sl.properties
|
||||
/account/i18n/messages_sv.properties
|
||||
/account/i18n/messages_th.properties
|
||||
/account/i18n/messages_tr.properties
|
||||
/account/i18n/messages_uk.properties
|
||||
/account/i18n/messages_vi.properties
|
||||
/account/i18n/messages_zh_Hans.properties
|
||||
/account/i18n/messages_zh_Hant.properties
|
||||
/account/organizations/Organizations.tsx
|
||||
/account/resources/EditTheResource.tsx
|
||||
/account/resources/PermissionRequest.tsx
|
||||
/account/resources/Resources.tsx
|
||||
/account/resources/ResourcesTab.tsx
|
||||
/account/resources/ResourceToolbar.tsx
|
||||
/account/resources/SharedWith.tsx
|
||||
/account/resources/ShareTheResource.tsx
|
||||
/account/root/header.module.css
|
||||
/account/utils/formatDate.ts
|
||||
/account/utils/joinPath.ts
|
||||
/account/utils/useAccountAlerts.ts
|
||||
/account/utils/usePromise.ts
|
||||
/account/verifiable-credentials/CredentialRow.tsx
|
||||
/account/verifiable-credentials/IssuedCredentialsModal.tsx
|
||||
/account/verifiable-credentials/UserAttributesDialog.tsx
|
||||
/account/verifiable-credentials/VerifiableCredentials.tsx
|
||||
/account/assets/passkeys/1password-dark.svg
|
||||
/account/assets/passkeys/1password-light.svg
|
||||
/account/assets/passkeys/acs-light.png
|
||||
/account/assets/passkeys/aliasvault-light.svg
|
||||
/account/assets/passkeys/allthenticator-light.png
|
||||
/account/assets/passkeys/android-light.png
|
||||
/account/assets/passkeys/apple-light.svg
|
||||
/account/assets/passkeys/arculus-light.png
|
||||
/account/assets/passkeys/atkey-light.png
|
||||
/account/assets/passkeys/atkey1-light.png
|
||||
/account/assets/passkeys/atkey2-light.png
|
||||
/account/assets/passkeys/atkey4-light.png
|
||||
/account/assets/passkeys/atlkey-light.png
|
||||
/account/assets/passkeys/atos-light.png
|
||||
/account/assets/passkeys/authenton1-light.png
|
||||
/account/assets/passkeys/bitwarden-light.svg
|
||||
/account/assets/passkeys/burp-light.svg
|
||||
/account/assets/passkeys/cardos-light.png
|
||||
/account/assets/passkeys/chipwon-light.png
|
||||
/account/assets/passkeys/chrome-light.svg
|
||||
/account/assets/passkeys/chunghwa-light.png
|
||||
/account/assets/passkeys/crayonic-light.png
|
||||
/account/assets/passkeys/cryptnox-light.png
|
||||
/account/assets/passkeys/dapple-light.png
|
||||
/account/assets/passkeys/dashlane-dark.svg
|
||||
/account/assets/passkeys/dashlane-light.svg
|
||||
/account/assets/passkeys/deepnet-light.png
|
||||
/account/assets/passkeys/devolutions-light.svg
|
||||
/account/assets/passkeys/edge-light.svg
|
||||
/account/assets/passkeys/egomet-light.png
|
||||
/account/assets/passkeys/ellipticsecure-light.png
|
||||
/account/assets/passkeys/enpass-dark.svg
|
||||
/account/assets/passkeys/enpass-light.svg
|
||||
/account/assets/passkeys/ensurity-light.png
|
||||
/account/assets/passkeys/ensurity1-light.png
|
||||
/account/assets/passkeys/ensurity2-light.png
|
||||
/account/assets/passkeys/ensurity3-light.png
|
||||
/account/assets/passkeys/enterprise-light.svg
|
||||
/account/assets/passkeys/ess-light.png
|
||||
/account/assets/passkeys/ewbm-light.png
|
||||
/account/assets/passkeys/excelsecu-light.png
|
||||
/account/assets/passkeys/feitian-light.png
|
||||
/account/assets/passkeys/fido-light.png
|
||||
/account/assets/passkeys/fido1-light.png
|
||||
/account/assets/passkeys/foongtone-light.png
|
||||
/account/assets/passkeys/goldkey-light.png
|
||||
/account/assets/passkeys/google-light.png
|
||||
/account/assets/passkeys/google1-light.svg
|
||||
/account/assets/passkeys/gotrust-light.png
|
||||
/account/assets/passkeys/gstag-light.png
|
||||
/account/assets/passkeys/heimlane-light.svg
|
||||
/account/assets/passkeys/hid-light.png
|
||||
/account/assets/passkeys/hideez-light.png
|
||||
/account/assets/passkeys/hyper-light.png
|
||||
/account/assets/passkeys/hyper1-light.png
|
||||
/account/assets/passkeys/hypr-light.png
|
||||
/account/assets/passkeys/icloud-dark.svg
|
||||
/account/assets/passkeys/icloud-light.svg
|
||||
/account/assets/passkeys/id-one-light.png
|
||||
/account/assets/passkeys/ideem-light.png
|
||||
/account/assets/passkeys/idemia-light.png
|
||||
/account/assets/passkeys/idex-light.png
|
||||
/account/assets/passkeys/idmelon-light.svg
|
||||
/account/assets/passkeys/idmelon1-light.png
|
||||
/account/assets/passkeys/iist-light.png
|
||||
/account/assets/passkeys/initial-light.svg
|
||||
/account/assets/passkeys/ipasswords-light.svg
|
||||
/account/assets/passkeys/kaspersky-light.svg
|
||||
/account/assets/passkeys/keepassdx-dark.svg
|
||||
/account/assets/passkeys/keepassdx-light.svg
|
||||
/account/assets/passkeys/keepassxc-dark.svg
|
||||
/account/assets/passkeys/keepassxc-light.svg
|
||||
/account/assets/passkeys/keeper-light.svg
|
||||
/account/assets/passkeys/key-id-light.png
|
||||
/account/assets/passkeys/keyvault-light.png
|
||||
/account/assets/passkeys/keyxentic-light.png
|
||||
/account/assets/passkeys/konai-light.png
|
||||
/account/assets/passkeys/kqc-light.png
|
||||
/account/assets/passkeys/lastpass-dark.svg
|
||||
/account/assets/passkeys/lastpass-light.svg
|
||||
/account/assets/passkeys/ledger-light.png
|
||||
/account/assets/passkeys/logmeonce-dark.svg
|
||||
/account/assets/passkeys/logmeonce-light.svg
|
||||
/account/assets/passkeys/mettlesemi-light.png
|
||||
/account/assets/passkeys/microsoft-light.svg
|
||||
/account/assets/passkeys/neowave-light.png
|
||||
/account/assets/passkeys/nitrokey-light.png
|
||||
/account/assets/passkeys/nordpass-dark.svg
|
||||
/account/assets/passkeys/nordpass-light.svg
|
||||
/account/assets/passkeys/nxp-light.png
|
||||
/account/assets/passkeys/nymi-light.png
|
||||
/account/assets/passkeys/octatco-light.png
|
||||
/account/assets/passkeys/onekey-light.png
|
||||
/account/assets/passkeys/onespan-light.png
|
||||
/account/assets/passkeys/onespan1-light.png
|
||||
/account/assets/passkeys/onespan2-light.png
|
||||
/account/assets/passkeys/onespan3-light.png
|
||||
/account/assets/passkeys/onespan4-light.png
|
||||
/account/assets/passkeys/onespan5-light.png
|
||||
/account/assets/passkeys/onespan7-light.png
|
||||
/account/assets/passkeys/onlykey-light.png
|
||||
/account/assets/passkeys/opensk-light.png
|
||||
/account/assets/passkeys/pone-light.png
|
||||
/account/assets/passkeys/precision-light.png
|
||||
/account/assets/passkeys/precision1-light.png
|
||||
/account/assets/passkeys/precision2-light.png
|
||||
/account/assets/passkeys/proton-light.svg
|
||||
/account/assets/passkeys/pwsafe-light.svg
|
||||
/account/assets/passkeys/rsa-light.png
|
||||
/account/assets/passkeys/samsung-light.svg
|
||||
/account/assets/passkeys/secora-light.png
|
||||
/account/assets/passkeys/secora1-light.png
|
||||
/account/assets/passkeys/securitag-light.png
|
||||
/account/assets/passkeys/sentry-light.png
|
||||
/account/assets/passkeys/sesame-light.svg
|
||||
/account/assets/passkeys/shalo-light.png
|
||||
/account/assets/passkeys/sherlocked-light.svg
|
||||
/account/assets/passkeys/smartdisplayer-light.png
|
||||
/account/assets/passkeys/solo-light.png
|
||||
/account/assets/passkeys/starsign-light.png
|
||||
/account/assets/passkeys/sticky-light.svg
|
||||
/account/assets/passkeys/swissbit-light.png
|
||||
/account/assets/passkeys/t-shield-light.png
|
||||
/account/assets/passkeys/taglio-light.png
|
||||
/account/assets/passkeys/thales-light.png
|
||||
/account/assets/passkeys/thales1-dark.svg
|
||||
/account/assets/passkeys/thales1-light.svg
|
||||
/account/assets/passkeys/thales3-dark.svg
|
||||
/account/assets/passkeys/thales3-light.svg
|
||||
/account/assets/passkeys/thetis-light.png
|
||||
/account/assets/passkeys/token-light.png
|
||||
/account/assets/passkeys/token2-light.png
|
||||
/account/assets/passkeys/toothpic-dark.svg
|
||||
/account/assets/passkeys/toothpic-light.svg
|
||||
/account/assets/passkeys/truu-light.png
|
||||
/account/assets/passkeys/utrust-light.png
|
||||
/account/assets/passkeys/valmido-light.png
|
||||
/account/assets/passkeys/veridium-light.png
|
||||
/account/assets/passkeys/verimark-light.png
|
||||
/account/assets/passkeys/verimark1-light.png
|
||||
/account/assets/passkeys/verocard-light.png
|
||||
/account/assets/passkeys/vincss-light.png
|
||||
/account/assets/passkeys/vivokey-light.png
|
||||
/account/assets/passkeys/webcomm-light.png
|
||||
/account/assets/passkeys/windows-light.svg
|
||||
/account/assets/passkeys/winmagic-light.png
|
||||
/account/assets/passkeys/yubikey-light.png
|
||||
/account/assets/passkeys/zoho-light.svg
|
||||
/account/assets/passkeys/ztpass-light.png
|
||||
/account/components/datalist/EmptyRow.tsx
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/KcAccountUi.tsx" --revert
|
||||
*/
|
||||
|
||||
import "@patternfly/patternfly/patternfly-addons.css";
|
||||
import "@patternfly/react-core/dist/styles/base.css";
|
||||
import "./theme.css";
|
||||
import "../console-ui.css";
|
||||
import "./workspace.css";
|
||||
|
||||
import { useReducer, useEffect } from "react";
|
||||
import { startColorSchemeManagement } from "./colorScheme";
|
||||
import { KeycloakProvider } from "../shared/keycloak-ui-shared";
|
||||
import { environment } from "./environment";
|
||||
import { i18n } from "./i18n/i18n";
|
||||
import { Root } from "./root/Root";
|
||||
import { SessionExpirationWarningOverlay } from "../shared/SessionExpirationWarningOverlay";
|
||||
import { getKcContext } from "./KcContext";
|
||||
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
document.documentElement.dataset.kcThemeName = kcContext.themeName;
|
||||
document.title = kcContext.themeName === "class" ? "612 / 账户中心" : "NEXUS / ID";
|
||||
|
||||
const prI18nInitialized = i18n.init();
|
||||
startColorSchemeManagement();
|
||||
|
||||
export default function KcAccountUi() {
|
||||
const [isI18nInitialized, setI18nInitialized] = useReducer(() => true, false);
|
||||
|
||||
useEffect(() => {
|
||||
prI18nInitialized.then(() => setI18nInitialized());
|
||||
}, []);
|
||||
|
||||
if (!isI18nInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<KeycloakProvider environment={environment}>
|
||||
<Root />
|
||||
<SessionExpirationWarningOverlay warnUserSecondsBeforeAutoLogout={45} />
|
||||
</KeycloakProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { ExtendKcContext } from "keycloakify/account";
|
||||
import type { KcEnvName, ThemeName } from "../kc.gen";
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/KcContext.ts" --revert
|
||||
*/
|
||||
|
||||
export type KcContextExtension = {
|
||||
themeName: ThemeName;
|
||||
properties: Record<KcEnvName, string> & {};
|
||||
import { type KcContextLike, createGetKcContext } from "@keycloakify/keycloak-account-ui";
|
||||
import type { KcEnvName } from "../kc.gen";
|
||||
|
||||
export type KcContext = KcContextLike & {
|
||||
themeType: "account";
|
||||
themeName: string;
|
||||
properties: Record<KcEnvName, string>;
|
||||
};
|
||||
|
||||
export type KcContextExtensionPerPage = {};
|
||||
|
||||
export type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
||||
export const { getKcContext } = createGetKcContext<KcContext>();
|
||||
|
||||
+27
-40
@@ -1,47 +1,34 @@
|
||||
import { Suspense } from "react";
|
||||
import type { ClassKey } from "keycloakify/account";
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/KcPage.tsx" --revert
|
||||
*/
|
||||
|
||||
import { lazy } from "react";
|
||||
import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/account/DefaultPage";
|
||||
import Template from "./Template";
|
||||
import "./theme.css";
|
||||
import { oidcEarlyInit } from "oidc-spa/entrypoint";
|
||||
import { browserRuntimeFreeze } from "oidc-spa/browser-runtime-freeze";
|
||||
import { DPoP } from "oidc-spa/DPoP";
|
||||
|
||||
const KcAccountUi = lazy(() => import("./KcAccountUi"));
|
||||
|
||||
export default function KcPage(props: { kcContext: KcContext }) {
|
||||
const { kcContext } = props;
|
||||
|
||||
const { i18n } = useI18n({ kcContext });
|
||||
const { shouldLoadApp } = oidcEarlyInit({
|
||||
BASE_URL: kcContext.baseUrl.path,
|
||||
sessionRestorationMethod: import.meta.env.DEV ? "full page redirect" : "auto",
|
||||
securityDefenses: {
|
||||
...browserRuntimeFreeze({ excludes: ["fetch"] }),
|
||||
...DPoP({ mode: "auto" })
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
default:
|
||||
return (
|
||||
<DefaultPage
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
classes={classes}
|
||||
Template={Template}
|
||||
doUseDefaultCss={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
if (!shouldLoadApp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <KcAccountUiLoader kcContext={kcContext} KcAccountUi={KcAccountUi} />;
|
||||
}
|
||||
|
||||
const classes = {
|
||||
kcHtmlClass: "nexus-account-html",
|
||||
kcBodyClass: "nexus-account-body",
|
||||
kcButtonClass: "nexus-account-button",
|
||||
kcButtonPrimaryClass: "nexus-account-button--primary",
|
||||
kcButtonDefaultClass: "nexus-account-button--default",
|
||||
kcButtonLargeClass: "nexus-account-button--large",
|
||||
kcFormClass: "nexus-account-form",
|
||||
kcFormGroupClass: "nexus-account-field",
|
||||
kcLabelClass: "nexus-account-label",
|
||||
kcInputClass: "nexus-account-input",
|
||||
kcInputWrapperClass: "nexus-account-input-wrapper",
|
||||
kcInputErrorMessageClass: "nexus-account-field-error"
|
||||
} satisfies { [key in ClassKey]?: string };
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { createGetKcContextMock } from "keycloakify/account/KcContext";
|
||||
import type { KcContextExtension, KcContextExtensionPerPage } from "./KcContext";
|
||||
import KcPage from "./KcPage";
|
||||
import { themeNames, kcEnvDefaults } from "../kc.gen";
|
||||
|
||||
const kcContextExtension: KcContextExtension = {
|
||||
themeName: themeNames[0],
|
||||
properties: {
|
||||
...kcEnvDefaults
|
||||
}
|
||||
};
|
||||
const kcContextExtensionPerPage: KcContextExtensionPerPage = {};
|
||||
|
||||
export const { getKcContextMock } = createGetKcContextMock({
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage,
|
||||
overrides: {},
|
||||
overridesPerPage: {}
|
||||
});
|
||||
|
||||
export function createKcPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
|
||||
const { pageId } = params;
|
||||
|
||||
function KcPageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
|
||||
const { kcContext: overrides } = props;
|
||||
|
||||
const kcContextMock = getKcContextMock({
|
||||
pageId,
|
||||
overrides
|
||||
});
|
||||
|
||||
return <KcPage kcContext={kcContextMock} />;
|
||||
}
|
||||
|
||||
return { KcPageStory };
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import { useInitialize } from "keycloakify/account/Template.useInitialize";
|
||||
import type { TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
|
||||
const { url, features, realm, message, referrer } = kcContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = msgStr("accountManagementTitle");
|
||||
}, [msgStr]);
|
||||
|
||||
useSetClassName({ qualifiedName: "html", className: kcClsx("kcHtmlClass") });
|
||||
useSetClassName({ qualifiedName: "body", className: kcClsx("kcBodyClass") });
|
||||
|
||||
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
|
||||
|
||||
if (!isReadyToRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const navigationItems = [
|
||||
{ id: "account", href: url.accountUrl, label: msg("account"), visible: true },
|
||||
{ id: "password", href: url.passwordUrl, label: msg("password"), visible: features.passwordUpdateSupported },
|
||||
{ id: "totp", href: url.totpUrl, label: msg("authenticator"), visible: true },
|
||||
{ id: "social", href: url.socialUrl, label: msg("federatedIdentity"), visible: features.identityFederation },
|
||||
{ id: "sessions", href: url.sessionsUrl, label: msg("sessions"), visible: true },
|
||||
{ id: "applications", href: url.applicationsUrl, label: msg("applications"), visible: true },
|
||||
{ id: "log", href: url.logUrl, label: msg("log"), visible: features.log },
|
||||
{
|
||||
id: "authorization",
|
||||
href: url.resourceUrl,
|
||||
label: msg("myResources"),
|
||||
visible: realm.userManagedAccessAllowed && features.authorization
|
||||
}
|
||||
].filter(item => item.visible);
|
||||
|
||||
return (
|
||||
<div className="nexus-account-shell">
|
||||
<header className="nexus-account-header">
|
||||
<a className="nexus-account-brand" href={url.accountUrl} aria-label={msgStr("accountManagementTitle")}>
|
||||
<span className="nexus-account-brand__mark">N</span>
|
||||
<span>
|
||||
NEXUS <b>/ ID</b>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className="nexus-account-header__realm">
|
||||
<span>IDENTITY CONTROL</span>
|
||||
<strong>{msg("accountManagementTitle")}</strong>
|
||||
</div>
|
||||
|
||||
<nav className="nexus-account-utility" aria-label="Account utilities">
|
||||
{enabledLanguages.length > 1 && (
|
||||
<details className="nexus-account-language">
|
||||
<summary>{currentLanguage.label}</summary>
|
||||
<ul>
|
||||
{enabledLanguages.map(({ languageTag, label, href }) => (
|
||||
<li key={languageTag}>
|
||||
<a href={href}>{label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
{referrer?.url && (
|
||||
<a href={referrer.url} id="referrer">
|
||||
{msg("backTo", referrer.name)}
|
||||
</a>
|
||||
)}
|
||||
<a className="nexus-account-signout" href={url.getLogoutUrl()}>
|
||||
{msg("doSignOut")}
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div className="nexus-account-layout">
|
||||
<aside className="nexus-account-sidebar">
|
||||
<div className="nexus-account-orbit" aria-hidden="true">
|
||||
<span />
|
||||
</div>
|
||||
<p className="nexus-account-sidebar__label">ACCOUNT MATRIX</p>
|
||||
<nav aria-label={msgStr("accountManagementTitle")}>
|
||||
<ul>
|
||||
{navigationItems.map(item => (
|
||||
<li key={item.id} className={clsx(active === item.id && "active")}>
|
||||
<a href={item.href}>
|
||||
<span className="nexus-account-nav__signal" />
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="nexus-account-sidebar__status">
|
||||
<span /> SECURE SESSION
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="nexus-account-content" id="nexus-account-main">
|
||||
{message !== undefined && (
|
||||
<div className={clsx("nexus-account-alert", `nexus-account-alert--${message.type}`)} role="status">
|
||||
<span className="nexus-account-alert__signal" />
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(message.summary)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<section className="nexus-account-panel">{children}</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title">
|
||||
<title id="title">NEXUS ID</title>
|
||||
<g transform="translate(8 9)">
|
||||
<rect width="34" height="34" rx="10" fill="#356fd6"/>
|
||||
<path d="M9 10h16M9 17h11M9 24h16" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="24.5" cy="17" r="2.2" fill="#f0b92e"/>
|
||||
</g>
|
||||
<text x="54" y="31" fill="#17324d" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="19" font-weight="700" letter-spacing="1.2">NEXUS</text>
|
||||
<text x="133" y="31" fill="#667a8e" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="15" font-weight="600" letter-spacing="1">WORKSPACE</text>
|
||||
<rect x="54" y="38" width="31" height="3" rx="1.5" fill="#279477"/>
|
||||
<rect x="88" y="38" width="31" height="3" rx="1.5" fill="#e9ad38"/>
|
||||
<rect x="122" y="38" width="31" height="3" rx="1.5" fill="#e86452"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 929 B |
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/colorScheme.ts" --revert
|
||||
*/
|
||||
|
||||
/* IMPORTANT NOTE:
|
||||
* If you modify how the light/dark mode is implemented
|
||||
* you must also update public/admin/early-color-scheme.js
|
||||
* This scrip is ran before this to avoid white flashes
|
||||
*/
|
||||
|
||||
import { getKcContext } from "./KcContext";
|
||||
import { startConsoleColorScheme } from "../shared/consoleColorScheme";
|
||||
|
||||
export function startColorSchemeManagement() {
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
startConsoleColorScheme(kcContext.darkMode !== false);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/components/page/Page.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
PageSection,
|
||||
Text,
|
||||
TextContent,
|
||||
Title
|
||||
} from "../../../shared/@patternfly/react-core";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
type PageProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => {
|
||||
return (
|
||||
<article className="account-business-page">
|
||||
<PageSection variant="light" className="account-view-header">
|
||||
<div className="account-view-header__layout">
|
||||
<div className="account-view-header__copy">
|
||||
<span className="account-view-header__eyebrow">
|
||||
Identity workspace / Account
|
||||
</span>
|
||||
<TextContent>
|
||||
<Title headingLevel="h1" data-testid="page-heading">
|
||||
{title}
|
||||
</Title>
|
||||
<Text component="p">{description}</Text>
|
||||
</TextContent>
|
||||
</div>
|
||||
<div className="account-view-header__context" aria-hidden="true">
|
||||
<span>SELF SERVICE</span>
|
||||
<strong>ID</strong>
|
||||
<i />
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
<PageSection variant="light" className="account-view-body">
|
||||
<div className="account-view-body__surface">{children}</div>
|
||||
</PageSection>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { i18nBuilder } from "keycloakify/account";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
||||
export { useI18n, type I18n };
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "account.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "account/account.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithSuccessMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
type: "success",
|
||||
summary: "Your account information has been updated."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "password.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "account/password.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/personal-info/PersonalInfo.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
UserProfileFields,
|
||||
beerify,
|
||||
debeerify,
|
||||
setUserProfileServerError,
|
||||
useEnvironment
|
||||
} from "../../shared/keycloak-ui-shared";
|
||||
import {
|
||||
ActionGroup,
|
||||
Alert,
|
||||
AlertVariant,
|
||||
Button,
|
||||
ExpandableSection,
|
||||
Form,
|
||||
Spinner
|
||||
} from "../../shared/@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
|
||||
import { TFunction } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { ErrorOption, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getPersonalInfo, getSupportedLocales, savePersonalInfo } from "../api/methods";
|
||||
import { UserProfileMetadata, UserRepresentation } from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { type AccountEnvironment } from "..";
|
||||
import type { TFuncKey } from "../i18n-type";
|
||||
import { useAccountAlerts } from "../utils/useAccountAlerts";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
export const PersonalInfo = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
const [userProfileMetadata, setUserProfileMetadata] = useState<UserProfileMetadata>();
|
||||
const [supportedLocales, setSupportedLocales] = useState<string[]>([]);
|
||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||
const { handleSubmit, reset, setValue, setError } = form;
|
||||
const profile = form.watch();
|
||||
const { addAlert } = useAccountAlerts();
|
||||
|
||||
usePromise(
|
||||
signal =>
|
||||
Promise.all([
|
||||
getPersonalInfo({ signal, context }),
|
||||
getSupportedLocales({ signal, context })
|
||||
]),
|
||||
([personalInfo, supportedLocales]) => {
|
||||
setUserProfileMetadata(personalInfo.userProfileMetadata);
|
||||
setSupportedLocales(supportedLocales);
|
||||
reset(personalInfo);
|
||||
Object.entries(personalInfo.attributes || {}).forEach(([k, v]) =>
|
||||
setValue(`attributes[${beerify(k)}]`, v)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async (user: UserRepresentation) => {
|
||||
try {
|
||||
const attributes = Object.fromEntries(
|
||||
Object.entries(user.attributes || {}).map(([k, v]) => [debeerify(k), v])
|
||||
);
|
||||
await savePersonalInfo(context, { ...user, attributes });
|
||||
const locale = attributes["locale"]?.toString();
|
||||
if (locale) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("languageChanged", { detail: { language: locale } })
|
||||
);
|
||||
}
|
||||
await context.keycloak.updateToken();
|
||||
addAlert(t("accountUpdatedMessage"));
|
||||
} catch (error) {
|
||||
addAlert(t("accountUpdatedError"), AlertVariant.danger);
|
||||
|
||||
setUserProfileServerError(
|
||||
{ responseData: { errors: error as any } },
|
||||
(name: string | number, error: unknown) =>
|
||||
setError(name as string, error as ErrorOption),
|
||||
((key: TFuncKey, param?: object) => t(key, param as any)) as TFunction
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!userProfileMetadata) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const allFieldsReadOnly = () =>
|
||||
userProfileMetadata.attributes
|
||||
.map(a => a.readOnly)
|
||||
.reduce((p, c) => p && c, true);
|
||||
|
||||
const {
|
||||
updateEmailFeatureEnabled,
|
||||
updateEmailActionEnabled,
|
||||
isRegistrationEmailAsUsername,
|
||||
isEditUserNameAllowed
|
||||
} = context.environment.features;
|
||||
|
||||
const displayName =
|
||||
[profile.firstName, profile.lastName].filter(Boolean).join(" ").trim() ||
|
||||
profile.username ||
|
||||
t("personalInfo");
|
||||
const initials =
|
||||
[profile.firstName, profile.lastName]
|
||||
.filter(Boolean)
|
||||
.map(value => value!.trim().charAt(0))
|
||||
.join("")
|
||||
.slice(0, 2)
|
||||
.toUpperCase() || displayName.slice(0, 2).toUpperCase();
|
||||
const profileLocale =
|
||||
profile.attributes?.locale?.toString() || context.environment.locale;
|
||||
|
||||
return (
|
||||
<Page title={t("personalInfo")} description={t("personalInfoDescription")}>
|
||||
<div className="nexus-profile-layout">
|
||||
<aside className="nexus-identity-card" aria-label={t("personalInfo")}>
|
||||
<div className="nexus-identity-card__topline">
|
||||
<span>Identity record</span>
|
||||
<i aria-hidden="true" />
|
||||
</div>
|
||||
<div className="nexus-identity-card__avatar" aria-hidden="true">
|
||||
<span>{initials}</span>
|
||||
</div>
|
||||
<div className="nexus-identity-card__name">
|
||||
<span>{t("personalInfo")}</span>
|
||||
<h2>{displayName}</h2>
|
||||
<p>{profile.email || profile.username}</p>
|
||||
</div>
|
||||
<dl className="nexus-identity-card__facts">
|
||||
<div>
|
||||
<dt>{t("username")}</dt>
|
||||
<dd>{profile.username || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("email")}</dt>
|
||||
<dd>{profile.email || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Locale</dt>
|
||||
<dd>{profileLocale}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="nexus-identity-card__footer">
|
||||
<span aria-hidden="true" />
|
||||
NEXUS identity directory
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="nexus-profile-editor">
|
||||
<header className="nexus-profile-editor__header">
|
||||
<span>Profile data</span>
|
||||
<div>
|
||||
<h2>{t("personalInfo")}</h2>
|
||||
<p>{t("personalInfoDescription")}</p>
|
||||
</div>
|
||||
</header>
|
||||
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
|
||||
<UserProfileFields
|
||||
form={form}
|
||||
userProfileMetadata={userProfileMetadata}
|
||||
supportedLocales={supportedLocales}
|
||||
currentLocale={context.environment.locale}
|
||||
t={
|
||||
((key: unknown, params) =>
|
||||
t(key as TFuncKey, params as any)) as TFunction
|
||||
}
|
||||
renderer={attribute => {
|
||||
const annotations = attribute.annotations
|
||||
? attribute.annotations
|
||||
: {};
|
||||
return attribute.name === "email" &&
|
||||
updateEmailFeatureEnabled &&
|
||||
updateEmailActionEnabled &&
|
||||
annotations["kc.required.action.supported"] &&
|
||||
(!isRegistrationEmailAsUsername ||
|
||||
isEditUserNameAllowed) ? (
|
||||
<Button
|
||||
id="update-email-btn"
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
context.keycloak.login({
|
||||
action: "UPDATE_EMAIL"
|
||||
})
|
||||
}
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
>
|
||||
{t("updateEmail")}
|
||||
</Button>
|
||||
) : undefined;
|
||||
}}
|
||||
/>
|
||||
{!allFieldsReadOnly() && (
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="save"
|
||||
type="submit"
|
||||
id="save-btn"
|
||||
variant="primary"
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
id="cancel-btn"
|
||||
variant="link"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
)}
|
||||
{context.environment.features.deleteAccountAllowed && (
|
||||
<ExpandableSection
|
||||
data-testid="delete-account"
|
||||
toggleText={t("deleteAccount")}
|
||||
>
|
||||
<Alert
|
||||
isInline
|
||||
title={t("deleteAccount")}
|
||||
variant="danger"
|
||||
actionLinks={
|
||||
<Button
|
||||
id="delete-account-btn"
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
context.keycloak.login({
|
||||
action: "delete_account"
|
||||
})
|
||||
}
|
||||
className="delete-button"
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{t("deleteAccountWarning")}
|
||||
</Alert>
|
||||
</ExpandableSection>
|
||||
)}
|
||||
</Form>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalInfo;
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/Header.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import logoSvgUrl from "../assets/logo.svg";
|
||||
import { KeycloakMasthead, label, useEnvironment } from "../../shared/keycloak-ui-shared";
|
||||
import { Button } from "../../shared/@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHref } from "react-router-dom";
|
||||
|
||||
import { AccountEnvironment } from "..";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { ThemeModeToggle } from "../../shared/ThemeModeToggle";
|
||||
|
||||
import style from "./header.module.css";
|
||||
|
||||
const ReferrerLink = () => {
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return environment.referrerUrl ? (
|
||||
<Button
|
||||
data-testid="referrer-link"
|
||||
component="a"
|
||||
href={environment.referrerUrl.replace("_hash_", "#")}
|
||||
variant="link"
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
>
|
||||
{t("backTo", {
|
||||
app: label(t, environment.referrerName, environment.referrerUrl)
|
||||
})}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : "/";
|
||||
const internalLogoHref = useHref(logoUrl);
|
||||
|
||||
// User can indicate that he wants an internal URL by starting it with "/"
|
||||
const indexHref = logoUrl.startsWith("/") ? internalLogoHref : logoUrl;
|
||||
|
||||
return (
|
||||
<KeycloakMasthead
|
||||
data-testid="page-header"
|
||||
keycloak={keycloak}
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: indexHref,
|
||||
src: logoSvgUrl,
|
||||
alt: t("logo"),
|
||||
className: style.brand
|
||||
}}
|
||||
toolbarItems={[
|
||||
<ThemeModeToggle key="theme-mode" />,
|
||||
<ReferrerLink key="link" />
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/PageNav.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEnvironment } from "../../shared/keycloak-ui-shared";
|
||||
import {
|
||||
Nav,
|
||||
NavExpandable,
|
||||
NavItem,
|
||||
NavList,
|
||||
PageSidebar,
|
||||
PageSidebarBody,
|
||||
Spinner
|
||||
} from "../../shared/@patternfly/react-core";
|
||||
import {
|
||||
Children,
|
||||
PropsWithChildren,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
Suspense,
|
||||
useMemo,
|
||||
useState
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { matchPath, useHref, useLinkClickHandler, useLocation } from "react-router-dom";
|
||||
import {
|
||||
ApplicationsIcon,
|
||||
BuildingIcon,
|
||||
DesktopIcon,
|
||||
FolderOpenAltIcon,
|
||||
KeyIcon,
|
||||
LinkIcon,
|
||||
ShieldAltIcon,
|
||||
UserAltIcon,
|
||||
UserFriendsIcon
|
||||
} from "../../shared/@patternfly/react-icons";
|
||||
|
||||
import fetchContentJson from "../content/fetchContent";
|
||||
import type { TFuncKey } from "../i18n-type";
|
||||
import type { AccountEnvironment, Feature } from "..";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
type RootMenuItem = {
|
||||
id?: string;
|
||||
label: TFuncKey;
|
||||
path: string;
|
||||
isVisible?: keyof Feature;
|
||||
modulePath?: string;
|
||||
};
|
||||
|
||||
type MenuItemWithChildren = {
|
||||
label: TFuncKey;
|
||||
children: MenuItem[];
|
||||
isVisible?: keyof Feature;
|
||||
};
|
||||
|
||||
export type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||
|
||||
export const PageNav = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>();
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
|
||||
usePromise(signal => fetchContentJson({ signal, context }), setMenuItems);
|
||||
return (
|
||||
<PageSidebar>
|
||||
<PageSidebarBody>
|
||||
<div className="nexus-nav-context">
|
||||
<span className="nexus-nav-context__eyebrow">NEXUS / ID</span>
|
||||
<strong>Identity control</strong>
|
||||
<span className="nexus-nav-context__rule" aria-hidden="true" />
|
||||
</div>
|
||||
<Nav>
|
||||
<NavList>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{menuItems
|
||||
?.filter(menuItem =>
|
||||
menuItem.isVisible
|
||||
? context.environment.features[menuItem.isVisible]
|
||||
: true
|
||||
)
|
||||
.map(menuItem => (
|
||||
<NavMenuItem
|
||||
key={menuItem.label as string}
|
||||
menuItem={menuItem}
|
||||
/>
|
||||
))}
|
||||
</Suspense>
|
||||
</NavList>
|
||||
</Nav>
|
||||
</PageSidebarBody>
|
||||
</PageSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
const menuIcons: Record<string, JSX.Element> = {
|
||||
personalInfo: <UserAltIcon />,
|
||||
accountSecurity: <ShieldAltIcon />,
|
||||
signingIn: <KeyIcon />,
|
||||
deviceActivity: <DesktopIcon />,
|
||||
linkedAccounts: <LinkIcon />,
|
||||
applications: <ApplicationsIcon />,
|
||||
verifiableCredentials: <ShieldAltIcon />,
|
||||
groups: <UserFriendsIcon />,
|
||||
organizations: <BuildingIcon />,
|
||||
resources: <FolderOpenAltIcon />
|
||||
};
|
||||
|
||||
const NavLabel = ({ label }: { label: TFuncKey }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<span className="nexus-nav-label">
|
||||
<span className="nexus-nav-label__icon" aria-hidden="true">
|
||||
{menuIcons[label as string] ?? <UserAltIcon />}
|
||||
</span>
|
||||
<span>{t(label)}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type NavMenuItemProps = {
|
||||
menuItem: MenuItem;
|
||||
};
|
||||
|
||||
function NavMenuItem({ menuItem }: NavMenuItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const { pathname } = useLocation();
|
||||
const isActive = useMemo(
|
||||
() => matchMenuItem(pathname, menuItem, environment.baseUrl),
|
||||
[pathname, menuItem, environment.baseUrl]
|
||||
);
|
||||
|
||||
if ("path" in menuItem) {
|
||||
return (
|
||||
<NavLink path={menuItem.path} isActive={isActive}>
|
||||
<NavLabel label={menuItem.label} />
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavExpandable
|
||||
data-testid={menuItem.label}
|
||||
title={<NavLabel label={menuItem.label} />}
|
||||
isActive={isActive}
|
||||
isExpanded={isActive}
|
||||
>
|
||||
{menuItem.children
|
||||
.filter(menuItem =>
|
||||
menuItem.isVisible ? environment.features[menuItem.isVisible] : true
|
||||
)
|
||||
.map(child => (
|
||||
<NavMenuItem key={child.label as string} menuItem={child} />
|
||||
))}
|
||||
</NavExpandable>
|
||||
);
|
||||
}
|
||||
|
||||
function getFullUrl(path: string, baseUrl: string) {
|
||||
return `${new URL(baseUrl).pathname}${path}`;
|
||||
}
|
||||
|
||||
function matchMenuItem(
|
||||
currentPath: string,
|
||||
menuItem: MenuItem,
|
||||
baseUrl: string
|
||||
): boolean {
|
||||
if ("path" in menuItem) {
|
||||
return !!matchPath(getFullUrl(menuItem.path, baseUrl), currentPath);
|
||||
}
|
||||
|
||||
return menuItem.children.some(child => matchMenuItem(currentPath, child, baseUrl));
|
||||
}
|
||||
|
||||
type NavLinkProps = {
|
||||
path: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export const NavLink = ({
|
||||
path,
|
||||
isActive,
|
||||
children
|
||||
}: PropsWithChildren<NavLinkProps>) => {
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const menuItemPath = getFullUrl(path, environment.baseUrl) + location.search;
|
||||
const href = useHref(menuItemPath);
|
||||
const handleClick = useLinkClickHandler(menuItemPath);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
data-testid={path}
|
||||
to={href}
|
||||
isActive={isActive}
|
||||
onClick={event =>
|
||||
// PatternFly does not have the correct type for this event, so we need to cast it.
|
||||
handleClick(event as unknown as ReactMouseEvent<HTMLAnchorElement>)
|
||||
}
|
||||
>
|
||||
{Children.toArray(children)}
|
||||
</NavItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/Root.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
ErrorPage,
|
||||
useAlerts,
|
||||
useEnvironment,
|
||||
KeycloakContext
|
||||
} from "../../shared/keycloak-ui-shared";
|
||||
import { AlertVariant, Page, Spinner } from "../../shared/@patternfly/react-core";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
Outlet,
|
||||
RouteObject,
|
||||
RouterProvider
|
||||
} from "react-router-dom";
|
||||
import fetchContentJson from "../content/fetchContent";
|
||||
import { type AccountEnvironment } from "..";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { Header } from "./Header";
|
||||
import { MenuItem, PageNav } from "./PageNav";
|
||||
import { routes } from "../routes";
|
||||
|
||||
function mapRoutes(
|
||||
context: KeycloakContext<AccountEnvironment>,
|
||||
content: MenuItem[]
|
||||
): RouteObject[] {
|
||||
return content
|
||||
.map(item => {
|
||||
if ("children" in item) {
|
||||
return mapRoutes(context, item.children);
|
||||
}
|
||||
|
||||
// Do not add route disabled via feature flags
|
||||
if (item.isVisible && !context.environment.features[item.isVisible]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
element:
|
||||
"path" in item
|
||||
? routes.find(r => r.path === (item.id ?? item.path))?.element
|
||||
: undefined
|
||||
};
|
||||
})
|
||||
.filter(item => !!item)
|
||||
.flat();
|
||||
}
|
||||
|
||||
function CatchAllRedirect() {
|
||||
const { t } = useTranslation();
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
useEffect(() => {
|
||||
addAlert(t("pageNotFound"), AlertVariant.warning);
|
||||
}, [addAlert, t]);
|
||||
|
||||
return <Navigate to="." replace />;
|
||||
}
|
||||
|
||||
export const Root = () => {
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
const [content, setContent] = useState<RouteObject[]>();
|
||||
|
||||
usePromise(
|
||||
signal => fetchContentJson({ signal, context }),
|
||||
content => {
|
||||
setContent([
|
||||
{
|
||||
path: decodeURIComponent(
|
||||
new URL(context.environment.baseUrl).pathname
|
||||
),
|
||||
element: (
|
||||
<Page header={<Header />} sidebar={<PageNav />} isManagedSidebar>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<div
|
||||
className="account-workspace-route"
|
||||
data-workspace="personal-identity"
|
||||
>
|
||||
<div className="account-workspace-route__content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</Page>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
...mapRoutes(context, content),
|
||||
{ path: "*", element: <CatchAllRedirect /> }
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
if (!content) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <RouterProvider router={createBrowserRouter(content)} />;
|
||||
};
|
||||
+11
-626
@@ -1,634 +1,19 @@
|
||||
:root {
|
||||
--nexus-account-void: #050713;
|
||||
--nexus-account-chrome: #091020;
|
||||
--nexus-account-panel: #0f172a;
|
||||
--nexus-account-panel-raised: #141e34;
|
||||
--nexus-account-ice: #63e6ff;
|
||||
--nexus-account-violet: #8b7cff;
|
||||
--nexus-account-text: #f4f7ff;
|
||||
--nexus-account-muted: #95a2be;
|
||||
--nexus-account-line: rgba(151, 172, 219, 0.18);
|
||||
--nexus-account-success: #54e6a4;
|
||||
--nexus-account-danger: #ff668f;
|
||||
--nexus-account-display: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
|
||||
--nexus-account-body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
--nexus-account-mono: "Cascadia Code", Consolas, monospace;
|
||||
}
|
||||
/*
|
||||
* Account compatibility layer.
|
||||
* Shared colors and PatternFly primitives live in ../console-ui.css, while
|
||||
* account business composition lives in workspace.css.
|
||||
*/
|
||||
|
||||
.nexus-account-html,
|
||||
.nexus-account-body,
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nexus-account-body {
|
||||
min-height: 100vh;
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--nexus-account-text);
|
||||
background: var(--nexus-account-void);
|
||||
font-family: var(--nexus-account-body-font);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.nexus-account-body *,
|
||||
.nexus-account-body *::before,
|
||||
.nexus-account-body *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nexus-account-body button,
|
||||
.nexus-account-body input,
|
||||
.nexus-account-body select,
|
||||
.nexus-account-body textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.nexus-account-body a {
|
||||
color: #a9eefe;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nexus-account-body a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.nexus-account-shell {
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(
|
||||
circle at 10% 60%,
|
||||
rgba(99, 230, 255, 0.07),
|
||||
transparent 29rem
|
||||
),
|
||||
radial-gradient(circle at 88% 15%, rgba(139, 124, 255, 0.07), transparent 27rem),
|
||||
linear-gradient(rgba(127, 155, 207, 0.028) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(127, 155, 207, 0.028) 1px, transparent 1px),
|
||||
var(--nexus-account-void);
|
||||
background-size:
|
||||
auto,
|
||||
auto,
|
||||
52px 52px,
|
||||
52px 52px,
|
||||
auto;
|
||||
}
|
||||
|
||||
.nexus-account-header {
|
||||
position: sticky;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
display: grid;
|
||||
min-height: 76px;
|
||||
align-items: center;
|
||||
grid-template-columns: 260px 1fr auto;
|
||||
gap: 28px;
|
||||
padding: 0 clamp(22px, 4vw, 58px);
|
||||
border-bottom: 1px solid var(--nexus-account-line);
|
||||
background: rgba(6, 10, 23, 0.88);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.nexus-account-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--nexus-account-text) !important;
|
||||
font-family: var(--nexus-account-display);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.nexus-account-brand b {
|
||||
color: var(--nexus-account-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.nexus-account-brand__mark {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
place-items: center;
|
||||
color: var(--nexus-account-ice);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.nexus-account-brand__mark::before {
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border: 1px solid rgba(99, 230, 255, 0.52);
|
||||
box-shadow:
|
||||
inset 0 0 14px rgba(99, 230, 255, 0.1),
|
||||
0 0 16px rgba(99, 230, 255, 0.08);
|
||||
content: "";
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.nexus-account-header__realm {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nexus-account-header__realm span,
|
||||
.nexus-account-sidebar__label,
|
||||
.nexus-account-sidebar__status {
|
||||
color: var(--nexus-account-muted);
|
||||
font-family: var(--nexus-account-mono);
|
||||
font-size: 0.63rem;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.nexus-account-header__realm strong {
|
||||
overflow: hidden;
|
||||
color: #dce6fb;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 550;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nexus-account-utility {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.nexus-account-language {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nexus-account-language summary {
|
||||
color: var(--nexus-account-muted);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nexus-account-language ul {
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
right: 0;
|
||||
min-width: 150px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 7px;
|
||||
border: 1px solid var(--nexus-account-line);
|
||||
background: var(--nexus-account-panel-raised);
|
||||
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.38);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nexus-account-language li a {
|
||||
display: block;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.nexus-account-signout {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--nexus-account-line);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.nexus-account-layout {
|
||||
display: grid;
|
||||
min-height: calc(100vh - 76px);
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: calc(100vh - 76px);
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
padding: 40px 28px 28px clamp(24px, 4vw, 58px);
|
||||
border-right: 1px solid var(--nexus-account-line);
|
||||
background: rgba(8, 13, 28, 0.78);
|
||||
}
|
||||
|
||||
.nexus-account-orbit {
|
||||
position: absolute;
|
||||
right: -130px;
|
||||
bottom: -100px;
|
||||
width: 330px;
|
||||
height: 330px;
|
||||
border: 1px solid rgba(99, 230, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nexus-account-orbit::before,
|
||||
.nexus-account-orbit::after {
|
||||
position: absolute;
|
||||
border: 1px dashed rgba(139, 124, 255, 0.13);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nexus-account-orbit::before {
|
||||
inset: 18%;
|
||||
}
|
||||
|
||||
.nexus-account-orbit::after {
|
||||
inset: 36%;
|
||||
}
|
||||
|
||||
.nexus-account-orbit span {
|
||||
position: absolute;
|
||||
top: 37%;
|
||||
left: -4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--nexus-account-ice);
|
||||
box-shadow: 0 0 15px var(--nexus-account-ice);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar__label {
|
||||
margin: 0 0 22px;
|
||||
color: var(--nexus-account-ice);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar nav,
|
||||
.nexus-account-sidebar nav ul {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
color: var(--nexus-account-muted);
|
||||
font-size: 0.84rem;
|
||||
transition: 150ms ease;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar li a:hover {
|
||||
border-color: var(--nexus-account-line);
|
||||
color: var(--nexus-account-text);
|
||||
background: rgba(99, 230, 255, 0.04);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar li.active a {
|
||||
border-color: rgba(99, 230, 255, 0.24);
|
||||
color: var(--nexus-account-text);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(99, 230, 255, 0.11),
|
||||
rgba(139, 124, 255, 0.04)
|
||||
);
|
||||
}
|
||||
|
||||
.nexus-account-nav__signal {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #52617e;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar li.active .nexus-account-nav__signal {
|
||||
background: var(--nexus-account-ice);
|
||||
box-shadow: 0 0 9px var(--nexus-account-ice);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar__status {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: auto;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--nexus-account-line);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar__status span {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--nexus-account-success);
|
||||
box-shadow: 0 0 8px var(--nexus-account-success);
|
||||
}
|
||||
|
||||
.nexus-account-content {
|
||||
min-width: 0;
|
||||
padding: clamp(28px, 5vw, 70px);
|
||||
}
|
||||
|
||||
.nexus-account-panel {
|
||||
width: min(100%, 1080px);
|
||||
min-height: 420px;
|
||||
padding: clamp(28px, 4vw, 52px);
|
||||
border: 1px solid var(--nexus-account-line);
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(145deg, rgba(18, 27, 49, 0.94), rgba(10, 16, 32, 0.92));
|
||||
box-shadow:
|
||||
0 28px 80px rgba(0, 0, 0, 0.28),
|
||||
inset 0 1px rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.nexus-account-panel > .row:first-child {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.nexus-account-panel h2 {
|
||||
margin: 0;
|
||||
color: var(--nexus-account-text);
|
||||
font-family: var(--nexus-account-display);
|
||||
font-size: clamp(2rem, 3vw, 2.7rem);
|
||||
font-weight: 520;
|
||||
letter-spacing: -0.035em;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.nexus-account-panel h2::after {
|
||||
display: block;
|
||||
width: 46px;
|
||||
height: 2px;
|
||||
margin-top: 17px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--nexus-account-ice),
|
||||
var(--nexus-account-violet)
|
||||
);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nexus-account-panel .subtitle {
|
||||
color: var(--nexus-account-muted);
|
||||
font-family: var(--nexus-account-mono);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.nexus-account-panel .required {
|
||||
color: var(--nexus-account-danger);
|
||||
}
|
||||
|
||||
.nexus-account-form,
|
||||
.nexus-account-panel .form-horizontal {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nexus-account-field,
|
||||
.nexus-account-panel .form-group {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: minmax(120px, 180px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.nexus-account-label,
|
||||
.nexus-account-panel .control-label {
|
||||
color: #bdc8df;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nexus-account-input,
|
||||
.nexus-account-panel .form-control {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(151, 172, 219, 0.24);
|
||||
border-radius: 2px;
|
||||
color: var(--nexus-account-text);
|
||||
outline: 0;
|
||||
background: rgba(4, 8, 21, 0.68);
|
||||
transition: 150ms ease;
|
||||
}
|
||||
|
||||
.nexus-account-input:focus,
|
||||
.nexus-account-panel .form-control:focus {
|
||||
border-color: var(--nexus-account-ice);
|
||||
box-shadow: 0 0 0 3px rgba(99, 230, 255, 0.08);
|
||||
}
|
||||
|
||||
.nexus-account-input:disabled,
|
||||
.nexus-account-panel .form-control:disabled {
|
||||
color: #6f7b96;
|
||||
background: rgba(5, 8, 18, 0.5);
|
||||
}
|
||||
|
||||
.nexus-account-button,
|
||||
.nexus-account-panel .btn {
|
||||
display: inline-flex;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 8px 8px 0;
|
||||
padding: 9px 18px;
|
||||
border: 1px solid var(--nexus-account-line);
|
||||
border-radius: 2px;
|
||||
color: var(--nexus-account-text);
|
||||
background: rgba(18, 27, 49, 0.9);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.nexus-account-button--primary {
|
||||
border-color: transparent;
|
||||
color: #06111c;
|
||||
background: linear-gradient(105deg, var(--nexus-account-ice), #8cf0ec 52%, #a8a1ff);
|
||||
}
|
||||
|
||||
.nexus-account-panel .submit {
|
||||
grid-column: 2;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.nexus-account-panel table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
color: #dce4f5;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.nexus-account-panel th,
|
||||
.nexus-account-panel td {
|
||||
padding: 13px 14px;
|
||||
border: 1px solid var(--nexus-account-line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.nexus-account-panel th {
|
||||
color: var(--nexus-account-muted);
|
||||
background: rgba(5, 9, 21, 0.7);
|
||||
font-family: var(--nexus-account-mono);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nexus-account-panel tr:nth-child(even) td {
|
||||
background: rgba(255, 255, 255, 0.018);
|
||||
}
|
||||
|
||||
.nexus-account-alert {
|
||||
display: flex;
|
||||
width: min(100%, 1080px);
|
||||
align-items: flex-start;
|
||||
gap: 11px;
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgba(99, 230, 255, 0.25);
|
||||
color: #dce6f8;
|
||||
background: rgba(99, 230, 255, 0.06);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.nexus-account-alert--error {
|
||||
border-color: rgba(255, 102, 143, 0.32);
|
||||
background: rgba(255, 102, 143, 0.07);
|
||||
}
|
||||
|
||||
.nexus-account-alert__signal {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
flex: 0 0 auto;
|
||||
margin-top: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--nexus-account-ice);
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.nexus-account-alert--error .nexus-account-alert__signal {
|
||||
color: var(--nexus-account-danger);
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.nexus-account-field-error,
|
||||
.nexus-account-panel .has-error {
|
||||
color: #ff93ae;
|
||||
}
|
||||
|
||||
.nexus-account-body :focus-visible {
|
||||
outline: 2px solid var(--nexus-account-ice);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.nexus-account-header {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.nexus-account-header__realm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nexus-account-layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar {
|
||||
min-height: auto;
|
||||
padding: 18px 20px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--nexus-account-line);
|
||||
}
|
||||
|
||||
.nexus-account-sidebar__label,
|
||||
.nexus-account-sidebar__status,
|
||||
.nexus-account-orbit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar ul {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 6px;
|
||||
padding-bottom: 4px !important;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar ul::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nexus-account-sidebar li {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nexus-account-content {
|
||||
padding: 24px 20px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.nexus-account-header {
|
||||
min-height: 66px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.nexus-account-brand {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.nexus-account-utility > a:not(.nexus-account-signout),
|
||||
.nexus-account-language {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nexus-account-layout {
|
||||
min-height: calc(100vh - 66px);
|
||||
}
|
||||
|
||||
.nexus-account-content {
|
||||
padding: 18px 12px 32px;
|
||||
}
|
||||
|
||||
.nexus-account-panel {
|
||||
padding: 26px 20px;
|
||||
}
|
||||
|
||||
.nexus-account-panel > .row:first-child {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.nexus-account-field,
|
||||
.nexus-account-panel .form-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nexus-account-panel .submit {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.nexus-account-panel {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nexus-account-body *,
|
||||
.nexus-account-body *::before,
|
||||
.nexus-account-body *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
#root {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/* Account workspace: self-service identity tasks share one business-page skeleton. */
|
||||
|
||||
.account-workspace-route,
|
||||
.account-workspace-route__content,
|
||||
.account-business-page {
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nexus-nav-context {
|
||||
display: grid;
|
||||
gap: 0.28rem;
|
||||
margin: 0 0.35rem 1.25rem;
|
||||
padding: 0.15rem 0.45rem 1.15rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.nexus-nav-context__eyebrow {
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nexus-nav-context strong {
|
||||
color: var(--console-ink);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 740;
|
||||
}
|
||||
|
||||
.nexus-nav-context__rule {
|
||||
width: 4.5rem;
|
||||
height: 3px;
|
||||
margin-top: 0.48rem;
|
||||
border-radius: 99px;
|
||||
background: var(--console-primary);
|
||||
}
|
||||
|
||||
.nexus-nav-label {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6rem minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.68rem;
|
||||
}
|
||||
|
||||
.nexus-nav-label__icon {
|
||||
display: grid;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 7px;
|
||||
color: var(--console-faint);
|
||||
background: color-mix(in srgb, var(--console-surface) 58%, transparent);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover .nexus-nav-label__icon,
|
||||
.pf-v5-c-nav__link:focus .nexus-nav-label__icon,
|
||||
.pf-v5-c-nav__link.pf-m-current .nexus-nav-label__icon {
|
||||
border-color: color-mix(in srgb, var(--console-primary) 30%, var(--console-line));
|
||||
color: var(--console-primary-deep);
|
||||
background: color-mix(in srgb, var(--console-primary) 9%, var(--console-surface));
|
||||
}
|
||||
|
||||
.account-view-header {
|
||||
position: relative;
|
||||
padding-top: clamp(1.9rem, 4vw, 3.7rem);
|
||||
padding-bottom: clamp(1.5rem, 2.8vw, 2.4rem);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.account-view-header::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
width: 3px;
|
||||
border-radius: 99px;
|
||||
background: var(--console-primary);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.account-view-header__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 2rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.account-view-header__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-view-header__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 0.62rem;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.67rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.account-view-header h1 {
|
||||
margin: 0;
|
||||
color: var(--console-ink);
|
||||
font-size: clamp(2.15rem, 3.6vw, 3.35rem);
|
||||
font-weight: 740;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.account-view-header p {
|
||||
max-width: 72ch;
|
||||
margin: 0.85rem 0 0;
|
||||
color: var(--console-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.account-view-header__context {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.8rem;
|
||||
min-width: 156px;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
color: var(--console-muted);
|
||||
background: color-mix(in srgb, var(--console-surface) 78%, transparent);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.account-view-header__context span {
|
||||
font-size: 0.59rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.account-view-header__context strong {
|
||||
grid-row: span 2;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 1.45rem;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.account-view-header__context i {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(90deg, var(--console-teal), var(--console-primary));
|
||||
}
|
||||
|
||||
.account-view-body {
|
||||
padding-top: 0;
|
||||
padding-bottom: clamp(3.5rem, 7vw, 6rem);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.account-view-body__surface {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Lists, security devices, applications and permissions become operational decks. */
|
||||
.account-view-body__surface > .pf-v5-c-data-list,
|
||||
.account-view-body__surface > .pf-v5-c-table,
|
||||
.account-view-body__surface > .pf-v5-c-card,
|
||||
.account-view-body__surface > .pf-v5-c-stack,
|
||||
.account-view-body__surface > .pf-v5-c-form,
|
||||
.account-view-body__surface > form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__item-row {
|
||||
min-height: 72px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__cell {
|
||||
color: var(--console-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__cell strong,
|
||||
.account-view-body .pf-v5-c-data-list__cell b,
|
||||
.account-view-body .pf-v5-c-data-list__cell h2,
|
||||
.account-view-body .pf-v5-c-data-list__cell h3 {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-stack > .pf-v5-c-stack__item + .pf-v5-c-stack__item {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-title.pf-m-xl,
|
||||
.account-view-body .pf-v5-c-title.pf-m-lg {
|
||||
margin-bottom: 0.85rem;
|
||||
padding-bottom: 0.72rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form {
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form__group {
|
||||
padding-block: 0.28rem;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form__actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--console-surface) 92%, transparent);
|
||||
box-shadow: 0 -10px 28px color-mix(in srgb, var(--console-ink) 8%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
/* Personal information is a two-column identity record, not a generic form. */
|
||||
.nexus-profile-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(245px, 0.58fr) minmax(0, 1.65fr);
|
||||
align-items: start;
|
||||
gap: clamp(1.25rem, 2.6vw, 2.5rem);
|
||||
}
|
||||
|
||||
.nexus-identity-card,
|
||||
.nexus-profile-editor {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.nexus-identity-card {
|
||||
min-height: 0;
|
||||
padding: 1.45rem;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
}
|
||||
|
||||
.nexus-identity-card::before {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 4px;
|
||||
background: var(--console-primary);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nexus-identity-card__topline,
|
||||
.nexus-identity-card__name > span,
|
||||
.nexus-identity-card__facts dt,
|
||||
.nexus-identity-card__footer,
|
||||
.nexus-identity-card__name p {
|
||||
color: var(--console-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nexus-identity-card__topline i {
|
||||
border-color: var(--console-primary);
|
||||
border-radius: 50%;
|
||||
background: var(--console-primary);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--console-primary) 12%, transparent);
|
||||
}
|
||||
|
||||
.nexus-identity-card__avatar {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 1.8rem 0 1.35rem;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(145deg, var(--console-primary), var(--console-teal));
|
||||
box-shadow: 0 10px 24px color-mix(in srgb, var(--console-primary) 22%, transparent);
|
||||
}
|
||||
|
||||
.nexus-identity-card__avatar span {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.nexus-identity-card__name h2,
|
||||
.nexus-identity-card__facts dd {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.nexus-identity-card__name h2 {
|
||||
font-size: 1.55rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.nexus-identity-card__facts > div {
|
||||
border-top-color: var(--console-line);
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header {
|
||||
padding: 1.55rem clamp(1.25rem, 2.5vw, 2rem);
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header > span {
|
||||
color: var(--console-primary-deep);
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header h2 {
|
||||
color: var(--console-ink);
|
||||
font-size: 1.28rem;
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header p {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.nexus-profile-editor > .pf-v5-c-form {
|
||||
padding: clamp(1.35rem, 3vw, 2.2rem);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.account-view-header::before {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.account-view-header__layout {
|
||||
grid-template-columns: 1fr;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.account-view-header__context {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.account-view-header h1 {
|
||||
font-size: 2.15rem;
|
||||
}
|
||||
|
||||
.nexus-profile-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__item-row {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form__actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "admin/App.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import { SessionExpirationWarningOverlay } from "../shared/SessionExpirationWarningOverlay";
|
||||
import {
|
||||
ErrorBoundaryFallback,
|
||||
ErrorBoundaryProvider,
|
||||
KeycloakSpinner,
|
||||
mainPageContentId
|
||||
} from "../shared/keycloak-ui-shared";
|
||||
import { Flex, FlexItem, Page } from "../shared/@patternfly/react-core";
|
||||
import { PropsWithChildren, Suspense, useEffect } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { AdminClientProvider } from "./admin-client";
|
||||
import { Header } from "./PageHeader";
|
||||
import { PageNav } from "./PageNav";
|
||||
import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
|
||||
import { ErrorRenderer } from "./components/error/ErrorRenderer";
|
||||
import { RecentRealmsProvider } from "./context/RecentRealms";
|
||||
import { AccessContextProvider } from "./context/access/Access";
|
||||
import { RealmContextProvider } from "./context/realm-context/RealmContext";
|
||||
import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
|
||||
import { WhoAmIContextProvider } from "./context/whoami/WhoAmI";
|
||||
import { SubGroups } from "./groups/SubGroupsContext";
|
||||
import { AuthWall } from "./root/AuthWall";
|
||||
import { Banners } from "./Banners";
|
||||
|
||||
export const AppContexts = ({ children }: PropsWithChildren) => (
|
||||
<ErrorBoundaryProvider>
|
||||
<ErrorBoundaryFallback fallback={ErrorRenderer}>
|
||||
<ServerInfoProvider>
|
||||
<RealmContextProvider>
|
||||
<WhoAmIContextProvider>
|
||||
<RecentRealmsProvider>
|
||||
<AccessContextProvider>
|
||||
<SubGroups>{children}</SubGroups>
|
||||
</AccessContextProvider>
|
||||
</RecentRealmsProvider>
|
||||
</WhoAmIContextProvider>
|
||||
</RealmContextProvider>
|
||||
</ServerInfoProvider>
|
||||
</ErrorBoundaryFallback>
|
||||
</ErrorBoundaryProvider>
|
||||
);
|
||||
|
||||
export const App = () => {
|
||||
const hrefEndsWithHashSlash = location.href.endsWith("#/");
|
||||
useEffect(() => {
|
||||
if (!hrefEndsWithHashSlash) return;
|
||||
history.replaceState(null, "", location.pathname);
|
||||
}, [hrefEndsWithHashSlash, location.pathname]);
|
||||
|
||||
return (
|
||||
<AdminClientProvider>
|
||||
<AppContexts>
|
||||
<Flex
|
||||
direction={{ default: "column" }}
|
||||
flexWrap={{ default: "nowrap" }}
|
||||
spaceItems={{ default: "spaceItemsNone" }}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<FlexItem>
|
||||
<Banners />
|
||||
</FlexItem>
|
||||
<FlexItem grow={{ default: "grow" }} style={{ minHeight: 0 }}>
|
||||
<Page
|
||||
header={<Header />}
|
||||
isManagedSidebar
|
||||
sidebar={<PageNav />}
|
||||
breadcrumb={<PageBreadCrumbs />}
|
||||
mainContainerId={mainPageContentId}
|
||||
>
|
||||
<ErrorBoundaryFallback fallback={ErrorRenderer}>
|
||||
<Suspense fallback={<KeycloakSpinner />}>
|
||||
<AuthWall>
|
||||
<div
|
||||
className="admin-workspace-route"
|
||||
data-workspace="realm-operations"
|
||||
>
|
||||
<div className="admin-workspace-route__content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</AuthWall>
|
||||
</Suspense>
|
||||
</ErrorBoundaryFallback>
|
||||
</Page>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<SessionExpirationWarningOverlay warnUserSecondsBeforeAutoLogout={45} />
|
||||
</AppContexts>
|
||||
</AdminClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "admin/KcAdminUi.tsx" --revert
|
||||
*/
|
||||
|
||||
import "@patternfly/patternfly/patternfly-addons.css";
|
||||
import "@patternfly/react-core/dist/styles/base.css";
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { startColorSchemeManagement } from "./colorScheme";
|
||||
import { createHashRouter, RouterProvider } from "react-router-dom";
|
||||
import { i18n } from "./i18n/i18n";
|
||||
import { Root } from "./Root";
|
||||
import { routes } from "./routes";
|
||||
import { getKcContext } from "./KcContext";
|
||||
|
||||
import "./index.css";
|
||||
import "../console-ui.css";
|
||||
import "./workspace.css";
|
||||
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
document.documentElement.dataset.kcThemeName = kcContext.themeName;
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Root />,
|
||||
children: routes
|
||||
}
|
||||
]);
|
||||
const prI18nInitialized = i18n.init();
|
||||
startColorSchemeManagement();
|
||||
|
||||
export default function KcAdminUi() {
|
||||
const [isI18nInitialized, setI18nInitialized] = useReducer(() => true, false);
|
||||
|
||||
useEffect(() => {
|
||||
prI18nInitialized.then(() => setI18nInitialized());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isI18nInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateDocumentTitle = () => {
|
||||
document.title = i18n.t("nexusAdminConsoleTitle");
|
||||
};
|
||||
|
||||
updateDocumentTitle();
|
||||
i18n.on("languageChanged", updateDocumentTitle);
|
||||
|
||||
return () => {
|
||||
i18n.off("languageChanged", updateDocumentTitle);
|
||||
};
|
||||
}, [isI18nInitialized]);
|
||||
|
||||
if (!isI18nInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "admin/PageHeader.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import logoSvgUrl from "./assets/logo.svg";
|
||||
import { KeycloakMasthead, useEnvironment, useHelp } from "../shared/keycloak-ui-shared";
|
||||
import { DropdownItem, ToolbarItem } from "../shared/@patternfly/react-core";
|
||||
import { HelpIcon } from "../shared/@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHref } from "react-router-dom";
|
||||
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 { toDashboard } from "./dashboard/routes/Dashboard";
|
||||
import type { Environment } from "./environment-types";
|
||||
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
|
||||
import { joinPath } from "./utils/joinPath";
|
||||
import { useIsFeatureDisabled, Feature } from "./utils/useIsFeatureEnabled";
|
||||
import useToggle from "./utils/useToggle";
|
||||
import { ThemeModeToggle } from "../shared/ThemeModeToggle";
|
||||
|
||||
const ManageAccountDropdownItem = () => {
|
||||
const { keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const isFeatureDisabled = useIsFeatureDisabled();
|
||||
|
||||
if (isFeatureDisabled(Feature.AccountV3)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key="manage account"
|
||||
id="manage-account"
|
||||
onClick={() => keycloak.accountManagement()}
|
||||
>
|
||||
{t("manageAccount")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerInfoDropdownItem = () => {
|
||||
const { realm } = useRealm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key="server info"
|
||||
component={props => <Link {...props} to={toDashboard({ realm })} />}
|
||||
>
|
||||
{t("realmInfo")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ClearCachesDropdownItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, toggleModal] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownItem key="clear caches" onClick={() => toggleModal()}>
|
||||
{t("clearCachesTitle")}
|
||||
</DropdownItem>
|
||||
{open && <PageHeaderClearCachesModal onClose={() => toggleModal()} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const HelpDropdownItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const { enabled, toggleHelp } = useHelp();
|
||||
return (
|
||||
<DropdownItem data-testId="helpIcon" icon={<HelpIcon />} onClick={toggleHelp}>
|
||||
{enabled ? t("helpEnabled") : t("helpDisabled")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const kebabDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
|
||||
<ManageAccountDropdownItem key="kebab Manage Account" />,
|
||||
<ServerInfoDropdownItem key="kebab Server Info" />,
|
||||
...(isMasterRealm && isManager
|
||||
? [<ClearCachesDropdownItem key="Clear Caches" />]
|
||||
: []),
|
||||
<HelpDropdownItem key="kebab Help" />
|
||||
];
|
||||
|
||||
const userDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
|
||||
<ManageAccountDropdownItem key="Manage Account" />,
|
||||
<ServerInfoDropdownItem key="Server info" />,
|
||||
...(isMasterRealm && isManager
|
||||
? [<ClearCachesDropdownItem key="Clear Caches" />]
|
||||
: [])
|
||||
];
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment<Environment>();
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const contextLogo = usePreviewLogo();
|
||||
const customLogo = contextLogo?.logo;
|
||||
|
||||
const isMasterRealm = realm === environment.masterRealm;
|
||||
const isManager = hasAccess("manage-realm");
|
||||
|
||||
const url = useHref(toDashboard({ realm }));
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : url;
|
||||
|
||||
return (
|
||||
<KeycloakMasthead
|
||||
data-testid="page-header"
|
||||
keycloak={keycloak}
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: logoUrl,
|
||||
src: customLogo?.trim() ? customLogo : logoSvgUrl,
|
||||
alt: t("logo"),
|
||||
className: "keycloak__pageheader_brand"
|
||||
}}
|
||||
dropdownItems={userDropdownItems(isMasterRealm, isManager)}
|
||||
kebabDropdownItems={kebabDropdownItems(isMasterRealm, isManager)}
|
||||
toolbarItems={[
|
||||
<ToolbarItem key="theme-mode">
|
||||
<ThemeModeToggle />
|
||||
</ToolbarItem>,
|
||||
<ToolbarItem
|
||||
key="help"
|
||||
align={{ default: "alignRight" }}
|
||||
visibility={{
|
||||
default: "hidden",
|
||||
md: "visible"
|
||||
}} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */
|
||||
>
|
||||
<HelpHeader />
|
||||
</ToolbarItem>
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEnvironment } from "../shared/keycloak-ui-shared";
|
||||
import {
|
||||
Label,
|
||||
Nav,
|
||||
NavExpandable,
|
||||
NavGroup,
|
||||
PageSidebar,
|
||||
PageSidebarBody
|
||||
} from "../shared/@patternfly/react-core";
|
||||
import { FormEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavLink, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useAccess } from "./context/access/Access";
|
||||
import { useRealm } from "./context/realm-context/RealmContext";
|
||||
import { useServerInfo } from "./context/server-info/ServerInfoProvider";
|
||||
import type { Environment } from "./environment-types";
|
||||
import { toPage } from "./page/routes";
|
||||
import { routes } from "./routes";
|
||||
import { resolveDisplayName } from "./util";
|
||||
import useIsFeatureEnabled, { Feature } from "./utils/useIsFeatureEnabled";
|
||||
|
||||
import "./page-nav.css";
|
||||
|
||||
type LeftNavProps = {
|
||||
title: string;
|
||||
path: string;
|
||||
code: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const LeftNav = ({ title, path, code, id }: LeftNavProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { hasAccess } = useAccess();
|
||||
const { realm } = useRealm();
|
||||
const encodedRealm = encodeURIComponent(realm);
|
||||
const route = routes.find(
|
||||
route => route.path.replace(/\/:.+?(\?|(?:(?!\/).)*|$)/g, "") === (id || path)
|
||||
);
|
||||
|
||||
const accessAllowed =
|
||||
route &&
|
||||
(route.handle.access instanceof Array
|
||||
? hasAccess(...route.handle.access)
|
||||
: hasAccess(route.handle.access));
|
||||
|
||||
if (!accessAllowed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const name = "nav-item" + (path || "-overview").replace("/", "-");
|
||||
return (
|
||||
<li>
|
||||
<NavLink
|
||||
id={name}
|
||||
data-testid={name}
|
||||
to={`/${encodedRealm}${path}`}
|
||||
end={path === ""}
|
||||
className={({ isActive }) =>
|
||||
`pf-v5-c-nav__link${isActive ? " pf-m-current" : ""}`
|
||||
}
|
||||
>
|
||||
<span className="admin-nav-code" aria-hidden="true">
|
||||
{code}
|
||||
</span>
|
||||
<span>{t(title)}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageNav = () => {
|
||||
const { t } = useTranslation();
|
||||
const { environment } = useEnvironment<Environment>();
|
||||
const { hasSomeAccess } = useAccess();
|
||||
const { componentTypes } = useServerInfo();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
const pages = componentTypes?.["org.keycloak.services.ui.extend.UiPageProvider"];
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const { realm, realmRepresentation } = useRealm();
|
||||
|
||||
type SelectedItem = {
|
||||
groupId: number | string;
|
||||
itemId: number | string;
|
||||
to: string;
|
||||
event: FormEvent<HTMLInputElement>;
|
||||
};
|
||||
|
||||
const onSelect = (item: SelectedItem) => {
|
||||
navigate(item.to);
|
||||
item.event.preventDefault();
|
||||
};
|
||||
|
||||
const showManage = hasSomeAccess(
|
||||
"view-realm",
|
||||
"query-groups",
|
||||
"query-users",
|
||||
"query-clients",
|
||||
"query-organizations",
|
||||
"view-events"
|
||||
);
|
||||
const showConfigure = hasSomeAccess(
|
||||
"view-realm",
|
||||
"query-clients",
|
||||
"view-identity-providers"
|
||||
);
|
||||
const showWorkflows =
|
||||
hasSomeAccess("realm-admin", "admin") && isFeatureEnabled(Feature.Workflows);
|
||||
const showManageRealm = environment.masterRealm === environment.realm;
|
||||
const realmName = resolveDisplayName(t, realmRepresentation.displayName, realm);
|
||||
const managePaths = [
|
||||
"/organizations",
|
||||
"/clients",
|
||||
"/client-scopes",
|
||||
"/roles",
|
||||
"/users",
|
||||
"/groups",
|
||||
"/sessions",
|
||||
"/events"
|
||||
];
|
||||
const configurePaths = [
|
||||
"/realm-settings",
|
||||
"/authentication",
|
||||
"/permissions",
|
||||
"/identity-providers",
|
||||
"/user-federation",
|
||||
"/workflows",
|
||||
"/page-section"
|
||||
];
|
||||
const isManageActive = managePaths.some(path => pathname.includes(path));
|
||||
const isConfigureActive = configurePaths.some(path => pathname.includes(path));
|
||||
|
||||
return (
|
||||
<PageSidebar className="keycloak__page_nav__nav">
|
||||
<PageSidebarBody>
|
||||
<div className="admin-nav-context">
|
||||
<span className="admin-nav-context__eyebrow">
|
||||
{t("adminWorkspaceRealmEyebrow")}
|
||||
</span>
|
||||
<strong data-testid="currentRealm">{realmName}</strong>
|
||||
<div className="admin-nav-context__meta">
|
||||
<span
|
||||
className={`admin-nav-status${
|
||||
realmRepresentation.enabled === false
|
||||
? " is-disabled"
|
||||
: ""
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{realmRepresentation.enabled === false
|
||||
? t("disabled")
|
||||
: t("enabled")}
|
||||
</div>
|
||||
</div>
|
||||
<Nav onSelect={(_event, item) => onSelect(item as SelectedItem)}>
|
||||
<NavGroup>
|
||||
<LeftNav
|
||||
title="adminWorkspaceOverview"
|
||||
path=""
|
||||
id="/"
|
||||
code="OVR"
|
||||
/>
|
||||
</NavGroup>
|
||||
{showManageRealm && (
|
||||
<NavGroup>
|
||||
<LeftNav title="manageRealms" path="/realms" code="RLM" />
|
||||
</NavGroup>
|
||||
)}
|
||||
{showManage && (
|
||||
<NavExpandable
|
||||
className="admin-nav-group"
|
||||
groupId="manage"
|
||||
title={t("manage")}
|
||||
isActive={isManageActive}
|
||||
isExpanded={isManageActive}
|
||||
>
|
||||
{isFeatureEnabled(Feature.Organizations) &&
|
||||
realmRepresentation.organizationsEnabled && (
|
||||
<LeftNav
|
||||
title="organizations"
|
||||
path="/organizations"
|
||||
code="ORG"
|
||||
/>
|
||||
)}
|
||||
<LeftNav title="clients" path="/clients" code="APP" />
|
||||
<LeftNav
|
||||
title="clientScopes"
|
||||
path="/client-scopes"
|
||||
code="SCP"
|
||||
/>
|
||||
<LeftNav title="realmRoles" path="/roles" code="ROL" />
|
||||
<LeftNav title="users" path="/users" code="USR" />
|
||||
<LeftNav title="groups" path="/groups" code="GRP" />
|
||||
<LeftNav title="sessions" path="/sessions" code="SES" />
|
||||
<LeftNav title="events" path="/events" code="EVT" />
|
||||
</NavExpandable>
|
||||
)}
|
||||
{showConfigure && (
|
||||
<NavExpandable
|
||||
className="admin-nav-group"
|
||||
groupId="configure"
|
||||
title={t("configure")}
|
||||
isActive={isConfigureActive}
|
||||
isExpanded={isConfigureActive}
|
||||
>
|
||||
<LeftNav
|
||||
title="realmSettings"
|
||||
path="/realm-settings"
|
||||
code="CFG"
|
||||
/>
|
||||
<LeftNav
|
||||
title="authentication"
|
||||
path="/authentication"
|
||||
code="AUTH"
|
||||
/>
|
||||
{isFeatureEnabled(Feature.AdminFineGrainedAuthzV2) &&
|
||||
realmRepresentation.adminPermissionsEnabled && (
|
||||
<LeftNav
|
||||
title="permissions"
|
||||
path="/permissions"
|
||||
code="POL"
|
||||
/>
|
||||
)}
|
||||
<LeftNav
|
||||
title="identityProviders"
|
||||
path="/identity-providers"
|
||||
code="IDP"
|
||||
/>
|
||||
<LeftNav
|
||||
title="userFederation"
|
||||
path="/user-federation"
|
||||
code="DIR"
|
||||
/>
|
||||
{showWorkflows && (
|
||||
<LeftNav
|
||||
title="workflows"
|
||||
path="/workflows"
|
||||
code="FLOW"
|
||||
/>
|
||||
)}
|
||||
{isFeatureEnabled(Feature.DeclarativeUI) &&
|
||||
pages?.map(page => (
|
||||
<LeftNav
|
||||
key={page.id}
|
||||
title={page.id}
|
||||
path={toPage({ providerId: page.id }).pathname!}
|
||||
id="/page-section"
|
||||
code="EXT"
|
||||
/>
|
||||
))}
|
||||
</NavExpandable>
|
||||
)}
|
||||
</Nav>
|
||||
<div className="admin-nav-footer">
|
||||
<span>{t("adminWorkspaceConsoleLabel")}</span>
|
||||
<Label color="blue">{t("currentRealm")}</Label>
|
||||
</div>
|
||||
</PageSidebarBody>
|
||||
</PageSidebar>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title">
|
||||
<title id="title">NEXUS ID</title>
|
||||
<defs>
|
||||
<linearGradient id="signal" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#63e6ff"/>
|
||||
<stop offset="1" stop-color="#8b7cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(8 9)">
|
||||
<rect x="5" y="5" width="24" height="24" rx="1" fill="none" stroke="url(#signal)" stroke-width="1.5" transform="rotate(45 17 17)"/>
|
||||
<circle cx="17" cy="17" r="3.5" fill="#63e6ff"/>
|
||||
<circle cx="17" cy="17" r="8.5" fill="none" stroke="#63e6ff" stroke-opacity=".18"/>
|
||||
<rect width="34" height="34" rx="10" fill="#356fd6"/>
|
||||
<path d="M9 10h16M9 17h11M9 24h16" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="24.5" cy="17" r="2.2" fill="#f0b92e"/>
|
||||
</g>
|
||||
<text x="52" y="32" fill="#f4f7ff" font-family="Bahnschrift,Segoe UI,sans-serif" font-size="20" font-weight="600" letter-spacing="3">NEXUS</text>
|
||||
<text x="151" y="32" fill="#95a2be" font-family="Bahnschrift,Segoe UI,sans-serif" font-size="18" font-weight="400" letter-spacing="2">/ ID</text>
|
||||
<text x="54" y="31" fill="#17324d" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="19" font-weight="700" letter-spacing="1.2">NEXUS</text>
|
||||
<text x="133" y="31" fill="#667a8e" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="15" font-weight="600" letter-spacing="1">WORKSPACE</text>
|
||||
<rect x="54" y="38" width="31" height="3" rx="1.5" fill="#279477"/>
|
||||
<rect x="88" y="38" width="31" height="3" rx="1.5" fill="#e9ad38"/>
|
||||
<rect x="122" y="38" width="31" height="3" rx="1.5" fill="#e86452"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 929 B |
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "admin/colorScheme.ts" --revert
|
||||
*/
|
||||
|
||||
/* IMPORTANT NOTE:
|
||||
* If you modify how the light/dark mode is implemented
|
||||
* you must also update public/admin/early-color-scheme.js
|
||||
* This scrip is ran before this to avoid white flashes
|
||||
*/
|
||||
|
||||
import { getKcContext } from "./KcContext";
|
||||
import { startConsoleColorScheme } from "../shared/consoleColorScheme";
|
||||
|
||||
export function startColorSchemeManagement() {
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
startConsoleColorScheme(kcContext.darkMode !== false);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
DropdownList,
|
||||
MenuToggle,
|
||||
PageSection,
|
||||
Switch
|
||||
} from "../../../shared/@patternfly/react-core";
|
||||
import { Fragment, ReactElement, ReactNode, isValidElement, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpItem, useHelp } from "../../../shared/keycloak-ui-shared";
|
||||
import { FormattedLink } from "../external-link/FormattedLink";
|
||||
import "../../help-urls";
|
||||
|
||||
export type ViewHeaderProps = {
|
||||
titleKey: string;
|
||||
className?: string;
|
||||
badges?: ViewHeaderBadge[];
|
||||
isDropdownDisabled?: boolean;
|
||||
subKey?: string | ReactNode;
|
||||
actionsDropdownId?: string;
|
||||
helpUrl?: string | undefined;
|
||||
dropdownItems?: ReactElement[];
|
||||
lowerDropdownItems?: any;
|
||||
lowerDropdownMenuTitle?: any;
|
||||
lowerButton?: any;
|
||||
isEnabled?: boolean;
|
||||
onToggle?: (value: boolean) => void;
|
||||
divider?: boolean;
|
||||
helpTextKey?: string;
|
||||
isReadOnly?: boolean;
|
||||
actionDropdownTitle?: string;
|
||||
noTranslate?: boolean;
|
||||
};
|
||||
|
||||
export type ViewHeaderBadge = {
|
||||
id?: string;
|
||||
text?: string | ReactNode;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const ViewHeader = ({
|
||||
actionsDropdownId,
|
||||
className,
|
||||
titleKey,
|
||||
badges,
|
||||
isDropdownDisabled,
|
||||
subKey,
|
||||
helpUrl,
|
||||
dropdownItems,
|
||||
lowerDropdownMenuTitle,
|
||||
lowerDropdownItems,
|
||||
lowerButton,
|
||||
isEnabled = true,
|
||||
onToggle,
|
||||
divider = true,
|
||||
helpTextKey,
|
||||
isReadOnly = false,
|
||||
actionDropdownTitle = "action",
|
||||
noTranslate = false
|
||||
}: ViewHeaderProps) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { enabled } = useHelp();
|
||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isLowerDropdownOpen, setIsLowerDropdownOpen] = useState(false);
|
||||
const toKey = (value: string) => value.replace(/\s/g, "-");
|
||||
const title = noTranslate || !i18n.exists(titleKey) ? titleKey : t(titleKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant="light" className="admin-view-header">
|
||||
<div className="admin-view-header__layout">
|
||||
<div className="admin-view-header__copy">
|
||||
<span className="admin-view-header__eyebrow">
|
||||
{t("adminWorkspaceSectionEyebrow")}
|
||||
</span>
|
||||
<div className="admin-view-header__title-row">
|
||||
<h1 className={className} data-testid="view-header">
|
||||
{title}
|
||||
</h1>
|
||||
{badges?.length ? (
|
||||
<div className="admin-view-header__badges">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={badge.id ?? index}>
|
||||
{isValidElement(badge.text) ? (
|
||||
badge.text
|
||||
) : (
|
||||
<Badge
|
||||
data-testid={badge.id}
|
||||
isRead={badge.readonly}
|
||||
>
|
||||
{badge.text}
|
||||
</Badge>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{enabled && (subKey || helpUrl) && (
|
||||
<p
|
||||
id="view-header-subkey"
|
||||
className="admin-view-header__description"
|
||||
>
|
||||
{isValidElement(subKey)
|
||||
? subKey
|
||||
: subKey
|
||||
? t(subKey as string)
|
||||
: ""}
|
||||
{helpUrl && (
|
||||
<FormattedLink
|
||||
title={t("learnMore")}
|
||||
href={helpUrl}
|
||||
isInline
|
||||
className="pf-v5-u-ml-md"
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{(onToggle || dropdownItems) && (
|
||||
<div className="admin-view-header__actions">
|
||||
{onToggle && (
|
||||
<div className="admin-view-header__switch">
|
||||
<Switch
|
||||
id={`${toKey(titleKey)}-switch`}
|
||||
data-testid={`${titleKey}-switch`}
|
||||
label={t("enabled")}
|
||||
labelOff={t("disabled")}
|
||||
isDisabled={isReadOnly}
|
||||
isChecked={isEnabled}
|
||||
aria-label={t("enabled")}
|
||||
onChange={(_event, value) => onToggle(value)}
|
||||
/>
|
||||
{helpTextKey && (
|
||||
<HelpItem
|
||||
helpText={t(helpTextKey)}
|
||||
fieldLabelId={`${toKey(titleKey)}-switch`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{dropdownItems && (
|
||||
<Dropdown
|
||||
popperProps={{ position: "right" }}
|
||||
onOpenChange={setDropdownOpen}
|
||||
toggle={ref => (
|
||||
<MenuToggle
|
||||
ref={ref}
|
||||
isDisabled={isDropdownDisabled}
|
||||
id={actionsDropdownId}
|
||||
onClick={() =>
|
||||
setDropdownOpen(value => !value)
|
||||
}
|
||||
data-testid="action-dropdown"
|
||||
variant="primary"
|
||||
>
|
||||
{t(actionDropdownTitle)}
|
||||
</MenuToggle>
|
||||
)}
|
||||
isOpen={isDropdownOpen}
|
||||
>
|
||||
<DropdownList>{dropdownItems}</DropdownList>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(lowerDropdownItems || lowerButton) && (
|
||||
<div className="admin-view-header__lower-actions">
|
||||
{lowerDropdownItems && (
|
||||
<Dropdown
|
||||
className="keycloak__user-federation__dropdown"
|
||||
onOpenChange={setIsLowerDropdownOpen}
|
||||
toggle={ref => (
|
||||
<MenuToggle
|
||||
ref={ref}
|
||||
onClick={() =>
|
||||
setIsLowerDropdownOpen(value => !value)
|
||||
}
|
||||
variant="primary"
|
||||
id="ufToggleId"
|
||||
>
|
||||
{t(lowerDropdownMenuTitle)}
|
||||
</MenuToggle>
|
||||
)}
|
||||
isOpen={isLowerDropdownOpen}
|
||||
>
|
||||
<DropdownList>{lowerDropdownItems}</DropdownList>
|
||||
</Dropdown>
|
||||
)}
|
||||
{lowerButton && (
|
||||
<Button
|
||||
variant={lowerButton.variant}
|
||||
onClick={lowerButton.onClick}
|
||||
data-testid="viewHeader-lower-btn"
|
||||
>
|
||||
{lowerButton.lowerButtonTitle}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageSection>
|
||||
{divider && (
|
||||
<Divider component="div" className="admin-view-header__divider" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import FeatureRepresentation, {
|
||||
FeatureType
|
||||
} from "@keycloak/keycloak-admin-client/lib/defs/featureRepresentation";
|
||||
import {
|
||||
HelpItem,
|
||||
KeycloakSpinner,
|
||||
useEnvironment
|
||||
} from "../../shared/keycloak-ui-shared";
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
Grid,
|
||||
GridItem,
|
||||
Label,
|
||||
List,
|
||||
ListItem,
|
||||
ListVariant,
|
||||
PageSection,
|
||||
Tab,
|
||||
TabTitleText
|
||||
} from "../../shared/@patternfly/react-core";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { RoutableTabs, useRoutableTab } from "../components/routable-tabs/RoutableTabs";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import type { Environment } from "../environment-types";
|
||||
import helpUrls from "../help-urls";
|
||||
import { resolveDisplayName } from "../util";
|
||||
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
|
||||
import { ProviderInfo } from "./ProviderInfo";
|
||||
import { DashboardTab, toDashboard } from "./routes/Dashboard";
|
||||
|
||||
import "./dashboard.css";
|
||||
|
||||
type WorkspaceLink = {
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
path: string;
|
||||
code: string;
|
||||
tone: "blue" | "green" | "amber" | "coral";
|
||||
access: string[];
|
||||
};
|
||||
|
||||
const workspaceLinks: WorkspaceLink[] = [
|
||||
{
|
||||
titleKey: "users",
|
||||
descriptionKey: "adminWorkspaceUsersDescription",
|
||||
path: "/users",
|
||||
code: "USR",
|
||||
tone: "blue",
|
||||
access: ["query-users", "view-users", "manage-users"]
|
||||
},
|
||||
{
|
||||
titleKey: "clients",
|
||||
descriptionKey: "adminWorkspaceClientsDescription",
|
||||
path: "/clients",
|
||||
code: "APP",
|
||||
tone: "green",
|
||||
access: ["query-clients", "view-clients", "manage-clients"]
|
||||
},
|
||||
{
|
||||
titleKey: "groups",
|
||||
descriptionKey: "adminWorkspaceGroupsDescription",
|
||||
path: "/groups",
|
||||
code: "GRP",
|
||||
tone: "amber",
|
||||
access: ["query-groups", "view-users", "manage-users"]
|
||||
},
|
||||
{
|
||||
titleKey: "realmSettings",
|
||||
descriptionKey: "adminWorkspaceRealmSettingsDescription",
|
||||
path: "/realm-settings",
|
||||
code: "CFG",
|
||||
tone: "coral",
|
||||
access: ["view-realm", "manage-realm"]
|
||||
},
|
||||
{
|
||||
titleKey: "authentication",
|
||||
descriptionKey: "adminWorkspaceAuthenticationDescription",
|
||||
path: "/authentication",
|
||||
code: "AUTH",
|
||||
tone: "blue",
|
||||
access: ["view-realm", "manage-realm"]
|
||||
},
|
||||
{
|
||||
titleKey: "events",
|
||||
descriptionKey: "adminWorkspaceEventsDescription",
|
||||
path: "/events",
|
||||
code: "EVT",
|
||||
tone: "green",
|
||||
access: ["view-events", "manage-events"]
|
||||
}
|
||||
];
|
||||
|
||||
const BooleanStatus = ({ value }: { value?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className={`admin-dashboard-status${value ? " is-positive" : ""}`}>
|
||||
<i aria-hidden="true" />
|
||||
{value ? t("enabled") : t("disabled")}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const OperationsOverview = () => {
|
||||
const { t } = useTranslation();
|
||||
const { realm, realmRepresentation: realmInfo } = useRealm();
|
||||
const { hasSomeAccess } = useAccess();
|
||||
const encodedRealm = encodeURIComponent(realm);
|
||||
const realmDisplayName = resolveDisplayName(t, realmInfo.displayName, realm);
|
||||
const availableLinks = workspaceLinks.filter(link => hasSomeAccess(...link.access));
|
||||
|
||||
const postureItems = [
|
||||
{
|
||||
label: t("adminWorkspaceRealmStatus"),
|
||||
value: <BooleanStatus value={realmInfo.enabled !== false} />
|
||||
},
|
||||
{
|
||||
label: t("userRegistration"),
|
||||
value: <BooleanStatus value={realmInfo.registrationAllowed === true} />
|
||||
},
|
||||
{
|
||||
label: t("verifyEmail"),
|
||||
value: <BooleanStatus value={realmInfo.verifyEmail === true} />
|
||||
},
|
||||
{
|
||||
label: t("bruteForceDetection"),
|
||||
value: <BooleanStatus value={realmInfo.bruteForceProtected === true} />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="admin-dashboard-overview">
|
||||
<section className="admin-dashboard-hero">
|
||||
<div className="admin-dashboard-hero__copy">
|
||||
<span className="admin-dashboard-eyebrow">
|
||||
{t("adminWorkspaceRealmEyebrow")}
|
||||
</span>
|
||||
<h1>{realmDisplayName}</h1>
|
||||
<p>{t("adminWorkspaceOverviewDescription")}</p>
|
||||
<div className="admin-dashboard-hero__meta">
|
||||
<span>{t("realmName")}</span>
|
||||
<strong>{realm}</strong>
|
||||
<BooleanStatus value={realmInfo.enabled !== false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-dashboard-hero__signal" aria-hidden="true">
|
||||
<span>IDENTITY</span>
|
||||
<i />
|
||||
<strong>{availableLinks.length}</strong>
|
||||
<small>{t("adminWorkspaceAvailableAreas")}</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="admin-dashboard-layout">
|
||||
<section
|
||||
className="admin-dashboard-primary"
|
||||
aria-labelledby="workspace-title"
|
||||
>
|
||||
<header className="admin-dashboard-section-header">
|
||||
<div>
|
||||
<span>{t("adminWorkspaceOperateEyebrow")}</span>
|
||||
<h2 id="workspace-title">{t("adminWorkspaceManageTitle")}</h2>
|
||||
</div>
|
||||
<p>{t("adminWorkspaceManageDescription")}</p>
|
||||
</header>
|
||||
<div className="admin-dashboard-link-grid">
|
||||
{availableLinks.map(link => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={`/${encodedRealm}${link.path}`}
|
||||
className={`admin-dashboard-link-card is-${link.tone}`}
|
||||
>
|
||||
<span className="admin-dashboard-link-card__code">
|
||||
{link.code}
|
||||
</span>
|
||||
<span className="admin-dashboard-link-card__copy">
|
||||
<strong>{t(link.titleKey)}</strong>
|
||||
<small>{t(link.descriptionKey)}</small>
|
||||
</span>
|
||||
<span
|
||||
className="admin-dashboard-link-card__arrow"
|
||||
aria-hidden="true"
|
||||
>
|
||||
→
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="admin-dashboard-aside">
|
||||
<section className="admin-dashboard-posture">
|
||||
<header>
|
||||
<span>{t("adminWorkspacePostureEyebrow")}</span>
|
||||
<h2>{t("adminWorkspacePostureTitle")}</h2>
|
||||
</header>
|
||||
<dl>
|
||||
{postureItems.map(item => (
|
||||
<div key={item.label}>
|
||||
<dt>{item.label}</dt>
|
||||
<dd>{item.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
{hasSomeAccess("view-realm", "manage-realm") && (
|
||||
<Link
|
||||
className="admin-dashboard-text-link"
|
||||
to={`/${encodedRealm}/realm-settings`}
|
||||
>
|
||||
{t("adminWorkspaceReviewSettings")} <span>→</span>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
<section className="admin-dashboard-guide">
|
||||
<span>{t("adminWorkspaceHelpEyebrow")}</span>
|
||||
<h2>{t("adminWorkspaceHelpTitle")}</h2>
|
||||
<p>{t("adminWorkspaceHelpDescription")}</p>
|
||||
<a href={helpUrls.documentation} target="_blank" rel="noreferrer">
|
||||
{t("viewDocumentation")} <span>↗</span>
|
||||
</a>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type FeatureItemProps = { feature: FeatureRepresentation };
|
||||
|
||||
const FeatureItem = ({ feature }: FeatureItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const color =
|
||||
feature.type === FeatureType.Default ||
|
||||
feature.type === FeatureType.DisabledByDefault
|
||||
? "green"
|
||||
: feature.type === FeatureType.Preview ||
|
||||
feature.type === FeatureType.PreviewDisabledByDefault
|
||||
? "blue"
|
||||
: feature.type === FeatureType.Experimental
|
||||
? "orange"
|
||||
: "grey";
|
||||
return (
|
||||
<ListItem className="pf-v5-u-mb-sm">
|
||||
{feature.name}
|
||||
<Label color={color}>{t(feature.type.toLowerCase())}</Label>
|
||||
{feature.deprecated && feature.type !== FeatureType.Deprecated && (
|
||||
<>
|
||||
<Label color="grey">{t("deprecated")}</Label>
|
||||
</>
|
||||
)}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerInformation = () => {
|
||||
const { t } = useTranslation();
|
||||
const serverInfo = useServerInfo();
|
||||
const localeSort = useLocaleSort();
|
||||
const sortedFeatures = useMemo(
|
||||
() => localeSort(serverInfo.features ?? [], mapByKey("name")),
|
||||
[serverInfo.features]
|
||||
);
|
||||
const enabledFeatures = sortedFeatures.filter(feature => feature.enabled);
|
||||
const disabledFeatures = sortedFeatures.filter(feature => !feature.enabled);
|
||||
|
||||
if (Object.keys(serverInfo).length === 0) {
|
||||
return <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection variant="light" className="admin-dashboard-technical">
|
||||
<Grid hasGutter>
|
||||
<GridItem lg={4} sm={12}>
|
||||
<Card className="keycloak__dashboard_card">
|
||||
<CardTitle>{t("serverInfo")}</CardTitle>
|
||||
<CardBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("version")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{serverInfo.systemInfo?.version}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("processorCount")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{serverInfo.cpuInfo?.processorCount}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("usedMemory")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{serverInfo.memoryInfo?.usedFormated} /{" "}
|
||||
{serverInfo.memoryInfo?.totalFormated}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</GridItem>
|
||||
<GridItem lg={8} sm={12}>
|
||||
<Card className="keycloak__dashboard_card">
|
||||
<CardTitle>{t("profile")}</CardTitle>
|
||||
<CardBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("enabledFeatures")}{" "}
|
||||
<HelpItem
|
||||
fieldLabelId="enabledFeatures"
|
||||
helpText={t("infoEnabledFeatures")}
|
||||
/>
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<List variant={ListVariant.inline}>
|
||||
{enabledFeatures.map(feature => (
|
||||
<FeatureItem
|
||||
key={feature.name}
|
||||
feature={feature}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("disabledFeatures")}{" "}
|
||||
<HelpItem
|
||||
fieldLabelId="disabledFeatures"
|
||||
helpText={t("infoDisabledFeatures")}
|
||||
/>
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<List variant={ListVariant.inline}>
|
||||
{disabledFeatures.map(feature => (
|
||||
<FeatureItem
|
||||
key={feature.name}
|
||||
feature={feature}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
||||
const MasterDashboard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const useTab = (tab: DashboardTab) => useRoutableTab(toDashboard({ realm, tab }));
|
||||
const welcomeTab = useTab("welcome");
|
||||
const infoTab = useTab("info");
|
||||
const providersTab = useTab("providers");
|
||||
|
||||
return (
|
||||
<RoutableTabs
|
||||
data-testid="dashboard-tabs"
|
||||
defaultLocation={toDashboard({ realm, tab: "welcome" })}
|
||||
isBox
|
||||
mountOnEnter
|
||||
className="admin-dashboard-tabs"
|
||||
>
|
||||
<Tab
|
||||
id="welcome"
|
||||
data-testid="welcomeTab"
|
||||
title={<TabTitleText>{t("adminWorkspaceOverview")}</TabTitleText>}
|
||||
{...welcomeTab}
|
||||
>
|
||||
<OperationsOverview />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="info"
|
||||
data-testid="infoTab"
|
||||
title={<TabTitleText>{t("serverInfo")}</TabTitleText>}
|
||||
{...infoTab}
|
||||
>
|
||||
<ServerInformation />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="providers"
|
||||
data-testid="providersTab"
|
||||
title={<TabTitleText>{t("providerInfo")}</TabTitleText>}
|
||||
{...providersTab}
|
||||
>
|
||||
<ProviderInfo />
|
||||
</Tab>
|
||||
</RoutableTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default function DashboardSection() {
|
||||
const { realm } = useRealm();
|
||||
const { environment } = useEnvironment<Environment>();
|
||||
return realm === environment.masterRealm ? (
|
||||
<MasterDashboard />
|
||||
) : (
|
||||
<OperationsOverview />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
.admin-dashboard-overview {
|
||||
padding: clamp(1.25rem, 3vw, 3rem) clamp(1.25rem, 3.2vw, 3.75rem) 5rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-overview > * {
|
||||
width: 100%;
|
||||
max-width: 1380px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(220px, 0.55fr);
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
min-height: 260px;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 20px;
|
||||
color: var(--console-ink);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--console-surface) 0%,
|
||||
color-mix(in srgb, var(--console-primary) 8%, var(--console-surface)) 58%,
|
||||
color-mix(in srgb, var(--console-teal) 12%, var(--console-surface)) 100%
|
||||
);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(var(--console-line) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--console-line) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
content: "";
|
||||
mask-image: linear-gradient(90deg, transparent, #000 65%);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero::after {
|
||||
position: absolute;
|
||||
inset: auto 0 0;
|
||||
height: 5px;
|
||||
background: linear-gradient(90deg, var(--console-primary), var(--console-teal));
|
||||
content: "";
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: clamp(1.7rem, 4vw, 3.25rem);
|
||||
}
|
||||
|
||||
.admin-dashboard-eyebrow,
|
||||
.admin-dashboard-section-header span,
|
||||
.admin-dashboard-posture header span,
|
||||
.admin-dashboard-guide > span {
|
||||
display: block;
|
||||
margin-bottom: 0.55rem;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-dashboard-eyebrow {
|
||||
color: var(--console-teal);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero h1 {
|
||||
margin: 0;
|
||||
color: var(--console-ink);
|
||||
font-size: clamp(2.4rem, 5vw, 4.8rem);
|
||||
font-weight: 740;
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__copy > p {
|
||||
max-width: 60ch;
|
||||
margin: 1.25rem 0 1.6rem;
|
||||
color: var(--console-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.65rem 0.9rem;
|
||||
color: var(--console-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__meta strong {
|
||||
color: var(--console-ink);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero .admin-dashboard-status {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: end;
|
||||
padding: clamp(1.5rem, 3vw, 2.5rem);
|
||||
border-left: 1px solid var(--console-line);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal > span {
|
||||
color: var(--console-faint);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal i {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 0.75rem 0 1rem;
|
||||
background: linear-gradient(90deg, transparent, var(--console-line-strong));
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal strong {
|
||||
color: var(--console-primary-deep);
|
||||
font-size: clamp(3.5rem, 7vw, 6.5rem);
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.08em;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal small {
|
||||
margin-top: 0.75rem;
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.admin-dashboard-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 0.34fr);
|
||||
align-items: start;
|
||||
gap: clamp(1.25rem, 2.5vw, 2.25rem);
|
||||
margin-top: clamp(1.5rem, 3vw, 2.5rem);
|
||||
}
|
||||
|
||||
.admin-dashboard-primary,
|
||||
.admin-dashboard-posture,
|
||||
.admin-dashboard-guide {
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.admin-dashboard-primary {
|
||||
padding: clamp(1.25rem, 2.5vw, 2rem);
|
||||
}
|
||||
|
||||
.admin-dashboard-section-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 0.55fr);
|
||||
align-items: end;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.admin-dashboard-section-header h2,
|
||||
.admin-dashboard-posture h2,
|
||||
.admin-dashboard-guide h2 {
|
||||
margin: 0;
|
||||
color: var(--console-ink);
|
||||
font-size: 1.35rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.admin-dashboard-section-header p,
|
||||
.admin-dashboard-guide p {
|
||||
margin: 0;
|
||||
color: var(--console-muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card {
|
||||
display: grid;
|
||||
grid-template-columns: 3.1rem minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
min-height: 104px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface-soft);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
box-shadow 150ms ease,
|
||||
transform 150ms ease;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card:hover {
|
||||
border-color: color-mix(in srgb, var(--card-tone) 45%, var(--console-line));
|
||||
color: var(--console-ink);
|
||||
box-shadow: 0 12px 28px rgba(23, 50, 77, 0.09);
|
||||
text-decoration: none;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card.is-blue {
|
||||
--card-tone: var(--console-primary);
|
||||
}
|
||||
.admin-dashboard-link-card.is-green {
|
||||
--card-tone: var(--console-teal);
|
||||
}
|
||||
.admin-dashboard-link-card.is-amber {
|
||||
--card-tone: var(--console-amber);
|
||||
}
|
||||
.admin-dashboard-link-card.is-coral {
|
||||
--card-tone: var(--console-coral);
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card__code {
|
||||
display: grid;
|
||||
width: 3.1rem;
|
||||
height: 3.1rem;
|
||||
border-radius: 11px;
|
||||
color: color-mix(in srgb, var(--card-tone) 86%, var(--console-ink));
|
||||
background: color-mix(in srgb, var(--card-tone) 13%, var(--console-surface));
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card__copy {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card__copy strong {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card__copy small {
|
||||
color: var(--console-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card__arrow {
|
||||
color: var(--card-tone);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-aside {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-posture,
|
||||
.admin-dashboard-guide {
|
||||
padding: 1.35rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-posture dl {
|
||||
margin: 1.1rem 0 0;
|
||||
}
|
||||
|
||||
.admin-dashboard-posture dl > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.85rem 0;
|
||||
border-top: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.admin-dashboard-posture dt {
|
||||
color: var(--console-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-posture dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-dashboard-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.42rem;
|
||||
color: var(--console-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-dashboard-status i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--console-coral);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-coral) 12%, transparent);
|
||||
}
|
||||
|
||||
.admin-dashboard-status.is-positive i {
|
||||
background: var(--console-teal);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-teal) 12%, transparent);
|
||||
}
|
||||
|
||||
.admin-dashboard-text-link,
|
||||
.admin-dashboard-guide a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--console-line);
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin-dashboard-guide {
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, var(--console-primary) 8%, var(--console-surface)),
|
||||
var(--console-surface)
|
||||
);
|
||||
}
|
||||
|
||||
.admin-dashboard-guide p {
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-tabs > .pf-v5-c-tabs {
|
||||
padding-inline: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.admin-dashboard-technical {
|
||||
padding: clamp(1.25rem, 3vw, 3rem) clamp(1.25rem, 3.2vw, 3.75rem) 5rem;
|
||||
}
|
||||
|
||||
.keycloak__dashboard_card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero {
|
||||
color: #edf6fb;
|
||||
background: linear-gradient(135deg, #112d43, #173f5e 58%, #1c625e);
|
||||
box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero::before {
|
||||
background-image: linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-eyebrow {
|
||||
color: #83dcc5;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero h1,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__meta strong,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__signal strong {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__copy > p,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero .admin-dashboard-status {
|
||||
color: rgba(235, 246, 255, 0.8);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__meta,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__signal small {
|
||||
color: rgba(235, 246, 255, 0.64);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__signal {
|
||||
border-left-color: rgba(255, 255, 255, 0.13);
|
||||
}
|
||||
|
||||
@media (max-width: 1050px) {
|
||||
.admin-dashboard-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-dashboard-aside {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-dashboard-overview {
|
||||
padding: 1rem 1rem 3.5rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero h1 {
|
||||
font-size: clamp(2.25rem, 12vw, 3.5rem);
|
||||
}
|
||||
|
||||
.admin-dashboard-section-header,
|
||||
.admin-dashboard-link-grid,
|
||||
.admin-dashboard-aside {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-dashboard-link-card {
|
||||
min-height: 92px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
adminWorkspaceOverview=Realm overview
|
||||
adminWorkspaceRealmEyebrow=Realm workspace
|
||||
adminWorkspaceConsoleLabel=Identity operations
|
||||
adminWorkspaceSectionEyebrow=Administration workspace
|
||||
adminWorkspaceOverviewDescription=Manage identities, applications, access and security posture from one operational workspace.
|
||||
adminWorkspaceAvailableAreas=available areas
|
||||
adminWorkspaceOperateEyebrow=Operations
|
||||
adminWorkspaceManageTitle=Manage this realm
|
||||
adminWorkspaceManageDescription=Open the area that matches the task you need to complete. Only areas available to your role are shown.
|
||||
adminWorkspaceUsersDescription=Find people, maintain profiles and manage credentials.
|
||||
adminWorkspaceClientsDescription=Configure applications, protocols and client access.
|
||||
adminWorkspaceGroupsDescription=Organize people and apply shared access rules.
|
||||
adminWorkspaceRealmSettingsDescription=Control realm behavior, themes, tokens and policies.
|
||||
adminWorkspaceAuthenticationDescription=Design sign-in flows and required actions.
|
||||
adminWorkspaceEventsDescription=Review user and administrator activity.
|
||||
adminWorkspacePostureEyebrow=Current state
|
||||
adminWorkspacePostureTitle=Security posture
|
||||
adminWorkspaceRealmStatus=Realm status
|
||||
adminWorkspaceReviewSettings=Review realm settings
|
||||
adminWorkspaceHelpEyebrow=Reference
|
||||
adminWorkspaceHelpTitle=Need implementation details?
|
||||
adminWorkspaceHelpDescription=Use the official documentation when changing protocols, authentication flows or production security settings.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
adminWorkspaceOverview=领域总览
|
||||
adminWorkspaceRealmEyebrow=领域工作区
|
||||
adminWorkspaceConsoleLabel=身份运营
|
||||
adminWorkspaceSectionEyebrow=管理工作区
|
||||
adminWorkspaceOverviewDescription=在一个工作区中管理身份、应用、访问权限与安全配置。
|
||||
adminWorkspaceAvailableAreas=个可用业务区
|
||||
adminWorkspaceOperateEyebrow=业务操作
|
||||
adminWorkspaceManageTitle=管理当前领域
|
||||
adminWorkspaceManageDescription=从当前任务出发进入对应业务区,仅显示你有权限操作的功能。
|
||||
adminWorkspaceUsersDescription=查找人员、维护资料并管理登录凭据。
|
||||
adminWorkspaceClientsDescription=配置应用、协议以及客户端访问规则。
|
||||
adminWorkspaceGroupsDescription=组织人员并应用统一的访问权限。
|
||||
adminWorkspaceRealmSettingsDescription=管理领域行为、主题、令牌和安全策略。
|
||||
adminWorkspaceAuthenticationDescription=设计登录流程和必要操作。
|
||||
adminWorkspaceEventsDescription=查看用户及管理员的操作记录。
|
||||
adminWorkspacePostureEyebrow=当前状态
|
||||
adminWorkspacePostureTitle=安全配置概览
|
||||
adminWorkspaceRealmStatus=领域状态
|
||||
adminWorkspaceReviewSettings=检查领域设置
|
||||
adminWorkspaceHelpEyebrow=使用参考
|
||||
adminWorkspaceHelpTitle=需要配置说明?
|
||||
adminWorkspaceHelpDescription=修改协议、认证流程或生产安全设置时,可查阅官方文档。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
adminWorkspaceOverview=領域總覽
|
||||
adminWorkspaceRealmEyebrow=領域工作區
|
||||
adminWorkspaceConsoleLabel=身分營運
|
||||
adminWorkspaceSectionEyebrow=管理工作區
|
||||
adminWorkspaceOverviewDescription=在同一個工作區中管理身分、應用程式、存取權限與安全設定。
|
||||
adminWorkspaceAvailableAreas=個可用業務區
|
||||
adminWorkspaceOperateEyebrow=業務操作
|
||||
adminWorkspaceManageTitle=管理目前領域
|
||||
adminWorkspaceManageDescription=從目前任務進入對應業務區,僅顯示你有權限操作的功能。
|
||||
adminWorkspaceUsersDescription=尋找人員、維護資料並管理登入憑證。
|
||||
adminWorkspaceClientsDescription=設定應用程式、協定與用戶端存取規則。
|
||||
adminWorkspaceGroupsDescription=組織人員並套用統一的存取權限。
|
||||
adminWorkspaceRealmSettingsDescription=管理領域行為、主題、權杖與安全政策。
|
||||
adminWorkspaceAuthenticationDescription=設計登入流程與必要操作。
|
||||
adminWorkspaceEventsDescription=查看使用者及管理員的操作記錄。
|
||||
adminWorkspacePostureEyebrow=目前狀態
|
||||
adminWorkspacePostureTitle=安全設定概覽
|
||||
adminWorkspaceRealmStatus=領域狀態
|
||||
adminWorkspaceReviewSettings=檢查領域設定
|
||||
adminWorkspaceHelpEyebrow=使用參考
|
||||
adminWorkspaceHelpTitle=需要設定說明?
|
||||
adminWorkspaceHelpDescription=修改協定、驗證流程或正式環境安全設定時,可查閱官方文件。
|
||||
+11
-217
@@ -1,232 +1,26 @@
|
||||
:root,
|
||||
.pf-v5-theme-dark {
|
||||
--nexus-admin-void: #070b16;
|
||||
--nexus-admin-chrome: #0b1120;
|
||||
--nexus-admin-panel: #ffffff;
|
||||
--nexus-admin-canvas: #f3f6fb;
|
||||
--nexus-admin-ice: #2dc7e5;
|
||||
--nexus-admin-violet: #7569f2;
|
||||
--nexus-admin-text: #172033;
|
||||
--nexus-admin-muted: #63708a;
|
||||
--nexus-admin-line: #dce3ef;
|
||||
--pf-v5-global--primary-color--100: #007f9b;
|
||||
--pf-v5-global--primary-color--200: #00677e;
|
||||
--pf-v5-global--link--Color: #00758d;
|
||||
--pf-v5-global--link--Color--hover: #004f62;
|
||||
--pf-v5-global--BackgroundColor--100: var(--nexus-admin-panel);
|
||||
--pf-v5-global--BackgroundColor--150: #f7f9fc;
|
||||
--pf-v5-global--BackgroundColor--200: var(--nexus-admin-canvas);
|
||||
--pf-v5-global--Color--100: var(--nexus-admin-text);
|
||||
--pf-v5-global--Color--200: var(--nexus-admin-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--nexus-admin-line);
|
||||
--pf-v5-global--BorderColor--200: #c9d3e2;
|
||||
--pf-v5-c-page__main-section--m-light--BackgroundColor: var(--nexus-admin-canvas);
|
||||
color-scheme: light;
|
||||
}
|
||||
/*
|
||||
* Admin compatibility layer.
|
||||
* Visual tokens and shared PatternFly composition live in ../console-ui.css.
|
||||
* Keep this file intentionally small so theme-specific legacy rules cannot
|
||||
* override the light/dark workspace palette.
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--nexus-admin-text);
|
||||
background: var(--nexus-admin-canvas);
|
||||
font-family: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-page,
|
||||
.pf-v5-c-page__main,
|
||||
.pf-v5-c-page__main-section {
|
||||
background-color: var(--nexus-admin-canvas);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main {
|
||||
background-image: linear-gradient(rgba(32, 62, 104, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(32, 62, 104, 0.025) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead {
|
||||
border-bottom: 1px solid rgba(99, 230, 255, 0.18);
|
||||
background: linear-gradient(90deg, rgba(99, 230, 255, 0.035), transparent 36%),
|
||||
var(--nexus-admin-void);
|
||||
box-shadow: 0 8px 28px rgba(5, 9, 21, 0.18);
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 190px;
|
||||
height: 38px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar,
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
border-right: 1px solid rgba(117, 105, 242, 0.16);
|
||||
color: #d8e2f5;
|
||||
background: radial-gradient(
|
||||
circle at 20% 88%,
|
||||
rgba(99, 230, 255, 0.07),
|
||||
transparent 14rem
|
||||
),
|
||||
var(--nexus-admin-chrome);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link {
|
||||
border-radius: 2px;
|
||||
color: #9daac3;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover,
|
||||
.pf-v5-c-nav__link:focus {
|
||||
color: #ffffff;
|
||||
background: rgba(99, 230, 255, 0.06);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current,
|
||||
.pf-v5-c-nav__item.pf-m-current > .pf-v5-c-nav__link {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(99, 230, 255, 0.15),
|
||||
rgba(117, 105, 242, 0.08)
|
||||
);
|
||||
box-shadow: inset 2px 0 var(--nexus-admin-ice);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current::after {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-drawer__panel,
|
||||
.pf-v5-c-popover__content,
|
||||
.pf-v5-c-menu {
|
||||
border: 1px solid var(--nexus-admin-line);
|
||||
border-radius: 3px;
|
||||
background: var(--nexus-admin-panel);
|
||||
box-shadow: 0 15px 42px rgba(27, 43, 72, 0.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-title,
|
||||
.pf-v5-c-content h1,
|
||||
.pf-v5-c-content h2,
|
||||
.pf-v5-c-page__main-title {
|
||||
color: var(--nexus-admin-text);
|
||||
font-family: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
|
||||
letter-spacing: -0.018em;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
border-bottom: 1px solid rgba(220, 227, 239, 0.74);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.pf-v5-c-button {
|
||||
border-radius: 2px;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(105deg, #007f9b, #087f91 58%, #655bd7);
|
||||
box-shadow: 0 6px 18px rgba(0, 127, 155, 0.16);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control,
|
||||
.pf-v5-c-select__toggle,
|
||||
.pf-v5-c-menu-toggle {
|
||||
border-radius: 2px;
|
||||
border-color: #c9d3e2;
|
||||
background: #ffffff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:focus-within,
|
||||
.pf-v5-c-menu-toggle:focus {
|
||||
border-color: #008eaa;
|
||||
box-shadow: 0 0 0 3px rgba(45, 199, 229, 0.12);
|
||||
}
|
||||
|
||||
.pf-v5-c-table {
|
||||
border: 1px solid var(--nexus-admin-line);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pf-v5-c-table thead {
|
||||
color: #526079;
|
||||
background: #edf2f8;
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.pf-v5-c-table tr:hover > * {
|
||||
background-color: #f5fbfd;
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link::after {
|
||||
border-color: var(--nexus-admin-ice);
|
||||
}
|
||||
|
||||
.pf-v5-c-label {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.keycloak__form {
|
||||
max-width: 1100px;
|
||||
padding-bottom: var(--pf-v5-global--spacer--4xl);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: baseline;
|
||||
accent-color: #0087a1;
|
||||
}
|
||||
|
||||
.keycloak__form_actions {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
margin-left: calc(-1 * var(--pf-v5-global--spacer--lg));
|
||||
padding: var(--pf-v5-global--spacer--md);
|
||||
border-top: 1px solid var(--nexus-admin-line);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 -10px 30px rgba(27, 43, 72, 0.06);
|
||||
backdrop-filter: blur(14px);
|
||||
#root {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.keycloak__section_intro__help {
|
||||
width: fit-content;
|
||||
padding: var(--pf-v5-global--spacer--md) 0 var(--pf-v5-global--spacer--lg);
|
||||
color: var(--pf-v5-global--primary-color--100);
|
||||
}
|
||||
|
||||
div.w-tc-editor {
|
||||
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--nexus-admin-ice) !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
color: var(--console-primary-deep);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
.keycloak__page_nav__nav {
|
||||
--pf-v5-c-page__sidebar--Transition: all 120ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||
--pf-v5-c-page__sidebar--BackgroundColor: var(--console-nav);
|
||||
overflow: visible !important;
|
||||
color: var(--console-ink) !important;
|
||||
background: var(--console-nav) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-page__sidebar-body {
|
||||
overflow-y: visible !important;
|
||||
color: var(--console-ink) !important;
|
||||
background: var(--console-nav) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav {
|
||||
--pf-v5-c-nav__link--Color: var(--console-muted);
|
||||
--pf-v5-c-nav__link--hover--Color: var(--console-ink);
|
||||
--pf-v5-c-nav__link--focus--Color: var(--console-ink);
|
||||
--pf-v5-c-nav__link--hover--BackgroundColor: color-mix(
|
||||
in srgb,
|
||||
var(--console-surface) 82%,
|
||||
transparent
|
||||
);
|
||||
--pf-v5-c-nav__link--focus--BackgroundColor: color-mix(
|
||||
in srgb,
|
||||
var(--console-surface) 82%,
|
||||
transparent
|
||||
);
|
||||
--pf-v5-c-nav__link--m-current--Color: var(--console-primary-deep);
|
||||
--pf-v5-c-nav__link--m-current--BackgroundColor: color-mix(
|
||||
in srgb,
|
||||
var(--console-primary) 9%,
|
||||
var(--console-surface)
|
||||
);
|
||||
color: var(--console-ink);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link,
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link.pf-m-expandable {
|
||||
color: var(--console-muted) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link:hover,
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link:focus {
|
||||
color: var(--console-ink) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link.pf-m-current,
|
||||
.keycloak__page_nav__nav
|
||||
.pf-v5-c-nav__item:not(.pf-m-expandable).pf-m-current
|
||||
> .pf-v5-c-nav__link {
|
||||
color: var(--console-primary-deep) !important;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--console-primary) 9%,
|
||||
var(--console-surface)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.admin-nav-context {
|
||||
display: grid;
|
||||
gap: 0.32rem;
|
||||
margin: 0 0.35rem 1rem;
|
||||
padding: 0.2rem 0.45rem 1.15rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.admin-nav-group > .pf-v5-c-nav__link.pf-m-expandable {
|
||||
min-height: 40px;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--console-ink);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-nav-group .pf-v5-c-nav__subnav {
|
||||
margin-top: 0.25rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.admin-nav-context__eyebrow {
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.11em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-nav-context strong {
|
||||
overflow: hidden;
|
||||
color: var(--console-ink);
|
||||
font-size: 1.08rem;
|
||||
font-weight: 720;
|
||||
letter-spacing: -0.015em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.admin-nav-context__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--console-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.admin-nav-status {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--console-teal);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-teal) 12%, transparent);
|
||||
}
|
||||
|
||||
.admin-nav-status.is-disabled {
|
||||
background: var(--console-coral);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-coral) 12%, transparent);
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link {
|
||||
display: grid;
|
||||
grid-template-columns: 2.25rem minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.68rem;
|
||||
}
|
||||
|
||||
.admin-nav-code {
|
||||
display: grid;
|
||||
width: 2.25rem;
|
||||
height: 1.75rem;
|
||||
border: 1px solid color-mix(in srgb, var(--console-line-strong) 62%, transparent);
|
||||
border-radius: 7px;
|
||||
color: var(--console-faint);
|
||||
background: color-mix(in srgb, var(--console-surface) 58%, transparent);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 0.53rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.025em;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover .admin-nav-code,
|
||||
.pf-v5-c-nav__link:focus .admin-nav-code,
|
||||
.pf-v5-c-nav__link.pf-m-current .admin-nav-code {
|
||||
border-color: color-mix(in srgb, var(--console-primary) 30%, var(--console-line));
|
||||
color: var(--console-primary-deep);
|
||||
background: color-mix(in srgb, var(--console-primary) 8%, var(--console-surface));
|
||||
}
|
||||
|
||||
.admin-nav-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin: 1.4rem 0.4rem 0;
|
||||
padding: 1rem 0.4rem 0;
|
||||
border-top: 1px solid var(--console-line);
|
||||
color: var(--console-faint);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.keycloak__page_nav__nav,
|
||||
.keycloak__page_nav__nav .pf-v5-c-page__sidebar-body {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
.admin-workspace-route {
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.admin-workspace-route__content {
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.admin-view-header {
|
||||
position: relative;
|
||||
padding-top: clamp(1.7rem, 3vw, 2.7rem);
|
||||
padding-bottom: clamp(1.25rem, 2.5vw, 2rem);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.admin-view-header::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
width: 3px;
|
||||
border-radius: 99px;
|
||||
background: var(--console-primary);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.admin-view-header__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 1.5rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.admin-view-header__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-view-header__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 0.55rem;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.67rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-view-header__title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.65rem 0.9rem;
|
||||
}
|
||||
|
||||
.admin-view-header h1 {
|
||||
margin: 0;
|
||||
color: var(--console-ink);
|
||||
font-size: clamp(2rem, 3.2vw, 3rem);
|
||||
font-weight: 730;
|
||||
letter-spacing: -0.045em;
|
||||
line-height: 1.04;
|
||||
}
|
||||
|
||||
.admin-view-header__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.admin-view-header__description {
|
||||
max-width: 76ch;
|
||||
margin: 0.8rem 0 0;
|
||||
color: var(--console-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.admin-view-header__actions,
|
||||
.admin-view-header__switch,
|
||||
.admin-view-header__lower-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-view-header__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.admin-view-header__lower-actions {
|
||||
margin-top: 1.25rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.admin-view-header__divider {
|
||||
border-color: var(--console-line);
|
||||
}
|
||||
|
||||
/* List routes become a clear content deck below the shared business header. */
|
||||
.admin-workspace-route > .pf-v5-c-page__main-section:not(.admin-view-header),
|
||||
.admin-workspace-route > .pf-v5-c-tabs,
|
||||
.admin-workspace-route > form,
|
||||
.admin-workspace-route > .keycloak__form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-toolbar + .pf-v5-c-table,
|
||||
.admin-workspace-route .pf-v5-c-toolbar + .pf-v5-c-data-list {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-toolbar {
|
||||
padding: 0.35rem 0;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-table {
|
||||
--pf-v5-c-table--cell--PaddingTop: 0.9rem;
|
||||
--pf-v5-c-table--cell--PaddingBottom: 0.9rem;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-form__section-title {
|
||||
margin-top: 1.7rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
color: var(--console-ink);
|
||||
font-size: 1.12rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Every administration route follows the same operational content grammar. */
|
||||
.admin-workspace-route .pf-v5-c-page__main-section:not(.admin-view-header) {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-page__main-section:not(.admin-view-header) > form,
|
||||
.admin-workspace-route
|
||||
.pf-v5-c-page__main-section:not(.admin-view-header)
|
||||
> .pf-v5-c-form,
|
||||
.admin-workspace-route
|
||||
.pf-v5-c-page__main-section:not(.admin-view-header)
|
||||
> .keycloak__form {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-card + .pf-v5-c-card,
|
||||
.admin-workspace-route .pf-v5-c-panel + .pf-v5-c-panel,
|
||||
.admin-workspace-route .pf-v5-c-alert + .pf-v5-c-card {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-description-list {
|
||||
padding: 1.15rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-description-list__term {
|
||||
color: var(--console-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-description-list__description {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-form__actions,
|
||||
.admin-workspace-route .keycloak__form_actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 6;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--console-surface) 92%, transparent);
|
||||
box-shadow: 0 -10px 28px color-mix(in srgb, var(--console-ink) 8%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-wizard {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-wizard__nav {
|
||||
border-right-color: var(--console-line);
|
||||
background: var(--console-surface-soft);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-drawer__panel {
|
||||
background: var(--console-surface-raised);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-empty-state {
|
||||
min-height: 260px;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-view-header::before {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.admin-view-header__layout {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.admin-view-header__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-view-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-form__actions,
|
||||
.admin-workspace-route .keycloak__form_actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
/*
|
||||
* Shared account/admin workspace language.
|
||||
* This layer intentionally comes after the owned Keycloak styles so the two
|
||||
* consoles feel like one product without coupling their generated sources.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--console-ink: #1d2a33;
|
||||
--console-muted: #64727c;
|
||||
--console-faint: #87939b;
|
||||
--console-canvas: #f4f6f7;
|
||||
--console-surface: #ffffff;
|
||||
--console-surface-soft: #f8f9f9;
|
||||
--console-surface-raised: #ffffff;
|
||||
--console-surface-sunken: #edf0f1;
|
||||
--console-nav: #eef1f2;
|
||||
--console-line: #d9dfe2;
|
||||
--console-line-strong: #bac5ca;
|
||||
--console-primary: #34747a;
|
||||
--console-primary-deep: #245b61;
|
||||
--console-teal: #347f72;
|
||||
--console-amber: #d99b25;
|
||||
--console-coral: #dd5c4b;
|
||||
--console-danger: #c94046;
|
||||
--console-radius-sm: 8px;
|
||||
--console-radius: 14px;
|
||||
--console-shadow: 0 1px 2px rgba(23, 50, 77, 0.04),
|
||||
0 14px 38px rgba(23, 50, 77, 0.075);
|
||||
--pf-v5-global--primary-color--100: var(--console-primary);
|
||||
--pf-v5-global--primary-color--200: var(--console-primary-deep);
|
||||
--pf-v5-global--link--Color: var(--console-primary-deep);
|
||||
--pf-v5-global--link--Color--hover: #173f88;
|
||||
--pf-v5-global--BackgroundColor--100: var(--console-surface);
|
||||
--pf-v5-global--BackgroundColor--150: var(--console-surface-soft);
|
||||
--pf-v5-global--BackgroundColor--200: var(--console-canvas);
|
||||
--pf-v5-global--Color--100: var(--console-ink);
|
||||
--pf-v5-global--Color--200: var(--console-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--console-line);
|
||||
--pf-v5-global--BorderColor--200: var(--console-line-strong);
|
||||
--pf-v5-c-masthead--BackgroundColor: var(--console-surface);
|
||||
--pf-v5-c-masthead--c-toolbar--BackgroundColor: var(--console-surface);
|
||||
--pf-v5-c-page__sidebar--BackgroundColor: var(--console-nav);
|
||||
--pf-v5-c-page__main-section--m-light--BackgroundColor: transparent;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark {
|
||||
--console-ink: #eaf2f8;
|
||||
--console-muted: #a4b5c4;
|
||||
--console-faint: #8295a7;
|
||||
--console-canvas: #10171c;
|
||||
--console-surface: #182229;
|
||||
--console-surface-soft: #1d2930;
|
||||
--console-surface-raised: #202c34;
|
||||
--console-surface-sunken: #121b21;
|
||||
--console-nav: #141e24;
|
||||
--console-line: #304048;
|
||||
--console-line-strong: #52636b;
|
||||
--console-primary: #75adb0;
|
||||
--console-primary-deep: #93c2c4;
|
||||
--console-teal: #6aafa0;
|
||||
--console-amber: #f2c45c;
|
||||
--console-coral: #ff8173;
|
||||
--console-danger: #ff858b;
|
||||
--console-shadow: 0 18px 42px rgba(0, 0, 0, 0.26);
|
||||
--pf-v5-global--primary-color--100: var(--console-primary);
|
||||
--pf-v5-global--primary-color--200: var(--console-primary-deep);
|
||||
--pf-v5-global--link--Color: #9dbfff;
|
||||
--pf-v5-global--link--Color--hover: #c3d7ff;
|
||||
--pf-v5-global--BackgroundColor--100: var(--console-surface);
|
||||
--pf-v5-global--BackgroundColor--150: var(--console-surface-soft);
|
||||
--pf-v5-global--BackgroundColor--200: var(--console-canvas);
|
||||
--pf-v5-global--Color--100: var(--console-ink);
|
||||
--pf-v5-global--Color--200: var(--console-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--console-line);
|
||||
--pf-v5-global--BorderColor--200: var(--console-line-strong);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] {
|
||||
--console-ink: #202c33;
|
||||
--console-muted: #66757c;
|
||||
--console-canvas: #f6f7f7;
|
||||
--console-nav: #eff2f2;
|
||||
--console-surface-raised: #ffffff;
|
||||
--console-surface-sunken: #ecefef;
|
||||
--console-line: #d9dfe0;
|
||||
--console-line-strong: #b7c3c5;
|
||||
--console-primary: #3a7277;
|
||||
--console-primary-deep: #285b60;
|
||||
--console-teal: #3b7f70;
|
||||
--console-amber: #f0b92e;
|
||||
--console-coral: #ef6654;
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"].pf-v5-theme-dark {
|
||||
--console-ink: #e7f1f9;
|
||||
--console-muted: #9ab0c2;
|
||||
--console-canvas: #11181d;
|
||||
--console-surface: #19242a;
|
||||
--console-surface-soft: #1e2b31;
|
||||
--console-nav: #151f24;
|
||||
--console-surface-raised: #222f35;
|
||||
--console-surface-sunken: #121b20;
|
||||
--console-line: #314147;
|
||||
--console-line-strong: #53656c;
|
||||
--console-primary: #78aeb0;
|
||||
--console-primary-deep: #97c4c5;
|
||||
--console-teal: #70ad9f;
|
||||
--console-amber: #f2c45c;
|
||||
--console-coral: #ff8173;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
.pf-v5-c-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--console-ink);
|
||||
background: var(--console-canvas);
|
||||
font-family: "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
html[data-console-color-mode] body,
|
||||
html[data-console-color-mode] .pf-v5-c-page,
|
||||
html[data-console-color-mode] .pf-v5-c-page__sidebar,
|
||||
html[data-console-color-mode] .pf-v5-c-masthead {
|
||||
transition:
|
||||
color 180ms ease,
|
||||
background-color 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.pf-v5-c-page,
|
||||
.pf-v5-c-page__main,
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-section.pf-m-light {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main {
|
||||
min-width: 0;
|
||||
overflow-x: clip;
|
||||
background-color: var(--console-canvas);
|
||||
background-image: radial-gradient(
|
||||
circle at 92% 1%,
|
||||
color-mix(in srgb, var(--console-primary) 5%, transparent),
|
||||
transparent 30rem
|
||||
);
|
||||
}
|
||||
|
||||
/* Header: quiet, bright, and consistent across both consoles. */
|
||||
.pf-v5-c-masthead {
|
||||
--pf-v5-c-masthead--BackgroundColor: var(--console-surface);
|
||||
--pf-v5-c-masthead--c-toolbar--BackgroundColor: var(--console-surface);
|
||||
min-height: 68px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
color: var(--console-ink);
|
||||
background: color-mix(in srgb, var(--console-surface) 94%, transparent) !important;
|
||||
box-shadow: 0 6px 24px rgba(23, 50, 77, 0.055);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead,
|
||||
.pf-v5-c-masthead__main,
|
||||
.pf-v5-c-masthead__content,
|
||||
.pf-v5-c-masthead .pf-v5-c-toolbar,
|
||||
.pf-v5-c-masthead .pf-v5-c-toolbar__content {
|
||||
color: var(--console-ink) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-menu-toggle {
|
||||
--pf-v5-c-menu-toggle--Color: var(--console-ink);
|
||||
--pf-v5-c-menu-toggle--BackgroundColor: var(--console-surface-soft);
|
||||
color: var(--console-ink) !important;
|
||||
background: var(--console-surface-soft) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead::after {
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
width: min(28vw, 25rem);
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--console-primary));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 180px;
|
||||
height: 40px;
|
||||
filter: drop-shadow(0 1px 1px rgba(23, 50, 77, 0.1));
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark img.keycloak__pageheader_brand {
|
||||
padding: 0.2rem 0.45rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 10px;
|
||||
background: #f8fbfe;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-button.pf-m-link,
|
||||
.pf-v5-c-masthead .pf-v5-c-button.pf-m-plain {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-button:hover {
|
||||
color: var(--console-primary-deep);
|
||||
background: var(--console-nav);
|
||||
}
|
||||
|
||||
.console-theme-toggle.pf-v5-c-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.48rem;
|
||||
min-width: 5.15rem;
|
||||
min-height: 38px;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 999px;
|
||||
color: var(--console-muted);
|
||||
background: var(--console-surface-soft);
|
||||
}
|
||||
|
||||
.console-theme-toggle.pf-v5-c-button:hover {
|
||||
border-color: var(--console-line-strong);
|
||||
color: var(--console-primary-deep);
|
||||
background: var(--console-surface-raised);
|
||||
}
|
||||
|
||||
.console-theme-toggle__label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Navigation is now a calm secondary surface instead of a competing dark panel. */
|
||||
.pf-v5-c-page__sidebar,
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
color: var(--console-ink);
|
||||
background: var(--console-nav) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar {
|
||||
--pf-v5-c-page__sidebar--BackgroundColor: var(--console-nav);
|
||||
width: 272px;
|
||||
border-right: 1px solid var(--console-line);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
padding: 1.25rem 0.9rem 2rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar-body::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__section-title,
|
||||
.pf-v5-c-nav__link,
|
||||
.pf-v5-c-nav__group-title,
|
||||
.pf-v5-c-nav__link.pf-m-expandable {
|
||||
color: var(--console-muted) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__section-title {
|
||||
color: var(--console-ink);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__group-title {
|
||||
padding-top: 1.35rem;
|
||||
color: var(--console-faint);
|
||||
font-family: inherit;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__list {
|
||||
gap: 0.28rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link {
|
||||
min-height: 44px;
|
||||
padding: 0.72rem 0.9rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition:
|
||||
color 140ms ease,
|
||||
background-color 140ms ease,
|
||||
border-color 140ms ease;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover,
|
||||
.pf-v5-c-nav__link:focus {
|
||||
color: var(--console-ink) !important;
|
||||
border-color: color-mix(in srgb, var(--console-primary) 15%, var(--console-line));
|
||||
background: color-mix(in srgb, var(--console-surface) 82%, transparent) !important;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current,
|
||||
.pf-v5-c-nav__item:not(.pf-m-expandable).pf-m-current > .pf-v5-c-nav__link {
|
||||
color: var(--console-primary-deep) !important;
|
||||
border-color: color-mix(in srgb, var(--console-primary) 22%, var(--console-line));
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--console-primary) 9%,
|
||||
var(--console-surface)
|
||||
) !important;
|
||||
box-shadow: 0 8px 22px rgba(39, 82, 124, 0.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current::after {
|
||||
right: 0.8rem;
|
||||
width: 5px;
|
||||
height: 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--console-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
padding-inline: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section > *,
|
||||
.pf-v5-c-page__main-breadcrumb > * {
|
||||
width: 100%;
|
||||
max-width: 1380px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
background: color-mix(in srgb, var(--console-surface) 82%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section {
|
||||
padding-top: clamp(1.75rem, 3vw, 3rem);
|
||||
padding-bottom: clamp(3rem, 6vw, 5.5rem);
|
||||
}
|
||||
|
||||
.pf-v5-c-title,
|
||||
.pf-v5-c-content h1,
|
||||
.pf-v5-c-content h2,
|
||||
.pf-v5-c-page__main-title {
|
||||
color: var(--console-ink);
|
||||
font-family: "Segoe UI Variable Display", "Segoe UI", "PingFang SC", sans-serif;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
|
||||
.pf-v5-c-title.pf-m-2xl,
|
||||
.pf-v5-c-page__main-title {
|
||||
font-size: clamp(2rem, 3vw, 2.75rem);
|
||||
font-weight: 720;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.pf-v5-c-title.pf-m-2xl::before,
|
||||
.pf-v5-c-page__main-title::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-content > p,
|
||||
.pf-v5-c-text-content > p,
|
||||
.pf-v5-c-helper-text,
|
||||
.pf-v5-c-form__helper-text {
|
||||
color: var(--console-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* Every working surface shares the same edge, depth and spacing rhythm. */
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-data-list,
|
||||
.pf-v5-c-table,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-drawer__panel,
|
||||
.pf-v5-c-popover__content,
|
||||
.pf-v5-c-menu,
|
||||
.pf-v5-c-empty-state {
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.pf-v5-c-accordion,
|
||||
.pf-v5-c-panel,
|
||||
.pf-v5-c-description-list,
|
||||
.pf-v5-c-tree-view,
|
||||
.pf-v5-c-dual-list-selector,
|
||||
.pf-v5-c-wizard,
|
||||
.pf-v5-c-code-block {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.pf-v5-c-panel,
|
||||
.pf-v5-c-wizard__main,
|
||||
.pf-v5-c-drawer__content,
|
||||
.pf-v5-c-code-block {
|
||||
border-color: var(--console-line);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-table,
|
||||
.pf-v5-c-modal-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pf-v5-c-card__title {
|
||||
color: var(--console-ink);
|
||||
font-family: inherit;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.pf-v5-c-card__header,
|
||||
.pf-v5-c-modal-box__header {
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
}
|
||||
|
||||
.pf-v5-c-table thead,
|
||||
.pf-v5-c-data-list__item:first-child {
|
||||
color: var(--console-muted);
|
||||
background: var(--console-surface-soft);
|
||||
font-family: inherit;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pf-v5-c-table tr > *,
|
||||
.pf-v5-c-data-list__item {
|
||||
border-bottom-color: var(--console-line);
|
||||
}
|
||||
|
||||
.pf-v5-c-table tbody tr:hover > *,
|
||||
.pf-v5-c-data-list__item:not(:first-child):hover {
|
||||
background: color-mix(in srgb, var(--console-primary) 4%, var(--console-surface));
|
||||
}
|
||||
|
||||
.pf-v5-c-table tbody tr:nth-child(even) > * {
|
||||
background: color-mix(in srgb, var(--console-surface-soft) 55%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-toolbar__content {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__label-text {
|
||||
color: var(--console-ink);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control,
|
||||
.pf-v5-c-select__toggle,
|
||||
.pf-v5-c-menu-toggle,
|
||||
.pf-v5-c-number-input,
|
||||
.pf-v5-c-search-input {
|
||||
min-height: 42px;
|
||||
border-color: var(--console-line-strong);
|
||||
border-radius: var(--console-radius-sm);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
box-shadow: 0 1px 1px rgba(23, 50, 77, 0.025);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:hover,
|
||||
.pf-v5-c-menu-toggle:hover,
|
||||
.pf-v5-c-search-input:hover {
|
||||
border-color: color-mix(in srgb, var(--console-primary) 48%, var(--console-line));
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:focus-within,
|
||||
.pf-v5-c-menu-toggle:focus,
|
||||
.pf-v5-c-menu-toggle.pf-m-expanded,
|
||||
.pf-v5-c-search-input:focus-within {
|
||||
border-color: var(--console-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-primary) 15%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-button {
|
||||
min-height: 40px;
|
||||
border-radius: var(--console-radius-sm);
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary {
|
||||
color: #ffffff;
|
||||
background: var(--console-primary);
|
||||
box-shadow: 0 7px 18px color-mix(in srgb, var(--console-primary) 22%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary:hover {
|
||||
background: var(--console-primary-deep);
|
||||
box-shadow: 0 9px 22px color-mix(in srgb, var(--console-primary) 27%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-secondary {
|
||||
border-color: var(--console-line-strong);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-danger {
|
||||
background: var(--console-danger);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs {
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__scroll-button,
|
||||
.pf-v5-c-tabs__toggle {
|
||||
color: var(--console-muted);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link {
|
||||
color: var(--console-muted);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__item.pf-m-current .pf-v5-c-tabs__link {
|
||||
color: var(--console-primary-deep);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link::after {
|
||||
height: 3px;
|
||||
border-color: var(--console-primary);
|
||||
}
|
||||
|
||||
.pf-v5-c-label {
|
||||
border-radius: 999px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pf-v5-c-alert {
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
box-shadow: 0 8px 24px rgba(23, 50, 77, 0.06);
|
||||
}
|
||||
|
||||
.pf-v5-c-empty-state {
|
||||
border-style: dashed;
|
||||
background: color-mix(in srgb, var(--console-surface) 88%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-switch__input:checked ~ .pf-v5-c-switch__toggle,
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
accent-color: var(--console-primary);
|
||||
}
|
||||
|
||||
.keycloak__form {
|
||||
max-width: 1160px;
|
||||
}
|
||||
|
||||
.keycloak__form_actions {
|
||||
border-top-color: var(--console-line);
|
||||
background: color-mix(in srgb, var(--console-surface) 92%, transparent);
|
||||
box-shadow: 0 -10px 30px rgba(23, 50, 77, 0.055);
|
||||
}
|
||||
|
||||
.pf-v5-c-modal-box__body,
|
||||
.pf-v5-c-modal-box__footer,
|
||||
.pf-v5-c-menu__content,
|
||||
.pf-v5-c-popover__body,
|
||||
.pf-v5-c-tooltip__content {
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-menu__item {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.pf-v5-c-menu__item:hover,
|
||||
.pf-v5-c-menu__item:focus {
|
||||
background: var(--console-surface-sunken);
|
||||
}
|
||||
|
||||
div.w-tc-editor {
|
||||
border-color: var(--console-line);
|
||||
border-radius: var(--console-radius-sm);
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--console-primary) !important;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-v5-c-masthead {
|
||||
min-height: 60px;
|
||||
gap: 0.5rem;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead::after {
|
||||
width: 52vw;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar {
|
||||
width: min(88vw, 310px);
|
||||
min-width: 0;
|
||||
max-width: min(88vw, 310px);
|
||||
box-shadow: 16px 0 40px rgba(23, 50, 77, 0.16);
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 156px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.console-theme-toggle.pf-v5-c-button {
|
||||
min-width: 2.5rem;
|
||||
padding-inline: 0.65rem;
|
||||
}
|
||||
|
||||
.console-theme-toggle__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-data-list,
|
||||
.pf-v5-c-table,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-empty-state {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,8 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nexus-email-meta">
|
||||
IDENTITY ACCESS LAYER / <span class="nexus-email-online">SECURE MESSAGE</span><br>
|
||||
This message was generated automatically. Please do not reply.
|
||||
${msg("nexusIdentityAccessLayer")} / <span class="nexus-email-online">${msg("nexusSecureMessage")}</span><br>
|
||||
${msg("nexusAutomatedMessageNotice")}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# This file has been claimed for ownership from @keycloakify/email-native version 260007.0.0.
|
||||
# To relinquish ownership and restore this file to its original content, run the following command:
|
||||
#
|
||||
# $ npx keycloakify own --path "email/messages/messages_en.properties" --revert
|
||||
|
||||
# IMPORTANT: This file contains the base translation. Modifying it directly is not recommended.
|
||||
# To override or add custom messages, create a file named messages_en_override.properties in the same directory.
|
||||
# This file will be automatically loaded and merged with the base translation.
|
||||
# If you're implementing theme variants, you can also create variant-specific `.properties` files.
|
||||
# For example let's say you have defined `themeName: ["vanilla", "chocolate"]` then you can create the following files:
|
||||
# messages_en_override_vanilla.properties
|
||||
# messages_en_override_chocolate.properties
|
||||
|
||||
emailVerificationSubject=Verify email
|
||||
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {3}.\n\nIf you didn''t create this account, just ignore this message.
|
||||
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {3}.</p><p>If you didn''t create this account, just ignore this message.</p>
|
||||
orgInviteSubject=Invitation to join the {0} organization
|
||||
orgInviteBody=You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
||||
orgInviteBodyHtml=<p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
||||
orgInviteBodyPersonalized=Hi, "{5}" "{6}".\n\n You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
||||
orgInviteBodyPersonalizedHtml=<p>Hi, {5} {6}.</p><p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
||||
emailUpdateConfirmationSubject=Verify new email
|
||||
emailUpdateConfirmationBody=To update your {2} account with email address {1}, click the link below\n\n{0}\n\nThis link will expire within {3}.\n\nIf you don''t want to proceed with this modification, just ignore this message.
|
||||
emailUpdateConfirmationBodyHtml=<p>To update your {2} account with email address {1}, click the link below</p><p><a href="{0}">{0}</a></p><p>This link will expire within {3}.</p><p>If you don''t want to proceed with this modification, just ignore this message.</p>
|
||||
emailTestSubject=[KEYCLOAK] - SMTP test message
|
||||
emailTestBody=This is a test message
|
||||
emailTestBodyHtml=<p>This is a test message</p>
|
||||
identityProviderLinkSubject=Link {0}
|
||||
identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {5}.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
|
||||
identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2}. If this was you, click the link below to link accounts</p><p><a href="{3}">Link to confirm account linking</a></p><p>This link will expire within {5}.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>
|
||||
passwordResetSubject=Reset password
|
||||
passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {3}.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed.
|
||||
passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">Link to reset credentials</a></p><p>This link will expire within {3}.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>
|
||||
executeActionsSubject=Update Your Account
|
||||
executeActionsBody=Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you are unaware that your administrator has requested this, just ignore this message and nothing will be changed.
|
||||
executeActionsBodyHtml=<p>Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {4}.</p><p>If you are unaware that your administrator has requested this, just ignore this message and nothing will be changed.</p>
|
||||
eventLoginErrorSubject=Login error
|
||||
eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an administrator.
|
||||
eventLoginErrorBodyHtml=<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an administrator.</p>
|
||||
eventRemoveTotpSubject=Remove OTP
|
||||
eventRemoveTotpBody=OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.
|
||||
eventRemoveTotpBodyHtml=<p>OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.</p>
|
||||
eventUpdatePasswordSubject=Update password
|
||||
eventUpdatePasswordBody=Your password was changed on {0} from {1}. If this was not you, please contact an administrator.
|
||||
eventUpdatePasswordBodyHtml=<p>Your password was changed on {0} from {1}. If this was not you, please contact an administrator.</p>
|
||||
eventUpdateTotpSubject=Update OTP
|
||||
eventUpdateTotpBody=OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.
|
||||
eventUpdateTotpBodyHtml=<p>OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.</p>
|
||||
eventUpdateCredentialSubject=Update credential
|
||||
eventUpdateCredentialBody=Your {0} credential was changed on {1} from {2}. If this was not you, please contact an administrator.
|
||||
eventUpdateCredentialBodyHtml=<p>Your {0} credential was changed on {1} from {2}. If this was not you, please contact an administrator.</p>
|
||||
eventRemoveCredentialSubject=Remove credential
|
||||
eventRemoveCredentialBody=Credential {0} was removed from your account on {1} from {2}. If this was not you, please contact an administrator.
|
||||
eventRemoveCredentialBodyHtml=<p>Credential {0} was removed from your account on {1} from {2}. If this was not you, please contact an administrator.</p>
|
||||
eventUserDisabledByTemporaryLockoutSubject=User disabled by temporary lockout
|
||||
eventUserDisabledByTemporaryLockoutBody=Your user has been disabled temporarily because of multiple failed attemps on {0}. Please contact an administrator if needed.
|
||||
eventUserDisabledByTemporaryLockoutHtml=<p>Your user has been disabled temporarily because of multiple failed attemps on {0}. Please contact an administrator if needed.</p>
|
||||
eventUserDisabledByPermanentLockoutSubject=User disabled by permament lockout
|
||||
eventUserDisabledByPermanentLockoutBody=Your user has been disabled permanently because of multiple failed attemps on {0}. Please contact an administrator.
|
||||
eventUserDisabledByPermanentLockoutHtml=<p>Your user has been disabled permanently because of multiple failed attemps on {0}. Please contact an administrator.</p>
|
||||
|
||||
requiredAction.CONFIGURE_TOTP=Configure OTP
|
||||
requiredAction.TERMS_AND_CONDITIONS=Terms and Conditions
|
||||
requiredAction.UPDATE_PASSWORD=Update Password
|
||||
requiredAction.UPDATE_PROFILE=Update Profile
|
||||
requiredAction.VERIFY_EMAIL=Verify Email
|
||||
requiredAction.CONFIGURE_RECOVERY_AUTHN_CODES=Generate Recovery Codes
|
||||
|
||||
# units for link expiration timeout formatting
|
||||
# for languages which have more unit plural forms depending on the value (eg. Czech and other Slavic langs) you can override unit text for some other values like described in the Java choice format which is documented here. For Czech, it would be '{0,choice,0#minut|1#minuta|2#minuty|2<minut}'
|
||||
# https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/MessageFormat.html
|
||||
# https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/ChoiceFormat.html
|
||||
linkExpirationFormatter.timePeriodUnit.seconds={0,choice,0#seconds|1#second|1<seconds}
|
||||
linkExpirationFormatter.timePeriodUnit.minutes={0,choice,0#minutes|1#minute|1<minutes}
|
||||
linkExpirationFormatter.timePeriodUnit.hours={0,choice,0#hours|1#hour|1<hours}
|
||||
linkExpirationFormatter.timePeriodUnit.days={0,choice,0#days|1#day|1<days}
|
||||
|
||||
emailVerificationBodyCode=Please verify your email address by entering in the following code.\n\n{0}\n\n.
|
||||
emailVerificationBodyCodeHtml=<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>
|
||||
nexusIdentityAccessLayer=IDENTITY ACCESS LAYER
|
||||
nexusSecureMessage=SECURE MESSAGE
|
||||
nexusAutomatedMessageNotice=This message was generated automatically. Please do not reply.
|
||||
@@ -0,0 +1,40 @@
|
||||
# This file has been claimed for ownership from @keycloakify/email-native version 260007.0.0.
|
||||
# To relinquish ownership and restore this file to its original content, run the following command:
|
||||
#
|
||||
# $ npx keycloakify own --path "email/messages/messages_zh_CN.properties" --revert
|
||||
|
||||
# IMPORTANT: This file contains the base translation. Modifying it directly is not recommended.
|
||||
# To override or add custom messages, create a file named messages_zh_CN_override.properties in the same directory.
|
||||
# This file will be automatically loaded and merged with the base translation.
|
||||
# If you're implementing theme variants, you can also create variant-specific `.properties` files.
|
||||
# For example let's say you have defined `themeName: ["vanilla", "chocolate"]` then you can create the following files:
|
||||
# messages_zh_CN_override_vanilla.properties
|
||||
# messages_zh_CN_override_chocolate.properties
|
||||
|
||||
emailVerificationSubject=验证电子邮件
|
||||
emailVerificationBody=用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您没有注册用户,请忽略这条消息。
|
||||
emailVerificationBodyHtml=<p>用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后过期.</p><p>如果您没有注册用户,请忽略这条消息。</p>
|
||||
identityProviderLinkSubject=链接 {0}
|
||||
identityProviderLinkBody=有用户想要将账户 "{1}" 与用户{2}的账户"{0}" 做链接 . 如果是本人操作,请点击以下链接完成链接请求\n\n{3}\n\n这个链接会在 {4} 分钟后过期.\n\n如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.
|
||||
identityProviderLinkBodyHtml=<p>有用户想要将账户 <b>{1}</b> 与用户{2} 的账户<b>{0}</b> 做链接 . 如果是本人操作,请点击以下链接完成链接请求</p><p><a href="{3}">{3}</a></p><p>这个链接会在 {4} 分钟后过期。</p><p>如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.</p>
|
||||
passwordResetSubject=重置密码
|
||||
passwordResetBody=有用户要求修改账户 {2} 的密码.如是本人操作,请点击下面链接进行重置.\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您不想重置您的密码,请忽略这条消息,密码不会改变。
|
||||
passwordResetBodyHtml=<p>有用户要求修改账户 {2} 的密码如是本人操作,请点击下面链接进行重置.</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后过期</p><p>如果您不想重置您的密码,请忽略这条消息,密码不会改变。</p>
|
||||
executeActionsSubject=更新您的账户
|
||||
executeActionsBody=您的管理员要求您更新账户 {2}. 点击以下链接开始更新\n\n{0}\n\n这个链接会在 {1} 分钟后失效.\n\n如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。
|
||||
executeActionsBodyHtml=<p>您的管理员要求您更新账户{2}. 点击以下链接开始更新.</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后失效.</p><p>如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。</p>
|
||||
eventLoginErrorSubject=登录错误
|
||||
eventLoginErrorBody=在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.
|
||||
eventLoginErrorBodyHtml=<p>在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.</p>
|
||||
eventRemoveTotpSubject=删除 OTP
|
||||
eventRemoveTotpBody=OTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员
|
||||
eventRemoveTotpBodyHtml=<p>OTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员。</p>
|
||||
eventUpdatePasswordSubject=更新密码
|
||||
eventUpdatePasswordBody=您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员
|
||||
eventUpdatePasswordBodyHtml=<p>您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员</p>
|
||||
eventUpdateTotpSubject=更新 OTP
|
||||
eventUpdateTotpBody=您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。
|
||||
eventUpdateTotpBodyHtml=<p>您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。</p>
|
||||
nexusIdentityAccessLayer=身份访问层
|
||||
nexusSecureMessage=安全消息
|
||||
nexusAutomatedMessageNotice=此邮件由系统自动生成,请勿直接回复。
|
||||
@@ -0,0 +1,65 @@
|
||||
# This file has been claimed for ownership from @keycloakify/email-native version 260007.0.0.
|
||||
# To relinquish ownership and restore this file to its original content, run the following command:
|
||||
#
|
||||
# $ npx keycloakify own --path "email/messages/messages_zh_TW.properties" --revert
|
||||
|
||||
# IMPORTANT: This file contains the base translation. Modifying it directly is not recommended.
|
||||
# To override or add custom messages, create a file named messages_zh_TW_override.properties in the same directory.
|
||||
# This file will be automatically loaded and merged with the base translation.
|
||||
# If you're implementing theme variants, you can also create variant-specific `.properties` files.
|
||||
# For example let's say you have defined `themeName: ["vanilla", "chocolate"]` then you can create the following files:
|
||||
# messages_zh_TW_override_vanilla.properties
|
||||
# messages_zh_TW_override_chocolate.properties
|
||||
|
||||
emailVerificationSubject=驗證電子信箱
|
||||
emailVerificationBody=有人使用了這個電子信箱建立了一個 {2} 帳號。如果這是您的操作,請點擊下列連結驗證您的電子信箱\n\n{0}\n\n此連結將在 {3} 內失效。\n\n如果這不是您的操作,請忽略此訊息。
|
||||
emailVerificationBodyHtml=<p>有人使用了這個電子信箱建立了一個 {2} 帳號。如果這是您的操作,請點擊下列連結驗證您的電子信箱</p><p><a href="{0}">驗證電子信箱連結</a></p><p>此連結將在 {3} 內失效。</p><p>如果您沒有建立帳號,請忽略此訊息。</p>
|
||||
emailUpdateConfirmationSubject=驗證新的電子信箱
|
||||
emailUpdateConfirmationBody=為了更新您的 {2} 帳號的電子信箱 {1},請點擊下列連結\n\n{0}\n\n此連結將在 {3} 內失效。\n\n如果您不想進行這個修改,請忽略此訊息。
|
||||
emailUpdateConfirmationBodyHtml=<p>為了更新您的 {2} 帳號的電子信箱 {1},請點擊下列連結</p><p><a href="{0}">{0}</a></p><p>此連結將在 {3} 內失效。</p><p>如果您不想進行這個修改,請忽略此訊息。</p>
|
||||
emailTestSubject=[KEYCLOAK] - SMTP 測試訊息
|
||||
emailTestBody=這是一個測試訊息
|
||||
emailTestBodyHtml=<p>這是一個測試訊息</p>
|
||||
identityProviderLinkSubject=連結到 {0}
|
||||
identityProviderLinkBody=有人試圖連結您的 "{1}" 帳號與使用者 {2} 的 "{0}" 帳號。如果這是您的操作,請點擊下列連結連結帳號\n\n{3}\n\n此連結將在 {5} 內失效。\n\n如果這不是您的操作,請忽略此訊息。如果您連結帳號,您將能夠透過 {0} 登入 {1}。
|
||||
identityProviderLinkBodyHtml=<p>有人試圖連結您的 <b>{1}</b> 帳號與使用者 {2} 的 <b>{0}</b> 帳號。如果這是您的操作,請點擊下列連結連結帳號</p><p><a href="{3}">確認連結帳號</a></p><p>此連結將在 {5} 內失效。</p><p>如果這不是您的操作,請忽略此訊息。如果您連結帳號,您將能夠透過 {0} 登入 {1}。</p>
|
||||
passwordResetSubject=重設密碼
|
||||
passwordResetBody=有人剛剛要求重設您 {2} 的密碼。如果這是您的操作,請點擊下列連結重設它們。\n\n{0}\n\n此連結和代碼將在 {3} 內失效。\n\n如果您不想重設您的密碼,請忽略此訊息。
|
||||
passwordResetBodyHtml=<p>有人剛剛要求重設您 {2} 的密碼。如果這是您的操作,請點擊下列連結重設它們。</p><p><a href="{0}">重設密碼連結</a></p><p>此連結和代碼將在 {3} 內失效。</p><p>如果您不想重設您的密碼,請忽略此訊息,甚麼都不會改變。</p>
|
||||
executeActionsSubject=更新您的帳號
|
||||
executeActionsBody=您的管理員剛剛要求您更新您的 {2} 帳號,透過執行以下動作:{3}。點擊下列連結開始這個流程。\n\n{0}\n\n此連結將在 {4} 內失效。\n\n如果您不知道您的管理員剛剛要求這個,請忽略此訊息,甚麼都不會改變。
|
||||
executeActionsBodyHtml=<p>您的管理員剛剛要求您更新您的 {2} 帳號,透過執行以下動作:{3}。點擊下列連結開始這個流程。</p><p><a href="{0}">更新帳號連結</a></p><p>此連結將在 {4} 內失效。</p><p>如果您不知道您的管理員剛剛要求這個,請忽略此訊息,甚麼都不會改變。</p>
|
||||
eventLoginErrorSubject=登入錯誤
|
||||
eventLoginErrorBody=登入失敗,偵測到您的帳號在 {0} 從 {1} 進行登入。如果這不是您的操作,請聯絡系統管理員。
|
||||
eventLoginErrorBodyHtml=<p>登入失敗,偵測到您的帳號在 {0} 從 {1} 進行登入。如果這不是您的操作,請聯絡系統管理員。</p>
|
||||
eventRemoveTotpSubject=移除 OTP 設定
|
||||
eventRemoveTotpBody=在 {0} OTP 設定已從您的帳號 {1} 移除。如果這不是您的操作,請聯絡系統管理員。
|
||||
eventRemoveTotpBodyHtml=<p>在 {0} OTP 設定已從您的帳號 {1} 移除。如果這不是您的操作,請聯絡系統管理員。</p>
|
||||
eventUpdatePasswordSubject=更新密碼
|
||||
eventUpdatePasswordBody=您的密碼已在 {0} 從 {1} 更改。如果這不是您的操作,請聯絡系統管理員。
|
||||
eventUpdatePasswordBodyHtml=<p>您的密碼已在 {0} 從 {1} 更改。如果這不是您的操作,請聯絡系統管理員。</p>
|
||||
eventUpdateTotpSubject=更新 OTP 設定
|
||||
eventUpdateTotpBody=您帳號的 OTP 設定已在 {0} 從 {1} 更新。如果這不是您的操作,請聯絡系統管理員。
|
||||
eventUpdateTotpBodyHtml=<p>您帳號的 OTP 設定已在 {0} 從 {1} 更新。如果這不是您的操作,請聯絡系統管理員。</p>
|
||||
|
||||
requiredAction.CONFIGURE_TOTP=設定 OTP
|
||||
requiredAction.TERMS_AND_CONDITIONS=服務條款
|
||||
requiredAction.UPDATE_PASSWORD=更新密碼
|
||||
requiredAction.UPDATE_PROFILE=更新個人資訊
|
||||
requiredAction.VERIFY_EMAIL=驗證電子信箱
|
||||
requiredAction.CONFIGURE_RECOVERY_AUTHN_CODES=產生復原代碼
|
||||
|
||||
# units for link expiration timeout formatting
|
||||
# for languages which have more unit plural forms depending on the value (eg. Czech and other Slavic langs) you can override unit text for some other values like described in the Java choice format which is documented here. For Czech, it would be '{0,choice,0#minut|1#minuta|2#minuty|2<minut}'
|
||||
# https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/MessageFormat.html
|
||||
# https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/ChoiceFormat.html
|
||||
linkExpirationFormatter.timePeriodUnit.seconds=秒
|
||||
linkExpirationFormatter.timePeriodUnit.minutes=分
|
||||
linkExpirationFormatter.timePeriodUnit.hours=小時
|
||||
linkExpirationFormatter.timePeriodUnit.days=天
|
||||
|
||||
emailVerificationBodyCode=驗證您的電子信箱,請輸入以下代碼。\n\n{0}\n\n.
|
||||
emailVerificationBodyCodeHtml=<p>驗證您的電子信箱,請輸入以下代碼。</p><p><b>{0}</b></p>
|
||||
nexusIdentityAccessLayer=身分存取層
|
||||
nexusSecureMessage=安全訊息
|
||||
nexusAutomatedMessageNotice=此郵件由系統自動產生,請勿直接回覆。
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
// This file is auto-generated by the `update-kc-gen` command. Do not edit it manually.
|
||||
// Hash: 822f146dfd89023346d41d4f77fba9745b7f55365c95ca37f85806c01ab23dc4
|
||||
// Hash: 7dbbfb317418e9799cdce9a990519799f98af7dbfe27f1d0438c6738cffb7515
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
import { lazy, Suspense, type ReactNode } from "react";
|
||||
|
||||
export type ThemeName = "keycloakify-starter";
|
||||
export type ThemeName = "keycloakify-starter" | "class";
|
||||
|
||||
export const themeNames: ThemeName[] = ["keycloakify-starter"];
|
||||
export const themeNames: ThemeName[] = ["keycloakify-starter", "class"];
|
||||
|
||||
export type KcEnvName = never;
|
||||
|
||||
|
||||
+46
-2
@@ -5,6 +5,7 @@ import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/login/DefaultPage";
|
||||
import Template from "./Template";
|
||||
import "./theme.css";
|
||||
import "./class-theme.css";
|
||||
const UserProfileFormFields = lazy(
|
||||
() => import("keycloakify/login/UserProfileFormFields")
|
||||
);
|
||||
@@ -25,7 +26,11 @@ export default function KcPage(props: { kcContext: KcContext }) {
|
||||
<DefaultPage
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
classes={classes}
|
||||
classes={
|
||||
kcContext.themeName === "class"
|
||||
? classThemeClasses
|
||||
: nexusThemeClasses
|
||||
}
|
||||
Template={Template}
|
||||
doUseDefaultCss={false}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
@@ -38,7 +43,7 @@ export default function KcPage(props: { kcContext: KcContext }) {
|
||||
);
|
||||
}
|
||||
|
||||
const classes = {
|
||||
const nexusThemeClasses = {
|
||||
kcBodyClass: "nexus-body",
|
||||
kcLoginClass: "nexus-login",
|
||||
kcHeaderClass: "nexus-header",
|
||||
@@ -76,3 +81,42 @@ const classes = {
|
||||
kcLocaleItemClass: "nexus-locale__item",
|
||||
kcInputErrorMessageClass: "nexus-field__error"
|
||||
} satisfies { [key in ClassKey]?: string };
|
||||
|
||||
const classThemeClasses = {
|
||||
kcBodyClass: "class-body",
|
||||
kcLoginClass: "class-login",
|
||||
kcHeaderClass: "class-header",
|
||||
kcHeaderWrapperClass: "class-header__name",
|
||||
kcFormCardClass: "class-card",
|
||||
kcFormHeaderClass: "class-card__header",
|
||||
kcFormGroupClass: "class-field",
|
||||
kcLabelClass: "class-label",
|
||||
kcInputClass: "class-input",
|
||||
kcInputGroup: "class-input-group",
|
||||
kcFormSettingClass: "class-form-settings",
|
||||
kcFormOptionsWrapperClass: "class-form-options",
|
||||
kcFormPasswordVisibilityButtonClass: "class-password-toggle",
|
||||
kcButtonClass: "class-button",
|
||||
kcButtonBlockClass: "class-button--block",
|
||||
kcButtonLargeClass: "class-button--large",
|
||||
kcButtonPrimaryClass: "class-button--primary",
|
||||
kcButtonSecondaryClass: "class-button--secondary",
|
||||
kcButtonDefaultClass: "class-button--default",
|
||||
kcAlertClass: "class-alert",
|
||||
kcFeedbackErrorIcon: "class-feedback class-feedback--error",
|
||||
kcFeedbackWarningIcon: "class-feedback class-feedback--warning",
|
||||
kcFeedbackSuccessIcon: "class-feedback class-feedback--success",
|
||||
kcFeedbackInfoIcon: "class-feedback class-feedback--info",
|
||||
kcFormSocialAccountSectionClass: "class-social",
|
||||
kcFormSocialAccountListClass: "class-social__list",
|
||||
kcFormSocialAccountListButtonClass: "class-social__button",
|
||||
kcFormSocialAccountNameClass: "class-social__name",
|
||||
kcSignUpClass: "class-signup",
|
||||
kcInfoAreaWrapperClass: "class-signup__inner",
|
||||
kcLocaleMainClass: "class-locale",
|
||||
kcLocaleDropDownClass: "class-locale__dropdown",
|
||||
kcLocaleListClass: "class-locale__list",
|
||||
kcLocaleListItemClass: "class-locale__list-item",
|
||||
kcLocaleItemClass: "class-locale__item",
|
||||
kcInputErrorMessageClass: "class-field__error"
|
||||
} satisfies { [key in ClassKey]?: string };
|
||||
|
||||
+79
-29
@@ -4,38 +4,88 @@ import type { KcContext } from "./KcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { msg } = props.i18n;
|
||||
const isClassTheme = props.kcContext.themeName === "class";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="nexus-atmosphere" aria-hidden="true">
|
||||
<div className="nexus-grid" />
|
||||
<div className="nexus-orbit nexus-orbit--outer">
|
||||
<span className="nexus-orbit__node" />
|
||||
</div>
|
||||
<div className="nexus-orbit nexus-orbit--inner">
|
||||
<span className="nexus-orbit__node" />
|
||||
</div>
|
||||
<div className="nexus-core">
|
||||
<span>N</span>
|
||||
</div>
|
||||
</div>
|
||||
{isClassTheme ? (
|
||||
<>
|
||||
<div className="class-atmosphere" aria-hidden="true">
|
||||
<span className="class-registration-mark class-registration-mark--top" />
|
||||
<span className="class-registration-mark class-registration-mark--bottom" />
|
||||
<div className="class-mobile-brand">
|
||||
<span>612</span>
|
||||
<b>{msg("classArchiveName")}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="nexus-manifesto" aria-hidden="true">
|
||||
<div className="nexus-wordmark">
|
||||
<span className="nexus-wordmark__signal" />
|
||||
NEXUS <b>/ ID</b>
|
||||
</div>
|
||||
<div className="nexus-manifesto__copy">
|
||||
<p>IDENTITY ACCESS LAYER</p>
|
||||
<h2>
|
||||
Secure access.
|
||||
<span>Precisely orchestrated.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="nexus-system-status">
|
||||
<span>SECURE CHANNEL</span>
|
||||
<span className="nexus-system-status__state">SYSTEM ONLINE</span>
|
||||
</div>
|
||||
</aside>
|
||||
<aside className="class-record" aria-hidden="true">
|
||||
<div className="class-record__index">
|
||||
<span>ARCHIVE NO. 612</span>
|
||||
<span>{msg("classSchoolCohort")}</span>
|
||||
</div>
|
||||
|
||||
<div className="class-record__copy">
|
||||
<p>{msg("classIdentityArchive")}</p>
|
||||
<h2>
|
||||
{msg("classMemoriesRemain")}
|
||||
<span>{msg("classWelcomeBack")}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="class-record__ledger">
|
||||
<div>
|
||||
<strong>1095+</strong>
|
||||
<span>{msg("classDaysTogether")}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>612</strong>
|
||||
<span>{msg("classArchiveNumber")}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>∞</strong>
|
||||
<span>{msg("classMomentsRemembered")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="class-record__stamp">612</div>
|
||||
</aside>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="nexus-atmosphere" aria-hidden="true">
|
||||
<div className="nexus-grid" />
|
||||
<div className="nexus-orbit nexus-orbit--outer">
|
||||
<span className="nexus-orbit__node" />
|
||||
</div>
|
||||
<div className="nexus-orbit nexus-orbit--inner">
|
||||
<span className="nexus-orbit__node" />
|
||||
</div>
|
||||
<div className="nexus-core">
|
||||
<span>N</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="nexus-manifesto" aria-hidden="true">
|
||||
<div className="nexus-wordmark">
|
||||
<span className="nexus-wordmark__signal" />
|
||||
NEXUS <b>/ ID</b>
|
||||
</div>
|
||||
<div className="nexus-manifesto__copy">
|
||||
<p>{msg("nexusIdentityAccessLayer")}</p>
|
||||
<h2>
|
||||
{msg("nexusSecureAccess")}
|
||||
<span>{msg("nexusPreciselyOrchestrated")}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="nexus-system-status">
|
||||
<span>{msg("nexusSecureChannel")}</span>
|
||||
<span className="nexus-system-status__state">{msg("nexusSystemOnline")}</span>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)}
|
||||
|
||||
<BaseTemplate {...props} />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,831 @@
|
||||
:root {
|
||||
--class-ink: #18324a;
|
||||
--class-muted: #617387;
|
||||
--class-line: #b9cedd;
|
||||
--class-paper: #f7fbff;
|
||||
--class-surface: #ffffff;
|
||||
--class-soft: #e5f3ff;
|
||||
--class-blue: #3f7fd6;
|
||||
--class-blue-deep: #2868c7;
|
||||
--class-green: #23966f;
|
||||
--class-sun: #f6c54c;
|
||||
--class-coral: #f05a47;
|
||||
--class-danger: #c93636;
|
||||
--class-grid: rgba(24, 50, 74, 0.035);
|
||||
--class-input: #fbfdff;
|
||||
--class-input-focus: #ffffff;
|
||||
--class-danger-soft: #fff1ef;
|
||||
--class-success-soft: #eefaf5;
|
||||
--class-overlay: rgba(247, 251, 255, 0.94);
|
||||
--class-display: "Arial Narrow", "DIN Condensed", "Noto Sans SC", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
--class-body-font: "Noto Sans SC", "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
|
||||
--class-utility: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
html.login-pf:has(body.class-body) {
|
||||
min-height: 100%;
|
||||
background: var(--class-paper);
|
||||
}
|
||||
|
||||
body.class-body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
color: var(--class-ink);
|
||||
background: var(--class-paper);
|
||||
font-family: var(--class-body-font);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.class-body button,
|
||||
.class-body input,
|
||||
.class-body select,
|
||||
.class-body textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.class-atmosphere {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
background-image: linear-gradient(var(--class-grid) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--class-grid) 1px, transparent 1px);
|
||||
background-position: calc(52% + 24px) 24px;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.class-registration-mark {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.class-registration-mark::before,
|
||||
.class-registration-mark::after {
|
||||
position: absolute;
|
||||
background: var(--class-coral);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.class-registration-mark::before {
|
||||
top: 11px;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.class-registration-mark::after {
|
||||
top: 0;
|
||||
left: 11px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.class-registration-mark--top {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
.class-registration-mark--bottom {
|
||||
bottom: 24px;
|
||||
}
|
||||
|
||||
.class-mobile-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.class-record {
|
||||
position: fixed;
|
||||
inset: 0 48% 0 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: clamp(32px, 4.5vw, 72px);
|
||||
color: #f7faf8;
|
||||
background: linear-gradient(rgba(247, 250, 248, 0.075) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(247, 250, 248, 0.075) 1px, transparent 1px),
|
||||
var(--class-blue);
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
|
||||
.class-record::before {
|
||||
position: absolute;
|
||||
right: clamp(28px, 4vw, 62px);
|
||||
bottom: -0.18em;
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
font-family: var(--class-display);
|
||||
font-size: clamp(16rem, 32vw, 37rem);
|
||||
font-weight: 950;
|
||||
letter-spacing: -0.12em;
|
||||
line-height: 0.75;
|
||||
content: "612";
|
||||
}
|
||||
|
||||
.class-record::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 9px;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--class-coral) 0 33.333%,
|
||||
var(--class-sun) 33.333% 66.666%,
|
||||
var(--class-green) 66.666%
|
||||
);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.class-record__index,
|
||||
.class-record__copy,
|
||||
.class-record__ledger,
|
||||
.class-record__stamp {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.class-record__index {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding-bottom: 13px;
|
||||
border-bottom: 1px solid rgba(247, 250, 248, 0.48);
|
||||
color: rgba(247, 250, 248, 0.74);
|
||||
font: 700 0.68rem/1.4 var(--class-utility);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.class-record__index span:first-child {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.class-record__copy {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.class-record__copy p {
|
||||
margin: 0 0 18px;
|
||||
color: var(--class-sun);
|
||||
font: 700 0.72rem/1.5 var(--class-utility);
|
||||
letter-spacing: 0.07em;
|
||||
}
|
||||
|
||||
.class-record__copy h2 {
|
||||
max-width: 7.8em;
|
||||
margin: 0;
|
||||
font-family: var(--class-display);
|
||||
font-size: clamp(3.6rem, 6.5vw, 7.4rem);
|
||||
font-weight: 950;
|
||||
letter-spacing: -0.06em;
|
||||
line-height: 0.92;
|
||||
}
|
||||
|
||||
.class-record__copy h2 span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.class-record__copy h2::after {
|
||||
display: block;
|
||||
width: 70px;
|
||||
height: 8px;
|
||||
margin-top: 25px;
|
||||
background: var(--class-coral);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.class-record__ledger {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
border-top: 1px solid rgba(247, 250, 248, 0.48);
|
||||
border-bottom: 1px solid rgba(247, 250, 248, 0.48);
|
||||
}
|
||||
|
||||
.class-record__ledger div {
|
||||
min-width: 0;
|
||||
padding: 16px 16px 17px 0;
|
||||
}
|
||||
|
||||
.class-record__ledger div + div {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid rgba(247, 250, 248, 0.48);
|
||||
}
|
||||
|
||||
.class-record__ledger strong,
|
||||
.class-record__ledger span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.class-record__ledger strong {
|
||||
font-family: var(--class-display);
|
||||
font-size: clamp(1.8rem, 3vw, 3rem);
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.class-record__ledger span {
|
||||
margin-top: 7px;
|
||||
color: rgba(247, 250, 248, 0.72);
|
||||
font: 700 0.58rem/1.35 var(--class-utility);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.class-record__stamp {
|
||||
position: absolute;
|
||||
top: clamp(92px, 13vh, 140px);
|
||||
right: clamp(32px, 5vw, 78px);
|
||||
display: grid;
|
||||
width: 72px;
|
||||
height: 58px;
|
||||
place-items: center;
|
||||
color: #ffffff;
|
||||
background: var(--class-coral);
|
||||
font: 900 1.35rem/1 var(--class-utility);
|
||||
letter-spacing: 0.04em;
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
|
||||
.class-login {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
width: 48%;
|
||||
margin-left: 52%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: clamp(42px, 6vw, 96px);
|
||||
}
|
||||
|
||||
.class-header {
|
||||
width: min(100%, 500px);
|
||||
margin: 0 auto 13px;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.class-header__name#kc-header-wrapper {
|
||||
padding: 0;
|
||||
color: var(--class-muted);
|
||||
background: none;
|
||||
font: 700 0.68rem/1.5 var(--class-utility);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.class-header__name#kc-header-wrapper::before {
|
||||
color: var(--class-blue-deep);
|
||||
content: "ARCHIVE ACCESS / ";
|
||||
}
|
||||
|
||||
.class-card {
|
||||
position: relative;
|
||||
width: min(100%, 500px);
|
||||
margin: 0 auto;
|
||||
padding: clamp(30px, 4vw, 50px);
|
||||
border: 1px solid var(--class-ink);
|
||||
border-radius: 0;
|
||||
color: var(--class-ink);
|
||||
background: var(--class-surface);
|
||||
box-shadow: 16px 16px 0 var(--class-soft);
|
||||
}
|
||||
|
||||
.class-card::before {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
left: -1px;
|
||||
height: 7px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--class-blue) 0 54%,
|
||||
var(--class-coral) 54% 72%,
|
||||
var(--class-sun) 72% 88%,
|
||||
var(--class-green) 88%
|
||||
);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.class-card__header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.class-card__header #kc-page-title {
|
||||
margin: 0;
|
||||
color: var(--class-ink);
|
||||
font-family: var(--class-display);
|
||||
font-size: clamp(2.25rem, 3.8vw, 3.4rem);
|
||||
font-weight: 950;
|
||||
letter-spacing: -0.045em;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.class-card__header #kc-page-title::after {
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 6px;
|
||||
margin-top: 16px;
|
||||
background: var(--class-coral);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.class-card a {
|
||||
color: var(--class-blue-deep);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.class-card a:hover {
|
||||
color: var(--class-coral);
|
||||
}
|
||||
|
||||
.class-field {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
.class-label {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--class-ink);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.class-input,
|
||||
.class-card textarea.form-control,
|
||||
.class-card select.form-control {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--class-line);
|
||||
border-radius: 0;
|
||||
color: var(--class-ink);
|
||||
outline: 0;
|
||||
background: var(--class-input);
|
||||
box-shadow: none;
|
||||
font-size: 0.94rem;
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
box-shadow 150ms ease,
|
||||
background 150ms ease;
|
||||
}
|
||||
|
||||
.class-input:hover {
|
||||
border-color: var(--class-ink);
|
||||
}
|
||||
|
||||
.class-input:focus,
|
||||
.class-card textarea.form-control:focus,
|
||||
.class-card select.form-control:focus {
|
||||
border-color: var(--class-blue-deep);
|
||||
background: var(--class-input-focus);
|
||||
box-shadow: inset 5px 0 0 var(--class-blue);
|
||||
}
|
||||
|
||||
.class-input[aria-invalid="true"] {
|
||||
border-color: var(--class-danger);
|
||||
box-shadow: inset 5px 0 0 var(--class-danger);
|
||||
}
|
||||
|
||||
.class-input-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.class-input-group .class-input {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.class-password-toggle,
|
||||
.class-input-group .pf-c-button.pf-m-control {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
place-items: center;
|
||||
border: 1px solid var(--class-line);
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
color: var(--class-muted);
|
||||
background: var(--class-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.class-password-toggle i::before {
|
||||
content: "\25C9";
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.class-field__error {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
color: var(--class-danger);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.class-form-settings.class-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: -1px 0 25px;
|
||||
color: var(--class-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.class-form-settings.class-field #kc-form-options,
|
||||
.class-form-settings.class-field > div {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.class-form-settings input[type="checkbox"] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0 7px 0 0;
|
||||
accent-color: var(--class-blue);
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
.class-button {
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--class-ink);
|
||||
border-radius: 0;
|
||||
color: var(--class-ink);
|
||||
background: var(--class-soft);
|
||||
box-shadow: none;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.035em;
|
||||
transition:
|
||||
color 150ms ease,
|
||||
background 150ms ease,
|
||||
transform 150ms ease;
|
||||
}
|
||||
|
||||
.class-button--primary {
|
||||
color: var(--class-ink);
|
||||
background: var(--class-sun);
|
||||
}
|
||||
|
||||
.class-button--block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.class-button:hover:not(:disabled) {
|
||||
color: #ffffff;
|
||||
background: var(--class-blue-deep);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.class-button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.class-button:focus-visible,
|
||||
.class-card a:focus-visible,
|
||||
.class-card button:focus-visible,
|
||||
.class-card input:focus-visible {
|
||||
outline: 3px solid rgba(240, 90, 71, 0.42);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.class-alert {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 0 0 24px;
|
||||
padding: 12px 13px;
|
||||
border: 1px solid var(--class-line);
|
||||
border-left: 6px solid var(--class-blue);
|
||||
border-radius: 0;
|
||||
color: var(--class-ink);
|
||||
background: var(--class-soft);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.class-alert.pf-m-danger {
|
||||
border-left-color: var(--class-danger);
|
||||
background: var(--class-danger-soft);
|
||||
}
|
||||
|
||||
.class-alert.pf-m-success {
|
||||
border-left-color: var(--class-green);
|
||||
background: var(--class-success-soft);
|
||||
}
|
||||
|
||||
.class-feedback {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: 6px;
|
||||
background: var(--class-blue);
|
||||
}
|
||||
|
||||
.class-feedback--error,
|
||||
.class-feedback--warning {
|
||||
background: var(--class-danger);
|
||||
}
|
||||
|
||||
.class-feedback--success {
|
||||
background: var(--class-green);
|
||||
}
|
||||
|
||||
.class-social {
|
||||
margin-top: 29px;
|
||||
}
|
||||
|
||||
.class-social hr {
|
||||
height: 1px;
|
||||
margin: 0 0 23px;
|
||||
border: 0;
|
||||
background: var(--class-line);
|
||||
}
|
||||
|
||||
.class-social h2 {
|
||||
margin: 0 0 12px;
|
||||
color: var(--class-muted);
|
||||
font: 700 0.64rem/1.5 var(--class-utility);
|
||||
letter-spacing: 0.07em;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.class-social__list,
|
||||
.class-social__list.pf-l-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.class-social__list li,
|
||||
.class-social__list .pf-l-grid__item {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.class-social__button {
|
||||
display: flex;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 9px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--class-line);
|
||||
border-radius: 0;
|
||||
color: var(--class-ink);
|
||||
background: var(--class-input-focus);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.class-social__button:hover {
|
||||
border-color: var(--class-blue);
|
||||
background: var(--class-soft);
|
||||
}
|
||||
|
||||
.class-signup {
|
||||
margin: 27px 0 0;
|
||||
color: var(--class-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.class-signup__inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.class-locale {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.class-locale #kc-current-locale-link {
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--class-line);
|
||||
border-radius: 0;
|
||||
color: var(--class-muted);
|
||||
background: var(--class-overlay);
|
||||
font: 700 0.66rem/1.4 var(--class-utility);
|
||||
}
|
||||
|
||||
.class-locale__list {
|
||||
right: 0;
|
||||
left: auto;
|
||||
min-width: 150px;
|
||||
border: 1px solid var(--class-ink);
|
||||
color: var(--class-ink);
|
||||
background: var(--class-surface);
|
||||
box-shadow: 8px 8px 0 var(--class-soft);
|
||||
}
|
||||
|
||||
.class-locale__list:not([hidden]) {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: calc(100% + 6px);
|
||||
display: none;
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.class-locale__dropdown:hover .class-locale__list,
|
||||
.class-locale__dropdown:focus-within .class-locale__list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.class-locale__item {
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
#kc-username {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 11px 13px;
|
||||
border: 1px solid var(--class-line);
|
||||
background: var(--class-soft);
|
||||
}
|
||||
|
||||
.class-card .select-auth-container,
|
||||
.class-card .pf-c-tile,
|
||||
.class-card .kc-recovery-codes-list {
|
||||
color: var(--class-ink);
|
||||
}
|
||||
|
||||
.class-card .select-auth-box-parent,
|
||||
.class-card .pf-c-tile {
|
||||
border-color: var(--class-line);
|
||||
border-radius: 0;
|
||||
background: var(--class-surface);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--class-ink: #e7f1f9;
|
||||
--class-muted: #9ab0c2;
|
||||
--class-line: #456177;
|
||||
--class-paper: #08131e;
|
||||
--class-surface: #0f2030;
|
||||
--class-soft: #173149;
|
||||
--class-blue: #6da8f6;
|
||||
--class-blue-deep: #91bdff;
|
||||
--class-green: #54c99d;
|
||||
--class-sun: #f2c45c;
|
||||
--class-coral: #ff7465;
|
||||
--class-danger: #ff8585;
|
||||
--class-grid: rgba(145, 189, 255, 0.055);
|
||||
--class-input: #0a1926;
|
||||
--class-input-focus: #12283a;
|
||||
--class-danger-soft: #321d25;
|
||||
--class-success-soft: #102d27;
|
||||
--class-overlay: rgba(15, 32, 48, 0.96);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.class-record {
|
||||
background: linear-gradient(rgba(247, 250, 248, 0.055) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(247, 250, 248, 0.055) 1px, transparent 1px),
|
||||
#17477f;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
|
||||
.class-card {
|
||||
box-shadow: 16px 16px 0 #142a3e;
|
||||
}
|
||||
|
||||
.class-input,
|
||||
.class-card textarea.form-control,
|
||||
.class-card select.form-control {
|
||||
caret-color: var(--class-sun);
|
||||
}
|
||||
|
||||
.class-input::placeholder,
|
||||
.class-card textarea.form-control::placeholder {
|
||||
color: #7890a4;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.class-button--primary {
|
||||
color: #142638;
|
||||
}
|
||||
|
||||
.class-locale__list {
|
||||
box-shadow: 8px 8px 0 #142a3e;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1020px) {
|
||||
.class-record {
|
||||
inset: 0 54% 0 0;
|
||||
}
|
||||
|
||||
.class-login {
|
||||
width: 54%;
|
||||
margin-left: 46%;
|
||||
padding: 56px 38px;
|
||||
}
|
||||
|
||||
.class-record__ledger span {
|
||||
font-size: 0.52rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.class-record {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.class-atmosphere {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.class-mobile-brand {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
width: max-content;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--class-ink);
|
||||
font: 800 0.72rem/1.3 var(--class-utility);
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.class-mobile-brand > span {
|
||||
display: grid;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
place-items: center;
|
||||
color: #ffffff;
|
||||
background: var(--class-coral);
|
||||
}
|
||||
|
||||
.class-mobile-brand b {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.class-login {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
padding: 88px 24px 44px;
|
||||
}
|
||||
|
||||
.class-header,
|
||||
.class-card {
|
||||
width: min(100%, 540px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.class-login {
|
||||
justify-content: flex-start;
|
||||
padding: 82px 16px 30px;
|
||||
}
|
||||
|
||||
.class-card {
|
||||
padding: 31px 22px 28px;
|
||||
box-shadow: 9px 9px 0 var(--class-soft);
|
||||
}
|
||||
|
||||
.class-card__header #kc-page-title {
|
||||
font-size: 2.35rem;
|
||||
}
|
||||
|
||||
.class-social__list,
|
||||
.class-social__list.pf-l-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.class-form-settings.class-field {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.class-registration-mark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.class-body *,
|
||||
.class-body *::before,
|
||||
.class-body *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
+50
-1
@@ -3,7 +3,56 @@ import { i18nBuilder } from "keycloakify/login";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder
|
||||
.withThemeName<ThemeName>()
|
||||
.withCustomTranslations({
|
||||
en: {
|
||||
nexusIdentityAccessLayer: "IDENTITY ACCESS LAYER",
|
||||
nexusSecureAccess: "Secure access.",
|
||||
nexusPreciselyOrchestrated: "Precisely orchestrated.",
|
||||
nexusSecureChannel: "SECURE CHANNEL",
|
||||
nexusSystemOnline: "SYSTEM ONLINE",
|
||||
classArchiveName: "GAOPING NO.1 · CLASS 612",
|
||||
classSchoolCohort: "CLASS OF 2024 · GAOPING NO.1",
|
||||
classIdentityArchive: "CLASS MEMORY · IDENTITY ARCHIVE",
|
||||
classMemoriesRemain: "The memories remain.",
|
||||
classWelcomeBack: "Welcome back.",
|
||||
classDaysTogether: "DAYS TOGETHER",
|
||||
classArchiveNumber: "ARCHIVE NUMBER",
|
||||
classMomentsRemembered: "MOMENTS REMEMBERED"
|
||||
},
|
||||
"zh-CN": {
|
||||
nexusIdentityAccessLayer: "身份访问层",
|
||||
nexusSecureAccess: "安全访问。",
|
||||
nexusPreciselyOrchestrated: "精准编排。",
|
||||
nexusSecureChannel: "安全通道",
|
||||
nexusSystemOnline: "系统在线",
|
||||
classArchiveName: "高平一中 · 612班",
|
||||
classSchoolCohort: "2024届 · 高平一中",
|
||||
classIdentityArchive: "班级记忆 · 身份档案",
|
||||
classMemoriesRemain: "记忆仍在,",
|
||||
classWelcomeBack: "欢迎回来。",
|
||||
classDaysTogether: "一起经过的日子",
|
||||
classArchiveNumber: "班级档案编号",
|
||||
classMomentsRemembered: "还会被想起的瞬间"
|
||||
},
|
||||
"zh-TW": {
|
||||
nexusIdentityAccessLayer: "身分存取層",
|
||||
nexusSecureAccess: "安全存取。",
|
||||
nexusPreciselyOrchestrated: "精準編排。",
|
||||
nexusSecureChannel: "安全通道",
|
||||
nexusSystemOnline: "系統在線",
|
||||
classArchiveName: "高平一中 · 612班",
|
||||
classSchoolCohort: "2024屆 · 高平一中",
|
||||
classIdentityArchive: "班級記憶 · 身分檔案",
|
||||
classMemoriesRemain: "記憶仍在,",
|
||||
classWelcomeBack: "歡迎回來。",
|
||||
classDaysTogether: "一起經過的日子",
|
||||
classArchiveNumber: "班級檔案編號",
|
||||
classMomentsRemembered: "還會被想起的瞬間"
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
||||
|
||||
@@ -16,6 +16,37 @@ export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const SimplifiedChinese: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: { currentLanguageTag: "zh-CN" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const TraditionalChinese: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: { currentLanguageTag: "zh-TW" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const ClassTheme: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
themeName: "class",
|
||||
locale: { currentLanguageTag: "zh-CN" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithInvalidCredential: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "./@patternfly/react-core";
|
||||
import { MoonIcon, SunIcon } from "./@patternfly/react-icons";
|
||||
import {
|
||||
getConsoleColorMode,
|
||||
subscribeToConsoleColorMode,
|
||||
toggleConsoleColorMode
|
||||
} from "./consoleColorScheme";
|
||||
|
||||
function labels(isDark: boolean) {
|
||||
const isChinese = document.documentElement.lang.toLowerCase().startsWith("zh");
|
||||
|
||||
if (isChinese) {
|
||||
return {
|
||||
current: isDark ? "暗色" : "亮色",
|
||||
action: isDark ? "切换至亮色模式" : "切换至暗色模式"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
current: isDark ? "Dark" : "Light",
|
||||
action: isDark ? "Switch to light mode" : "Switch to dark mode"
|
||||
};
|
||||
}
|
||||
|
||||
export function ThemeModeToggle() {
|
||||
const [mode, setMode] = useState(getConsoleColorMode);
|
||||
|
||||
useEffect(() => subscribeToConsoleColorMode(setMode), []);
|
||||
|
||||
if (document.documentElement.dataset.consoleDarkAllowed === "false") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDark = mode === "dark";
|
||||
const copy = labels(isDark);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="plain"
|
||||
className="console-theme-toggle"
|
||||
aria-label={copy.action}
|
||||
title={copy.action}
|
||||
aria-pressed={isDark}
|
||||
icon={isDark ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleConsoleColorMode}
|
||||
>
|
||||
<span className="console-theme-toggle__label">{copy.current}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
export type ConsoleColorMode = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "nexus-console-color-mode";
|
||||
const DARK_THEME_CLASS = "pf-v5-theme-dark";
|
||||
const COLOR_MODE_EVENT = "nexus:color-mode-change";
|
||||
|
||||
function readStoredMode(): ConsoleColorMode | undefined {
|
||||
try {
|
||||
const value = localStorage.getItem(STORAGE_KEY);
|
||||
return value === "light" || value === "dark" ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function preferredMode(): ConsoleColorMode {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function getConsoleColorMode(): ConsoleColorMode {
|
||||
return document.documentElement.classList.contains(DARK_THEME_CLASS)
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export function setConsoleColorMode(
|
||||
mode: ConsoleColorMode,
|
||||
options: { persist?: boolean } = {}
|
||||
) {
|
||||
const { persist = true } = options;
|
||||
const isDark = mode === "dark";
|
||||
const root = document.documentElement;
|
||||
|
||||
document.getElementById("root-color-scheme-style")?.remove();
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "root-color-scheme-style";
|
||||
style.textContent = `:root { color-scheme: ${mode}; }`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
root.style.removeProperty("background-color");
|
||||
root.classList.toggle(DARK_THEME_CLASS, isDark);
|
||||
root.dataset.consoleColorMode = mode;
|
||||
|
||||
if (persist) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// Storage can be unavailable in hardened browser sessions.
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(COLOR_MODE_EVENT, { detail: mode }));
|
||||
}
|
||||
|
||||
export function startConsoleColorScheme(darkModeAllowed: boolean) {
|
||||
const root = document.documentElement;
|
||||
root.dataset.consoleDarkAllowed = String(darkModeAllowed);
|
||||
|
||||
if (!darkModeAllowed) {
|
||||
setConsoleColorMode("light", { persist: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setConsoleColorMode(readStoredMode() ?? preferredMode(), { persist: false });
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mediaQuery.addEventListener("change", event => {
|
||||
if (!readStoredMode()) {
|
||||
setConsoleColorMode(event.matches ? "dark" : "light", {
|
||||
persist: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleConsoleColorMode() {
|
||||
setConsoleColorMode(getConsoleColorMode() === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
export function subscribeToConsoleColorMode(listener: (mode: ConsoleColorMode) => void) {
|
||||
const handleChange = (event: Event) => {
|
||||
listener((event as CustomEvent<ConsoleColorMode>).detail);
|
||||
};
|
||||
|
||||
window.addEventListener(COLOR_MODE_EVENT, handleChange);
|
||||
return () => window.removeEventListener(COLOR_MODE_EVENT, handleChange);
|
||||
}
|
||||
+2
-1
@@ -7,7 +7,8 @@ export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
keycloakify({
|
||||
accountThemeImplementation: "Multi-Page"
|
||||
themeName: ["keycloakify-starter", "class"],
|
||||
accountThemeImplementation: "Single-Page"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user