Compare commits
2 Commits
2c57dc8cda
...
3f481acd8e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f481acd8e | |||
| 407da15e6d |
@@ -40,7 +40,7 @@ export default function LoginPage() {
|
||||
const co = cosmosRef.current;
|
||||
if (!co) return;
|
||||
const cs = ["rgba(var(--v5-primary-rgb),.8)", "rgba(var(--v5-cyan-rgb),.7)", "rgba(var(--v5-pink-rgb),.7)"];
|
||||
for (let i = 0; i < 150; i++) {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const s = document.createElement("div");
|
||||
s.className = "star" + (Math.random() > 0.83 ? " c" : "");
|
||||
if (s.classList.contains("c")) s.style.setProperty("--sc", cs[(Math.random() * 3) | 0]);
|
||||
@@ -52,7 +52,7 @@ export default function LoginPage() {
|
||||
co.appendChild(s);
|
||||
}
|
||||
const pc = ["var(--primary)", "var(--cyan)", "var(--pink)"];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
for (let i = 0; i < 0; i++) {
|
||||
const p = document.createElement("div");
|
||||
p.className = "particle";
|
||||
p.style.left = Math.random() * 100 + "%";
|
||||
@@ -130,7 +130,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className="logo"><h1>Invy.one</h1></div>
|
||||
<div className="login-sub">Cosmic Command Center</div>
|
||||
<div className="login-sub">엔터프라이즈 운영 센터에 로그인하세요</div>
|
||||
|
||||
<form onSubmit={handleLogin}>
|
||||
<div className="fg">
|
||||
@@ -149,10 +149,10 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-divider"><span>ready to launch</span></div>
|
||||
<div className="login-divider"><span>welcome</span></div>
|
||||
|
||||
<button type="submit" className="lbtn" disabled={isLoading} onMouseDown={handleRipple}>
|
||||
{isLoading ? <span className="spinner" /> : <><span>Launch</span><ArrowRight width={16} height={16} /></>}
|
||||
{isLoading ? <span className="spinner" /> : <><span>로그인</span><ArrowRight width={16} height={16} /></>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -863,7 +863,7 @@ select {
|
||||
/* ease-in-out-cubic — 대칭형 곡선으로 시간/progress 가 거의 linear 에 가깝게 진행됨.
|
||||
duration 은 themeTransition.ts 가 방향에 따라 --vt-duration 변수로 지정.
|
||||
검정→대비 강해서 빨라 보이는 perceived speed 비대칭을 보정하기 위해 dark 방향은 더 김. */
|
||||
animation: vt-soft-reveal var(--vt-duration, 1800ms) cubic-bezier(0.65, 0, 0.35, 1) forwards;
|
||||
animation: vt-soft-reveal var(--vt-duration, 500ms) cubic-bezier(0.65, 0, 0.35, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes vt-soft-reveal {
|
||||
|
||||
@@ -539,119 +539,40 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요.");
|
||||
};
|
||||
|
||||
const handleModeSwitch = useCallback((e?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const handleModeSwitch = useCallback((_e?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (modeTransition !== "idle") return;
|
||||
|
||||
// 강화된 mode transition — 옵션 b/c/e/f 적용 (d 버튼 burst 제거, mode-fade overlay 도 제거):
|
||||
// Phase 1 (0ms): 사이드바 items morph-out (stagger), 헤더 glow flash,
|
||||
// breadcrumb swap-out, 이탈 시 admin badge zoom-out
|
||||
// Phase 2 (350ms): React 가 모드 swap → 새 메뉴 morph-in (stagger), breadcrumb swap-in,
|
||||
// 진입 시 admin badge zoom-in
|
||||
// Phase 3 (~950ms): 모든 클래스 정리, idle 복귀
|
||||
const goingToAdmin = !isAdminMode;
|
||||
// Simplified mode transition — sidebar items stagger morph only, no burst/sweep/badge theatrics.
|
||||
// Phase 1 (0ms): sidebar items morph-out (stagger 20ms)
|
||||
// Phase 2 (180ms): React swaps mode, new items morph-in (stagger 20ms)
|
||||
// Phase 3 (~400ms): cleanup → idle
|
||||
setModeTransition("out");
|
||||
|
||||
// (b) 사이드바 items morph-out — stagger
|
||||
// sidebar items morph-out (stagger 20ms)
|
||||
const oldItems = Array.from(document.querySelectorAll<HTMLElement>(".v5-side .v5-si"));
|
||||
oldItems.forEach((it, i) => {
|
||||
it.style.animationDelay = `${i * 35}ms`;
|
||||
it.style.animationDelay = `${i * 20}ms`;
|
||||
it.classList.add("mode-morph-out");
|
||||
});
|
||||
|
||||
// (c) 헤더 glow line flash
|
||||
const hdrGlow = document.querySelector<HTMLElement>(".v5-hdr-glow");
|
||||
if (hdrGlow) {
|
||||
hdrGlow.classList.remove("mode-flash");
|
||||
void hdrGlow.offsetWidth; // reflow → 재시작 가능하게
|
||||
hdrGlow.classList.add("mode-flash");
|
||||
}
|
||||
|
||||
// (d) 토글 버튼 burst — 디자인시스템 mode-burst 포팅
|
||||
// 클릭 좌표에 ring 1개 + radial particle 10개. admin 진입은 cyan, 사용자 복귀는 primary.
|
||||
const targetEl = e?.currentTarget as HTMLElement | undefined;
|
||||
const rect = targetEl?.getBoundingClientRect();
|
||||
const bx = rect ? rect.left + rect.width / 2 : (e?.clientX ?? window.innerWidth - 80);
|
||||
const by = rect ? rect.top + rect.height / 2 : (e?.clientY ?? 25);
|
||||
const burst = document.createElement("div");
|
||||
burst.className = `v5-mode-burst${goingToAdmin ? " admin" : ""}`;
|
||||
burst.style.left = `${bx}px`;
|
||||
burst.style.top = `${by}px`;
|
||||
const ring = document.createElement("span");
|
||||
ring.className = "burst-ring";
|
||||
burst.appendChild(ring);
|
||||
const N = 10;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const p = document.createElement("span");
|
||||
p.className = "burst-particle";
|
||||
const angle = (i / N) * Math.PI * 2;
|
||||
const dist = 36 + Math.random() * 22;
|
||||
p.style.setProperty("--tx", `${Math.cos(angle) * dist}px`);
|
||||
p.style.setProperty("--ty", `${Math.sin(angle) * dist}px`);
|
||||
p.style.animationDelay = `${i * 8}ms`;
|
||||
burst.appendChild(p);
|
||||
}
|
||||
document.body.appendChild(burst);
|
||||
setTimeout(() => burst.remove(), 1100);
|
||||
|
||||
// (d2) 헤더 하단 좌→우 sweep (admin 진입은 cyan→primary→pink, 사용자 복귀는 primary)
|
||||
const hdrEl = document.querySelector<HTMLElement>(".v5-hdr");
|
||||
if (hdrEl) {
|
||||
const sweep = document.createElement("div");
|
||||
sweep.className = "v5-mode-sweep";
|
||||
sweep.setAttribute("data-mode", goingToAdmin ? "admin" : "user");
|
||||
hdrEl.appendChild(sweep);
|
||||
setTimeout(() => sweep.remove(), 900);
|
||||
}
|
||||
|
||||
// (e) breadcrumb swap-out
|
||||
const bc = document.querySelector<HTMLElement>(".v5-hdr-bc");
|
||||
bc?.classList.remove("mode-swap-in");
|
||||
bc?.classList.add("mode-swap-out");
|
||||
|
||||
// (f) admin badge zoom-out (이탈 시에만)
|
||||
if (!goingToAdmin) {
|
||||
const badge = document.querySelector<HTMLElement>(".v5-admin-badge");
|
||||
badge?.classList.remove("mode-zoom-in");
|
||||
badge?.classList.add("mode-zoom-out");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// Phase 2: 모드 swap → React 재렌더 (새 메뉴/탭/breadcrumb)
|
||||
setTabMode(isAdminMode ? "user" : "admin");
|
||||
setModeTransition("in");
|
||||
|
||||
// 다음 프레임에 새 items 에 morph-in 적용
|
||||
requestAnimationFrame(() => {
|
||||
const newItems = Array.from(document.querySelectorAll<HTMLElement>(".v5-side .v5-si"));
|
||||
newItems.forEach((it, i) => {
|
||||
it.style.animationDelay = `${i * 45}ms`;
|
||||
it.style.animationDelay = `${i * 20}ms`;
|
||||
it.classList.add("mode-morph-in");
|
||||
});
|
||||
|
||||
// breadcrumb swap-in (텍스트는 이미 새 모드 기준)
|
||||
const newBc = document.querySelector<HTMLElement>(".v5-hdr-bc");
|
||||
newBc?.classList.remove("mode-swap-out");
|
||||
newBc?.classList.add("mode-swap-in");
|
||||
|
||||
// (f) admin badge zoom-in (진입 시에만)
|
||||
if (goingToAdmin) {
|
||||
const newBadge = document.querySelector<HTMLElement>(".v5-admin-badge");
|
||||
newBadge?.classList.remove("mode-zoom-out");
|
||||
newBadge?.classList.add("mode-zoom-in");
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 3: 모든 애니메이션 클래스 정리
|
||||
setTimeout(() => {
|
||||
setModeTransition("idle");
|
||||
document.querySelectorAll<HTMLElement>(".v5-side .v5-si").forEach((it) => {
|
||||
it.classList.remove("mode-morph-in", "mode-morph-out");
|
||||
it.style.animationDelay = "";
|
||||
});
|
||||
document.querySelector<HTMLElement>(".v5-hdr-bc")?.classList.remove("mode-swap-in", "mode-swap-out");
|
||||
document.querySelector<HTMLElement>(".v5-admin-badge")?.classList.remove("mode-zoom-in", "mode-zoom-out");
|
||||
}, 600);
|
||||
}, 350);
|
||||
}, 300);
|
||||
}, 180);
|
||||
}, [isAdminMode, setTabMode, modeTransition]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -788,7 +709,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
}}
|
||||
>
|
||||
<span className="ic">{menu.icon}</span>
|
||||
<span className="truncate">{menu.name}</span>
|
||||
<span className="truncate" title={menu.name || ""}>{menu.name?.trim() || "(이름 없음)"}</span>
|
||||
{menu.hasChildren && !sidebarCollapsed && (
|
||||
<span className="ml-auto">
|
||||
{isExpanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
@@ -998,6 +919,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<span className="v5-hdr-sep" aria-hidden="true" />
|
||||
|
||||
{/* Theme toggle — single sun/moon icon (디자인시스템 준수, 2026-04-21) */}
|
||||
<button
|
||||
className="v5-hdr-icon"
|
||||
@@ -1036,7 +959,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
<SlidersHorizontal size={16} />
|
||||
</button>
|
||||
|
||||
{/* Admin toggle (gear ↔ home) — 이벤트 전달해서 burst origin 으로 사용 */}
|
||||
{isAdmin && <span className="v5-hdr-sep" aria-hidden="true" />}
|
||||
|
||||
{/* Admin toggle (gear ↔ home) */}
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="v5-hdr-icon v5-mode-toggle"
|
||||
@@ -1111,16 +1036,13 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
||||
<aside className={`v5-side v5-side-anim ${isAdminMode ? "v5-admin-side" : ""} ${sidebarCollapsed ? "collapsed" : ""} ${isMobile ? (sidebarOpen ? "mobile-open" : "") : ""} ${modeTransition === "out" ? "slide-out" : modeTransition === "in" ? "slide-in" : ""}`}
|
||||
style={isMobile ? { position: "fixed", left: 0, top: 0, bottom: 0, zIndex: 30, paddingTop: "60px", width: "260px", transform: sidebarOpen ? "none" : "translateX(-100%)" } : undefined}
|
||||
>
|
||||
{/* SUPER_ADMIN company info */}
|
||||
{/* SUPER_ADMIN company info (slim, borderless) */}
|
||||
{(user as ExtendedUserInfo)?.user_type === "SUPER_ADMIN" && !sidebarCollapsed && (
|
||||
<div style={{ padding: ".5rem .6rem", marginBottom: ".25rem" }}>
|
||||
<div className="flex items-center gap-2 rounded-lg p-2" style={{ background: "var(--v5-surface)", border: "1px solid var(--v5-glass-border)", borderRadius: "10px", fontSize: ".7rem" }}>
|
||||
<Building2 size={14} style={{ color: "var(--v5-primary)", flexShrink: 0 }} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p style={{ fontSize: ".55rem", color: "var(--v5-text-muted)" }}>현재 관리 회사</p>
|
||||
<p className="truncate font-semibold" style={{ fontSize: ".72rem", color: "var(--v5-text)" }}>{currentCompanyName || "로딩 중..."}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-2.5 py-1.5" title={currentCompanyName || ""}>
|
||||
<Building2 size={12} style={{ color: "var(--v5-primary)", flexShrink: 0 }} />
|
||||
<p className="truncate font-medium min-w-0 flex-1" style={{ fontSize: ".78rem", color: "var(--v5-text)" }}>
|
||||
{currentCompanyName || "로딩 중..."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { LayoutGrid, Plus } from "lucide-react";
|
||||
import { LayoutGrid, Plus, Sparkles } from "lucide-react";
|
||||
import { useDashboardStore } from "@/stores/dashboardStore";
|
||||
|
||||
export function EmptyDashboard() {
|
||||
@@ -8,50 +8,72 @@ export function EmptyDashboard() {
|
||||
const openLib = useDashboardStore((s) => s.openLib);
|
||||
|
||||
const hasDashboard = !!activeDashboardId;
|
||||
const handleCenterClick = () => {
|
||||
if (hasDashboard) openLib();
|
||||
};
|
||||
|
||||
// 탭이 없는 상태: 메뉴 선택 유도
|
||||
if (!hasDashboard) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center px-6">
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-[var(--v5-primary)]/8 text-[var(--v5-primary)]">
|
||||
<LayoutGrid className="h-7 w-7" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-[1.125rem] font-semibold text-foreground">열린 탭이 없습니다</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
왼쪽 사이드바에서 메뉴를 선택해 시작하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 대시보드는 있으나 위젯이 없는 상태: 3가지 CTA 제시
|
||||
return (
|
||||
<div
|
||||
className={`flex h-full items-center justify-center bg-white dark:bg-transparent ${hasDashboard ? "cursor-pointer" : ""}`}
|
||||
onClick={handleCenterClick}
|
||||
role={hasDashboard ? "button" : undefined}
|
||||
tabIndex={hasDashboard ? 0 : undefined}
|
||||
onKeyDown={(e) => {
|
||||
if (hasDashboard && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
openLib();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4 text-center transition-transform hover:scale-[1.02]">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-[var(--v5-primary)]/10 text-[var(--v5-primary)] ring-1 ring-[var(--v5-primary)]/30">
|
||||
{hasDashboard ? <Plus className="h-10 w-10" /> : <LayoutGrid className="h-10 w-10 text-muted-foreground" />}
|
||||
<div className="flex h-full items-center justify-center bg-white px-6 dark:bg-transparent">
|
||||
<div className="flex w-full max-w-xl flex-col items-center gap-6 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-[var(--v5-primary)]/20 to-[var(--v5-cyan)]/15 text-[var(--v5-primary)] ring-1 ring-[var(--v5-primary)]/25">
|
||||
<Sparkles className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
{hasDashboard ? "이제 템플릿을 추가합니다" : "열린 탭이 없습니다"}
|
||||
<h2 className="text-[1.25rem] font-semibold text-foreground">
|
||||
새 대시보드가 준비됐습니다
|
||||
</h2>
|
||||
<p className="max-w-sm text-sm text-muted-foreground">
|
||||
{hasDashboard
|
||||
? "이 영역을 클릭하거나 상단의 '템플릿 추가' 버튼을 눌러 첫 카드를 배치하세요."
|
||||
: "왼쪽 사이드바에서 메뉴를 클릭하거나 드래그하여 탭을 추가하세요."}
|
||||
템플릿으로 빠르게 시작하거나, 위젯을 직접 배치해 대시보드를 구성하세요.
|
||||
</p>
|
||||
</div>
|
||||
{hasDashboard && (
|
||||
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openLib();
|
||||
}}
|
||||
className="mt-1 inline-flex items-center gap-1.5 rounded-md bg-[var(--v5-primary)] px-3 py-1.5 text-[0.7rem] font-semibold text-white hover:opacity-90"
|
||||
onClick={openLib}
|
||||
className="group flex flex-col items-start gap-1.5 rounded-xl border border-[var(--v5-primary)]/25 bg-[var(--v5-primary)]/[0.04] px-4 py-3.5 text-left transition-all hover:border-[var(--v5-primary)]/60 hover:bg-[var(--v5-primary)]/[0.08] hover:-translate-y-0.5"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
템플릿 추가
|
||||
<div className="flex items-center gap-2 text-[var(--v5-primary)]">
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
<span className="text-sm font-semibold">템플릿에서 시작</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
미리 만들어진 레이아웃과 위젯 조합으로 빠르게 구성
|
||||
</p>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openLib}
|
||||
className="group flex flex-col items-start gap-1.5 rounded-xl border border-border bg-card px-4 py-3.5 text-left transition-all hover:border-[var(--v5-primary)]/50 hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="text-sm font-semibold">위젯 직접 추가</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
빈 캔버스에 원하는 위젯을 하나씩 배치
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[0.7rem] text-muted-foreground/70">
|
||||
팁: 상단의 '편집' 버튼으로 언제든 레이아웃을 바꿀 수 있어요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
Defaults: 30px height, 0.7rem text, primary fills, glow on hover.
|
||||
================================================================= */
|
||||
.v5-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: .35rem;
|
||||
height: 30px; padding: 0 var(--v5-sp-3);
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
|
||||
height: 34px; padding: 0 var(--v5-sp-4);
|
||||
font-family: var(--v5-font-sans);
|
||||
font-size: .7rem; font-weight: var(--v5-fw-semi);
|
||||
font-size: .8125rem; font-weight: var(--v5-fw-semi);
|
||||
border: 1px solid transparent; border-radius: var(--v5-radius-md);
|
||||
background: transparent; color: var(--v5-text);
|
||||
cursor: pointer; user-select: none; white-space: nowrap;
|
||||
@@ -25,7 +25,7 @@
|
||||
transform .12s var(--v5-ease-move),
|
||||
opacity .2s var(--v5-ease-move);
|
||||
}
|
||||
.v5-btn svg { width: 13px; height: 13px; stroke-width: 1.75; flex-shrink: 0; }
|
||||
.v5-btn svg { width: 14px; height: 14px; stroke-width: 1.75; flex-shrink: 0; }
|
||||
.v5-btn:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.v5-btn:active:not(:disabled) { transform: scale(.98); }
|
||||
|
||||
@@ -63,13 +63,20 @@
|
||||
opacity: .92; box-shadow: var(--v5-glow-danger); transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* size: sm — compact 26px */
|
||||
/* size: sm — compact 28px */
|
||||
.v5-btn.sm {
|
||||
height: 26px; padding: 0 var(--v5-sp-2);
|
||||
font-size: .64rem; border-radius: var(--v5-radius-sm);
|
||||
height: 28px; padding: 0 var(--v5-sp-3);
|
||||
font-size: .75rem; border-radius: var(--v5-radius-sm);
|
||||
}
|
||||
.v5-btn.sm svg { width: 12px; height: 12px; }
|
||||
|
||||
/* size: lg — prominent 40px */
|
||||
.v5-btn.lg {
|
||||
height: 40px; padding: 0 var(--v5-sp-5);
|
||||
font-size: .875rem; border-radius: var(--v5-radius-md-2);
|
||||
}
|
||||
.v5-btn.lg svg { width: 16px; height: 16px; }
|
||||
|
||||
/* focus ring (keyboard) */
|
||||
.v5-btn:focus-visible {
|
||||
outline: none;
|
||||
@@ -82,13 +89,13 @@
|
||||
Pill, tiny caps, status-tinted.
|
||||
================================================================= */
|
||||
.v5-bdg {
|
||||
display: inline-flex; align-items: center; gap: .3rem;
|
||||
padding: .18rem .5rem;
|
||||
font-size: .58rem; font-weight: var(--v5-fw-bold);
|
||||
letter-spacing: var(--v5-ls-wide); text-transform: uppercase;
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
padding: .22rem .55rem;
|
||||
font-size: .72rem; font-weight: var(--v5-fw-semi);
|
||||
border-radius: var(--v5-radius-pill);
|
||||
background: var(--v5-bg-subtle); color: var(--v5-text-sec);
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.v5-bdg .v5-bdg-dot {
|
||||
width: 5px; height: 5px; border-radius: 50%;
|
||||
@@ -122,9 +129,9 @@
|
||||
gap: .6rem; margin-bottom: .4rem;
|
||||
}
|
||||
.v5-card-title {
|
||||
font-size: .66rem; font-weight: var(--v5-fw-bold);
|
||||
text-transform: uppercase; letter-spacing: var(--v5-ls-wide);
|
||||
color: var(--v5-text-muted);
|
||||
font-size: var(--v5-fs-body-sm); font-weight: 700;
|
||||
color: var(--v5-text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
@@ -138,11 +145,9 @@
|
||||
.v5-page-head-l { min-width: 0; flex: 1; }
|
||||
.v5-page-head-r { display: flex; align-items: center; gap: .4rem; flex-shrink: 0; }
|
||||
.v5-crumbs {
|
||||
font-size: .6rem; color: var(--v5-text-muted);
|
||||
display: flex; align-items: center; gap: .35rem;
|
||||
font-family: var(--v5-font-mono);
|
||||
letter-spacing: var(--v5-ls-wide);
|
||||
margin-bottom: .25rem;
|
||||
font-size: .72rem; color: var(--v5-text-muted);
|
||||
display: flex; align-items: center; gap: .4rem;
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
.v5-crumbs .sep { opacity: .5; }
|
||||
.v5-page-title {
|
||||
@@ -152,8 +157,8 @@
|
||||
color: var(--v5-text);
|
||||
}
|
||||
.v5-page-sub {
|
||||
font-size: .68rem; color: var(--v5-text-muted);
|
||||
margin-top: .15rem;
|
||||
font-size: var(--v5-fs-body-sm); color: var(--v5-text-muted);
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
@@ -172,18 +177,16 @@
|
||||
}
|
||||
.v5-tbl thead th {
|
||||
text-align: left;
|
||||
padding: var(--v5-sp-3) var(--v5-sp-4);
|
||||
font-size: var(--v5-fs-caption);
|
||||
font-weight: var(--v5-fw-bold);
|
||||
letter-spacing: var(--v5-ls-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--v5-text-muted);
|
||||
padding: .7rem var(--v5-sp-4);
|
||||
font-size: var(--v5-fs-caption-lg);
|
||||
font-weight: 600;
|
||||
color: var(--v5-text-sec);
|
||||
background: var(--v5-bg-subtle);
|
||||
border-bottom: 1px solid var(--v5-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.v5-tbl tbody td {
|
||||
padding: .55rem var(--v5-sp-4);
|
||||
padding: .65rem var(--v5-sp-4);
|
||||
border-bottom: 1px solid var(--v5-border-subtle);
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -205,12 +208,12 @@
|
||||
Large number with trend pill underneath.
|
||||
================================================================= */
|
||||
.v5-kpi-num {
|
||||
font-size: 1.85rem; font-weight: 800;
|
||||
font-size: var(--v5-fs-display); font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.05;
|
||||
color: var(--v5-text);
|
||||
display: flex; align-items: baseline; gap: .55rem;
|
||||
display: flex; align-items: baseline; gap: .6rem;
|
||||
}
|
||||
.v5-kpi-num.kpi-cyan { color: rgb(var(--v5-cyan-rgb)); }
|
||||
.v5-kpi-num.kpi-green { color: rgb(var(--v5-green-rgb)); }
|
||||
@@ -218,18 +221,18 @@
|
||||
.v5-kpi-num.kpi-amber { color: rgb(var(--v5-amber-rgb)); }
|
||||
|
||||
.v5-kpi-delta {
|
||||
display: inline-flex; align-items: center; gap: .15rem;
|
||||
font-size: .62rem; font-weight: var(--v5-fw-bold);
|
||||
padding: .12rem .4rem; border-radius: var(--v5-radius-sm);
|
||||
display: inline-flex; align-items: center; gap: .2rem;
|
||||
font-size: .75rem; font-weight: var(--v5-fw-bold);
|
||||
padding: .18rem .5rem; border-radius: var(--v5-radius-sm);
|
||||
}
|
||||
.v5-kpi-delta.up { background: rgba(var(--v5-green-rgb), .12); color: rgb(var(--v5-green-rgb)); }
|
||||
.v5-kpi-delta.down { background: rgba(var(--v5-red-rgb), .12); color: rgb(var(--v5-red-rgb)); }
|
||||
.v5-kpi-delta svg { width: 11px; height: 11px; stroke-width: 2; }
|
||||
|
||||
.v5-kpi-sub {
|
||||
font-size: .62rem;
|
||||
font-size: .75rem;
|
||||
color: var(--v5-text-muted);
|
||||
margin-top: .25rem;
|
||||
margin-top: .3rem;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
@@ -287,9 +290,9 @@
|
||||
.v5-feed-txt b { color: var(--v5-text); font-weight: var(--v5-fw-semi); }
|
||||
.v5-feed-txt .tm {
|
||||
display: block;
|
||||
font-size: .6rem; color: var(--v5-text-muted);
|
||||
font-size: .72rem; color: var(--v5-text-muted);
|
||||
font-family: var(--v5-font-mono);
|
||||
margin-top: .15rem;
|
||||
margin-top: .2rem;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
@@ -390,7 +393,7 @@
|
||||
animation: v5-overlay-in .22s var(--v5-ease-enter);
|
||||
}
|
||||
.v5-modal {
|
||||
width: 100%; max-width: 420px;
|
||||
width: 100%; max-width: 560px;
|
||||
background: var(--v5-surface-solid);
|
||||
border: 1px solid var(--v5-border);
|
||||
border-radius: var(--v5-radius-lg-2);
|
||||
@@ -398,6 +401,9 @@
|
||||
padding: var(--v5-sp-5);
|
||||
animation: v5-modal-in .3s var(--v5-ease-enter);
|
||||
}
|
||||
.v5-modal.sm { max-width: 420px; }
|
||||
.v5-modal.lg { max-width: 720px; }
|
||||
.v5-modal.xl { max-width: 960px; }
|
||||
.v5-modal-head {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
gap: .6rem; margin-bottom: .7rem;
|
||||
@@ -409,10 +415,10 @@
|
||||
color: var(--v5-text);
|
||||
}
|
||||
.v5-modal-body {
|
||||
font-size: var(--v5-fs-body-sm);
|
||||
font-size: var(--v5-fs-body);
|
||||
color: var(--v5-text-sec);
|
||||
line-height: 1.55;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
.v5-modal-body b { color: var(--v5-text); font-weight: var(--v5-fw-bold); }
|
||||
.v5-modal-foot {
|
||||
|
||||
@@ -64,15 +64,15 @@
|
||||
--v5-font-mono: 'JetBrains Mono', 'D2Coding', ui-monospace, SFMono-Regular, Monaco, Consolas, monospace;
|
||||
|
||||
/* ===== Type scale (deliberately dense, ERP). Do not go above these. ===== */
|
||||
--v5-fs-caption:0.60rem; /* labels, table headers, chips */
|
||||
--v5-fs-caption-lg:0.68rem;
|
||||
--v5-fs-body-sm:0.72rem; /* table rows */
|
||||
--v5-fs-body:0.78rem; /* default body */
|
||||
--v5-fs-body-lg:0.85rem; /* max body */
|
||||
--v5-fs-h3:0.92rem;
|
||||
--v5-fs-h2:1.00rem;
|
||||
--v5-fs-h1:1.12rem; /* page/section title */
|
||||
--v5-fs-display:1.60rem; /* KPI numbers only */
|
||||
--v5-fs-caption:0.75rem; /* labels, table headers, chips */
|
||||
--v5-fs-caption-lg:0.8125rem;
|
||||
--v5-fs-body-sm:0.8125rem; /* table rows */
|
||||
--v5-fs-body:0.875rem; /* default body */
|
||||
--v5-fs-body-lg:0.9375rem; /* max body */
|
||||
--v5-fs-h3:1.0625rem;
|
||||
--v5-fs-h2:1.1875rem;
|
||||
--v5-fs-h1:1.375rem; /* page/section title */
|
||||
--v5-fs-display:2rem; /* KPI numbers only */
|
||||
|
||||
--v5-fw-regular:400;
|
||||
--v5-fw-semi:600;
|
||||
@@ -220,7 +220,7 @@ html:not(.dark) .v5-hdr{
|
||||
.v5-hdr-logo{font-size:1.05rem;font-weight:900;letter-spacing:-.03em;
|
||||
background:linear-gradient(135deg,var(--v5-primary),var(--v5-cyan));-webkit-background-clip:text;
|
||||
-webkit-text-fill-color:transparent;background-clip:text;cursor:default;}
|
||||
.v5-hdr-bc{font-size:.72rem;color:var(--v5-text-muted);}
|
||||
.v5-hdr-bc{font-size:.8125rem;color:var(--v5-text-muted);}
|
||||
.v5-hdr-bc b{color:var(--v5-text);font-weight:600;}
|
||||
.v5-hdr-r{display:flex;align-items:center;gap:.65rem;}
|
||||
|
||||
@@ -479,8 +479,8 @@ html:not(.dark) .v5-side{
|
||||
transition:opacity .3s,height .3s,padding .3s;}
|
||||
.v5-side-sec:first-child{padding-top:.25rem;}
|
||||
|
||||
.v5-si{padding:.5rem .7rem;border-radius:10px;font-size:.77rem;color:var(--v5-text-sec);cursor:pointer;
|
||||
transition:all .25s cubic-bezier(.4,0,.2,1);font-weight:450;display:flex;align-items:center;gap:.6rem;
|
||||
.v5-si{padding:.55rem .75rem;border-radius:10px;font-size:.8125rem;color:var(--v5-text-sec);cursor:pointer;
|
||||
transition:all .25s cubic-bezier(.4,0,.2,1);font-weight:500;display:flex;align-items:center;gap:.6rem;
|
||||
position:relative;overflow:hidden;height:auto;}
|
||||
.v5-si .ic{width:16px;height:16px;display:flex;align-items:center;justify-content:center;opacity:.65;flex-shrink:0;}
|
||||
.v5-si:hover{background:var(--v5-surface-hover);color:var(--v5-text);transform:translateX(2px);}
|
||||
@@ -1446,3 +1446,6 @@ html.vt-color-changing .settings-color-swatch.on,
|
||||
html.vt-color-changing .v5-bell,
|
||||
html.vt-color-changing .v5-admin-btn{
|
||||
animation:v5-color-refresh .55s cubic-bezier(.34,1.4,.64,1) both;}
|
||||
|
||||
/* ===== Header separator (v5 redesign 2026-04-22) ===== */
|
||||
.v5-hdr-sep{display:inline-block;width:1px;height:18px;background:var(--v5-border);margin:0 .35rem;flex-shrink:0;opacity:.7;}
|
||||
|
||||
Reference in New Issue
Block a user