시스템 상태
에이전트·서버·연동 서비스의 상태를 한눈에 보여줍니다. 정상은 조용하게, 이상은 눈에 띄게.
Composition
이 패턴이 조합하는 컴포넌트입니다. 세부 옵션은 각 컴포넌트 페이지를 참고하세요.
1. 서비스 상태 보드
SIEM 연동 연결 끊김09:12 부터 이벤트 전송이 실패하고 있습니다. 인증 토큰을 확인하세요.
수집 서버
정책 배포
업데이트 서버
SIEM 연동
DB 점검
라이선스
HTML
<div class="stack" style="max-width:800px">
<div class="notice danger" role="alert"><span class="ico"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg></span><div><b>SIEM 연동 연결 끊김</b>09:12 부터 이벤트 전송이 실패하고 있습니다. 인증 토큰을 확인하세요.<div class="actions"><button type="button" class="btn sm primary">연동 설정</button><button type="button" class="btn sm text">다시 시도</button></div></div></div>
<div class="status-list">
<div class="status-item"><span class="indicator ok">수집 서버</span><span class="meta">가동 99.98%</span></div>
<div class="status-item"><span class="indicator ok">정책 배포</span><span class="meta">rev 2041</span></div>
<div class="status-item"><span class="indicator warn">업데이트 서버</span><span class="meta">지연 3분</span></div>
<div class="status-item"><span class="indicator danger">SIEM 연동</span><span class="meta">09:12 끊김</span></div>
<div class="status-item"><span class="indicator info">DB 점검</span><span class="meta">02:00~04:00</span></div>
<div class="status-item"><span class="indicator ok">라이선스</span><span class="meta">842 / 1,000</span></div>
</div>
</div>CSS · .indicator, .count, .with-count, .steps, .card, .card-grid, .tag, .notice, .toast, .toast-stack, .banner, .spinner, .spinner-wrap, .skeleton, .skeleton-row, .progress, .progress-row, .status-list, .status-item, .stack
/* JS Design System v0.6 · .indicator, .count, .with-count, .steps, .card, .card-grid, .tag, .notice, .toast, .toast-stack, .banner, .spinner, .spinner-wrap, .skeleton, .skeleton-row, .progress, .progress-row, .status-list, .status-item, .stack · 토큰(var(--*))은 assets/style.css :root 참고 */
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.tabs .tab .count{font-size:12px;font-weight:700;padding:1px 6px;border-radius:var(--radius-full);background:var(--bg-panel);color:var(--text-secondary);min-width:0;height:auto}
.tabs .tab.on .count{background:var(--accent-subtle);color:var(--accent-text)}
.sidenav a .count{margin-left:auto}
.sidenav.collapsed a span,.sidenav.collapsed .group,.sidenav.collapsed a .count{display:none}
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.notice{display:flex;gap:12px;align-items:flex-start;border-radius:var(--radius-lg);padding:14px 18px;font-size:14px;border:1px solid var(--border-default);width:100%;background:var(--bg-surface);line-height:1.55}
.notice b{display:block;margin-bottom:2px}
.notice .ico{flex:none;width:20px;height:20px;display:grid;place-items:center;color:var(--info-text);margin-top:1px}
.notice .ico svg{width:20px;height:20px}
.notice .close{margin-left:auto;flex:none;border:0;background:transparent;color:var(--text-tertiary);font:inherit;line-height:1;cursor:pointer;padding:2px;display:grid;place-items:center}
.notice .close svg{width:14px;height:14px}
.notice .close:hover{color:var(--text-primary)}
.notice .actions{display:flex;gap:8px;margin-top:8px}
.notice.info{background:var(--info-subtle);border-color:color-mix(in srgb,var(--info) 30%,transparent)}
.notice.success{background:var(--success-subtle);border-color:color-mix(in srgb,var(--success) 30%,transparent)}
.notice.success .ico{color:var(--success-text)}
.notice.warning{background:var(--warning-subtle);border-color:color-mix(in srgb,var(--warning) 30%,transparent)}
.notice.warning .ico{color:var(--warning-text)}
.notice.danger{background:var(--danger-subtle);border-color:color-mix(in srgb,var(--danger) 30%,transparent)}
.notice.danger .ico{color:var(--danger-text)}
.toast{display:inline-flex;align-items:center;gap:12px;width:360px;max-width:100%;padding:12px 16px;border-radius:var(--radius-lg);background:var(--bg-inverse);color:var(--text-inverse);font-size:14px;box-shadow:var(--shadow-3)}
.toast .ico{flex:none;width:20px;height:20px;display:grid;place-items:center;color:var(--success)}
.toast .ico svg{width:20px;height:20px}
.toast.danger .ico{color:var(--danger)}
.toast .action{margin-left:auto;background:none;border:0;color:var(--accent-inverse);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
.toast .action:hover{text-decoration:underline}
.toast-stack{display:flex;flex-direction:column;gap:8px;align-items:flex-end}
.toast-stack.fixed{position:fixed;right:24px;bottom:24px;z-index:110}
.banner{display:flex;align-items:center;gap:12px;width:100%;padding:10px 20px;background:var(--accent);color:var(--accent-on);font-size:14px;font-weight:500}
.banner .btn{margin-left:auto}
.banner.warning{background:var(--warning);color:var(--warning-on)}
.banner.danger{background:var(--danger);color:var(--danger-on)}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--border-default);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
.spinner.sm{width:16px;height:16px;border-width:2px}
.spinner.lg{width:40px;height:40px;border-width:4px}
.spinner-wrap{display:inline-flex;flex-direction:column;align-items:center;gap:12px;font-size:12px;color:var(--text-tertiary)}
.skeleton{display:block;height:14px;border-radius:6px;background:linear-gradient(90deg,var(--bg-panel) 25%,var(--bg-subtle) 50%,var(--bg-panel) 75%);background-size:200% 100%;animation:shimmer 1.4s infinite}
.skeleton.title{height:20px;width:40%}
.skeleton.text{width:100%}
.skeleton.text.short{width:70%}
.skeleton.circle{width:40px;height:40px;border-radius:50%;flex:none}
.skeleton.rect{height:120px;border-radius:var(--radius-md)}
.skeleton-row{display:flex;align-items:center;gap:12px;width:100%}
.skeleton-row .lines{flex:1;display:flex;flex-direction:column;gap:8px}
.progress{width:100%;height:8px;border-radius:var(--radius-full);background:var(--fill-strong);overflow:hidden}
.progress i{display:block;height:100%;background:var(--accent-graphic);border-radius:var(--radius-full);transition:width var(--motion-base)}
.progress.sm{height:4px}
.progress.success i{background:var(--success)}
.progress.danger i{background:var(--danger)}
.progress-row{display:flex;flex-direction:column;gap:6px;width:100%;font-size:12px;color:var(--text-secondary)}
.progress-row .row{display:flex;justify-content:space-between}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.count{display:inline-grid;place-items:center;min-width:20px;height:20px;padding:0 6px;border-radius:var(--radius-full);background:var(--danger);color:var(--danger-on);font-size:12px;font-weight:700;line-height:1;}
.count.neutral{background:var(--bg-panel);color:var(--text-secondary)}
.count.accent{background:var(--accent);color:var(--accent-on)}
.count.dot{min-width:8px;width:8px;height:8px;padding:0}
.with-count{position:relative;display:inline-flex}
.with-count .count{position:absolute;top:-6px;right:-6px}
.steps{display:flex;align-items:center;gap:8px;list-style:none;padding:0;margin:0;font-size:12px;max-width:none}
.steps li{display:flex;align-items:center;gap:8px;color:var(--text-tertiary);margin:0;white-space:nowrap}
.steps li i{width:24px;height:24px;border-radius:50%;display:inline-grid;place-items:center;font-style:normal;font-size:12px;font-weight:700;background:var(--fill-strong);color:var(--text-secondary);flex:none}
.steps li i svg{width:12px;height:12px;stroke-width:3}
.steps li.done i{background:var(--success);color:var(--success-on)}
.steps li.on{color:var(--text-primary);font-weight:600}
.steps li.on i{background:var(--accent);color:var(--accent-on)}
.steps li+li::before{content:"";width:32px;height:1px;background:var(--border-strong)}
.steps.vertical{flex-direction:column;align-items:flex-start}
.steps.vertical li+li::before{display:none}
.tile .tag{position:absolute;top:12px;right:12px}
.stack{display:flex;flex-direction:column;gap:16px;width:100%}
.status-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;width:100%}
.status-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);font-size:14px}
.status-item .indicator{font-weight:600}
.status-item .meta{margin-left:auto;font-size:12px;color:var(--text-tertiary);}
.onboard .card{width:100%}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes shimmer{to{background-position:-200% 0}}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { Button, Notice, Stack, StatusItem, StatusList } from "@jiran/ds-react";
export default function Example() {
return (
<Stack style={{ maxWidth: 800 }}>
<Notice tone="danger" title="SIEM 연동 연결 끊김" actions={<><Button size="sm">연동 설정</Button><Button size="sm" variant="text">다시 시도</Button></>}>
09:12 부터 이벤트 전송이 실패하고 있습니다. 인증 토큰을 확인하세요.
</Notice>
<StatusList>
<StatusItem tone="ok" meta="가동 99.98%">수집 서버</StatusItem>
<StatusItem tone="ok" meta="rev 2041">정책 배포</StatusItem>
<StatusItem tone="warn" meta="지연 3분">업데이트 서버</StatusItem>
<StatusItem tone="danger" meta="09:12 끊김">SIEM 연동</StatusItem>
<StatusItem tone="info" meta="02:00~04:00">DB 점검</StatusItem>
<StatusItem tone="ok" meta="842 / 1,000">라이선스</StatusItem>
</StatusList>
</Stack>
);
}2. 에이전트 상태 요약
연결 상태 비율 + 진행 중 작업.
에이전트 연결
정상온라인1,247 / 1,284
오프라인 37대 · 10분 이상 미체크인 12대
진행 중 작업
정책 rev 2041 배포66%
842 / 1,284대 완료 · 예상 3분
HTML
<div class="row-2" style="max-width:800px">
<div class="card"><div class="card-head"><h3 class="title">에이전트 연결</h3><span class="tag ok">정상</span></div><div class="progress-row"><div class="row"><span>온라인</span><span>1,247 / 1,284</span></div><div class="progress success" role="progressbar" aria-valuenow="97"><i style="width:97%"></i></div></div><p class="desc">오프라인 37대 · 10분 이상 미체크인 12대</p></div>
<div class="card"><div class="card-head"><h3 class="title">진행 중 작업</h3><span class="spinner sm" role="status" aria-label="진행 중"></span></div><div class="progress-row"><div class="row"><span>정책 rev 2041 배포</span><span>66%</span></div><div class="progress" role="progressbar" aria-valuenow="66"><i style="width:66%"></i></div></div><p class="desc">842 / 1,284대 완료 · 예상 3분</p></div>
</div>CSS · .indicator, .count, .with-count, .steps, .card, .card-grid, .tag, .notice, .toast, .toast-stack, .banner, .spinner, .spinner-wrap, .skeleton, .skeleton-row, .progress, .progress-row, .status-list, .status-item, .stack
/* JS Design System v0.6 · .indicator, .count, .with-count, .steps, .card, .card-grid, .tag, .notice, .toast, .toast-stack, .banner, .spinner, .spinner-wrap, .skeleton, .skeleton-row, .progress, .progress-row, .status-list, .status-item, .stack · 토큰(var(--*))은 assets/style.css :root 참고 */
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.tabs .tab .count{font-size:12px;font-weight:700;padding:1px 6px;border-radius:var(--radius-full);background:var(--bg-panel);color:var(--text-secondary);min-width:0;height:auto}
.tabs .tab.on .count{background:var(--accent-subtle);color:var(--accent-text)}
.sidenav a .count{margin-left:auto}
.sidenav.collapsed a span,.sidenav.collapsed .group,.sidenav.collapsed a .count{display:none}
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.notice{display:flex;gap:12px;align-items:flex-start;border-radius:var(--radius-lg);padding:14px 18px;font-size:14px;border:1px solid var(--border-default);width:100%;background:var(--bg-surface);line-height:1.55}
.notice b{display:block;margin-bottom:2px}
.notice .ico{flex:none;width:20px;height:20px;display:grid;place-items:center;color:var(--info-text);margin-top:1px}
.notice .ico svg{width:20px;height:20px}
.notice .close{margin-left:auto;flex:none;border:0;background:transparent;color:var(--text-tertiary);font:inherit;line-height:1;cursor:pointer;padding:2px;display:grid;place-items:center}
.notice .close svg{width:14px;height:14px}
.notice .close:hover{color:var(--text-primary)}
.notice .actions{display:flex;gap:8px;margin-top:8px}
.notice.info{background:var(--info-subtle);border-color:color-mix(in srgb,var(--info) 30%,transparent)}
.notice.success{background:var(--success-subtle);border-color:color-mix(in srgb,var(--success) 30%,transparent)}
.notice.success .ico{color:var(--success-text)}
.notice.warning{background:var(--warning-subtle);border-color:color-mix(in srgb,var(--warning) 30%,transparent)}
.notice.warning .ico{color:var(--warning-text)}
.notice.danger{background:var(--danger-subtle);border-color:color-mix(in srgb,var(--danger) 30%,transparent)}
.notice.danger .ico{color:var(--danger-text)}
.toast{display:inline-flex;align-items:center;gap:12px;width:360px;max-width:100%;padding:12px 16px;border-radius:var(--radius-lg);background:var(--bg-inverse);color:var(--text-inverse);font-size:14px;box-shadow:var(--shadow-3)}
.toast .ico{flex:none;width:20px;height:20px;display:grid;place-items:center;color:var(--success)}
.toast .ico svg{width:20px;height:20px}
.toast.danger .ico{color:var(--danger)}
.toast .action{margin-left:auto;background:none;border:0;color:var(--accent-inverse);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
.toast .action:hover{text-decoration:underline}
.toast-stack{display:flex;flex-direction:column;gap:8px;align-items:flex-end}
.toast-stack.fixed{position:fixed;right:24px;bottom:24px;z-index:110}
.banner{display:flex;align-items:center;gap:12px;width:100%;padding:10px 20px;background:var(--accent);color:var(--accent-on);font-size:14px;font-weight:500}
.banner .btn{margin-left:auto}
.banner.warning{background:var(--warning);color:var(--warning-on)}
.banner.danger{background:var(--danger);color:var(--danger-on)}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--border-default);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
.spinner.sm{width:16px;height:16px;border-width:2px}
.spinner.lg{width:40px;height:40px;border-width:4px}
.spinner-wrap{display:inline-flex;flex-direction:column;align-items:center;gap:12px;font-size:12px;color:var(--text-tertiary)}
.skeleton{display:block;height:14px;border-radius:6px;background:linear-gradient(90deg,var(--bg-panel) 25%,var(--bg-subtle) 50%,var(--bg-panel) 75%);background-size:200% 100%;animation:shimmer 1.4s infinite}
.skeleton.title{height:20px;width:40%}
.skeleton.text{width:100%}
.skeleton.text.short{width:70%}
.skeleton.circle{width:40px;height:40px;border-radius:50%;flex:none}
.skeleton.rect{height:120px;border-radius:var(--radius-md)}
.skeleton-row{display:flex;align-items:center;gap:12px;width:100%}
.skeleton-row .lines{flex:1;display:flex;flex-direction:column;gap:8px}
.progress{width:100%;height:8px;border-radius:var(--radius-full);background:var(--fill-strong);overflow:hidden}
.progress i{display:block;height:100%;background:var(--accent-graphic);border-radius:var(--radius-full);transition:width var(--motion-base)}
.progress.sm{height:4px}
.progress.success i{background:var(--success)}
.progress.danger i{background:var(--danger)}
.progress-row{display:flex;flex-direction:column;gap:6px;width:100%;font-size:12px;color:var(--text-secondary)}
.progress-row .row{display:flex;justify-content:space-between}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.count{display:inline-grid;place-items:center;min-width:20px;height:20px;padding:0 6px;border-radius:var(--radius-full);background:var(--danger);color:var(--danger-on);font-size:12px;font-weight:700;line-height:1;}
.count.neutral{background:var(--bg-panel);color:var(--text-secondary)}
.count.accent{background:var(--accent);color:var(--accent-on)}
.count.dot{min-width:8px;width:8px;height:8px;padding:0}
.with-count{position:relative;display:inline-flex}
.with-count .count{position:absolute;top:-6px;right:-6px}
.steps{display:flex;align-items:center;gap:8px;list-style:none;padding:0;margin:0;font-size:12px;max-width:none}
.steps li{display:flex;align-items:center;gap:8px;color:var(--text-tertiary);margin:0;white-space:nowrap}
.steps li i{width:24px;height:24px;border-radius:50%;display:inline-grid;place-items:center;font-style:normal;font-size:12px;font-weight:700;background:var(--fill-strong);color:var(--text-secondary);flex:none}
.steps li i svg{width:12px;height:12px;stroke-width:3}
.steps li.done i{background:var(--success);color:var(--success-on)}
.steps li.on{color:var(--text-primary);font-weight:600}
.steps li.on i{background:var(--accent);color:var(--accent-on)}
.steps li+li::before{content:"";width:32px;height:1px;background:var(--border-strong)}
.steps.vertical{flex-direction:column;align-items:flex-start}
.steps.vertical li+li::before{display:none}
.tile .tag{position:absolute;top:12px;right:12px}
.stack{display:flex;flex-direction:column;gap:16px;width:100%}
.status-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;width:100%}
.status-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);font-size:14px}
.status-item .indicator{font-weight:600}
.status-item .meta{margin-left:auto;font-size:12px;color:var(--text-tertiary);}
.onboard .card{width:100%}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes shimmer{to{background-position:-200% 0}}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { Card, CardDesc, CardHead, CardTitle, Progress, Row2, Spinner, Tag } from "@jiran/ds-react";
export default function Example() {
return (
<Row2 style={{ maxWidth: 800 }}>
<Card>
<CardHead><CardTitle>에이전트 연결</CardTitle><Tag tone="ok">정상</Tag></CardHead>
<Progress label="온라인" value={1247} max={1284} valueText="1,247 / 1,284" tone="success" />
<CardDesc>오프라인 37대 · 10분 이상 미체크인 12대</CardDesc>
</Card>
<Card>
<CardHead><CardTitle>진행 중 작업</CardTitle><Spinner size="sm" label="진행 중" /></CardHead>
<Progress label="정책 rev 2041 배포" value={66} />
<CardDesc>842 / 1,284대 완료 · 예상 3분</CardDesc>
</Card>
</Row2>
);
}Usage
React: @jiran/ds-react 패키지를 설치하고 style.css 를 한 번 import 하면 위 React 탭 코드를 그대로 쓸 수 있습니다(React Package). HTML: assets/style.css 를 연결하고(일부만 필요하면 아래 컴포넌트 CSS 복사), 동작이 필요하면 assets/ds.js 를 함께 넣습니다 — 이 페이지의 프리뷰가 그 스크립트로 동작합니다. 색은 전부 토큰(var(--accent) 등)을 참조하므로 --accent-* 만 제품 컬러로 바꾸면 됩니다.
React @jiran/ds-react
npm i @jiran/ds-react
// main.tsx — 앱 진입점에서 한 번
import "@jiran/ds-react/style.css";
// 컴포넌트
import { StatusList, StatusItem, Indicator, Count, WithCount, Steps, Card, CardHead, CardTitle, CardDesc, CardFoot, CardGrid, Tag, Notice, Toast, ToastStack, Banner, ToastProvider, useToast, Spinner, Skeleton, SkeletonRow, Progress } from "@jiran/ds-react";HTML
<!-- 1) 토큰 + 전체 컴포넌트 스타일 한 번에 연결 -->
<link rel="stylesheet" href="assets/style.css">
<!-- 동작(드롭다운·탭·달력·표 정렬/선택·토스트 등) — 순수 JS 한 파일. React 제품은 불필요 -->
<script src="assets/ds.js" defer></script>
<!-- 2) 또는 아래 "Component CSS" 만 프로젝트 CSS 에 붙이고,
:root 토큰은 Foundations › Design Token 에서 복사 -->Component CSS .indicator, .count, .with-count, .steps, .card, .card-grid, .tag, .notice, .toast, .toast-stack, .banner, .spinner, .spinner-wrap, .skeleton, .skeleton-row, .progress, .progress-row, .status-list, .status-item, .stack
/* JS Design System v0.6 · .indicator, .count, .with-count, .steps, .card, .card-grid, .tag, .notice, .toast, .toast-stack, .banner, .spinner, .spinner-wrap, .skeleton, .skeleton-row, .progress, .progress-row, .status-list, .status-item, .stack · 토큰(var(--*))은 assets/style.css :root 참고 */
.tag{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:var(--radius-full);line-height:1.6;background:var(--bg-panel);color:var(--text-secondary);white-space:nowrap}
.tag::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
.tag.critical{background:var(--sev-critical-bg);color:var(--sev-critical-text)}
.tag.high{background:var(--sev-high-bg);color:var(--sev-high-text)}
.tag.medium{background:var(--sev-medium-bg);color:var(--sev-medium-text)}
.tag.low{background:var(--sev-low-bg);color:var(--sev-low-text)}
.tag.info{background:var(--sev-info-bg);color:var(--sev-info-text)}
.tag.ok{background:var(--success-subtle);color:var(--success-text)}
.tag.accent{background:var(--accent-subtle);color:var(--accent-text)}
.tag.sm{font-size:12px;padding:1px 8px}
.tag.lg{font-size:12px;padding:5px 12px}
.tag.plain::before{display:none}
.tag.outline{background:transparent;box-shadow:inset 0 0 0 1px currentColor}
.tag.solid.critical{background:var(--sev-critical);color:var(--sev-critical-on)}
.tag.solid.high{background:var(--sev-high);color:var(--sev-high-on)}
.tag.solid.medium{background:var(--sev-medium);color:var(--sev-medium-on)}
.tag.solid.low{background:var(--sev-low);color:var(--sev-low-on)}
.tag.solid.info{background:var(--sev-info);color:var(--sev-info-on)}
.tag.solid.ok{background:var(--success);color:var(--success-on)}
.tag.solid.accent{background:var(--accent);color:var(--accent-on)}
.tabs .tab .count{font-size:12px;font-weight:700;padding:1px 6px;border-radius:var(--radius-full);background:var(--bg-panel);color:var(--text-secondary);min-width:0;height:auto}
.tabs .tab.on .count{background:var(--accent-subtle);color:var(--accent-text)}
.sidenav a .count{margin-left:auto}
.sidenav.collapsed a span,.sidenav.collapsed .group,.sidenav.collapsed a .count{display:none}
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.notice{display:flex;gap:12px;align-items:flex-start;border-radius:var(--radius-lg);padding:14px 18px;font-size:14px;border:1px solid var(--border-default);width:100%;background:var(--bg-surface);line-height:1.55}
.notice b{display:block;margin-bottom:2px}
.notice .ico{flex:none;width:20px;height:20px;display:grid;place-items:center;color:var(--info-text);margin-top:1px}
.notice .ico svg{width:20px;height:20px}
.notice .close{margin-left:auto;flex:none;border:0;background:transparent;color:var(--text-tertiary);font:inherit;line-height:1;cursor:pointer;padding:2px;display:grid;place-items:center}
.notice .close svg{width:14px;height:14px}
.notice .close:hover{color:var(--text-primary)}
.notice .actions{display:flex;gap:8px;margin-top:8px}
.notice.info{background:var(--info-subtle);border-color:color-mix(in srgb,var(--info) 30%,transparent)}
.notice.success{background:var(--success-subtle);border-color:color-mix(in srgb,var(--success) 30%,transparent)}
.notice.success .ico{color:var(--success-text)}
.notice.warning{background:var(--warning-subtle);border-color:color-mix(in srgb,var(--warning) 30%,transparent)}
.notice.warning .ico{color:var(--warning-text)}
.notice.danger{background:var(--danger-subtle);border-color:color-mix(in srgb,var(--danger) 30%,transparent)}
.notice.danger .ico{color:var(--danger-text)}
.toast{display:inline-flex;align-items:center;gap:12px;width:360px;max-width:100%;padding:12px 16px;border-radius:var(--radius-lg);background:var(--bg-inverse);color:var(--text-inverse);font-size:14px;box-shadow:var(--shadow-3)}
.toast .ico{flex:none;width:20px;height:20px;display:grid;place-items:center;color:var(--success)}
.toast .ico svg{width:20px;height:20px}
.toast.danger .ico{color:var(--danger)}
.toast .action{margin-left:auto;background:none;border:0;color:var(--accent-inverse);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
.toast .action:hover{text-decoration:underline}
.toast-stack{display:flex;flex-direction:column;gap:8px;align-items:flex-end}
.toast-stack.fixed{position:fixed;right:24px;bottom:24px;z-index:110}
.banner{display:flex;align-items:center;gap:12px;width:100%;padding:10px 20px;background:var(--accent);color:var(--accent-on);font-size:14px;font-weight:500}
.banner .btn{margin-left:auto}
.banner.warning{background:var(--warning);color:var(--warning-on)}
.banner.danger{background:var(--danger);color:var(--danger-on)}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--border-default);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
.spinner.sm{width:16px;height:16px;border-width:2px}
.spinner.lg{width:40px;height:40px;border-width:4px}
.spinner-wrap{display:inline-flex;flex-direction:column;align-items:center;gap:12px;font-size:12px;color:var(--text-tertiary)}
.skeleton{display:block;height:14px;border-radius:6px;background:linear-gradient(90deg,var(--bg-panel) 25%,var(--bg-subtle) 50%,var(--bg-panel) 75%);background-size:200% 100%;animation:shimmer 1.4s infinite}
.skeleton.title{height:20px;width:40%}
.skeleton.text{width:100%}
.skeleton.text.short{width:70%}
.skeleton.circle{width:40px;height:40px;border-radius:50%;flex:none}
.skeleton.rect{height:120px;border-radius:var(--radius-md)}
.skeleton-row{display:flex;align-items:center;gap:12px;width:100%}
.skeleton-row .lines{flex:1;display:flex;flex-direction:column;gap:8px}
.progress{width:100%;height:8px;border-radius:var(--radius-full);background:var(--fill-strong);overflow:hidden}
.progress i{display:block;height:100%;background:var(--accent-graphic);border-radius:var(--radius-full);transition:width var(--motion-base)}
.progress.sm{height:4px}
.progress.success i{background:var(--success)}
.progress.danger i{background:var(--danger)}
.progress-row{display:flex;flex-direction:column;gap:6px;width:100%;font-size:12px;color:var(--text-secondary)}
.progress-row .row{display:flex;justify-content:space-between}
.indicator{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.indicator::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--interaction-inactive);flex:none}
.indicator.ok::before{background:var(--success)}
.indicator.warn::before{background:var(--warning)}
.indicator.danger::before{background:var(--danger)}
.indicator.info::before{background:var(--info)}
.indicator.accent::before{background:var(--accent-graphic)}
.indicator.pulse::before{box-shadow:0 0 0 0 var(--danger);animation:pulse 1.6s infinite}
.count{display:inline-grid;place-items:center;min-width:20px;height:20px;padding:0 6px;border-radius:var(--radius-full);background:var(--danger);color:var(--danger-on);font-size:12px;font-weight:700;line-height:1;}
.count.neutral{background:var(--bg-panel);color:var(--text-secondary)}
.count.accent{background:var(--accent);color:var(--accent-on)}
.count.dot{min-width:8px;width:8px;height:8px;padding:0}
.with-count{position:relative;display:inline-flex}
.with-count .count{position:absolute;top:-6px;right:-6px}
.steps{display:flex;align-items:center;gap:8px;list-style:none;padding:0;margin:0;font-size:12px;max-width:none}
.steps li{display:flex;align-items:center;gap:8px;color:var(--text-tertiary);margin:0;white-space:nowrap}
.steps li i{width:24px;height:24px;border-radius:50%;display:inline-grid;place-items:center;font-style:normal;font-size:12px;font-weight:700;background:var(--fill-strong);color:var(--text-secondary);flex:none}
.steps li i svg{width:12px;height:12px;stroke-width:3}
.steps li.done i{background:var(--success);color:var(--success-on)}
.steps li.on{color:var(--text-primary);font-weight:600}
.steps li.on i{background:var(--accent);color:var(--accent-on)}
.steps li+li::before{content:"";width:32px;height:1px;background:var(--border-strong)}
.steps.vertical{flex-direction:column;align-items:flex-start}
.steps.vertical li+li::before{display:none}
.tile .tag{position:absolute;top:12px;right:12px}
.stack{display:flex;flex-direction:column;gap:16px;width:100%}
.status-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;width:100%}
.status-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);font-size:14px}
.status-item .indicator{font-weight:600}
.status-item .meta{margin-left:auto;font-size:12px;color:var(--text-tertiary);}
.onboard .card{width:100%}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes shimmer{to{background-position:-200% 0}}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}