Compare commits

...
4 Commits
  • keycloakify-starter 亮色改为雾白、冰蓝工作区,不再大面积深色。
    暗色使用深海蓝灰,并保持足够的层级和对比度。
    class 主题同样支持亮暗模式。
    Account/Admin 页头增加模式切换按钮,选择会持久保存。
    仍遵守 Keycloak 的 darkMode=false 配置,关闭时只允许亮色。
    Account 统一为“页面上下文 → 身份业务区 → 操作区”结构。
    Admin 统一为“领域上下文 → 业务内容 → 固定操作区”结构。
    表格、表单、详情、向导、弹窗、空状态、安全设备及应用授权组件全部纳入统一设计系统。
    移动端自动收起侧栏、双栏变单栏,并调整固定操作区。
  • 首页改为“领域运营工作台”,非 master 领域也有完整管理首页。
    根据权限展示用户、客户端、组、领域设置、认证、事件等业务入口。
    新增安全配置概览:领域状态、注册、邮箱验证、暴力破解保护。
    侧栏增加领域上下文、状态和业务分组。
    重构通用页面标题组件,统一标题、说明、状态、开关和操作区。
    增加统一工作区容器,并优化表格、表单及详情页布局。
    补充简体中文、繁体中文和英文文案。
  • 顶栏、背景和侧边导航
    页面标题、面包屑和内容间距
    卡片、表格、筛选工具栏、分页
    表单、按钮、标签、弹窗和空状态
    固定保存操作区
    深色模式与移动端
