1
0
forked from baron/baron-sso
Files
baron-sso/common/shell/index.ts

171 lines
5.9 KiB
TypeScript

export type ShellTheme = "light" | "dark";
export type ShellTranslator = (
key: string,
fallback: string,
vars?: Record<string, string | number>,
) => string;
type ShellSessionStatusParams = {
expiresAtSec?: number | null;
nowMs: number;
t: ShellTranslator;
};
type ShellProfileSummaryParams = {
profileName?: string | null;
profileEmail?: string | null;
fallbackName: string;
fallbackEmail: string;
};
export const SHELL_THEME_STORAGE_KEY = "admin_theme";
export const SHELL_SESSION_EXPIRY_STORAGE_KEY =
"baron_session_expiry_enabled";
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;
export function readShellTheme(): ShellTheme {
return window.localStorage.getItem(SHELL_THEME_STORAGE_KEY) === "dark"
? "dark"
: "light";
}
export function applyShellTheme(theme: ShellTheme) {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
window.localStorage.setItem(SHELL_THEME_STORAGE_KEY, theme);
}
export function readShellSessionExpiryEnabled() {
return window.localStorage.getItem(SHELL_SESSION_EXPIRY_STORAGE_KEY) !== "false";
}
export function writeShellSessionExpiryEnabled(isEnabled: boolean) {
window.localStorage.setItem(
SHELL_SESSION_EXPIRY_STORAGE_KEY,
String(isEnabled),
);
}
export function buildShellProfileSummary({
profileName,
profileEmail,
fallbackName,
fallbackEmail,
}: ShellProfileSummaryParams) {
const resolvedName = profileName?.trim() || fallbackName;
const resolvedEmail = profileEmail?.trim() || fallbackEmail;
return {
name: resolvedName,
email: resolvedEmail,
initial: resolvedName.charAt(0).toUpperCase(),
};
}
export function buildShellSessionStatus({
expiresAtSec,
nowMs,
t,
}: ShellSessionStatusParams) {
const remainingMs =
typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null;
const remainingTotalSec =
remainingMs !== null ? Math.max(0, Math.floor(remainingMs / 1000)) : null;
const remainingMinutes =
remainingTotalSec !== null ? Math.floor(remainingTotalSec / 60) : null;
const remainingSeconds =
remainingTotalSec !== null ? remainingTotalSec % 60 : null;
let toneClass =
"border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
let text = t("ui.dev.session.active", "세션 활성");
if (remainingMs === null) {
toneClass = "border-border bg-card text-muted-foreground";
text = t("ui.dev.session.unknown", "알 수 없음");
} else if (remainingMs <= 0) {
toneClass =
"border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300";
text = t("ui.dev.session.expired", "세션 만료");
} else if (
remainingMinutes !== null &&
remainingSeconds !== null &&
remainingMinutes <= 5
) {
toneClass =
"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300";
text = t(
"ui.dev.session.expiring",
"만료 임박: {{minutes}}분 {{seconds}}초 남음",
{
minutes: remainingMinutes,
seconds: remainingSeconds,
},
);
} else {
text = t(
"ui.dev.session.remaining",
"만료 예정: {{minutes}}분 {{seconds}}초 남음",
{
minutes: remainingMinutes ?? 0,
seconds: remainingSeconds ?? 0,
},
);
}
return {
toneClass,
text,
};
}