新增真实账户数据驱动的身份档案卡

个人资料改为“身份卡 + 编辑工作区”双栏布局
侧栏加入语义图标、产品域标题和更明确的选中状态
页面首屏增加身份控制台层级标识
用户名、邮箱、姓名、语言会随表单内容实时呈现
390px 移动端自动堆叠,无横向溢出
相关组件已由 Keycloakify 正式认领,后续同步不会覆盖
This commit is contained in:
2026-08-04 15:42:46 +08:00 Unverified
parent 78743fb436
commit c1b36b4d72
6 changed files with 1763 additions and 262 deletions
+3 -3
View File
@@ -7,6 +7,8 @@
# admin/index.css
# admin/KcAdminUi.tsx
# account/assets/logo.svg
# account/personal-info/PersonalInfo.tsx
# account/root/PageNav.tsx
# admin/assets/logo.svg
# admin/i18n/messages_en.properties
# admin/i18n/messages_zh_Hans.properties
@@ -15,6 +17,7 @@
# email/messages/messages_en.properties
# email/messages/messages_zh_CN.properties
# email/messages/messages_zh_TW.properties
# account/components/page/Page.tsx
# === Owned files end =====
# === @keycloakify/email-native v260007.0.0 ===
@@ -904,7 +907,6 @@
/account/i18n/messages_zh_Hans.properties
/account/i18n/messages_zh_Hant.properties
/account/organizations/Organizations.tsx
/account/personal-info/PersonalInfo.tsx
/account/resources/EditTheResource.tsx
/account/resources/PermissionRequest.tsx
/account/resources/Resources.tsx
@@ -914,7 +916,6 @@
/account/resources/ShareTheResource.tsx
/account/root/header.module.css
/account/root/Header.tsx
/account/root/PageNav.tsx
/account/root/Root.tsx
/account/utils/formatDate.ts
/account/utils/joinPath.ts
@@ -1074,4 +1075,3 @@
/account/assets/passkeys/zoho-light.svg
/account/assets/passkeys/ztpass-light.png
/account/components/datalist/EmptyRow.tsx
/account/components/page/Page.tsx
+51
View File
@@ -0,0 +1,51 @@
/**
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
* To relinquish ownership and restore this file to its original content, run the following command:
*
* $ npx keycloakify own --path "account/components/page/Page.tsx" --revert
*/
/* eslint-disable */
// @ts-nocheck
import {
PageSection,
Text,
TextContent,
Title
} from "../../../shared/@patternfly/react-core";
import { PropsWithChildren } from "react";
type PageProps = {
title: string;
description: string;
};
export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => {
return (
<>
<PageSection variant="light">
<div className="nexus-page-intro">
<div className="nexus-page-intro__copy">
<span className="nexus-page-intro__kicker">
NEXUS / Identity control
</span>
<TextContent>
<Title headingLevel="h1" data-testid="page-heading">
{title}
</Title>
<Text component="p">{description}</Text>
</TextContent>
</div>
<div className="nexus-page-intro__mark" aria-hidden="true">
<span>ID</span>
<i />
<span>ACCOUNT</span>
</div>
</div>
</PageSection>
<PageSection variant="light">{children}</PageSection>
</>
);
};
+259
View File
@@ -0,0 +1,259 @@
/**
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
* To relinquish ownership and restore this file to its original content, run the following command:
*
* $ npx keycloakify own --path "account/personal-info/PersonalInfo.tsx" --revert
*/
/* eslint-disable */
// @ts-nocheck
import {
UserProfileFields,
beerify,
debeerify,
setUserProfileServerError,
useEnvironment
} from "../../shared/keycloak-ui-shared";
import {
ActionGroup,
Alert,
AlertVariant,
Button,
ExpandableSection,
Form,
Spinner
} from "../../shared/@patternfly/react-core";
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
import { TFunction } from "i18next";
import { useState } from "react";
import { ErrorOption, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { getPersonalInfo, getSupportedLocales, savePersonalInfo } from "../api/methods";
import { UserProfileMetadata, UserRepresentation } from "../api/representations";
import { Page } from "../components/page/Page";
import { type AccountEnvironment } from "..";
import type { TFuncKey } from "../i18n-type";
import { useAccountAlerts } from "../utils/useAccountAlerts";
import { usePromise } from "../utils/usePromise";
export const PersonalInfo = () => {
const { t } = useTranslation();
const context = useEnvironment<AccountEnvironment>();
const [userProfileMetadata, setUserProfileMetadata] = useState<UserProfileMetadata>();
const [supportedLocales, setSupportedLocales] = useState<string[]>([]);
const form = useForm<UserRepresentation>({ mode: "onChange" });
const { handleSubmit, reset, setValue, setError } = form;
const profile = form.watch();
const { addAlert } = useAccountAlerts();
usePromise(
signal =>
Promise.all([
getPersonalInfo({ signal, context }),
getSupportedLocales({ signal, context })
]),
([personalInfo, supportedLocales]) => {
setUserProfileMetadata(personalInfo.userProfileMetadata);
setSupportedLocales(supportedLocales);
reset(personalInfo);
Object.entries(personalInfo.attributes || {}).forEach(([k, v]) =>
setValue(`attributes[${beerify(k)}]`, v)
);
}
);
const onSubmit = async (user: UserRepresentation) => {
try {
const attributes = Object.fromEntries(
Object.entries(user.attributes || {}).map(([k, v]) => [debeerify(k), v])
);
await savePersonalInfo(context, { ...user, attributes });
const locale = attributes["locale"]?.toString();
if (locale) {
window.dispatchEvent(
new CustomEvent("languageChanged", { detail: { language: locale } })
);
}
await context.keycloak.updateToken();
addAlert(t("accountUpdatedMessage"));
} catch (error) {
addAlert(t("accountUpdatedError"), AlertVariant.danger);
setUserProfileServerError(
{ responseData: { errors: error as any } },
(name: string | number, error: unknown) =>
setError(name as string, error as ErrorOption),
((key: TFuncKey, param?: object) => t(key, param as any)) as TFunction
);
}
};
if (!userProfileMetadata) {
return <Spinner />;
}
const allFieldsReadOnly = () =>
userProfileMetadata.attributes
.map(a => a.readOnly)
.reduce((p, c) => p && c, true);
const {
updateEmailFeatureEnabled,
updateEmailActionEnabled,
isRegistrationEmailAsUsername,
isEditUserNameAllowed
} = context.environment.features;
const displayName =
[profile.firstName, profile.lastName].filter(Boolean).join(" ").trim() ||
profile.username ||
t("personalInfo");
const initials =
[profile.firstName, profile.lastName]
.filter(Boolean)
.map(value => value!.trim().charAt(0))
.join("")
.slice(0, 2)
.toUpperCase() || displayName.slice(0, 2).toUpperCase();
const profileLocale =
profile.attributes?.locale?.toString() || context.environment.locale;
return (
<Page title={t("personalInfo")} description={t("personalInfoDescription")}>
<div className="nexus-profile-layout">
<aside className="nexus-identity-card" aria-label={t("personalInfo")}>
<div className="nexus-identity-card__topline">
<span>Identity record</span>
<i aria-hidden="true" />
</div>
<div className="nexus-identity-card__avatar" aria-hidden="true">
<span>{initials}</span>
</div>
<div className="nexus-identity-card__name">
<span>{t("personalInfo")}</span>
<h2>{displayName}</h2>
<p>{profile.email || profile.username}</p>
</div>
<dl className="nexus-identity-card__facts">
<div>
<dt>{t("username")}</dt>
<dd>{profile.username || "—"}</dd>
</div>
<div>
<dt>{t("email")}</dt>
<dd>{profile.email || "—"}</dd>
</div>
<div>
<dt>Locale</dt>
<dd>{profileLocale}</dd>
</div>
</dl>
<div className="nexus-identity-card__footer">
<span aria-hidden="true" />
NEXUS identity directory
</div>
</aside>
<section className="nexus-profile-editor">
<header className="nexus-profile-editor__header">
<span>Profile data</span>
<div>
<h2>{t("personalInfo")}</h2>
<p>{t("personalInfoDescription")}</p>
</div>
</header>
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
<UserProfileFields
form={form}
userProfileMetadata={userProfileMetadata}
supportedLocales={supportedLocales}
currentLocale={context.environment.locale}
t={
((key: unknown, params) =>
t(key as TFuncKey, params as any)) as TFunction
}
renderer={attribute => {
const annotations = attribute.annotations
? attribute.annotations
: {};
return attribute.name === "email" &&
updateEmailFeatureEnabled &&
updateEmailActionEnabled &&
annotations["kc.required.action.supported"] &&
(!isRegistrationEmailAsUsername ||
isEditUserNameAllowed) ? (
<Button
id="update-email-btn"
variant="link"
onClick={() =>
context.keycloak.login({
action: "UPDATE_EMAIL"
})
}
icon={<ExternalLinkSquareAltIcon />}
iconPosition="right"
>
{t("updateEmail")}
</Button>
) : undefined;
}}
/>
{!allFieldsReadOnly() && (
<ActionGroup>
<Button
data-testid="save"
type="submit"
id="save-btn"
variant="primary"
>
{t("save")}
</Button>
<Button
data-testid="cancel"
id="cancel-btn"
variant="link"
onClick={() => reset()}
>
{t("cancel")}
</Button>
</ActionGroup>
)}
{context.environment.features.deleteAccountAllowed && (
<ExpandableSection
data-testid="delete-account"
toggleText={t("deleteAccount")}
>
<Alert
isInline
title={t("deleteAccount")}
variant="danger"
actionLinks={
<Button
id="delete-account-btn"
variant="danger"
onClick={() =>
context.keycloak.login({
action: "delete_account"
})
}
className="delete-button"
>
{t("delete")}
</Button>
}
>
{t("deleteAccountWarning")}
</Alert>
</ExpandableSection>
)}
</Form>
</section>
</div>
</Page>
);
};
export default PersonalInfo;
+209
View File
@@ -0,0 +1,209 @@
/**
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
* To relinquish ownership and restore this file to its original content, run the following command:
*
* $ npx keycloakify own --path "account/root/PageNav.tsx" --revert
*/
/* eslint-disable */
// @ts-nocheck
import { useEnvironment } from "../../shared/keycloak-ui-shared";
import {
Nav,
NavExpandable,
NavItem,
NavList,
PageSidebar,
PageSidebarBody,
Spinner
} from "../../shared/@patternfly/react-core";
import {
PropsWithChildren,
MouseEvent as ReactMouseEvent,
Suspense,
useMemo,
useState
} from "react";
import { useTranslation } from "react-i18next";
import { matchPath, useHref, useLinkClickHandler, useLocation } from "react-router-dom";
import {
ApplicationsIcon,
BuildingIcon,
DesktopIcon,
FolderOpenAltIcon,
KeyIcon,
LinkIcon,
ShieldAltIcon,
UserAltIcon,
UserFriendsIcon
} from "../../shared/@patternfly/react-icons";
import fetchContentJson from "../content/fetchContent";
import type { TFuncKey } from "../i18n-type";
import type { AccountEnvironment, Feature } from "..";
import { usePromise } from "../utils/usePromise";
type RootMenuItem = {
id?: string;
label: TFuncKey;
path: string;
isVisible?: keyof Feature;
modulePath?: string;
};
type MenuItemWithChildren = {
label: TFuncKey;
children: MenuItem[];
isVisible?: keyof Feature;
};
export type MenuItem = RootMenuItem | MenuItemWithChildren;
export const PageNav = () => {
const [menuItems, setMenuItems] = useState<MenuItem[]>();
const context = useEnvironment<AccountEnvironment>();
usePromise(signal => fetchContentJson({ signal, context }), setMenuItems);
return (
<PageSidebar>
<PageSidebarBody>
<div className="nexus-nav-context">
<span className="nexus-nav-context__eyebrow">NEXUS / ID</span>
<strong>Identity control</strong>
<span className="nexus-nav-context__rule" aria-hidden="true" />
</div>
<Nav>
<NavList>
<Suspense fallback={<Spinner />}>
{menuItems
?.filter(menuItem =>
menuItem.isVisible
? context.environment.features[menuItem.isVisible]
: true
)
.map(menuItem => (
<NavMenuItem
key={menuItem.label as string}
menuItem={menuItem}
/>
))}
</Suspense>
</NavList>
</Nav>
</PageSidebarBody>
</PageSidebar>
);
};
const menuIcons: Record<string, JSX.Element> = {
personalInfo: <UserAltIcon />,
accountSecurity: <ShieldAltIcon />,
signingIn: <KeyIcon />,
deviceActivity: <DesktopIcon />,
linkedAccounts: <LinkIcon />,
applications: <ApplicationsIcon />,
verifiableCredentials: <ShieldAltIcon />,
groups: <UserFriendsIcon />,
organizations: <BuildingIcon />,
resources: <FolderOpenAltIcon />
};
const NavLabel = ({ label }: { label: TFuncKey }) => {
const { t } = useTranslation();
return (
<span className="nexus-nav-label">
<span className="nexus-nav-label__icon" aria-hidden="true">
{menuIcons[label as string] ?? <UserAltIcon />}
</span>
<span>{t(label)}</span>
</span>
);
};
type NavMenuItemProps = {
menuItem: MenuItem;
};
function NavMenuItem({ menuItem }: NavMenuItemProps) {
const { t } = useTranslation();
const { environment } = useEnvironment<AccountEnvironment>();
const { pathname } = useLocation();
const isActive = useMemo(
() => matchMenuItem(pathname, menuItem, environment.baseUrl),
[pathname, menuItem, environment.baseUrl]
);
if ("path" in menuItem) {
return (
<NavLink path={menuItem.path} isActive={isActive}>
<NavLabel label={menuItem.label} />
</NavLink>
);
}
return (
<NavExpandable
data-testid={menuItem.label}
title={<NavLabel label={menuItem.label} />}
isActive={isActive}
isExpanded={isActive}
>
{menuItem.children
.filter(menuItem =>
menuItem.isVisible ? environment.features[menuItem.isVisible] : true
)
.map(child => (
<NavMenuItem key={child.label as string} menuItem={child} />
))}
</NavExpandable>
);
}
function getFullUrl(path: string, baseUrl: string) {
return `${new URL(baseUrl).pathname}${path}`;
}
function matchMenuItem(
currentPath: string,
menuItem: MenuItem,
baseUrl: string
): boolean {
if ("path" in menuItem) {
return !!matchPath(getFullUrl(menuItem.path, baseUrl), currentPath);
}
return menuItem.children.some(child => matchMenuItem(currentPath, child, baseUrl));
}
type NavLinkProps = {
path: string;
isActive: boolean;
};
export const NavLink = ({
path,
isActive,
children
}: PropsWithChildren<NavLinkProps>) => {
const { environment } = useEnvironment<AccountEnvironment>();
const menuItemPath = getFullUrl(path, environment.baseUrl) + location.search;
const href = useHref(menuItemPath);
const handleClick = useLinkClickHandler(menuItemPath);
return (
<NavItem
data-testid={path}
to={href}
isActive={isActive}
onClick={event =>
// PatternFly does not have the correct type for this event, so we need to cast it.
handleClick(event as unknown as ReactMouseEvent<HTMLAnchorElement>)
}
>
{children}
</NavItem>
);
};
+793 -138
View File
File diff suppressed because it is too large Load Diff
+448 -121
View File
@@ -1,45 +1,57 @@
:root {
--nexus-admin-void: #070b16;
--nexus-admin-chrome: #0b1120;
--nexus-admin-void: #07101e;
--nexus-admin-chrome: #0b1728;
--nexus-admin-panel: #ffffff;
--nexus-admin-canvas: #f3f6fb;
--nexus-admin-ice: #2dc7e5;
--nexus-admin-violet: #7569f2;
--nexus-admin-text: #172033;
--nexus-admin-muted: #63708a;
--nexus-admin-line: #dce3ef;
--pf-v5-global--primary-color--100: #007f9b;
--pf-v5-global--primary-color--200: #00677e;
--pf-v5-global--link--Color: #00758d;
--pf-v5-global--link--Color--hover: #004f62;
--nexus-admin-canvas: #f2f5f8;
--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: #f7f9fc;
--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: #c9d3e2;
--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: #101827;
--nexus-admin-canvas: #080d18;
--nexus-admin-text: #edf3ff;
--nexus-admin-muted: #a6b2c8;
--nexus-admin-line: #2d3b52;
--pf-v5-global--primary-color--100: #55d9ef;
--pf-v5-global--primary-color--200: #82e5f5;
--pf-v5-global--link--Color: #67dcef;
--pf-v5-global--link--Color--hover: #a6effa;
--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: #151f30;
--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: #3a4a63;
--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;
}
@@ -51,9 +63,12 @@ body,
}
body {
margin: 0;
color: var(--nexus-admin-text);
background: var(--nexus-admin-canvas);
font-family: "Segoe UI Variable", "Segoe UI", sans-serif;
font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
font-size: 15px;
-webkit-font-smoothing: antialiased;
}
.pf-v5-c-page,
@@ -63,73 +78,195 @@ body {
}
.pf-v5-c-page__main {
background-image: linear-gradient(rgba(32, 62, 104, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(32, 62, 104, 0.025) 1px, transparent 1px);
background-size: 44px 44px;
position: relative;
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 {
border-bottom: 1px solid rgba(99, 230, 255, 0.18);
background: linear-gradient(90deg, rgba(99, 230, 255, 0.035), transparent 36%),
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 8px 28px rgba(5, 9, 21, 0.18);
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: 190px;
height: 38px;
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 {
border-right: 1px solid rgba(117, 105, 242, 0.16);
color: #d8e2f5;
color: #dce8f5;
background: radial-gradient(
circle at 20% 88%,
rgba(99, 230, 255, 0.07),
transparent 14rem
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 {
border-radius: 2px;
color: #9daac3;
font-size: 0.86rem;
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(99, 230, 255, 0.06);
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(99, 230, 255, 0.15),
rgba(117, 105, 242, 0.08)
);
box-shadow: inset 2px 0 var(--nexus-admin-ice);
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 {
border-color: transparent;
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-card,
.pf-v5-c-modal-box,
.pf-v5-c-drawer__panel,
.pf-v5-c-popover__content,
.pf-v5-c-menu {
border: 1px solid var(--nexus-admin-line);
border-radius: 3px;
background: var(--nexus-admin-panel);
box-shadow: 0 15px 42px rgba(27, 43, 72, 0.08);
.pf-v5-c-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,
@@ -138,115 +275,252 @@ img.keycloak__pageheader_brand {
.pf-v5-c-page__main-title {
color: var(--nexus-admin-text);
font-family: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
letter-spacing: -0.018em;
letter-spacing: -0.025em;
}
.pf-v5-c-page__main-breadcrumb {
border-bottom: 1px solid rgba(220, 227, 239, 0.74);
background: color-mix(in srgb, var(--nexus-admin-panel) 72%, transparent);
backdrop-filter: blur(12px);
.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-button {
border-radius: 2px;
font-weight: 620;
.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-button.pf-m-primary {
color: #ffffff;
background: linear-gradient(105deg, #007f9b, #087f91 58%, #655bd7);
box-shadow: 0 6px 18px rgba(0, 127, 155, 0.16);
.pf-v5-c-content > p,
.pf-v5-c-helper-text {
color: var(--nexus-admin-muted);
line-height: 1.65;
}
.pf-v5-c-button.pf-m-primary:hover {
filter: brightness(1.08);
}
.pf-v5-c-form-control,
.pf-v5-c-select__toggle,
.pf-v5-c-menu-toggle {
border-radius: 2px;
border-color: var(--pf-v5-global--BorderColor--200);
color: var(--nexus-admin-text);
.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: none;
box-shadow: var(--nexus-admin-shadow);
}
.pf-v5-c-form-control:focus-within,
.pf-v5-c-menu-toggle:focus {
border-color: #008eaa;
box-shadow: 0 0 0 3px rgba(45, 199, 229, 0.12);
.pf-v5-c-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-panel) 72%,
var(--nexus-admin-canvas)
var(--nexus-admin-canvas) 64%,
var(--nexus-admin-panel)
);
font-family: "Cascadia Code", Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.025em;
font-size: 0.69rem;
letter-spacing: 0.045em;
text-transform: uppercase;
}
.pf-v5-c-table tr:hover > * {
.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-ice) 7%,
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: #007f8e;
box-shadow: 0 7px 18px rgba(0, 127, 142, 0.18);
}
.pf-v5-c-button.pf-m-primary:hover {
background: #006f7c;
box-shadow: 0 9px 22px rgba(0, 127, 142, 0.24);
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 {
border-color: var(--nexus-admin-ice);
height: 3px;
border-color: var(--nexus-admin-signal);
}
.pf-v5-c-label {
border-radius: 2px;
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: 1100px;
max-width: 1160px;
padding-bottom: var(--pf-v5-global--spacer--4xl);
}
input[type="checkbox"] {
width: 18px;
height: 18px;
vertical-align: baseline;
accent-color: #0087a1;
}
.keycloak__form_actions {
position: fixed;
position: sticky;
z-index: 15;
bottom: 0;
width: 100%;
margin-left: calc(-1 * var(--pf-v5-global--spacer--lg));
padding: var(--pf-v5-global--spacer--md);
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(27, 43, 72, 0.06);
backdrop-filter: blur(14px);
box-shadow: 0 -10px 30px rgba(20, 32, 51, 0.06);
backdrop-filter: blur(16px);
}
.pf-v5-theme-dark .pf-v5-c-page__main {
background-image: linear-gradient(rgba(99, 230, 255, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 230, 255, 0.035) 1px, transparent 1px);
}
.pf-v5-theme-dark .pf-v5-c-card,
.pf-v5-theme-dark .pf-v5-c-modal-box,
.pf-v5-theme-dark .pf-v5-c-drawer__panel,
.pf-v5-theme-dark .pf-v5-c-popover__content,
.pf-v5-theme-dark .pf-v5-c-menu {
box-shadow: 0 15px 42px rgba(0, 0, 0, 0.28);
input[type="checkbox"],
input[type="radio"] {
accent-color: var(--nexus-admin-signal);
}
.keycloak__section_intro__help {
@@ -256,19 +530,72 @@ input[type="checkbox"] {
}
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-ice) !important;
outline-offset: 2px;
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;
}
}