forked from baron/baron-sso
admin/dev 사이드바 프레임 공통화
This commit is contained in:
@@ -21,12 +21,14 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
AppSidebar,
|
||||||
type ShellTranslator,
|
type ShellTranslator,
|
||||||
applyShellTheme,
|
applyShellTheme,
|
||||||
buildShellProfileSummary,
|
buildShellProfileSummary,
|
||||||
buildShellSessionStatus,
|
buildShellSessionStatus,
|
||||||
readShellSessionExpiryEnabled,
|
readShellSessionExpiryEnabled,
|
||||||
readShellTheme,
|
readShellTheme,
|
||||||
|
type ShellSidebarNavItem,
|
||||||
shellLayoutClasses,
|
shellLayoutClasses,
|
||||||
writeShellSessionExpiryEnabled,
|
writeShellSessionExpiryEnabled,
|
||||||
} from "../../../../common/shell";
|
} from "../../../../common/shell";
|
||||||
@@ -41,19 +43,38 @@ import {
|
|||||||
import LanguageSelector from "../common/LanguageSelector";
|
import LanguageSelector from "../common/LanguageSelector";
|
||||||
import RoleSwitcher from "./RoleSwitcher";
|
import RoleSwitcher from "./RoleSwitcher";
|
||||||
|
|
||||||
interface NavItem {
|
const staticNavItems: ShellSidebarNavItem[] = [
|
||||||
label: string;
|
{
|
||||||
to: string;
|
labelKey: "ui.admin.nav.overview",
|
||||||
icon: React.ComponentType<{ size?: number | string }>;
|
labelFallback: "Overview",
|
||||||
isExternal?: boolean;
|
to: "/",
|
||||||
}
|
icon: LayoutDashboard,
|
||||||
|
end: true,
|
||||||
const staticNavItems: NavItem[] = [
|
},
|
||||||
{ label: "ui.admin.nav.overview", to: "/", icon: LayoutDashboard },
|
{
|
||||||
{ label: "ui.admin.nav.users", to: "/users", icon: Users },
|
labelKey: "ui.admin.nav.users",
|
||||||
{ label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key },
|
labelFallback: "Users",
|
||||||
{ label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs },
|
to: "/users",
|
||||||
{ label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound },
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "ui.admin.nav.api_keys",
|
||||||
|
labelFallback: "API Keys",
|
||||||
|
to: "/api-keys",
|
||||||
|
icon: Key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "ui.admin.nav.audit_logs",
|
||||||
|
labelFallback: "Audit Logs",
|
||||||
|
to: "/audit-logs",
|
||||||
|
icon: NotebookTabs,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "ui.admin.nav.auth_guard",
|
||||||
|
labelFallback: "Auth Guard",
|
||||||
|
to: "/auth",
|
||||||
|
icon: KeyRound,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type SessionStatusProps = {
|
type SessionStatusProps = {
|
||||||
@@ -145,7 +166,7 @@ function AppLayout() {
|
|||||||
._IS_TEST_MODE === true,
|
._IS_TEST_MODE === true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const navItems = React.useMemo(() => {
|
const navItems = React.useMemo<ShellSidebarNavItem[]>(() => {
|
||||||
const items = [...staticNavItems];
|
const items = [...staticNavItems];
|
||||||
const isTest =
|
const isTest =
|
||||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||||
@@ -167,36 +188,42 @@ function AppLayout() {
|
|||||||
|
|
||||||
if (isSuperAdmin) {
|
if (isSuperAdmin) {
|
||||||
filteredItems.splice(1, 0, {
|
filteredItems.splice(1, 0, {
|
||||||
label: "ui.admin.nav.tenants",
|
labelKey: "ui.admin.nav.tenants",
|
||||||
|
labelFallback: "Tenants",
|
||||||
to: "/tenants",
|
to: "/tenants",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
});
|
});
|
||||||
filteredItems.splice(2, 0, {
|
filteredItems.splice(2, 0, {
|
||||||
label: "ui.admin.nav.org_chart",
|
labelKey: "ui.admin.nav.org_chart",
|
||||||
|
labelFallback: "Org Chart",
|
||||||
to: orgfrontUrl,
|
to: orgfrontUrl,
|
||||||
icon: Network,
|
icon: Network,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
});
|
});
|
||||||
filteredItems.splice(4, 0, {
|
filteredItems.splice(4, 0, {
|
||||||
label: "ui.admin.nav.user_projection",
|
labelKey: "ui.admin.nav.user_projection",
|
||||||
|
labelFallback: "User Projection",
|
||||||
to: "/system/projections/users",
|
to: "/system/projections/users",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
});
|
});
|
||||||
filteredItems.splice(5, 0, {
|
filteredItems.splice(5, 0, {
|
||||||
label: "ui.admin.nav.data_integrity",
|
labelKey: "ui.admin.nav.data_integrity",
|
||||||
|
labelFallback: "Data Integrity",
|
||||||
to: "/system/data-integrity",
|
to: "/system/data-integrity",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
});
|
});
|
||||||
} else if (isTenantAdmin || manageableCount > 0) {
|
} else if (isTenantAdmin || manageableCount > 0) {
|
||||||
if (manageableCount <= 1 && profile?.tenantId) {
|
if (manageableCount <= 1 && profile?.tenantId) {
|
||||||
filteredItems.splice(1, 0, {
|
filteredItems.splice(1, 0, {
|
||||||
label: "ui.admin.nav.my_tenant",
|
labelKey: "ui.admin.nav.my_tenant",
|
||||||
|
labelFallback: "My Tenant",
|
||||||
to: `/tenants/${profile.tenantId}`,
|
to: `/tenants/${profile.tenantId}`,
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
});
|
});
|
||||||
} else if (manageableCount > 1) {
|
} else if (manageableCount > 1) {
|
||||||
filteredItems.splice(1, 0, {
|
filteredItems.splice(1, 0, {
|
||||||
label: "ui.admin.nav.tenants",
|
labelKey: "ui.admin.nav.tenants",
|
||||||
|
labelFallback: "Tenants",
|
||||||
to: "/tenants",
|
to: "/tenants",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
});
|
});
|
||||||
@@ -205,7 +232,8 @@ function AppLayout() {
|
|||||||
manageableCount <= 1 && profile?.tenantId ? 2 : 2,
|
manageableCount <= 1 && profile?.tenantId ? 2 : 2,
|
||||||
0,
|
0,
|
||||||
{
|
{
|
||||||
label: "ui.admin.nav.org_chart",
|
labelKey: "ui.admin.nav.org_chart",
|
||||||
|
labelFallback: "Org Chart",
|
||||||
to: orgfrontUrl,
|
to: orgfrontUrl,
|
||||||
icon: Network,
|
icon: Network,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@@ -214,7 +242,8 @@ function AppLayout() {
|
|||||||
} else {
|
} else {
|
||||||
// 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
|
// 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
|
||||||
filteredItems.splice(1, 0, {
|
filteredItems.splice(1, 0, {
|
||||||
label: "ui.admin.nav.org_chart",
|
labelKey: "ui.admin.nav.org_chart",
|
||||||
|
labelFallback: "Org Chart",
|
||||||
to: orgfrontUrl,
|
to: orgfrontUrl,
|
||||||
icon: Network,
|
icon: Network,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@@ -442,6 +471,66 @@ function AppLayout() {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const sidebarNavContent = (
|
||||||
|
<div className={shellLayoutClasses.navList}>
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const { labelKey, labelFallback, to, icon: Icon, isExternal } = item;
|
||||||
|
|
||||||
|
if (isExternal) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={to}
|
||||||
|
href={to}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={[
|
||||||
|
shellLayoutClasses.navItemBase,
|
||||||
|
shellLayoutClasses.navItemIdle,
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<Icon size={18} />
|
||||||
|
<span>{t(labelKey, labelFallback)}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
end={item.end ?? to === "/"}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
[
|
||||||
|
shellLayoutClasses.navItemBase,
|
||||||
|
item.isActive !== undefined
|
||||||
|
? item.isActive
|
||||||
|
? shellLayoutClasses.navItemActive
|
||||||
|
: shellLayoutClasses.navItemIdle
|
||||||
|
: isActive
|
||||||
|
? shellLayoutClasses.navItemActive
|
||||||
|
: shellLayoutClasses.navItemIdle,
|
||||||
|
].join(" ")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon size={18} />
|
||||||
|
<span>{t(labelKey, labelFallback)}</span>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const sidebarFooterContent = (
|
||||||
|
<div className="border-t border-border/50 px-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={shellLayoutClasses.logoutButton}
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
<span>{t("ui.admin.nav.logout", "Logout")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
if (auth.isLoading) {
|
if (auth.isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -453,84 +542,13 @@ function AppLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={shellLayoutClasses.root}>
|
<div className={shellLayoutClasses.root}>
|
||||||
<aside className={shellLayoutClasses.asideStatic}>
|
<AppSidebar
|
||||||
<div className={shellLayoutClasses.brandSection}>
|
brandLabel={t("ui.admin.brand", "Baron 로그인")}
|
||||||
<div className={shellLayoutClasses.brandWrap}>
|
brandTitle={t("ui.admin.title", "Admin Control")}
|
||||||
<div className={shellLayoutClasses.brandIcon}>
|
brandIcon={<ShieldHalf size={20} />}
|
||||||
<ShieldHalf size={20} />
|
navContent={sidebarNavContent}
|
||||||
</div>
|
footerContent={sidebarFooterContent}
|
||||||
<div>
|
/>
|
||||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
|
||||||
{t("ui.admin.brand", "Baron 로그인")}
|
|
||||||
</p>
|
|
||||||
<h1 className="text-lg font-semibold">
|
|
||||||
{t("ui.admin.title", "Admin Control")}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav className={shellLayoutClasses.navWrap}>
|
|
||||||
<div className={shellLayoutClasses.navList}>
|
|
||||||
{navItems.map((item: NavItem) => {
|
|
||||||
const { label, to, icon: Icon, isExternal } = item;
|
|
||||||
const isOrgChart = location.pathname === "/tenants/org-chart";
|
|
||||||
const isTenantsRoot = to === "/tenants";
|
|
||||||
const isCustomActive = isTenantsRoot
|
|
||||||
? location.pathname.startsWith("/tenants") && !isOrgChart
|
|
||||||
: to === "/"
|
|
||||||
? location.pathname === "/"
|
|
||||||
: location.pathname.startsWith(to);
|
|
||||||
|
|
||||||
if (isExternal) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={to}
|
|
||||||
href={to}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={[
|
|
||||||
shellLayoutClasses.navItemBase,
|
|
||||||
shellLayoutClasses.navItemIdle,
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
<Icon size={18} />
|
|
||||||
<span>{t(label, label)}</span>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
className={() =>
|
|
||||||
[
|
|
||||||
shellLayoutClasses.navItemBase,
|
|
||||||
isCustomActive
|
|
||||||
? shellLayoutClasses.navItemActive
|
|
||||||
: shellLayoutClasses.navItemIdle,
|
|
||||||
].join(" ")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon size={18} />
|
|
||||||
<span>{t(label, label)}</span>
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-border/50 px-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleLogout}
|
|
||||||
className={shellLayoutClasses.logoutButton}
|
|
||||||
>
|
|
||||||
<LogOut size={18} />
|
|
||||||
<span>{t("ui.admin.nav.logout", "Logout")}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div className={shellLayoutClasses.contentWide}>
|
<div className={shellLayoutClasses.contentWide}>
|
||||||
<header className={shellLayoutClasses.headerElevated}>
|
<header className={shellLayoutClasses.headerElevated}>
|
||||||
|
|||||||
49
common/shell/AppSidebar.tsx
Normal file
49
common/shell/AppSidebar.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { ComponentType, ReactNode } from "react";
|
||||||
|
import { shellLayoutClasses } from "./layout";
|
||||||
|
|
||||||
|
export type ShellSidebarNavItem = {
|
||||||
|
labelKey: string;
|
||||||
|
labelFallback: string;
|
||||||
|
to: string;
|
||||||
|
icon: ComponentType<{ size?: number | string }>;
|
||||||
|
isExternal?: boolean;
|
||||||
|
end?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShellSidebarProps = {
|
||||||
|
brandLabel: string;
|
||||||
|
brandTitle: string;
|
||||||
|
brandIcon: ReactNode;
|
||||||
|
navContent: ReactNode;
|
||||||
|
footerContent: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppSidebar({
|
||||||
|
brandLabel,
|
||||||
|
brandTitle,
|
||||||
|
brandIcon,
|
||||||
|
navContent,
|
||||||
|
footerContent,
|
||||||
|
}: ShellSidebarProps) {
|
||||||
|
return (
|
||||||
|
<aside className={shellLayoutClasses.aside}>
|
||||||
|
<div>
|
||||||
|
<div className={shellLayoutClasses.brandSection}>
|
||||||
|
<div className={shellLayoutClasses.brandWrap}>
|
||||||
|
<div className={shellLayoutClasses.brandIcon}>{brandIcon}</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||||
|
{brandLabel}
|
||||||
|
</p>
|
||||||
|
<h1 className="text-lg font-semibold">{brandTitle}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className={shellLayoutClasses.navWrap}>{navContent}</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>{footerContent}</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,55 +22,9 @@ type ShellProfileSummaryParams = {
|
|||||||
export const SHELL_THEME_STORAGE_KEY = "admin_theme";
|
export const SHELL_THEME_STORAGE_KEY = "admin_theme";
|
||||||
export const SHELL_SESSION_EXPIRY_STORAGE_KEY =
|
export const SHELL_SESSION_EXPIRY_STORAGE_KEY =
|
||||||
"baron_session_expiry_enabled";
|
"baron_session_expiry_enabled";
|
||||||
|
export { AppSidebar } from "./AppSidebar";
|
||||||
export const shellLayoutClasses = {
|
export type { ShellSidebarNavItem } from "./AppSidebar";
|
||||||
root: "grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]",
|
export { shellLayoutClasses } from "./layout";
|
||||||
aside:
|
|
||||||
"flex flex-col justify-between border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur",
|
|
||||||
asideStatic:
|
|
||||||
"border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur",
|
|
||||||
brandSection:
|
|
||||||
"flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6",
|
|
||||||
brandWrap: "flex items-center gap-3 md:flex-col md:items-start",
|
|
||||||
brandIcon:
|
|
||||||
"grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]",
|
|
||||||
scopeBadge:
|
|
||||||
"hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2",
|
|
||||||
navWrap: "px-2 pb-4 md:px-3 md:pb-8",
|
|
||||||
navMeta:
|
|
||||||
"flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start",
|
|
||||||
navList: "flex flex-col gap-1",
|
|
||||||
navItemBase:
|
|
||||||
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
|
|
||||||
navItemActive:
|
|
||||||
"bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]",
|
|
||||||
navItemIdle: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
|
|
||||||
sidebarFooterNotice:
|
|
||||||
"hidden space-y-2 px-5 pb-6 pt-2 text-xs text-[var(--color-muted)] md:block",
|
|
||||||
logoutButton:
|
|
||||||
"flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive",
|
|
||||||
header: "sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur",
|
|
||||||
headerElevated:
|
|
||||||
"sticky top-0 z-50 border-b border-border bg-background/90 backdrop-blur",
|
|
||||||
headerInner: "flex items-center justify-between px-5 py-4 md:px-8",
|
|
||||||
headerTitleWrap: "flex flex-col gap-1",
|
|
||||||
headerActions: "flex items-center gap-2 text-sm",
|
|
||||||
actionButton:
|
|
||||||
"inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20",
|
|
||||||
sessionBadge:
|
|
||||||
"hidden rounded-full border px-3 py-2 text-xs font-medium md:inline-flex",
|
|
||||||
profileInitial:
|
|
||||||
"grid h-8 w-8 place-items-center rounded-full bg-primary/15 text-xs font-semibold text-primary",
|
|
||||||
profileMenu:
|
|
||||||
"absolute right-0 z-30 mt-2 w-72 rounded-xl border border-border bg-card p-3 shadow-xl",
|
|
||||||
profileCard:
|
|
||||||
"mt-2 flex flex-col gap-2 rounded-lg border border-border px-3 py-3",
|
|
||||||
settingsCard: "mt-2 rounded-lg border border-border px-3 py-3",
|
|
||||||
content: "relative",
|
|
||||||
contentWide: "relative min-w-0",
|
|
||||||
main: "px-5 py-6 md:px-10 md:py-10",
|
|
||||||
mainMinWidth: "min-w-0 px-5 py-6 md:px-10 md:py-10",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function readShellTheme(): ShellTheme {
|
export function readShellTheme(): ShellTheme {
|
||||||
return window.localStorage.getItem(SHELL_THEME_STORAGE_KEY) === "dark"
|
return window.localStorage.getItem(SHELL_THEME_STORAGE_KEY) === "dark"
|
||||||
|
|||||||
48
common/shell/layout.ts
Normal file
48
common/shell/layout.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export const shellLayoutClasses = {
|
||||||
|
root: "grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]",
|
||||||
|
aside:
|
||||||
|
"flex flex-col justify-between border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur",
|
||||||
|
asideStatic:
|
||||||
|
"border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur",
|
||||||
|
brandSection:
|
||||||
|
"flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6",
|
||||||
|
brandWrap: "flex items-center gap-3 md:flex-col md:items-start",
|
||||||
|
brandIcon:
|
||||||
|
"grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]",
|
||||||
|
scopeBadge:
|
||||||
|
"hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2",
|
||||||
|
navWrap: "px-2 pb-4 md:px-3 md:pb-8",
|
||||||
|
navMeta:
|
||||||
|
"flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start",
|
||||||
|
navList: "flex flex-col gap-1",
|
||||||
|
navItemBase:
|
||||||
|
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
|
||||||
|
navItemActive:
|
||||||
|
"bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]",
|
||||||
|
navItemIdle: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
|
||||||
|
sidebarFooterNotice:
|
||||||
|
"hidden space-y-2 px-5 pb-6 pt-2 text-xs text-[var(--color-muted)] md:block",
|
||||||
|
logoutButton:
|
||||||
|
"flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive",
|
||||||
|
header: "sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur",
|
||||||
|
headerElevated:
|
||||||
|
"sticky top-0 z-50 border-b border-border bg-background/90 backdrop-blur",
|
||||||
|
headerInner: "flex items-center justify-between px-5 py-4 md:px-8",
|
||||||
|
headerTitleWrap: "flex flex-col gap-1",
|
||||||
|
headerActions: "flex items-center gap-2 text-sm",
|
||||||
|
actionButton:
|
||||||
|
"inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20",
|
||||||
|
sessionBadge:
|
||||||
|
"hidden rounded-full border px-3 py-2 text-xs font-medium md:inline-flex",
|
||||||
|
profileInitial:
|
||||||
|
"grid h-8 w-8 place-items-center rounded-full bg-primary/15 text-xs font-semibold text-primary",
|
||||||
|
profileMenu:
|
||||||
|
"absolute right-0 z-30 mt-2 w-72 rounded-xl border border-border bg-card p-3 shadow-xl",
|
||||||
|
profileCard:
|
||||||
|
"mt-2 flex flex-col gap-2 rounded-lg border border-border px-3 py-3",
|
||||||
|
settingsCard: "mt-2 rounded-lg border border-border px-3 py-3",
|
||||||
|
content: "relative",
|
||||||
|
contentWide: "relative min-w-0",
|
||||||
|
main: "px-5 py-6 md:px-10 md:py-10",
|
||||||
|
mainMinWidth: "min-w-0 px-5 py-6 md:px-10 md:py-10",
|
||||||
|
} as const;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -15,12 +14,14 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
AppSidebar,
|
||||||
type ShellTranslator,
|
type ShellTranslator,
|
||||||
applyShellTheme,
|
applyShellTheme,
|
||||||
buildShellProfileSummary,
|
buildShellProfileSummary,
|
||||||
buildShellSessionStatus,
|
buildShellSessionStatus,
|
||||||
readShellSessionExpiryEnabled,
|
readShellSessionExpiryEnabled,
|
||||||
readShellTheme,
|
readShellTheme,
|
||||||
|
type ShellSidebarNavItem,
|
||||||
shellLayoutClasses,
|
shellLayoutClasses,
|
||||||
writeShellSessionExpiryEnabled,
|
writeShellSessionExpiryEnabled,
|
||||||
} from "../../../../common/shell";
|
} from "../../../../common/shell";
|
||||||
@@ -34,12 +35,13 @@ import {
|
|||||||
import LanguageSelector from "../common/LanguageSelector";
|
import LanguageSelector from "../common/LanguageSelector";
|
||||||
import { Toaster } from "../ui/toaster";
|
import { Toaster } from "../ui/toaster";
|
||||||
|
|
||||||
const navItems = [
|
const navItems: ShellSidebarNavItem[] = [
|
||||||
{
|
{
|
||||||
labelKey: "ui.dev.nav.overview",
|
labelKey: "ui.dev.nav.overview",
|
||||||
labelFallback: "Overview",
|
labelFallback: "Overview",
|
||||||
to: "/",
|
to: "/",
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
|
end: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelKey: "ui.dev.nav.developer_request",
|
labelKey: "ui.dev.nav.developer_request",
|
||||||
@@ -323,81 +325,50 @@ function AppLayout() {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const sidebarNavContent = (
|
||||||
|
<div className={shellLayoutClasses.navList}>
|
||||||
|
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
|
||||||
|
<NavLink
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
end={to === "/"}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
[
|
||||||
|
shellLayoutClasses.navItemBase,
|
||||||
|
isActive
|
||||||
|
? shellLayoutClasses.navItemActive
|
||||||
|
: shellLayoutClasses.navItemIdle,
|
||||||
|
].join(" ")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon size={18} />
|
||||||
|
<span>{t(labelKey, labelFallback)}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const sidebarFooterContent = (
|
||||||
|
<div className="border-t border-border/50 px-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={shellLayoutClasses.logoutButton}
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
<span>{t("ui.dev.nav.logout", "Logout")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={shellLayoutClasses.root}>
|
<div className={shellLayoutClasses.root}>
|
||||||
<aside className={shellLayoutClasses.aside}>
|
<AppSidebar
|
||||||
<div>
|
brandLabel={t("ui.dev.brand", "Baron Sign In")}
|
||||||
<div className={shellLayoutClasses.brandSection}>
|
brandTitle={t("ui.dev.console_title", "Developer Console")}
|
||||||
<div className={shellLayoutClasses.brandWrap}>
|
brandIcon={<ShieldHalf size={20} />}
|
||||||
<div className={shellLayoutClasses.brandIcon}>
|
navContent={sidebarNavContent}
|
||||||
<ShieldHalf size={20} />
|
footerContent={sidebarFooterContent}
|
||||||
</div>
|
/>
|
||||||
<div>
|
|
||||||
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
|
||||||
{t("ui.dev.brand", "Baron Sign In")}
|
|
||||||
</p>
|
|
||||||
<h1 className="text-lg font-semibold">
|
|
||||||
{t("ui.dev.console_title", "Developer Console")}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={shellLayoutClasses.scopeBadge}>
|
|
||||||
<BadgeCheck size={14} />
|
|
||||||
{t("ui.dev.scope_badge", "Scoped to /dev")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav className={shellLayoutClasses.navWrap}>
|
|
||||||
<div className={shellLayoutClasses.navMeta}>
|
|
||||||
<span className="rounded-full border border-border px-3 py-1">
|
|
||||||
{t("ui.dev.env_badge", "Env: dev")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={shellLayoutClasses.navList}>
|
|
||||||
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
|
|
||||||
<NavLink
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
end={to === "/"}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
[
|
|
||||||
shellLayoutClasses.navItemBase,
|
|
||||||
isActive
|
|
||||||
? shellLayoutClasses.navItemActive
|
|
||||||
: shellLayoutClasses.navItemIdle,
|
|
||||||
].join(" ")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon size={18} />
|
|
||||||
<span>{t(labelKey, labelFallback)}</span>
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="border-t border-border/50 px-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleLogout}
|
|
||||||
className={shellLayoutClasses.logoutButton}
|
|
||||||
>
|
|
||||||
<LogOut size={18} />
|
|
||||||
<span>{t("ui.dev.nav.logout", "Logout")}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={shellLayoutClasses.sidebarFooterNotice}>
|
|
||||||
<p>{t("msg.dev.sidebar.notice", "Developer Console")}</p>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
"msg.dev.sidebar.notice_detail",
|
|
||||||
"Register and manage client applications.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div className={shellLayoutClasses.content}>
|
<div className={shellLayoutClasses.content}>
|
||||||
<header className={shellLayoutClasses.header}>
|
<header className={shellLayoutClasses.header}>
|
||||||
|
|||||||
Reference in New Issue
Block a user