시스템 상태 카드 — 눈금 레이블+블록바+신호등 동일 높이 정렬
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,11 +3,13 @@ import React from 'react';
|
|||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
pct: number; // 0–100
|
pct: number; // 0–100
|
||||||
usedLabel: string; // e.g. "5.8GB"
|
usedLabel: string;
|
||||||
totalLabel: string; // e.g. "7.8GB"
|
totalLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLOCKS = 12;
|
const BLOCKS = 10; // 10단계 (0%~100%, 10%씩)
|
||||||
|
// 눈금 레이블: top→bottom (100%→0%)
|
||||||
|
const TICK_LABELS = ['100%', '80%', '60%', '40%', '20%', '0%'];
|
||||||
|
|
||||||
function trafficLevel(pct: number): 'green' | 'yellow' | 'red' {
|
function trafficLevel(pct: number): 'green' | 'yellow' | 'red' {
|
||||||
if (pct >= 90) return 'red';
|
if (pct >= 90) return 'red';
|
||||||
@@ -15,9 +17,17 @@ function trafficLevel(pct: number): 'green' | 'yellow' | 'red' {
|
|||||||
return 'green';
|
return 'green';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 리소스 카드 — 세로 블록 바 + 신호등 */
|
/** 블록 인덱스(0=bottom)에 따른 색상 — 낮은 사용률=초록, 높은 사용률=빨강 */
|
||||||
|
function blockTone(idx: number): string {
|
||||||
|
if (idx >= 8) return 'gc-sys-block--hi'; // 80–100% → 빨강
|
||||||
|
if (idx >= 5) return 'gc-sys-block--mid'; // 50–80% → 노랑
|
||||||
|
return 'gc-sys-block--lo'; // 0–50% → 초록
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 리소스 카드 — 눈금 + 세로 블록 바 + 신호등 (같은 높이 정렬) */
|
||||||
export default function DashboardSystemCard({ label, pct, usedLabel, totalLabel }: Props) {
|
export default function DashboardSystemCard({ label, pct, usedLabel, totalLabel }: Props) {
|
||||||
const clamped = Math.max(0, Math.min(100, pct));
|
const clamped = Math.max(0, Math.min(100, pct));
|
||||||
|
// 아래→위로 채움: filled 개수만큼 하단부터 켜짐
|
||||||
const filled = Math.round((clamped / 100) * BLOCKS);
|
const filled = Math.round((clamped / 100) * BLOCKS);
|
||||||
const level = trafficLevel(clamped);
|
const level = trafficLevel(clamped);
|
||||||
|
|
||||||
@@ -25,15 +35,23 @@ export default function DashboardSystemCard({ label, pct, usedLabel, totalLabel
|
|||||||
<div className="gc-sys-card">
|
<div className="gc-sys-card">
|
||||||
<span className="gc-sys-card-title">{label}</span>
|
<span className="gc-sys-card-title">{label}</span>
|
||||||
|
|
||||||
|
{/* 바 + 신호등을 같은 높이로 정렬 */}
|
||||||
<div className="gc-sys-card-body">
|
<div className="gc-sys-card-body">
|
||||||
|
|
||||||
|
{/* 눈금 레이블 열 */}
|
||||||
|
<div className="gc-sys-ticks" aria-hidden>
|
||||||
|
{TICK_LABELS.map(t => (
|
||||||
|
<span key={t} className="gc-sys-tick">{t}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 세로 블록 바 */}
|
{/* 세로 블록 바 */}
|
||||||
<div className="gc-sys-bar-wrap" aria-hidden>
|
<div className="gc-sys-bar-wrap" aria-hidden>
|
||||||
{Array.from({ length: BLOCKS }, (_, i) => {
|
{Array.from({ length: BLOCKS }, (_, i) => {
|
||||||
const idx = BLOCKS - 1 - i; // 위→낮음, 아래→높음
|
// i=0 → 화면 상단(100%), i=BLOCKS-1 → 화면 하단(0%)
|
||||||
const on = idx < filled;
|
const idxFromBottom = BLOCKS - 1 - i;
|
||||||
let tone = 'gc-sys-block--mid';
|
const on = idxFromBottom < filled;
|
||||||
if (idx >= BLOCKS - 3) tone = 'gc-sys-block--lo'; // 하위(초록)
|
const tone = blockTone(idxFromBottom);
|
||||||
else if (idx <= 2) tone = 'gc-sys-block--hi'; // 상위(빨강)
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
@@ -43,14 +61,18 @@ export default function DashboardSystemCard({ label, pct, usedLabel, totalLabel
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 신호등 */}
|
{/* 신호등 (바와 같은 높이) */}
|
||||||
<div className={`gc-traffic-light gc-traffic-light--${level}`} aria-label={`${label} 상태`}>
|
<div
|
||||||
|
className={`gc-traffic-light gc-traffic-light--${level} gc-traffic-light--sized`}
|
||||||
|
aria-label={`${label} 상태`}
|
||||||
|
>
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 하단 수치 */}
|
||||||
<div className="gc-sys-card-foot">
|
<div className="gc-sys-card-foot">
|
||||||
<span className="gc-sys-card-pct">{clamped.toFixed(0)}%</span>
|
<span className="gc-sys-card-pct">{clamped.toFixed(0)}%</span>
|
||||||
<span className="gc-sys-card-detail">{usedLabel} / {totalLabel}</span>
|
<span className="gc-sys-card-detail">{usedLabel} / {totalLabel}</span>
|
||||||
|
|||||||
@@ -340,62 +340,94 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
padding: 10px 8px;
|
padding: 12px 10px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--se-border);
|
border: 1px solid var(--se-border);
|
||||||
background: color-mix(in srgb, var(--se-bg) 60%, transparent);
|
background: color-mix(in srgb, var(--se-bg) 55%, transparent);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gc-sys-card-title {
|
.gc-sys-card-title {
|
||||||
font-size: 0.65rem;
|
font-size: 0.67rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--se-text-muted);
|
color: var(--se-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 바 영역 + 신호등 — 세로 높이 동일 */
|
||||||
.gc-sys-card-body {
|
.gc-sys-card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: stretch; /* 같은 높이 */
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 눈금 레이블 열 */
|
||||||
|
.gc-sys-ticks {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gc-sys-tick {
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: var(--se-text-dim, #666);
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 세로 블록 바 */
|
/* 세로 블록 바 */
|
||||||
.gc-sys-bar-wrap {
|
.gc-sys-bar-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gc-sys-block {
|
.gc-sys-block {
|
||||||
width: 18px;
|
width: 28px;
|
||||||
height: 8px;
|
flex: 1;
|
||||||
border-radius: 2px;
|
border-radius: 3px;
|
||||||
background: color-mix(in srgb, var(--se-text-dim, #666) 18%, #111);
|
background: color-mix(in srgb, var(--se-text-dim, #555) 15%, #0d0d0d);
|
||||||
opacity: 0.28;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gc-sys-block--on { opacity: 1; }
|
.gc-sys-block--on { opacity: 1; }
|
||||||
|
|
||||||
/* 하위 블록 (낮은 사용률 = 초록) */
|
/* 낮은 사용률 (하단 = 0~50%) → 초록 */
|
||||||
.gc-sys-block--lo.gc-sys-block--on {
|
.gc-sys-block--lo.gc-sys-block--on {
|
||||||
background: #34d399;
|
background: #34d399;
|
||||||
box-shadow: 0 0 4px rgba(52,211,153,0.6);
|
box-shadow: 0 0 5px rgba(52, 211, 153, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 중간 블록 (중간 사용률 = 노랑) */
|
/* 중간 사용률 (50~80%) → 노랑 */
|
||||||
.gc-sys-block--mid.gc-sys-block--on {
|
.gc-sys-block--mid.gc-sys-block--on {
|
||||||
background: #fbbf24;
|
background: #fbbf24;
|
||||||
box-shadow: 0 0 4px rgba(251,191,36,0.6);
|
box-shadow: 0 0 5px rgba(251, 191, 36, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 상위 블록 (높은 사용률 = 빨강) */
|
/* 높은 사용률 (80~100%) → 빨강 */
|
||||||
.gc-sys-block--hi.gc-sys-block--on {
|
.gc-sys-block--hi.gc-sys-block--on {
|
||||||
background: #f87171;
|
background: #f87171;
|
||||||
box-shadow: 0 0 4px rgba(248,113,113,0.6);
|
box-shadow: 0 0 5px rgba(248, 113, 113, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 신호등 — 바와 같은 높이에 맞춤 */
|
||||||
|
.gc-traffic-light--sized {
|
||||||
|
width: 44px;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gc-traffic-light--sized .gc-traffic-bulb {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gc-sys-card-foot {
|
.gc-sys-card-foot {
|
||||||
@@ -406,7 +438,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gc-sys-card-pct {
|
.gc-sys-card-pct {
|
||||||
font-size: 0.95rem;
|
font-size: 1rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--se-text);
|
color: var(--se-text);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user