온보딩
첫 사용자가 콘솔을 설정하는 단계형 흐름입니다. 진행 단계 표시, 한 단계 한 결정, 건너뛰기 허용.
Composition
이 패턴이 조합하는 컴포넌트입니다. 세부 옵션은 각 컴포넌트 페이지를 참고하세요.
1. 단계 화면
상단 Steps + 카드 한 장 + 하단 이전/다음. 선택형 단계는 Item Tile.
- 조직 정보
- 2보호 수준
- 3에이전트 배포
- 4완료
보호 수준을 선택하세요
나중에 정책 › 기본 정책에서 바꿀 수 있습니다.
HTML
<div class="onboard">
<ol class="steps"><li class="done"><i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5l10 -10"/></svg></i>조직 정보</li><li class="on" aria-current="step"><i>2</i>보호 수준</li><li><i>3</i>에이전트 배포</li><li><i>4</i>완료</li></ol>
<div class="card">
<h3 class="title">보호 수준을 선택하세요</h3>
<p class="desc">나중에 정책 › 기본 정책에서 바꿀 수 있습니다.</p>
<div class="tile-grid" data-single style="grid-template-columns:1fr 1fr">
<button type="button" class="tile on" aria-pressed="true"><span class="ico">S</span><b>표준</b><span>알려진 위협 차단. 권장</span></button>
<button type="button" class="tile" aria-pressed="false"><span class="ico">E</span><b>강화</b><span>의심 행위까지 차단</span></button>
</div>
<div class="card-foot"><button type="button" class="btn md text">건너뛰기</button><button type="button" class="btn md secondary" data-step="prev">이전</button><button type="button" class="btn md primary" data-step="next">다음</button></div>
</div>
</div>CSS · .indicator, .count, .with-count, .steps, .card, .card-grid, .tile, .tile-grid, .tag, .btn, .btn-group, .onboard
/* JS Design System v0.6 · .indicator, .count, .with-count, .steps, .card, .card-grid, .tile, .tile-grid, .tag, .btn, .btn-group, .onboard · 토큰(var(--*))은 assets/style.css :root 참고 */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:44px;padding:0 20px;border-radius:var(--radius-md);border:1px solid transparent;font:inherit;font-size:14px;font-weight:600;line-height:1;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background var(--motion-fast),border-color var(--motion-fast)}
.btn.sm{height:36px;padding:0 14px;font-size:12px}
.btn.md{height:44px;padding:0 20px;font-size:14px}
.btn.lg{height:52px;padding:0 26px;font-size:16px}
.btn.primary{background:var(--accent);color:var(--accent-on)}
.btn.primary:hover{background:var(--accent-hover)}
.btn.primary:active{background:var(--accent-pressed)}
.btn.secondary{background:transparent;border-color:var(--border-strong);color:var(--text-primary)}
.btn.secondary:hover{background:var(--fill-normal)}
.btn.secondary:active{background:var(--fill-strong)}
.btn.tertiary{background:var(--fill-alternative);color:var(--text-primary)}
.btn.tertiary:hover{background:var(--fill-normal)}
.btn.tertiary:active{background:var(--fill-strong)}
.btn.text{background:transparent;color:var(--accent-text);padding-left:8px;padding-right:8px}
.btn.text:hover{background:var(--accent-subtle)}
.btn.text:active{background:var(--fill-strong)}
.btn.danger{background:var(--danger);color:var(--danger-on)}
.btn.danger:hover{background:var(--danger-hover)}
.btn.danger:active{background:var(--danger-pressed)}
.btn.danger.secondary{background:transparent;color:var(--danger-text);border-color:var(--danger)}
.btn.danger.secondary:hover{background:var(--danger-subtle)}
.btn.danger.secondary:active{background:var(--fill-strong)}
.btn.icon{width:44px;padding:0}
.btn.icon.sm{width:36px}
.btn.icon.lg{width:52px}
.btn svg{width:18px;height:18px;flex:none}
.btn.sm svg{width:16px;height:16px}
.btn.lg svg{width:20px;height:20px}
.btn.block{width:100%}
.btn[disabled],.btn[aria-disabled="true"]{background:var(--interaction-disable);border-color:transparent;color:var(--text-disabled);cursor:not-allowed;pointer-events:none}
.btn.secondary[disabled],.btn.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable)}
.btn.danger.secondary[disabled],.btn.danger.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable);color:var(--text-disabled)}
.btn.text[disabled],.btn.text[aria-disabled="true"]{background:transparent}
.btn.loading{position:relative;color:transparent;pointer-events:none}
.btn.loading::after{content:"";position:absolute;width:16px;height:16px;border:2px solid var(--fill-strong);border-top-color:var(--accent-on);border-radius:50%;animation:spin .8s linear infinite}
.btn.secondary.loading::after,.btn.tertiary.loading::after{border-top-color:var(--accent)}
.btn.text.loading::after{border-top-color:var(--accent-text)}
.btn.danger.loading::after{border-top-color:var(--danger-on)}
.btn.danger.secondary.loading::after{border-top-color:var(--danger)}
.btn-group{display:inline-flex;gap:8px;align-items:center}
.btn-group.end{justify-content:flex-end;width:100%}
.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%}
.banner .btn{margin-left:auto}
.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}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;width:100%}
.tile{display:flex;flex-direction:column;gap:8px;padding:16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);cursor:pointer;position:relative;text-align:left;font:inherit;color:var(--text-primary);transition:border-color var(--motion-fast),box-shadow var(--motion-fast)}
.tile:hover{border-color:var(--border-strong);box-shadow:var(--shadow-1)}
.tile.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:var(--accent-subtle)}
.tile .ico{width:36px;height:36px;border-radius:var(--radius-lg);background:var(--accent-subtle);color:var(--accent-text);display:grid;place-items:center;font-weight:700;font-size:14px}
.tile.on .ico{background:var(--accent);color:var(--accent-on)}
.tile b{font-size:14px}
.tile span{font-size:12px;color:var(--text-tertiary)}
.tile .tag{position:absolute;top:12px;right:12px}
.tile.horizontal{flex-direction:row;align-items:center}
.tile.horizontal>div{display:flex;flex-direction:column;gap:2px;min-width:0}
.tile[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.empty .btn{margin-top:10px}
.status-item .indicator{font-weight:600}
.onboard{display:flex;flex-direction:column;gap:24px;align-items:center;width:100%;max-width:560px;margin:0 auto}
.onboard .card{width:100%}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { useState } from "react";
import { Button, Card, CardDesc, CardFoot, CardTitle, Onboard, Steps, Tile, TileGrid } from "@jiran/ds-react";
export default function Example() {
const [level, setLevel] = useState("standard");
return (
<Onboard>
<Steps items={["조직 정보", "보호 수준", "에이전트 배포", "완료"]} current={1} />
<Card>
<CardTitle>보호 수준을 선택하세요</CardTitle>
<CardDesc>나중에 정책 › 기본 정책에서 바꿀 수 있습니다.</CardDesc>
<TileGrid style={{ gridTemplateColumns: "1fr 1fr" }}>
<Tile icon="S" title="표준" desc="알려진 위협 차단. 권장" selected={level === "standard"} onSelectedChange={() => setLevel("standard")} />
<Tile icon="E" title="강화" desc="의심 행위까지 차단" selected={level === "hardened"} onSelectedChange={() => setLevel("hardened")} />
</TileGrid>
<CardFoot>
<Button variant="text">건너뛰기</Button>
<Button variant="secondary">이전</Button>
<Button>다음</Button>
</CardFoot>
</Card>
</Onboard>
);
}2. 완료
요약 + 첫 행동으로 이어지는 Primary.
- 조직 정보
- 보호 수준
- 에이전트 배포
- 4완료
설정이 완료되었습니다
표준 보호 · 에이전트 12대 연결 대기 중. 첫 이벤트가 들어오면 대시보드에서 확인할 수 있습니다.
HTML
<div class="onboard">
<ol class="steps"><li class="done"><i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5l10 -10"/></svg></i>조직 정보</li><li class="done"><i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5l10 -10"/></svg></i>보호 수준</li><li class="done"><i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5l10 -10"/></svg></i>에이전트 배포</li><li class="on" aria-current="step"><i>4</i>완료</li></ol>
<div class="card" style="align-items:center;text-align:center">
<span class="indicator ok" style="font-size:16px;font-weight:600">설정이 완료되었습니다</span>
<p class="desc">표준 보호 · 에이전트 12대 연결 대기 중. 첫 이벤트가 들어오면 대시보드에서 확인할 수 있습니다.</p>
<button type="button" class="btn lg primary">대시보드로 이동</button>
</div>
</div>CSS · .indicator, .count, .with-count, .steps, .card, .card-grid, .tile, .tile-grid, .tag, .btn, .btn-group, .onboard
/* JS Design System v0.6 · .indicator, .count, .with-count, .steps, .card, .card-grid, .tile, .tile-grid, .tag, .btn, .btn-group, .onboard · 토큰(var(--*))은 assets/style.css :root 참고 */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:44px;padding:0 20px;border-radius:var(--radius-md);border:1px solid transparent;font:inherit;font-size:14px;font-weight:600;line-height:1;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background var(--motion-fast),border-color var(--motion-fast)}
.btn.sm{height:36px;padding:0 14px;font-size:12px}
.btn.md{height:44px;padding:0 20px;font-size:14px}
.btn.lg{height:52px;padding:0 26px;font-size:16px}
.btn.primary{background:var(--accent);color:var(--accent-on)}
.btn.primary:hover{background:var(--accent-hover)}
.btn.primary:active{background:var(--accent-pressed)}
.btn.secondary{background:transparent;border-color:var(--border-strong);color:var(--text-primary)}
.btn.secondary:hover{background:var(--fill-normal)}
.btn.secondary:active{background:var(--fill-strong)}
.btn.tertiary{background:var(--fill-alternative);color:var(--text-primary)}
.btn.tertiary:hover{background:var(--fill-normal)}
.btn.tertiary:active{background:var(--fill-strong)}
.btn.text{background:transparent;color:var(--accent-text);padding-left:8px;padding-right:8px}
.btn.text:hover{background:var(--accent-subtle)}
.btn.text:active{background:var(--fill-strong)}
.btn.danger{background:var(--danger);color:var(--danger-on)}
.btn.danger:hover{background:var(--danger-hover)}
.btn.danger:active{background:var(--danger-pressed)}
.btn.danger.secondary{background:transparent;color:var(--danger-text);border-color:var(--danger)}
.btn.danger.secondary:hover{background:var(--danger-subtle)}
.btn.danger.secondary:active{background:var(--fill-strong)}
.btn.icon{width:44px;padding:0}
.btn.icon.sm{width:36px}
.btn.icon.lg{width:52px}
.btn svg{width:18px;height:18px;flex:none}
.btn.sm svg{width:16px;height:16px}
.btn.lg svg{width:20px;height:20px}
.btn.block{width:100%}
.btn[disabled],.btn[aria-disabled="true"]{background:var(--interaction-disable);border-color:transparent;color:var(--text-disabled);cursor:not-allowed;pointer-events:none}
.btn.secondary[disabled],.btn.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable)}
.btn.danger.secondary[disabled],.btn.danger.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable);color:var(--text-disabled)}
.btn.text[disabled],.btn.text[aria-disabled="true"]{background:transparent}
.btn.loading{position:relative;color:transparent;pointer-events:none}
.btn.loading::after{content:"";position:absolute;width:16px;height:16px;border:2px solid var(--fill-strong);border-top-color:var(--accent-on);border-radius:50%;animation:spin .8s linear infinite}
.btn.secondary.loading::after,.btn.tertiary.loading::after{border-top-color:var(--accent)}
.btn.text.loading::after{border-top-color:var(--accent-text)}
.btn.danger.loading::after{border-top-color:var(--danger-on)}
.btn.danger.secondary.loading::after{border-top-color:var(--danger)}
.btn-group{display:inline-flex;gap:8px;align-items:center}
.btn-group.end{justify-content:flex-end;width:100%}
.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%}
.banner .btn{margin-left:auto}
.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}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;width:100%}
.tile{display:flex;flex-direction:column;gap:8px;padding:16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);cursor:pointer;position:relative;text-align:left;font:inherit;color:var(--text-primary);transition:border-color var(--motion-fast),box-shadow var(--motion-fast)}
.tile:hover{border-color:var(--border-strong);box-shadow:var(--shadow-1)}
.tile.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:var(--accent-subtle)}
.tile .ico{width:36px;height:36px;border-radius:var(--radius-lg);background:var(--accent-subtle);color:var(--accent-text);display:grid;place-items:center;font-weight:700;font-size:14px}
.tile.on .ico{background:var(--accent);color:var(--accent-on)}
.tile b{font-size:14px}
.tile span{font-size:12px;color:var(--text-tertiary)}
.tile .tag{position:absolute;top:12px;right:12px}
.tile.horizontal{flex-direction:row;align-items:center}
.tile.horizontal>div{display:flex;flex-direction:column;gap:2px;min-width:0}
.tile[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.empty .btn{margin-top:10px}
.status-item .indicator{font-weight:600}
.onboard{display:flex;flex-direction:column;gap:24px;align-items:center;width:100%;max-width:560px;margin:0 auto}
.onboard .card{width:100%}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}React · @jiran/ds-react
import { Button, Card, CardDesc, Indicator, Onboard, Steps } from "@jiran/ds-react";
export default function Example() {
return (
<Onboard>
<Steps items={["조직 정보", "보호 수준", "에이전트 배포", "완료"]} current={3} />
<Card style={{ alignItems: "center", textAlign: "center" }}>
<Indicator tone="ok" style={{ fontSize: 16, fontWeight: 600 }}>설정이 완료되었습니다</Indicator>
<CardDesc>표준 보호 · 에이전트 12대 연결 대기 중. 첫 이벤트가 들어오면 대시보드에서 확인할 수 있습니다.</CardDesc>
<Button size="lg">대시보드로 이동</Button>
</Card>
</Onboard>
);
}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 { Onboard, Steps, Indicator, Count, WithCount, Card, CardHead, CardTitle, CardDesc, CardFoot, CardGrid, Tile, TileGrid, Button, ButtonGroup } 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, .tile, .tile-grid, .tag, .btn, .btn-group, .onboard
/* JS Design System v0.6 · .indicator, .count, .with-count, .steps, .card, .card-grid, .tile, .tile-grid, .tag, .btn, .btn-group, .onboard · 토큰(var(--*))은 assets/style.css :root 참고 */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:44px;padding:0 20px;border-radius:var(--radius-md);border:1px solid transparent;font:inherit;font-size:14px;font-weight:600;line-height:1;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background var(--motion-fast),border-color var(--motion-fast)}
.btn.sm{height:36px;padding:0 14px;font-size:12px}
.btn.md{height:44px;padding:0 20px;font-size:14px}
.btn.lg{height:52px;padding:0 26px;font-size:16px}
.btn.primary{background:var(--accent);color:var(--accent-on)}
.btn.primary:hover{background:var(--accent-hover)}
.btn.primary:active{background:var(--accent-pressed)}
.btn.secondary{background:transparent;border-color:var(--border-strong);color:var(--text-primary)}
.btn.secondary:hover{background:var(--fill-normal)}
.btn.secondary:active{background:var(--fill-strong)}
.btn.tertiary{background:var(--fill-alternative);color:var(--text-primary)}
.btn.tertiary:hover{background:var(--fill-normal)}
.btn.tertiary:active{background:var(--fill-strong)}
.btn.text{background:transparent;color:var(--accent-text);padding-left:8px;padding-right:8px}
.btn.text:hover{background:var(--accent-subtle)}
.btn.text:active{background:var(--fill-strong)}
.btn.danger{background:var(--danger);color:var(--danger-on)}
.btn.danger:hover{background:var(--danger-hover)}
.btn.danger:active{background:var(--danger-pressed)}
.btn.danger.secondary{background:transparent;color:var(--danger-text);border-color:var(--danger)}
.btn.danger.secondary:hover{background:var(--danger-subtle)}
.btn.danger.secondary:active{background:var(--fill-strong)}
.btn.icon{width:44px;padding:0}
.btn.icon.sm{width:36px}
.btn.icon.lg{width:52px}
.btn svg{width:18px;height:18px;flex:none}
.btn.sm svg{width:16px;height:16px}
.btn.lg svg{width:20px;height:20px}
.btn.block{width:100%}
.btn[disabled],.btn[aria-disabled="true"]{background:var(--interaction-disable);border-color:transparent;color:var(--text-disabled);cursor:not-allowed;pointer-events:none}
.btn.secondary[disabled],.btn.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable)}
.btn.danger.secondary[disabled],.btn.danger.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable);color:var(--text-disabled)}
.btn.text[disabled],.btn.text[aria-disabled="true"]{background:transparent}
.btn.loading{position:relative;color:transparent;pointer-events:none}
.btn.loading::after{content:"";position:absolute;width:16px;height:16px;border:2px solid var(--fill-strong);border-top-color:var(--accent-on);border-radius:50%;animation:spin .8s linear infinite}
.btn.secondary.loading::after,.btn.tertiary.loading::after{border-top-color:var(--accent)}
.btn.text.loading::after{border-top-color:var(--accent-text)}
.btn.danger.loading::after{border-top-color:var(--danger-on)}
.btn.danger.secondary.loading::after{border-top-color:var(--danger)}
.btn-group{display:inline-flex;gap:8px;align-items:center}
.btn-group.end{justify-content:flex-end;width:100%}
.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%}
.banner .btn{margin-left:auto}
.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}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.tile-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;width:100%}
.tile{display:flex;flex-direction:column;gap:8px;padding:16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);cursor:pointer;position:relative;text-align:left;font:inherit;color:var(--text-primary);transition:border-color var(--motion-fast),box-shadow var(--motion-fast)}
.tile:hover{border-color:var(--border-strong);box-shadow:var(--shadow-1)}
.tile.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);background:var(--accent-subtle)}
.tile .ico{width:36px;height:36px;border-radius:var(--radius-lg);background:var(--accent-subtle);color:var(--accent-text);display:grid;place-items:center;font-weight:700;font-size:14px}
.tile.on .ico{background:var(--accent);color:var(--accent-on)}
.tile b{font-size:14px}
.tile span{font-size:12px;color:var(--text-tertiary)}
.tile .tag{position:absolute;top:12px;right:12px}
.tile.horizontal{flex-direction:row;align-items:center}
.tile.horizontal>div{display:flex;flex-direction:column;gap:2px;min-width:0}
.tile[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}
.empty .btn{margin-top:10px}
.status-item .indicator{font-weight:600}
.onboard{display:flex;flex-direction:column;gap:24px;align-items:center;width:100%;max-width:560px;margin:0 auto}
.onboard .card{width:100%}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{70%{box-shadow:0 0 0 8px transparent}}