forked from baron/baron-sso
171 lines
5.9 KiB
TypeScript
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,
|
|
};
|
|
}
|