keycloakify-starter 亮色改为雾白、冰蓝工作区,不再大面积深色。
暗色使用深海蓝灰,并保持足够的层级和对比度。 class 主题同样支持亮暗模式。 Account/Admin 页头增加模式切换按钮,选择会持久保存。 仍遵守 Keycloak 的 darkMode=false 配置,关闭时只允许亮色。 Account 统一为“页面上下文 → 身份业务区 → 操作区”结构。 Admin 统一为“领域上下文 → 业务内容 → 固定操作区”结构。 表格、表单、详情、向导、弹窗、空状态、安全设备及应用授权组件全部纳入统一设计系统。 移动端自动收起侧栏、双栏变单栏,并调整固定操作区。
This commit is contained in:
+5
-5
@@ -1,17 +1,22 @@
|
||||
# This file is managed by Keycloakify, do not edit it manually.
|
||||
|
||||
# === Owned files start ===
|
||||
# account/colorScheme.ts
|
||||
# account/KcAccountUi.tsx
|
||||
# account/KcContext.ts
|
||||
# account/KcPage.tsx
|
||||
# admin/App.tsx
|
||||
# admin/colorScheme.ts
|
||||
# admin/index.css
|
||||
# admin/KcAdminUi.tsx
|
||||
# admin/page-nav.css
|
||||
# admin/PageHeader.tsx
|
||||
# admin/PageNav.tsx
|
||||
# account/assets/logo.svg
|
||||
# account/personal-info/PersonalInfo.tsx
|
||||
# account/root/Header.tsx
|
||||
# account/root/PageNav.tsx
|
||||
# account/root/Root.tsx
|
||||
# admin/assets/logo.svg
|
||||
# admin/dashboard/dashboard.css
|
||||
# admin/dashboard/Dashboard.tsx
|
||||
@@ -91,7 +96,6 @@
|
||||
/admin/admin-client.tsx
|
||||
/admin/banners.module.css
|
||||
/admin/Banners.tsx
|
||||
/admin/colorScheme.ts
|
||||
/admin/environment-types.ts
|
||||
/admin/environment.ts
|
||||
/admin/ForbiddenSection.tsx
|
||||
@@ -99,7 +103,6 @@
|
||||
/admin/i18next.d.ts
|
||||
/admin/KcContext.ts
|
||||
/admin/KcPage.tsx
|
||||
/admin/PageHeader.tsx
|
||||
/admin/PageHeaderClearCachesModal.tsx
|
||||
/admin/PageNotFoundSection.tsx
|
||||
/admin/Root.tsx
|
||||
@@ -841,7 +844,6 @@
|
||||
|
||||
# === @keycloakify/keycloak-account-ui v260700.0.3 ===
|
||||
/account/api.ts
|
||||
/account/colorScheme.ts
|
||||
/account/environment.ts
|
||||
/account/i18n-type.ts
|
||||
/account/i18next.d.ts
|
||||
@@ -915,8 +917,6 @@
|
||||
/account/resources/SharedWith.tsx
|
||||
/account/resources/ShareTheResource.tsx
|
||||
/account/root/header.module.css
|
||||
/account/root/Header.tsx
|
||||
/account/root/Root.tsx
|
||||
/account/utils/formatDate.ts
|
||||
/account/utils/joinPath.ts
|
||||
/account/utils/useAccountAlerts.ts
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -24,12 +24,12 @@ type PageProps = {
|
||||
|
||||
export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => {
|
||||
return (
|
||||
<>
|
||||
<PageSection variant="light">
|
||||
<div className="nexus-page-intro">
|
||||
<div className="nexus-page-intro__copy">
|
||||
<span className="nexus-page-intro__kicker">
|
||||
NEXUS / Identity control
|
||||
<article className="account-business-page">
|
||||
<PageSection variant="light" className="account-view-header">
|
||||
<div className="account-view-header__layout">
|
||||
<div className="account-view-header__copy">
|
||||
<span className="account-view-header__eyebrow">
|
||||
Identity workspace / Account
|
||||
</span>
|
||||
<TextContent>
|
||||
<Title headingLevel="h1" data-testid="page-heading">
|
||||
@@ -38,14 +38,16 @@ export const Page = ({ title, description, children }: PropsWithChildren<PagePro
|
||||
<Text component="p">{description}</Text>
|
||||
</TextContent>
|
||||
</div>
|
||||
<div className="nexus-page-intro__mark" aria-hidden="true">
|
||||
<span>ID</span>
|
||||
<div className="account-view-header__context" aria-hidden="true">
|
||||
<span>SELF SERVICE</span>
|
||||
<strong>ID</strong>
|
||||
<i />
|
||||
<span>ACCOUNT</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
<PageSection variant="light">{children}</PageSection>
|
||||
</>
|
||||
<PageSection variant="light" className="account-view-body">
|
||||
<div className="account-view-body__surface">{children}</div>
|
||||
</PageSection>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/Header.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import logoSvgUrl from "../assets/logo.svg";
|
||||
import { KeycloakMasthead, label, useEnvironment } from "../../shared/keycloak-ui-shared";
|
||||
import { Button } from "../../shared/@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHref } from "react-router-dom";
|
||||
|
||||
import { AccountEnvironment } from "..";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { ThemeModeToggle } from "../../shared/ThemeModeToggle";
|
||||
|
||||
import style from "./header.module.css";
|
||||
|
||||
const ReferrerLink = () => {
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return environment.referrerUrl ? (
|
||||
<Button
|
||||
data-testid="referrer-link"
|
||||
component="a"
|
||||
href={environment.referrerUrl.replace("_hash_", "#")}
|
||||
variant="link"
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
>
|
||||
{t("backTo", {
|
||||
app: label(t, environment.referrerName, environment.referrerUrl)
|
||||
})}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : "/";
|
||||
const internalLogoHref = useHref(logoUrl);
|
||||
|
||||
// User can indicate that he wants an internal URL by starting it with "/"
|
||||
const indexHref = logoUrl.startsWith("/") ? internalLogoHref : logoUrl;
|
||||
|
||||
return (
|
||||
<KeycloakMasthead
|
||||
data-testid="page-header"
|
||||
keycloak={keycloak}
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: indexHref,
|
||||
src: logoSvgUrl,
|
||||
alt: t("logo"),
|
||||
className: style.brand
|
||||
}}
|
||||
toolbarItems={[
|
||||
<ThemeModeToggle key="theme-mode" />,
|
||||
<ReferrerLink key="link" />
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,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)} />;
|
||||
};
|
||||
+220
-139
@@ -1,7 +1,15 @@
|
||||
/* Account-specific composition on top of the shared console language. */
|
||||
/* 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 {
|
||||
gap: 0.25rem;
|
||||
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);
|
||||
@@ -9,122 +17,255 @@
|
||||
|
||||
.nexus-nav-context__eyebrow {
|
||||
color: var(--console-primary-deep);
|
||||
font-family: inherit;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.11em;
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nexus-nav-context strong {
|
||||
color: var(--console-ink);
|
||||
font-family: inherit;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 720;
|
||||
font-weight: 740;
|
||||
}
|
||||
|
||||
.nexus-nav-context__rule {
|
||||
width: 4.25rem;
|
||||
width: 4.5rem;
|
||||
height: 3px;
|
||||
margin-top: 0.45rem;
|
||||
margin-top: 0.48rem;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--console-teal) 0 34%,
|
||||
var(--console-amber) 34% 67%,
|
||||
var(--console-coral) 67%
|
||||
);
|
||||
opacity: 1;
|
||||
mask-image: none;
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: var(--console-primary);
|
||||
transform: none;
|
||||
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));
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section:first-child {
|
||||
padding-top: clamp(2rem, 4vw, 3.5rem);
|
||||
padding-bottom: clamp(1.5rem, 2.5vw, 2.25rem);
|
||||
border-bottom: 0;
|
||||
.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;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section:first-child::after {
|
||||
display: none;
|
||||
.account-view-header::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
width: 3px;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--console-teal) 0 34%,
|
||||
var(--console-amber) 34% 67%,
|
||||
var(--console-coral) 67%
|
||||
);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nexus-page-intro {
|
||||
align-items: center;
|
||||
.account-view-header__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 2rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.nexus-page-intro__kicker {
|
||||
margin-bottom: 0.65rem;
|
||||
.account-view-header__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-view-header__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 0.62rem;
|
||||
color: var(--console-primary-deep);
|
||||
font-family: inherit;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.11em;
|
||||
}
|
||||
|
||||
.nexus-page-intro__mark {
|
||||
grid-template-columns: auto 3.5rem auto;
|
||||
gap: 0.65rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 999px;
|
||||
color: var(--console-muted);
|
||||
background: color-mix(in srgb, var(--console-surface) 76%, transparent);
|
||||
font-family: inherit;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 22px rgba(23, 50, 77, 0.055);
|
||||
}
|
||||
|
||||
.nexus-page-intro__mark > span:first-child {
|
||||
color: var(--console-primary-deep);
|
||||
font-family: inherit;
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.67rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nexus-page-intro__mark i {
|
||||
.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));
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section + .pf-v5-c-page__main-section {
|
||||
.account-view-body {
|
||||
padding-top: 0;
|
||||
padding-bottom: 5rem;
|
||||
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;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
color: var(--console-ink);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.nexus-identity-card::before {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
width: auto;
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
@@ -135,21 +276,12 @@
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nexus-identity-card::after {
|
||||
right: -4.5rem;
|
||||
bottom: -5.5rem;
|
||||
border-color: color-mix(in srgb, var(--console-primary) 10%, transparent);
|
||||
box-shadow:
|
||||
0 0 0 2.8rem color-mix(in srgb, var(--console-primary) 3%, transparent),
|
||||
0 0 0 5.6rem color-mix(in srgb, var(--console-teal) 2%, transparent);
|
||||
}
|
||||
|
||||
.nexus-identity-card__topline,
|
||||
.nexus-identity-card__name > span,
|
||||
.nexus-identity-card__facts dt,
|
||||
.nexus-identity-card__footer {
|
||||
.nexus-identity-card__footer,
|
||||
.nexus-identity-card__name p {
|
||||
color: var(--console-muted);
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -158,7 +290,6 @@
|
||||
border-radius: 50%;
|
||||
background: var(--console-primary);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--console-primary) 12%, transparent);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.nexus-identity-card__avatar {
|
||||
@@ -170,14 +301,11 @@
|
||||
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);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.nexus-identity-card__avatar span {
|
||||
font-family: inherit;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 760;
|
||||
transform: none;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.nexus-identity-card__name h2,
|
||||
@@ -186,27 +314,14 @@
|
||||
}
|
||||
|
||||
.nexus-identity-card__name h2 {
|
||||
font-family: inherit;
|
||||
font-size: 1.55rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.nexus-identity-card__name p {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.nexus-identity-card__facts > div {
|
||||
border-top-color: var(--console-line);
|
||||
}
|
||||
|
||||
.nexus-profile-editor {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header {
|
||||
padding: 1.55rem clamp(1.25rem, 2.5vw, 2rem);
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
@@ -219,13 +334,11 @@
|
||||
|
||||
.nexus-profile-editor__header > span {
|
||||
color: var(--console-primary-deep);
|
||||
font-family: inherit;
|
||||
font-weight: 750;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header h2 {
|
||||
color: var(--console-ink);
|
||||
font-family: inherit;
|
||||
font-size: 1.28rem;
|
||||
}
|
||||
|
||||
@@ -237,65 +350,33 @@
|
||||
padding: clamp(1.35rem, 3vw, 2.2rem);
|
||||
}
|
||||
|
||||
.nexus-profile-editor .pf-v5-c-form__group {
|
||||
padding-block: 0.45rem;
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card,
|
||||
html[data-kc-theme-name="class"] .nexus-profile-editor,
|
||||
html[data-kc-theme-name="class"] .pf-v5-c-card {
|
||||
border-radius: var(--console-radius);
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card {
|
||||
border-color: var(--console-line);
|
||||
color: var(--console-ink);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card::after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card__topline,
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card__name p,
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card__facts dt,
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card__footer {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card__facts dd {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] .nexus-identity-card__avatar {
|
||||
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);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nexus-page-intro__mark {
|
||||
.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;
|
||||
}
|
||||
|
||||
.nexus-identity-card {
|
||||
position: relative;
|
||||
top: auto;
|
||||
.account-view-body .pf-v5-c-data-list__item-row {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header {
|
||||
grid-template-columns: 1fr;
|
||||
.account-view-body .pf-v5-c-form__actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -81,8 +81,13 @@ export const App = () => {
|
||||
<ErrorBoundaryFallback fallback={ErrorRenderer}>
|
||||
<Suspense fallback={<KeycloakSpinner />}>
|
||||
<AuthWall>
|
||||
<div className="admin-workspace-route">
|
||||
<Outlet />
|
||||
<div
|
||||
className="admin-workspace-route"
|
||||
data-workspace="realm-operations"
|
||||
>
|
||||
<div className="admin-workspace-route__content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</AuthWall>
|
||||
</Suspense>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "admin/PageHeader.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import logoSvgUrl from "./assets/logo.svg";
|
||||
import { KeycloakMasthead, useEnvironment, useHelp } from "../shared/keycloak-ui-shared";
|
||||
import { DropdownItem, ToolbarItem } from "../shared/@patternfly/react-core";
|
||||
import { HelpIcon } from "../shared/@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHref } from "react-router-dom";
|
||||
import { PageHeaderClearCachesModal } from "./PageHeaderClearCachesModal";
|
||||
import { HelpHeader } from "./components/help-enabler/HelpHeader";
|
||||
import { useAccess } from "./context/access/Access";
|
||||
import { useRealm } from "./context/realm-context/RealmContext";
|
||||
import { toDashboard } from "./dashboard/routes/Dashboard";
|
||||
import type { Environment } from "./environment-types";
|
||||
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
|
||||
import { joinPath } from "./utils/joinPath";
|
||||
import { useIsFeatureDisabled, Feature } from "./utils/useIsFeatureEnabled";
|
||||
import useToggle from "./utils/useToggle";
|
||||
import { ThemeModeToggle } from "../shared/ThemeModeToggle";
|
||||
|
||||
const ManageAccountDropdownItem = () => {
|
||||
const { keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const isFeatureDisabled = useIsFeatureDisabled();
|
||||
|
||||
if (isFeatureDisabled(Feature.AccountV3)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key="manage account"
|
||||
id="manage-account"
|
||||
onClick={() => keycloak.accountManagement()}
|
||||
>
|
||||
{t("manageAccount")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerInfoDropdownItem = () => {
|
||||
const { realm } = useRealm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key="server info"
|
||||
component={props => <Link {...props} to={toDashboard({ realm })} />}
|
||||
>
|
||||
{t("realmInfo")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ClearCachesDropdownItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, toggleModal] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownItem key="clear caches" onClick={() => toggleModal()}>
|
||||
{t("clearCachesTitle")}
|
||||
</DropdownItem>
|
||||
{open && <PageHeaderClearCachesModal onClose={() => toggleModal()} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const HelpDropdownItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const { enabled, toggleHelp } = useHelp();
|
||||
return (
|
||||
<DropdownItem data-testId="helpIcon" icon={<HelpIcon />} onClick={toggleHelp}>
|
||||
{enabled ? t("helpEnabled") : t("helpDisabled")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const kebabDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
|
||||
<ManageAccountDropdownItem key="kebab Manage Account" />,
|
||||
<ServerInfoDropdownItem key="kebab Server Info" />,
|
||||
...(isMasterRealm && isManager
|
||||
? [<ClearCachesDropdownItem key="Clear Caches" />]
|
||||
: []),
|
||||
<HelpDropdownItem key="kebab Help" />
|
||||
];
|
||||
|
||||
const userDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
|
||||
<ManageAccountDropdownItem key="Manage Account" />,
|
||||
<ServerInfoDropdownItem key="Server info" />,
|
||||
...(isMasterRealm && isManager
|
||||
? [<ClearCachesDropdownItem key="Clear Caches" />]
|
||||
: [])
|
||||
];
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment<Environment>();
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const contextLogo = usePreviewLogo();
|
||||
const customLogo = contextLogo?.logo;
|
||||
|
||||
const isMasterRealm = realm === environment.masterRealm;
|
||||
const isManager = hasAccess("manage-realm");
|
||||
|
||||
const url = useHref(toDashboard({ realm }));
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : url;
|
||||
|
||||
return (
|
||||
<KeycloakMasthead
|
||||
data-testid="page-header"
|
||||
keycloak={keycloak}
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: logoUrl,
|
||||
src: customLogo?.trim() ? customLogo : logoSvgUrl,
|
||||
alt: t("logo"),
|
||||
className: "keycloak__pageheader_brand"
|
||||
}}
|
||||
dropdownItems={userDropdownItems(isMasterRealm, isManager)}
|
||||
kebabDropdownItems={kebabDropdownItems(isMasterRealm, isManager)}
|
||||
toolbarItems={[
|
||||
<ToolbarItem key="theme-mode">
|
||||
<ThemeModeToggle />
|
||||
</ToolbarItem>,
|
||||
<ToolbarItem
|
||||
key="help"
|
||||
align={{ default: "alignRight" }}
|
||||
visibility={{
|
||||
default: "hidden",
|
||||
md: "visible"
|
||||
}} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */
|
||||
>
|
||||
<HelpHeader />
|
||||
</ToolbarItem>
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
@@ -17,16 +17,21 @@
|
||||
min-height: 260px;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 20px;
|
||||
color: #f7fbff;
|
||||
background: linear-gradient(135deg, #193a5b, #245a89 58%, #277e78);
|
||||
box-shadow: 0 24px 52px rgba(23, 50, 77, 0.16);
|
||||
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(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
||||
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%);
|
||||
@@ -65,12 +70,12 @@
|
||||
}
|
||||
|
||||
.admin-dashboard-eyebrow {
|
||||
color: #92e3cf;
|
||||
color: var(--console-teal);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero h1 {
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
color: var(--console-ink);
|
||||
font-size: clamp(2.4rem, 5vw, 4.8rem);
|
||||
font-weight: 740;
|
||||
letter-spacing: -0.055em;
|
||||
@@ -80,7 +85,7 @@
|
||||
.admin-dashboard-hero__copy > p {
|
||||
max-width: 60ch;
|
||||
margin: 1.25rem 0 1.6rem;
|
||||
color: rgba(235, 246, 255, 0.78);
|
||||
color: var(--console-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
@@ -90,18 +95,18 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.65rem 0.9rem;
|
||||
color: rgba(235, 246, 255, 0.62);
|
||||
color: var(--console-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__meta strong {
|
||||
color: #ffffff;
|
||||
color: var(--console-ink);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero .admin-dashboard-status {
|
||||
color: rgba(235, 246, 255, 0.82);
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal {
|
||||
@@ -111,12 +116,12 @@
|
||||
align-content: center;
|
||||
justify-items: end;
|
||||
padding: clamp(1.5rem, 3vw, 2.5rem);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.13);
|
||||
border-left: 1px solid var(--console-line);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal > span {
|
||||
color: rgba(235, 246, 255, 0.48);
|
||||
color: var(--console-faint);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
@@ -126,11 +131,11 @@
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 0.75rem 0 1rem;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.38));
|
||||
background: linear-gradient(90deg, transparent, var(--console-line-strong));
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal strong {
|
||||
color: #ffffff;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: clamp(3.5rem, 7vw, 6.5rem);
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.08em;
|
||||
@@ -139,7 +144,7 @@
|
||||
|
||||
.admin-dashboard-hero__signal small {
|
||||
margin-top: 0.75rem;
|
||||
color: rgba(235, 246, 255, 0.66);
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.admin-dashboard-layout {
|
||||
@@ -361,6 +366,41 @@
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
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);
|
||||
@@ -130,6 +135,82 @@
|
||||
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;
|
||||
@@ -149,4 +230,9 @@
|
||||
.admin-view-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-form__actions,
|
||||
.admin-workspace-route .keycloak__form_actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
+117
-19
@@ -5,20 +5,22 @@
|
||||
*/
|
||||
|
||||
:root {
|
||||
--console-ink: #17324d;
|
||||
--console-muted: #667a8e;
|
||||
--console-faint: #8da0b1;
|
||||
--console-canvas: #f3f7fb;
|
||||
--console-ink: #15334c;
|
||||
--console-muted: #5f768a;
|
||||
--console-faint: #8399aa;
|
||||
--console-canvas: #edf5fa;
|
||||
--console-surface: #ffffff;
|
||||
--console-surface-soft: #f8fbfe;
|
||||
--console-nav: #edf4fa;
|
||||
--console-line: #d9e4ed;
|
||||
--console-line-strong: #b9cbd9;
|
||||
--console-primary: #356fd6;
|
||||
--console-primary-deep: #2459b4;
|
||||
--console-teal: #279477;
|
||||
--console-amber: #e9ad38;
|
||||
--console-coral: #e86452;
|
||||
--console-surface-soft: #f6fafc;
|
||||
--console-surface-raised: #ffffff;
|
||||
--console-surface-sunken: #e7f0f6;
|
||||
--console-nav: #e5f0f7;
|
||||
--console-line: #d2e0e9;
|
||||
--console-line-strong: #adc3d2;
|
||||
--console-primary: #2f72d6;
|
||||
--console-primary-deep: #1f58ad;
|
||||
--console-teal: #1e9476;
|
||||
--console-amber: #d99b25;
|
||||
--console-coral: #dd5c4b;
|
||||
--console-danger: #c94046;
|
||||
--console-radius-sm: 8px;
|
||||
--console-radius: 14px;
|
||||
@@ -43,12 +45,14 @@
|
||||
--console-ink: #eaf2f8;
|
||||
--console-muted: #a4b5c4;
|
||||
--console-faint: #8295a7;
|
||||
--console-canvas: #0d1823;
|
||||
--console-surface: #142332;
|
||||
--console-surface-soft: #182a3a;
|
||||
--console-nav: #101f2d;
|
||||
--console-line: #294052;
|
||||
--console-line-strong: #456176;
|
||||
--console-canvas: #09151f;
|
||||
--console-surface: #122331;
|
||||
--console-surface-soft: #172b3a;
|
||||
--console-surface-raised: #192d3d;
|
||||
--console-surface-sunken: #0d1d29;
|
||||
--console-nav: #0d1d29;
|
||||
--console-line: #294354;
|
||||
--console-line-strong: #49667a;
|
||||
--console-primary: #78a8ff;
|
||||
--console-primary-deep: #96baff;
|
||||
--console-teal: #61c7aa;
|
||||
@@ -75,6 +79,8 @@ html[data-kc-theme-name="class"] {
|
||||
--console-muted: #61778c;
|
||||
--console-canvas: #f4f9fd;
|
||||
--console-nav: #edf6fc;
|
||||
--console-surface-raised: #ffffff;
|
||||
--console-surface-sunken: #e8f3fa;
|
||||
--console-line: #d4e3ee;
|
||||
--console-line-strong: #abc4d7;
|
||||
--console-primary: #3f7fd6;
|
||||
@@ -91,6 +97,8 @@ html[data-kc-theme-name="class"].pf-v5-theme-dark {
|
||||
--console-surface: #102435;
|
||||
--console-surface-soft: #152c3e;
|
||||
--console-nav: #0d2132;
|
||||
--console-surface-raised: #193246;
|
||||
--console-surface-sunken: #0a1824;
|
||||
--console-line: #2d485d;
|
||||
--console-line-strong: #4d6c82;
|
||||
--console-primary: #76aafa;
|
||||
@@ -115,6 +123,16 @@ body {
|
||||
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,
|
||||
@@ -190,6 +208,31 @@ img.keycloak__pageheader_brand {
|
||||
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 {
|
||||
@@ -345,6 +388,24 @@ img.keycloak__pageheader_brand {
|
||||
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 {
|
||||
@@ -390,6 +451,10 @@ img.keycloak__pageheader_brand {
|
||||
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;
|
||||
}
|
||||
@@ -459,6 +524,12 @@ img.keycloak__pageheader_brand {
|
||||
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;
|
||||
@@ -505,6 +576,24 @@ input[type="radio"] {
|
||||
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);
|
||||
@@ -538,6 +627,15 @@ div.w-tc-editor {
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "./@patternfly/react-core";
|
||||
import { MoonIcon, SunIcon } from "./@patternfly/react-icons";
|
||||
import {
|
||||
getConsoleColorMode,
|
||||
subscribeToConsoleColorMode,
|
||||
toggleConsoleColorMode
|
||||
} from "./consoleColorScheme";
|
||||
|
||||
function labels(isDark: boolean) {
|
||||
const isChinese = document.documentElement.lang.toLowerCase().startsWith("zh");
|
||||
|
||||
if (isChinese) {
|
||||
return {
|
||||
current: isDark ? "暗色" : "亮色",
|
||||
action: isDark ? "切换至亮色模式" : "切换至暗色模式"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
current: isDark ? "Dark" : "Light",
|
||||
action: isDark ? "Switch to light mode" : "Switch to dark mode"
|
||||
};
|
||||
}
|
||||
|
||||
export function ThemeModeToggle() {
|
||||
const [mode, setMode] = useState(getConsoleColorMode);
|
||||
|
||||
useEffect(() => subscribeToConsoleColorMode(setMode), []);
|
||||
|
||||
if (document.documentElement.dataset.consoleDarkAllowed === "false") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDark = mode === "dark";
|
||||
const copy = labels(isDark);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="plain"
|
||||
className="console-theme-toggle"
|
||||
aria-label={copy.action}
|
||||
title={copy.action}
|
||||
aria-pressed={isDark}
|
||||
icon={isDark ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleConsoleColorMode}
|
||||
>
|
||||
<span className="console-theme-toggle__label">{copy.current}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
export type ConsoleColorMode = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "nexus-console-color-mode";
|
||||
const DARK_THEME_CLASS = "pf-v5-theme-dark";
|
||||
const COLOR_MODE_EVENT = "nexus:color-mode-change";
|
||||
|
||||
function readStoredMode(): ConsoleColorMode | undefined {
|
||||
try {
|
||||
const value = localStorage.getItem(STORAGE_KEY);
|
||||
return value === "light" || value === "dark" ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function preferredMode(): ConsoleColorMode {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function getConsoleColorMode(): ConsoleColorMode {
|
||||
return document.documentElement.classList.contains(DARK_THEME_CLASS)
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export function setConsoleColorMode(
|
||||
mode: ConsoleColorMode,
|
||||
options: { persist?: boolean } = {}
|
||||
) {
|
||||
const { persist = true } = options;
|
||||
const isDark = mode === "dark";
|
||||
const root = document.documentElement;
|
||||
|
||||
document.getElementById("root-color-scheme-style")?.remove();
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "root-color-scheme-style";
|
||||
style.textContent = `:root { color-scheme: ${mode}; }`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
root.style.removeProperty("background-color");
|
||||
root.classList.toggle(DARK_THEME_CLASS, isDark);
|
||||
root.dataset.consoleColorMode = mode;
|
||||
|
||||
if (persist) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// Storage can be unavailable in hardened browser sessions.
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(COLOR_MODE_EVENT, { detail: mode }));
|
||||
}
|
||||
|
||||
export function startConsoleColorScheme(darkModeAllowed: boolean) {
|
||||
const root = document.documentElement;
|
||||
root.dataset.consoleDarkAllowed = String(darkModeAllowed);
|
||||
|
||||
if (!darkModeAllowed) {
|
||||
setConsoleColorMode("light", { persist: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setConsoleColorMode(readStoredMode() ?? preferredMode(), { persist: false });
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mediaQuery.addEventListener("change", event => {
|
||||
if (!readStoredMode()) {
|
||||
setConsoleColorMode(event.matches ? "dark" : "light", {
|
||||
persist: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleConsoleColorMode() {
|
||||
setConsoleColorMode(getConsoleColorMode() === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
export function subscribeToConsoleColorMode(listener: (mode: ConsoleColorMode) => void) {
|
||||
const handleChange = (event: Event) => {
|
||||
listener((event as CustomEvent<ConsoleColorMode>).detail);
|
||||
};
|
||||
|
||||
window.addEventListener(COLOR_MODE_EVENT, handleChange);
|
||||
return () => window.removeEventListener(COLOR_MODE_EVENT, handleChange);
|
||||
}
|
||||
Reference in New Issue
Block a user