27 changed files with 3548 additions and 1839 deletions
+11 -11
View File
@@ -1,15 +1,25 @@
# This file is managed by Keycloakify, do not edit it manually. # This file is managed by Keycloakify, do not edit it manually.
# === Owned files start === # === Owned files start ===
# account/colorScheme.ts
# account/KcAccountUi.tsx # account/KcAccountUi.tsx
# account/KcContext.ts # account/KcContext.ts
# account/KcPage.tsx # account/KcPage.tsx
# admin/App.tsx
# admin/colorScheme.ts
# admin/index.css # admin/index.css
# admin/KcAdminUi.tsx # admin/KcAdminUi.tsx
# admin/page-nav.css
# admin/PageHeader.tsx
# admin/PageNav.tsx
# account/assets/logo.svg # account/assets/logo.svg
# account/personal-info/PersonalInfo.tsx # account/personal-info/PersonalInfo.tsx
# account/root/Header.tsx
# account/root/PageNav.tsx # account/root/PageNav.tsx
# account/root/Root.tsx
# admin/assets/logo.svg # admin/assets/logo.svg
# admin/dashboard/dashboard.css
# admin/dashboard/Dashboard.tsx
# admin/i18n/messages_en.properties # admin/i18n/messages_en.properties
# admin/i18n/messages_zh_Hans.properties # admin/i18n/messages_zh_Hans.properties
# admin/i18n/messages_zh_Hant.properties # admin/i18n/messages_zh_Hant.properties
@@ -18,6 +28,7 @@
# email/messages/messages_zh_CN.properties # email/messages/messages_zh_CN.properties
# email/messages/messages_zh_TW.properties # email/messages/messages_zh_TW.properties
# account/components/page/Page.tsx # account/components/page/Page.tsx
# admin/components/view-header/ViewHeader.tsx
# === Owned files end ===== # === Owned files end =====
# === @keycloakify/email-native v260007.0.0 === # === @keycloakify/email-native v260007.0.0 ===
@@ -83,10 +94,8 @@
# === @keycloakify/keycloak-admin-ui v260700.0.2 === # === @keycloakify/keycloak-admin-ui v260700.0.2 ===
/admin/admin-client.tsx /admin/admin-client.tsx
/admin/App.tsx
/admin/banners.module.css /admin/banners.module.css
/admin/Banners.tsx /admin/Banners.tsx
/admin/colorScheme.ts
/admin/environment-types.ts /admin/environment-types.ts
/admin/environment.ts /admin/environment.ts
/admin/ForbiddenSection.tsx /admin/ForbiddenSection.tsx
@@ -94,10 +103,7 @@
/admin/i18next.d.ts /admin/i18next.d.ts
/admin/KcContext.ts /admin/KcContext.ts
/admin/KcPage.tsx /admin/KcPage.tsx
/admin/page-nav.css
/admin/PageHeader.tsx
/admin/PageHeaderClearCachesModal.tsx /admin/PageHeaderClearCachesModal.tsx
/admin/PageNav.tsx
/admin/PageNotFoundSection.tsx /admin/PageNotFoundSection.tsx
/admin/Root.tsx /admin/Root.tsx
/admin/routes.tsx /admin/routes.tsx
@@ -134,8 +140,6 @@
/admin/clients/utils.ts /admin/clients/utils.ts
/admin/components/SwitchControl.tsx /admin/components/SwitchControl.tsx
/admin/context/RecentRealms.tsx /admin/context/RecentRealms.tsx
/admin/dashboard/dashboard.css
/admin/dashboard/Dashboard.tsx
/admin/dashboard/ProviderInfo.tsx /admin/dashboard/ProviderInfo.tsx
/admin/dashboard/routes.ts /admin/dashboard/routes.ts
/admin/events/AdminEvents.tsx /admin/events/AdminEvents.tsx
@@ -522,7 +526,6 @@
/admin/components/users/UserDataTableAttributeSearchForm.tsx /admin/components/users/UserDataTableAttributeSearchForm.tsx
/admin/components/users/UserDataTableToolbarItems.tsx /admin/components/users/UserDataTableToolbarItems.tsx
/admin/components/users/UserSelect.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/wizard-section-header.css
/admin/components/wizard-section-header/WizardSectionHeader.tsx /admin/components/wizard-section-header/WizardSectionHeader.tsx
/admin/context/access/Access.tsx /admin/context/access/Access.tsx
@@ -841,7 +844,6 @@
# === @keycloakify/keycloak-account-ui v260700.0.3 === # === @keycloakify/keycloak-account-ui v260700.0.3 ===
/account/api.ts /account/api.ts
/account/colorScheme.ts
/account/environment.ts /account/environment.ts
/account/i18n-type.ts /account/i18n-type.ts
/account/i18next.d.ts /account/i18next.d.ts
@@ -915,8 +917,6 @@
/account/resources/SharedWith.tsx /account/resources/SharedWith.tsx
/account/resources/ShareTheResource.tsx /account/resources/ShareTheResource.tsx
/account/root/header.module.css /account/root/header.module.css
/account/root/Header.tsx
/account/root/Root.tsx
/account/utils/formatDate.ts /account/utils/formatDate.ts
/account/utils/joinPath.ts /account/utils/joinPath.ts
/account/utils/useAccountAlerts.ts /account/utils/useAccountAlerts.ts
+2
View File
@@ -8,6 +8,8 @@
import "@patternfly/patternfly/patternfly-addons.css"; import "@patternfly/patternfly/patternfly-addons.css";
import "@patternfly/react-core/dist/styles/base.css"; import "@patternfly/react-core/dist/styles/base.css";
import "./theme.css"; import "./theme.css";
import "../console-ui.css";
import "./workspace.css";
import { useReducer, useEffect } from "react"; import { useReducer, useEffect } from "react";
import { startColorSchemeManagement } from "./colorScheme"; import { startColorSchemeManagement } from "./colorScheme";
+8 -11
View File
@@ -1,16 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title">
<title id="title">NEXUS ID</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)"> <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)"/> <rect width="34" height="34" rx="10" fill="#356fd6"/>
<circle cx="17" cy="17" r="3.5" fill="#63e6ff"/> <path d="M9 10h16M9 17h11M9 24h16" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
<circle cx="17" cy="17" r="8.5" fill="none" stroke="#63e6ff" stroke-opacity=".18"/> <circle cx="24.5" cy="17" r="2.2" fill="#f0b92e"/>
</g> </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="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="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="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> </svg>

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 929 B

