forked from baron/baron-sso
common shell frame/state helper 공용화
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
170
common/shell/index.ts
Normal file
170
common/shell/index.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user