export type ShellTheme = "light" | "dark"; export type ShellTranslator = ( key: string, fallback: string, vars?: Record, ) => 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, }; }