+21
View File
@@ -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);
}
+13 -11
View File
@@ -24,12 +24,12 @@ type PageProps = {
export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => { export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => {
return ( return (
<> <article className="account-business-page">
<PageSection variant="light"> <PageSection variant="light" className="account-view-header">
<div className="nexus-page-intro"> <div className="account-view-header__layout">
<div className="nexus-page-intro__copy"> <div className="account-view-header__copy">
<span className="nexus-page-intro__kicker"> <span className="account-view-header__eyebrow">
NEXUS / Identity control Identity workspace / Account
</span> </span>
<TextContent> <TextContent>
<Title headingLevel="h1" data-testid="page-heading"> <Title headingLevel="h1" data-testid="page-heading">
@@ -38,14 +38,16 @@ export const Page = ({ title, description, children }: PropsWithChildren<PagePro
<Text component="p">{description}</Text> <Text component="p">{description}</Text>
</TextContent> </TextContent>
</div> </div>
<div className="nexus-page-intro__mark" aria-hidden="true"> <div className="account-view-header__context" aria-hidden="true">
<span>ID</span> <span>SELF SERVICE</span>
<strong>ID</strong>
<i /> <i />
<span>ACCOUNT</span>
</div> </div>
</div> </div>
</PageSection> </PageSection>
<PageSection variant="light">{children}</PageSection> <PageSection variant="light" className="account-view-body">
</> <div className="account-view-body__surface">{children}</div>
</PageSection>
</article>
); );
}; };
+73
View File
@@ -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" />
]}
/>
);
};
+113
View File
@@ -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)} />;
};
+7 -1098
View File
File diff suppressed because it is too large Load Diff
+367
View File
@@ -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;
}
}
+102
View File
@@ -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>
);
};
+2
View File
@@ -16,6 +16,8 @@ import { routes } from "./routes";
import { getKcContext } from "./KcContext"; import { getKcContext } from "./KcContext";
import "./index.css"; import "./index.css";
import "../console-ui.css";
import "./workspace.css";
const { kcContext } = getKcContext(); const { kcContext } = getKcContext();
+150
View File
@@ -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>
]}
/>
);
};
+269
View File
@@ -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>
);
};
+8 -11
View File
@@ -1,16 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title">
<title id="title">NEXUS ID</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)"> <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)"/> <rect width="34" height="34" rx="10" fill="#356fd6"/>
<circle cx="17" cy="17" r="3.5" fill="#63e6ff"/> <path d="M9 10h16M9 17h11M9 24h16" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
<circle cx="17" cy="17" r="8.5" fill="none" stroke="#63e6ff" stroke-opacity=".18"/> <circle cx="24.5" cy="17" r="2.2" fill="#f0b92e"/>
</g> </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="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="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="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> </svg>

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 929 B

+21
View File
@@ -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" />
)}
</>
);
};
+428
View File
@@ -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}&nbsp;
<Label color={color}>{t(feature.type.toLowerCase())}</Label>
{feature.deprecated && feature.type !== FeatureType.Deprecated && (
<>
&nbsp;<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 />
);
}
+437
View File
@@ -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;
}
}
@@ -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.
@@ -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=修改协议、认证流程或生产安全设置时,可查阅官方文档。
@@ -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=修改協定、驗證流程或正式環境安全設定時,可查閱官方文件。
+9 -697
View File
@@ -1,714 +1,26 @@
:root { /*
--nexus-admin-void: #07101e; * Admin compatibility layer.
--nexus-admin-chrome: #0b1728; * Visual tokens and shared PatternFly composition live in ../console-ui.css.
--nexus-admin-panel: #ffffff; * Keep this file intentionally small so theme-specific legacy rules cannot
--nexus-admin-canvas: #f2f5f8; * override the light/dark workspace palette.
--nexus-admin-canvas-strong: #e8edf2; */
--nexus-admin-signal: #00a3b4;
--nexus-admin-signal-bright: #58d7e3;
--nexus-admin-indigo: #5d63d8;
--nexus-admin-mint: #8de3cf;
--nexus-admin-text: #142033;
--nexus-admin-muted: #607087;
--nexus-admin-line: #d8e0e8;
--nexus-admin-line-strong: #becad6;
--nexus-admin-danger: #c33d4d;
--nexus-admin-radius-sm: 5px;
--nexus-admin-radius-md: 9px;
--nexus-admin-shadow: 0 1px 2px rgba(20, 32, 51, 0.04),
0 12px 34px rgba(20, 32, 51, 0.07);
--pf-v5-global--primary-color--100: #007f8e;
--pf-v5-global--primary-color--200: #006773;
--pf-v5-global--link--Color: #007985;
--pf-v5-global--link--Color--hover: #005963;
--pf-v5-global--BackgroundColor--100: var(--nexus-admin-panel);
--pf-v5-global--BackgroundColor--150: #f8fafb;
--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: var(--nexus-admin-line-strong);
--pf-v5-c-page__main-section--m-light--BackgroundColor: var(--nexus-admin-canvas);
color-scheme: light;
}
.pf-v5-theme-dark {
--nexus-admin-panel: #111d2e;
--nexus-admin-canvas: #08111f;
--nexus-admin-canvas-strong: #0d192a;
--nexus-admin-text: #edf4fb;
--nexus-admin-muted: #a6b5c8;
--nexus-admin-line: #2a3a50;
--nexus-admin-line-strong: #40516a;
--nexus-admin-shadow: 0 1px 2px rgba(0, 0, 0, 0.18), 0 16px 40px rgba(0, 0, 0, 0.28);
--pf-v5-global--primary-color--100: #58d7e3;
--pf-v5-global--primary-color--200: #8de7ee;
--pf-v5-global--link--Color: #68dbe5;
--pf-v5-global--link--Color--hover: #a0edf2;
--pf-v5-global--BackgroundColor--100: var(--nexus-admin-panel);
--pf-v5-global--BackgroundColor--150: #162338;
--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: var(--nexus-admin-line-strong);
--pf-v5-c-page__main-section--m-light--BackgroundColor: var(--nexus-admin-canvas);
color-scheme: dark;
}
html, html,
body, body,
#root { #root {
min-height: 100%; height: 100%;
} }
body { body {
margin: 0; margin: 0;
color: var(--nexus-admin-text);
background: var(--nexus-admin-canvas);
font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
font-size: 15px;
-webkit-font-smoothing: antialiased;
} }
.pf-v5-c-page, #root {
.pf-v5-c-page__main,
.pf-v5-c-page__main-section {
background-color: var(--nexus-admin-canvas);
}
.pf-v5-c-page__main {
position: relative;
isolation: isolate; isolation: isolate;
background-image: radial-gradient(
circle at 94% 9%,
color-mix(in srgb, var(--nexus-admin-signal) 8%, transparent) 0,
transparent 24rem
),
linear-gradient(rgba(41, 73, 104, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(41, 73, 104, 0.025) 1px, transparent 1px);
background-size:
auto,
48px 48px,
48px 48px;
}
.pf-v5-c-masthead {
min-height: 72px;
border-bottom: 1px solid rgba(88, 215, 227, 0.2);
background: linear-gradient(90deg, rgba(88, 215, 227, 0.06), transparent 32%),
var(--nexus-admin-void);
box-shadow: 0 10px 30px rgba(3, 9, 20, 0.22);
}
.pf-v5-c-masthead::after {
position: absolute;
right: 0;
bottom: -1px;
width: min(36vw, 34rem);
height: 1px;
background: linear-gradient(90deg, transparent, var(--nexus-admin-signal-bright));
content: "";
opacity: 0.65;
}
img.keycloak__pageheader_brand {
width: 192px;
height: 42px;
object-fit: contain;
object-position: left center;
}
.pf-v5-c-masthead .pf-v5-c-button.pf-m-link,
.pf-v5-c-masthead .pf-v5-c-button.pf-m-plain {
color: #b9c8da;
}
.pf-v5-c-masthead .pf-v5-c-button:hover {
color: #ffffff;
}
.pf-v5-c-page__sidebar,
.pf-v5-c-page__sidebar-body {
color: #dce8f5;
background: radial-gradient(
circle at 28% 92%,
rgba(88, 215, 227, 0.1),
transparent 15rem
),
linear-gradient(165deg, rgba(93, 99, 216, 0.08), transparent 38%),
var(--nexus-admin-chrome);
}
.pf-v5-c-page__sidebar {
width: 284px;
border-right: 1px solid rgba(88, 215, 227, 0.14);
box-shadow: 12px 0 32px rgba(5, 13, 25, 0.08);
}
.pf-v5-c-page__sidebar-body {
position: relative;
padding: 1.25rem 0.9rem 2rem;
}
.pf-v5-c-page__sidebar-body::before {
display: block;
width: 72px;
height: 10px;
margin: 0.15rem 0.8rem 1.1rem;
background: radial-gradient(
circle,
var(--nexus-admin-signal-bright) 0 2px,
transparent 2.5px
)
left center / 12px 10px repeat-x;
content: "";
opacity: 0.9;
mask-image: linear-gradient(90deg, #000 55%, transparent);
}
.pf-v5-c-nav__section-title {
padding-inline: 1rem;
color: #d9e5f2;
font-family: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
font-size: 1rem;
letter-spacing: 0.01em;
}
.pf-v5-c-nav__section-title .pf-v5-c-label {
margin-top: 0.55rem;
border-color: rgba(88, 215, 227, 0.24);
color: var(--nexus-admin-signal-bright);
background: rgba(88, 215, 227, 0.08);
font-family: "Cascadia Code", Consolas, monospace;
font-size: 0.62rem;
text-transform: uppercase;
}
.pf-v5-c-nav__section-title,
.pf-v5-c-nav__link,
.pf-v5-c-nav__group-title {
color: #9fb0c6;
}
.pf-v5-c-nav__group-title {
padding: 1.25rem 1rem 0.45rem;
font-family: "Cascadia Code", Consolas, monospace;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.pf-v5-c-nav__list {
display: grid;
gap: 0.18rem;
}
.pf-v5-c-nav__link {
position: relative;
min-height: 42px;
padding: 0.68rem 0.9rem 0.68rem 1rem;
border-radius: var(--nexus-admin-radius-sm);
font-size: 0.88rem;
font-weight: 560;
transition:
color 150ms ease,
background-color 150ms ease,
transform 150ms ease;
}
.pf-v5-c-nav__link:hover,
.pf-v5-c-nav__link:focus {
color: #ffffff;
background: rgba(88, 215, 227, 0.07);
transform: translateX(2px);
}
.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(88, 215, 227, 0.16), rgba(93, 99, 216, 0.08));
box-shadow:
inset 3px 0 var(--nexus-admin-signal-bright),
0 6px 18px rgba(0, 0, 0, 0.12);
}
.pf-v5-c-nav__link.pf-m-current::after {
top: 50%;
right: 0.85rem;
width: 5px;
height: 5px;
border: 0;
border-radius: 50%;
background: var(--nexus-admin-mint);
box-shadow: 0 0 0 4px rgba(141, 227, 207, 0.1);
transform: translateY(-50%);
}
.pf-v5-c-page__main-section,
.pf-v5-c-page__main-breadcrumb {
padding-inline: clamp(1.25rem, 3vw, 3.5rem);
}
.pf-v5-c-page__main-section > *,
.pf-v5-c-page__main-breadcrumb > * {
width: 100%;
max-width: 1440px;
margin-inline: auto;
}
.pf-v5-c-page__main-breadcrumb {
border-bottom: 1px solid color-mix(in srgb, var(--nexus-admin-line) 82%, transparent);
background: color-mix(in srgb, var(--nexus-admin-panel) 80%, transparent);
backdrop-filter: blur(16px);
}
.pf-v5-c-page__main-section {
padding-top: clamp(1.5rem, 2.5vw, 2.75rem);
padding-bottom: 4rem;
}
.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.025em;
}
.pf-v5-c-title.pf-m-2xl,
.pf-v5-c-page__main-title {
font-size: clamp(2rem, 3vw, 2.8rem);
line-height: 1.05;
}
.pf-v5-c-title.pf-m-2xl::before,
.pf-v5-c-page__main-title::before {
display: inline-block;
width: 0.48rem;
height: 0.48rem;
margin: 0 0.72rem 0.2rem 0;
border: 2px solid var(--nexus-admin-signal);
content: "";
transform: rotate(45deg);
}
.pf-v5-c-content > p,
.pf-v5-c-helper-text {
color: var(--nexus-admin-muted);
line-height: 1.65;
}
.pf-v5-c-card,
.pf-v5-c-data-list,
.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: var(--nexus-admin-radius-md);
background: var(--nexus-admin-panel);
box-shadow: var(--nexus-admin-shadow);
}
.pf-v5-c-card {
overflow: hidden;
}
.pf-v5-c-card__title {
font-family: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
font-size: 1.12rem;
letter-spacing: -0.012em;
}
.pf-v5-c-table {
overflow: hidden;
border: 1px solid var(--nexus-admin-line);
border-radius: var(--nexus-admin-radius-md);
background: var(--nexus-admin-panel);
box-shadow: 0 8px 28px rgba(20, 32, 51, 0.055);
}
.pf-v5-c-table thead {
color: var(--nexus-admin-muted);
background: color-mix(
in srgb,
var(--nexus-admin-canvas) 64%,
var(--nexus-admin-panel)
);
font-family: "Cascadia Code", Consolas, monospace;
font-size: 0.69rem;
letter-spacing: 0.045em;
text-transform: uppercase;
}
.pf-v5-c-table tr > * {
border-bottom-color: var(--nexus-admin-line);
}
.pf-v5-c-table tbody tr {
transition: background-color 140ms ease;
}
.pf-v5-c-table tbody tr:hover > * {
background-color: color-mix(
in srgb,
var(--nexus-admin-signal) 5%,
var(--nexus-admin-panel)
);
}
.pf-v5-c-toolbar {
border-radius: var(--nexus-admin-radius-md);
}
.pf-v5-c-toolbar__content {
padding-inline: 0;
}
.pf-v5-c-form__label-text {
color: var(--nexus-admin-text);
font-weight: 620;
}
.pf-v5-c-form__helper-text {
color: var(--nexus-admin-muted);
line-height: 1.5;
}
.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: 40px;
border-color: var(--nexus-admin-line-strong);
border-radius: var(--nexus-admin-radius-sm);
color: var(--nexus-admin-text);
background: var(--nexus-admin-panel);
box-shadow: inset 0 1px 1px rgba(20, 32, 51, 0.025);
transition:
border-color 140ms ease,
box-shadow 140ms ease;
}
.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(--nexus-admin-signal) 58%,
var(--nexus-admin-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(--nexus-admin-signal);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--nexus-admin-signal) 15%, transparent);
}
.pf-v5-c-button {
min-height: 38px;
border-radius: var(--nexus-admin-radius-sm);
font-weight: 650;
letter-spacing: 0.005em;
transition:
transform 140ms ease,
box-shadow 140ms ease,
filter 140ms ease;
}
.pf-v5-c-button.pf-m-primary {
color: #ffffff;
background: var(--pf-v5-global--primary-color--100);
box-shadow: 0 7px 18px color-mix(in srgb, var(--nexus-admin-signal) 22%, transparent);
}
.pf-v5-c-button.pf-m-primary:hover {
background: var(--pf-v5-global--primary-color--200);
box-shadow: 0 9px 22px color-mix(in srgb, var(--nexus-admin-signal) 28%, transparent);
transform: translateY(-1px);
}
.pf-v5-c-button.pf-m-secondary {
border-color: var(--nexus-admin-line-strong);
color: var(--nexus-admin-text);
background: var(--nexus-admin-panel);
}
.pf-v5-c-button.pf-m-danger {
background: var(--nexus-admin-danger);
}
.pf-v5-c-tabs {
border-bottom: 1px solid var(--nexus-admin-line);
}
.pf-v5-c-tabs__link {
color: var(--nexus-admin-muted);
font-weight: 620;
}
.pf-v5-c-tabs__item.pf-m-current .pf-v5-c-tabs__link {
color: var(--nexus-admin-text);
}
.pf-v5-c-tabs__link::after {
height: 3px;
border-color: var(--nexus-admin-signal);
}
.pf-v5-c-label {
border-radius: 999px;
font-weight: 620;
}
.pf-v5-c-alert {
border: 1px solid var(--nexus-admin-line);
border-radius: var(--nexus-admin-radius-md);
box-shadow: 0 8px 24px rgba(20, 32, 51, 0.06);
}
.pf-v5-c-empty-state {
border: 1px dashed var(--nexus-admin-line-strong);
border-radius: var(--nexus-admin-radius-md);
background: color-mix(in srgb, var(--nexus-admin-panel) 70%, transparent);
}
.pf-v5-c-pagination {
color: var(--nexus-admin-muted);
}
.pf-v5-c-pagination .pf-v5-c-menu-toggle,
.pf-v5-c-pagination .pf-v5-c-button {
min-height: 34px;
}
.pf-v5-c-modal-box {
overflow: hidden;
}
.pf-v5-c-modal-box__header {
border-bottom: 1px solid var(--nexus-admin-line);
}
.pf-v5-c-switch__input:checked ~ .pf-v5-c-switch__toggle {
background: var(--nexus-admin-signal);
}
.keycloak__form {
max-width: 1160px;
padding-bottom: var(--pf-v5-global--spacer--4xl);
}
.keycloak__form_actions {
position: sticky;
z-index: 15;
bottom: 0;
margin: 2rem calc(-1 * clamp(1.25rem, 3vw, 3.5rem)) -4rem;
padding: 0.9rem clamp(1.25rem, 3vw, 3.5rem);
border-top: 1px solid var(--nexus-admin-line);
background: color-mix(in srgb, var(--nexus-admin-panel) 90%, transparent);
box-shadow: 0 -10px 30px rgba(20, 32, 51, 0.06);
backdrop-filter: blur(16px);
}
input[type="checkbox"],
input[type="radio"] {
accent-color: var(--nexus-admin-signal);
} }
.keycloak__section_intro__help { .keycloak__section_intro__help {
width: fit-content; width: fit-content;
padding: var(--pf-v5-global--spacer--md) 0 var(--pf-v5-global--spacer--lg); padding: var(--pf-v5-global--spacer--md) 0 var(--pf-v5-global--spacer--lg);
color: var(--pf-v5-global--primary-color--100); color: var(--console-primary-deep);
}
div.w-tc-editor {
border: 1px solid var(--nexus-admin-line);
border-radius: var(--nexus-admin-radius-sm);
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
}
:focus-visible {
outline: 2px solid var(--nexus-admin-signal-bright) !important;
outline-offset: 3px;
}
@media (min-width: 769px) {
.pf-v5-c-page__main-section > .pf-v5-c-card,
.pf-v5-c-page__main-section > .pf-v5-c-table,
.pf-v5-c-page__main-section > .pf-v5-c-form {
animation: nexus-admin-arrive 360ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
}
@keyframes nexus-admin-arrive {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.pf-v5-c-masthead {
min-height: 60px;
}
img.keycloak__pageheader_brand {
width: 158px;
height: 36px;
}
.pf-v5-c-page__sidebar {
width: min(88vw, 310px);
}
.pf-v5-c-page__main-section,
.pf-v5-c-page__main-breadcrumb {
padding-inline: 1rem;
}
.pf-v5-c-title.pf-m-2xl,
.pf-v5-c-page__main-title {
font-size: 2rem;
}
.keycloak__form_actions {
margin-inline: -1rem;
padding-inline: 1rem;
}
}
@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;
}
}
/* Keep the admin workspace in step with the class login/account variant. */
html[data-kc-theme-name="class"] {
--nexus-admin-void: #2868c7;
--nexus-admin-chrome: #3f7fd6;
--nexus-admin-panel: #ffffff;
--nexus-admin-canvas: #f7fbff;
--nexus-admin-canvas-strong: #e5f3ff;
--nexus-admin-signal: #f05a47;
--nexus-admin-signal-bright: #f6c54c;
--nexus-admin-indigo: #23966f;
--nexus-admin-mint: #f6c54c;
--nexus-admin-text: #18324a;
--nexus-admin-muted: #617387;
--nexus-admin-line: #c8dbe8;
--nexus-admin-line-strong: #9ebbd0;
--nexus-admin-danger: #c93636;
--nexus-admin-radius-sm: 2px;
--nexus-admin-radius-md: 4px;
--nexus-admin-shadow: 7px 7px 0 rgba(40, 104, 199, 0.1);
--pf-v5-global--primary-color--100: #3f7fd6;
--pf-v5-global--primary-color--200: #2868c7;
--pf-v5-global--link--Color: #2868c7;
--pf-v5-global--link--Color--hover: #174f9c;
--pf-v5-global--BackgroundColor--100: var(--nexus-admin-panel);
--pf-v5-global--BackgroundColor--150: #fbfdff;
--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: var(--nexus-admin-line-strong);
--pf-v5-c-page__main-section--m-light--BackgroundColor: var(--nexus-admin-canvas);
}
html[data-kc-theme-name="class"].pf-v5-theme-dark {
--nexus-admin-void: #0d2742;
--nexus-admin-chrome: #123453;
--nexus-admin-panel: #0f2030;
--nexus-admin-canvas: #08131e;
--nexus-admin-canvas-strong: #173149;
--nexus-admin-signal: #ff7465;
--nexus-admin-signal-bright: #f2c45c;
--nexus-admin-indigo: #54c99d;
--nexus-admin-mint: #f2c45c;
--nexus-admin-text: #e7f1f9;
--nexus-admin-muted: #9ab0c2;
--nexus-admin-line: #355168;
--nexus-admin-line-strong: #58738a;
--nexus-admin-danger: #ff8585;
--nexus-admin-shadow: 7px 7px 0 #142a3e;
--pf-v5-global--primary-color--100: #6da8f6;
--pf-v5-global--primary-color--200: #91bdff;
--pf-v5-global--link--Color: #91bdff;
--pf-v5-global--link--Color--hover: #bdd7ff;
--pf-v5-global--BackgroundColor--150: #14293b;
}
html[data-kc-theme-name="class"] .pf-v5-c-page__main {
background-image: linear-gradient(rgba(24, 50, 74, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(24, 50, 74, 0.035) 1px, transparent 1px);
background-position: 24px 24px;
background-size: 24px 24px;
}
html[data-kc-theme-name="class"] .pf-v5-c-masthead {
border-bottom-color: rgba(246, 197, 76, 0.75);
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), transparent 48%),
var(--nexus-admin-void);
box-shadow: 0 8px 0 rgba(63, 127, 214, 0.12);
}
html[data-kc-theme-name="class"] .pf-v5-c-masthead::after {
width: min(42vw, 38rem);
height: 4px;
background: linear-gradient(
90deg,
var(--nexus-admin-indigo) 0 34%,
var(--nexus-admin-signal-bright) 34% 67%,
var(--nexus-admin-signal) 67%
);
opacity: 1;
}
html[data-kc-theme-name="class"] .pf-v5-c-page__sidebar,
html[data-kc-theme-name="class"] .pf-v5-c-page__sidebar-body {
background: linear-gradient(165deg, rgba(255, 255, 255, 0.08), transparent 42%),
var(--nexus-admin-chrome);
}
html[data-kc-theme-name="class"] .pf-v5-c-page__sidebar {
border-right-color: rgba(246, 197, 76, 0.65);
box-shadow: 8px 0 0 rgba(63, 127, 214, 0.1);
}
html[data-kc-theme-name="class"] .pf-v5-c-nav__link,
html[data-kc-theme-name="class"] .pf-v5-c-nav__section-title,
html[data-kc-theme-name="class"] .pf-v5-c-nav__link.pf-m-expandable {
color: rgba(255, 255, 255, 0.78);
}
html[data-kc-theme-name="class"] .pf-v5-c-nav__link:hover,
html[data-kc-theme-name="class"] .pf-v5-c-nav__link:focus,
html[data-kc-theme-name="class"] .pf-v5-c-nav__link.pf-m-current,
html[data-kc-theme-name="class"] .pf-v5-c-nav__item.pf-m-current > .pf-v5-c-nav__link {
color: #ffffff;
background: linear-gradient(90deg, rgba(246, 197, 76, 0.2), rgba(240, 90, 71, 0.08));
}
html[data-kc-theme-name="class"] .pf-v5-c-card,
html[data-kc-theme-name="class"] .pf-v5-c-modal-box,
html[data-kc-theme-name="class"] .pf-v5-c-empty-state {
border-radius: 4px;
} }
+172
View File
@@ -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;
}
}
+233
View File
@@ -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;
}
}
+678
View File
@@ -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;
}
}
+51
View File
@@ -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>
);
}
+88
View File
@@ -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);
}