01설치 명령 복사 상자
npm·pnpm·yarn 탭을 누르면 같은 패키지의 설치 명령이 그 도구의 문법으로 갈립니다. 복사 버튼은 navigator.clipboard 로 실제 클립보드에 넣고, 복사됨 알림은 aria-live 로 한 번만 읽힙니다.
쓰임오픈소스 문서의 시작하기 절, 라이브러리 랜딩의 설치 안내
navigator.clipboardaria-livesteps(1, end)

<main class="stage">
<div class="ic is-demo" id="ic" data-mgr="npm">
<div class="ic__tabs" role="tablist" aria-label="패키지 관리자">
<button class="ic__tab" type="button" role="tab" id="tabNpm" aria-selected="true" data-mgr="npm">npm</button>
<button class="ic__tab" type="button" role="tab" id="tabPnpm" aria-selected="false" data-mgr="pnpm">pnpm</button>
<button class="ic__tab" type="button" role="tab" id="tabYarn" aria-selected="false" data-mgr="yarn">yarn</button>
</div>
<div class="ic__row">
<span class="ic__sign" aria-hidden="true">$</span>
<span class="ic__slot">
<code class="ic__cmd" data-mgr="npm">npm i <b>motionkit</b></code>
<code class="ic__cmd" data-mgr="pnpm">pnpm add <b>motionkit</b></code>
<code class="ic__cmd" data-mgr="yarn">yarn add <b>motionkit</b></code>
</span>
<button class="ic__copy" type="button" id="copyBtn">
<span class="ic__ico" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg></span>
<span class="ic__word">명령 복사</span>
</button>
</div>
<p class="ic__note" id="note" aria-live="polite">클립보드에 복사했습니다</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 설치 명령 복사 상자 — 같은 패키지를 npm·pnpm·yarn 문법으로 갈아 끼우고 그 줄을 클립보드에 넣는다.
// 명령 세 줄은 같은 격자 칸(grid-area: 1 / 1)에 겹쳐 두어 문법이 바뀌어도 줄 높이가 흔들리지 않는다.
// 격자 미리보기(.is-demo)는 npm → pnpm → yarn 을 steps(1, end) 로 컷 전환하고,
// 그 사이 복사 버튼이 한 번 채워졌다가 90% 안에 되돌아오는 2초 루프.
// 고정폭 서체는 font 단축이 아니라 font-family 낱개로 준다 (BD20).
$duration: $dur-quick;
$easing: $ease-spring;
$color: $chart-3;
$mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
$line: rgba(255, 255, 255, .16);
$dim: rgba(255, 255, 255, .58);
@include stage($stage-ink);
.ic {
display: flex; flex-direction: column; gap: $sp-2;
box-sizing: border-box; width: min(100%, 400px);
padding: $sp-3; border: 1px solid $line; border-radius: $r-card;
background: rgba(255, 255, 255, .05);
font-family: $mono;
}
// ── 관리자 탭: 인디케이터를 밀지 않고 탭마다 자기 밑줄을 켠다 (BD19 회피)
.ic__tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: $sp-1; }
.ic__tab {
box-sizing: border-box; width: 100%; padding: $sp-1 0;
border: 0; border-bottom: 2px solid transparent; border-radius: 0;
background: none; color: $dim; cursor: pointer;
font-family: inherit; font-weight: 700; font-size: clamp(11px, 3vw, 12px); line-height: 1.4;
transition: color $duration $easing, border-color $duration $easing;
}
.ic__tab[aria-selected="true"] { color: $subject-cream; border-bottom-color: $color; }
.ic__tab:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
// ── 명령 줄
.ic__row { display: flex; align-items: center; gap: $sp-2; min-width: 0; }
.ic__sign { flex: 0 0 auto; color: $color; font-weight: 700; font-size: clamp(12px, 3.2vw, 13px); line-height: 1.4; }
.ic__slot { display: grid; flex: 1 1 auto; min-width: 0; }
.ic__slot > * { grid-area: 1 / 1; }
.ic__cmd {
color: $subject-cream; font-family: inherit; font-weight: 500;
font-size: clamp(11px, 3vw, 13px); line-height: 1.4;
white-space: nowrap; overflow: hidden; visibility: hidden;
}
.ic__cmd b { color: $stage-yellow; font-weight: 700; }
.ic[data-mgr="npm"] .ic__cmd[data-mgr="npm"],
.ic[data-mgr="pnpm"] .ic__cmd[data-mgr="pnpm"],
.ic[data-mgr="yarn"] .ic__cmd[data-mgr="yarn"] { visibility: visible; }
// ── 복사 버튼: 채움은 배경 요소를 scaleX 로 늘려 왼쪽 변에서 자란다 (blog-demo 규칙 8 의 붙는 지점)
.ic__copy {
position: relative; overflow: hidden; flex: 0 0 auto;
display: inline-flex; align-items: center; gap: $sp-1;
box-sizing: border-box; min-height: 28px; padding: 0 $sp-2;
border: 1px solid $line; border-radius: $r-control;
background: rgba(255, 255, 255, .06); color: $subject-cream; cursor: pointer;
font-family: inherit; font-weight: 700; font-size: clamp(10px, 2.8vw, 11px); line-height: 1;
}
.ic__copy::before {
content: ""; position: absolute; inset: 0; background: $color;
transform: scaleX(0); transform-origin: left center;
transition: transform $duration $easing;
}
.ic.is-done .ic__copy::before { transform: scaleX(1); }
.ic.is-done .ic__copy { color: $stage-ink; }
.ic__copy:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
.ic__ico, .ic__word { position: relative; }
.ic__ico { display: grid; place-items: center; font-size: 14px; }
// ── 상태 줄: 복사 뒤에만 보인다. 자리는 늘 차지해 줄 수가 흔들리지 않는다.
// aria-live="polite" 를 단 칸이라 글자가 다시 들어오는 순간 한 번 읽힌다
.ic__note {
margin: 0; min-height: 14px; color: $color; visibility: hidden;
font-family: inherit; font-weight: 500; font-size: clamp(10px, 2.6vw, 11px); line-height: 1.3;
transform: translateY(4px); transition: transform $duration $easing;
}
.ic.is-done .ic__note { visibility: visible; transform: translateY(0); }
// ── 격자 미리보기 루프 (2초)
// 0~33% npm · 34~66% pnpm · 67~100% yarn 을 컷으로 갈고, 8~30% 에 복사 채움이 지나간다.
// 되돌림은 0% 경계에서만 일어나 복귀 프레임이 poster 로 뽑히지 않는다 (blog-demo 「복귀 90%」)
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab {
animation-duration: $dur-loop; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
.ic.is-demo .ic__cmd[data-mgr="npm"] { animation-name: icShow1; }
.ic.is-demo .ic__cmd[data-mgr="pnpm"] { animation-name: icShow2; }
.ic.is-demo .ic__cmd[data-mgr="yarn"] { animation-name: icShow3; }
@keyframes icShow1 {
0%, 33% { visibility: visible; }
34%, 100% { visibility: hidden; }
}
@keyframes icShow2 {
0%, 33% { visibility: hidden; }
34%, 66% { visibility: visible; }
67%, 100% { visibility: hidden; }
}
@keyframes icShow3 {
0%, 66% { visibility: hidden; }
67%, 100% { visibility: visible; }
}
.ic.is-demo .ic__tab:nth-child(1) { animation-name: icTab1; }
.ic.is-demo .ic__tab:nth-child(2) { animation-name: icTab2; }
.ic.is-demo .ic__tab:nth-child(3) { animation-name: icTab3; }
@keyframes icTab1 {
0%, 33% { color: $subject-cream; border-bottom-color: $color; }
34%, 100% { color: $dim; border-bottom-color: transparent; }
}
@keyframes icTab2 {
0%, 33% { color: $dim; border-bottom-color: transparent; }
34%, 66% { color: $subject-cream; border-bottom-color: $color; }
67%, 100% { color: $dim; border-bottom-color: transparent; }
}
@keyframes icTab3 {
0%, 66% { color: $dim; border-bottom-color: transparent; }
67%, 100% { color: $subject-cream; border-bottom-color: $color; }
}
// 채움은 연속 보간이라 steps 와 섞지 않는다 (266-09 실측: steps 짝은 clip-path·transform 을 컷으로 죽인다)
.ic.is-demo .ic__copy::before {
animation-name: icFill; animation-duration: $dur-loop; animation-timing-function: $easing;
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes icFill {
0%, 8% { transform: scaleX(0); }
20%, 74% { transform: scaleX(1); }
86%, 100% { transform: scaleX(0); }
}
.ic.is-demo .ic__copy {
animation-name: icBtn; animation-duration: $dur-loop; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes icBtn {
0%, 19% { color: $subject-cream; }
20%, 79% { color: $stage-ink; }
80%, 100% { color: $subject-cream; }
}
.ic.is-demo .ic__note {
animation-name: icNote; animation-duration: $dur-loop; animation-timing-function: $easing;
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes icNote {
0%, 19% { visibility: hidden; transform: translateY(4px); }
20% { visibility: visible; transform: translateY(4px); }
30%, 79% { visibility: visible; transform: translateY(0); }
80%, 100% { visibility: hidden; transform: translateY(0); }
}
@media (max-width: 340px) {
.ic { padding: $sp-2; gap: $sp-1; }
.ic__copy { padding: 0 $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab,
.ic.is-demo .ic__copy,
.ic.is-demo .ic__copy::before,
.ic.is-demo .ic__note { animation: none !important; }
.ic.is-demo .ic__cmd[data-mgr="npm"] { visibility: visible !important; }
.ic.is-demo .ic__cmd[data-mgr="pnpm"],
.ic.is-demo .ic__cmd[data-mgr="yarn"] { visibility: hidden !important; }
.ic__tab, .ic__copy::before, .ic__note { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ic {
display: flex;
flex-direction: column;
gap: 8px;
box-sizing: border-box;
width: min(100%, 400px);
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 16px;
background: rgba(255, 255, 255, 0.05);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
}
.ic__tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.ic__tab {
box-sizing: border-box;
width: 100%;
padding: 4px 0;
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
background: none;
color: rgba(255, 255, 255, 0.58);
cursor: pointer;
font-family: inherit;
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1.4;
transition: color 220ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ic__tab[aria-selected=true] {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
.ic__tab:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ic__row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.ic__sign {
flex: 0 0 auto;
color: #2b9d74;
font-weight: 700;
font-size: clamp(12px, 3.2vw, 13px);
line-height: 1.4;
}
.ic__slot {
display: grid;
flex: 1 1 auto;
min-width: 0;
}
.ic__slot > * {
grid-area: 1/1;
}
.ic__cmd {
color: #fff7e6;
font-family: inherit;
font-weight: 500;
font-size: clamp(11px, 3vw, 13px);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
visibility: hidden;
}
.ic__cmd b {
color: #ffd23f;
font-weight: 700;
}
.ic[data-mgr=npm] .ic__cmd[data-mgr=npm],
.ic[data-mgr=pnpm] .ic__cmd[data-mgr=pnpm],
.ic[data-mgr=yarn] .ic__cmd[data-mgr=yarn] {
visibility: visible;
}
.ic__copy {
position: relative;
overflow: hidden;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 4px;
box-sizing: border-box;
min-height: 28px;
padding: 0 8px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 12px;
background: rgba(255, 255, 255, 0.06);
color: #fff7e6;
cursor: pointer;
font-family: inherit;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1;
}
.ic__copy::before {
content: "";
position: absolute;
inset: 0;
background: #2b9d74;
transform: scaleX(0);
transform-origin: left center;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ic.is-done .ic__copy::before {
transform: scaleX(1);
}
.ic.is-done .ic__copy {
color: #17141a;
}
.ic__copy:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ic__ico, .ic__word {
position: relative;
}
.ic__ico {
display: grid;
place-items: center;
font-size: 14px;
}
.ic__note {
margin: 0;
min-height: 14px;
color: #2b9d74;
visibility: hidden;
font-family: inherit;
font-weight: 500;
font-size: clamp(10px, 2.6vw, 11px);
line-height: 1.3;
transform: translateY(4px);
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ic.is-done .ic__note {
visibility: visible;
transform: translateY(0);
}
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.ic.is-demo .ic__cmd[data-mgr=npm] {
animation-name: icShow1;
}
.ic.is-demo .ic__cmd[data-mgr=pnpm] {
animation-name: icShow2;
}
.ic.is-demo .ic__cmd[data-mgr=yarn] {
animation-name: icShow3;
}
@keyframes icShow1 {
0%, 33% {
visibility: visible;
}
34%, 100% {
visibility: hidden;
}
}
@keyframes icShow2 {
0%, 33% {
visibility: hidden;
}
34%, 66% {
visibility: visible;
}
67%, 100% {
visibility: hidden;
}
}
@keyframes icShow3 {
0%, 66% {
visibility: hidden;
}
67%, 100% {
visibility: visible;
}
}
.ic.is-demo .ic__tab:nth-child(1) {
animation-name: icTab1;
}
.ic.is-demo .ic__tab:nth-child(2) {
animation-name: icTab2;
}
.ic.is-demo .ic__tab:nth-child(3) {
animation-name: icTab3;
}
@keyframes icTab1 {
0%, 33% {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
34%, 100% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
}
@keyframes icTab2 {
0%, 33% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
34%, 66% {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
67%, 100% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
}
@keyframes icTab3 {
0%, 66% {
color: rgba(255, 255, 255, 0.58);
border-bottom-color: transparent;
}
67%, 100% {
color: #fff7e6;
border-bottom-color: #2b9d74;
}
}
.ic.is-demo .ic__copy::before {
animation-name: icFill;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes icFill {
0%, 8% {
transform: scaleX(0);
}
20%, 74% {
transform: scaleX(1);
}
86%, 100% {
transform: scaleX(0);
}
}
.ic.is-demo .ic__copy {
animation-name: icBtn;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes icBtn {
0%, 19% {
color: #fff7e6;
}
20%, 79% {
color: #17141a;
}
80%, 100% {
color: #fff7e6;
}
}
.ic.is-demo .ic__note {
animation-name: icNote;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes icNote {
0%, 19% {
visibility: hidden;
transform: translateY(4px);
}
20% {
visibility: visible;
transform: translateY(4px);
}
30%, 79% {
visibility: visible;
transform: translateY(0);
}
80%, 100% {
visibility: hidden;
transform: translateY(0);
}
}
@media (max-width: 340px) {
.ic {
padding: 8px;
gap: 4px;
}
.ic__copy {
padding: 0 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.ic.is-demo .ic__cmd,
.ic.is-demo .ic__tab,
.ic.is-demo .ic__copy,
.ic.is-demo .ic__copy::before,
.ic.is-demo .ic__note {
animation: none !important;
}
.ic.is-demo .ic__cmd[data-mgr=npm] {
visibility: visible !important;
}
.ic.is-demo .ic__cmd[data-mgr=pnpm],
.ic.is-demo .ic__cmd[data-mgr=yarn] {
visibility: hidden !important;
}
.ic__tab, .ic__copy::before, .ic__note {
transition: none !important;
}
}var ic = document.getElementById('ic');
var copyBtn = document.getElementById('copyBtn');
var word = copyBtn.querySelector('.ic__word');
var note = document.getElementById('note');
var tabs = Array.prototype.slice.call(ic.querySelectorAll('.ic__tab'));
var doneTimer = 0;
function pick(mgr) {
ic.classList.remove('is-demo');
ic.setAttribute('data-mgr', mgr);
tabs.forEach(function (t) { t.setAttribute('aria-selected', String(t.dataset.mgr === mgr)); });
}
function currentCommand() {
var mgr = ic.getAttribute('data-mgr') || 'npm';
var line = ic.querySelector('.ic__cmd[data-mgr="' + mgr + '"]');
return line ? line.textContent : '';
}
function showDone() {
ic.classList.add('is-done');
word.textContent = '복사됨';
clearTimeout(doneTimer);
// 알림을 지웠다가 다시 넣어야 같은 문장이라도 aria-live 가 한 번 더 읽는다
note.textContent = '';
doneTimer = setTimeout(function () {
note.textContent = '클립보드에 복사했습니다';
doneTimer = setTimeout(function () { ic.classList.remove('is-done'); word.textContent = '명령 복사'; }, 1400);
}, 60);
}
tabs.forEach(function (t) { t.addEventListener('click', function () { pick(t.dataset.mgr); }); });
copyBtn.addEventListener('click', function () {
ic.classList.remove('is-demo');
var text = currentCommand();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(showDone, showDone);
} else {
showDone();
}
});// InstallCopyTabs — vanilla 판(run/294/demo/01-terminal-install-copy-tabs/index.html) 의 React 포트.
// 같은 패키지를 npm·pnpm·yarn 문법으로 갈아 끼우고 그 줄을 클립보드에 넣는다.
// 탭이나 복사 버튼을 한 번 누르면 격자 미리보기 루프(demo)가 꺼지고 사람이 고른 상태만 남는다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./InstallCopyTabs.module.scss";
export type InstallCopyTabsProps = {
/** 격자 미리보기용 2초 자동 루프. 탭·복사 버튼을 누르면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
/** React 이벤트 제네릭이 ambient 에 없어 쓰는 필드만 지역 타입으로 선언한다 */
type Box<T> = { current: T };
const MANAGERS = ["npm", "pnpm", "yarn"];
/** 관리자마다 설치 명령의 앞부분만 다르다 — 뒤에 붙는 패키지 이름은 같다 */
const VERBS: { [k: string]: string } = { npm: "npm i", pnpm: "pnpm add", yarn: "yarn add" };
const PACKAGE_NAME = "motionkit";
const NOTE_TEXT = "클립보드에 복사했습니다";
export default function InstallCopyTabs({
demo = true,
duration = 220,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#2b9d74",
}: InstallCopyTabsProps) {
const [live, setLive] = useState(demo);
const [mgr, setMgr] = useState("npm");
const [done, setDone] = useState(false);
const [note, setNote] = useState(NOTE_TEXT);
const stepTimer = useRef<number>(0) as Box<number>;
const backTimer = useRef<number>(0) as Box<number>;
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
return () => {
window.clearTimeout(stepTimer.current);
window.clearTimeout(backTimer.current);
};
}, []);
function pick(next: string) {
setLive(false);
setMgr(next);
}
function showDone() {
setDone(true);
window.clearTimeout(stepTimer.current);
window.clearTimeout(backTimer.current);
// 알림을 지웠다가 다시 넣어야 같은 문장이라도 aria-live 가 한 번 더 읽는다
setNote("");
stepTimer.current = window.setTimeout(() => {
setNote(NOTE_TEXT);
backTimer.current = window.setTimeout(() => { setDone(false); }, 1400);
}, 60);
}
function copy() {
setLive(false);
const text = `${VERBS[mgr]} ${PACKAGE_NAME}`;
const clip = typeof navigator !== "undefined" ? navigator.clipboard : undefined;
if (clip) { clip.writeText(text).then(showDone, showDone); }
else { showDone(); }
}
const cls = [styles.root, live ? styles.isDemo : "", done ? styles.isDone : ""].filter(Boolean).join(" ");
return (
<main className={styles.stage} style={vars}>
<div className={cls} data-mgr={mgr}>
<div className={styles.tabs} role="tablist" aria-label="패키지 관리자">
{MANAGERS.map((name) => (
<button
className={styles.tab}
type="button"
role="tab"
key={name}
data-mgr={name}
aria-selected={mgr === name}
onClick={() => pick(name)}
>
{name}
</button>
))}
</div>
<div className={styles.row}>
<span className={styles.sign} aria-hidden="true">$</span>
<span className={styles.slot}>
{MANAGERS.map((name) => (
<code className={styles.cmd} key={name} data-mgr={name}>
{VERBS[name]} <b>{PACKAGE_NAME}</b>
</code>
))}
</span>
<button className={styles.copy} type="button" onClick={copy}>
<span className={styles.ico} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2" /><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" /></g></svg>
</span>
<span className={styles.word}>{done ? "복사됨" : "명령 복사"}</span>
</button>
</div>
<p className={styles.note} role="status">{note}</p>
</div>
</main>
);
}01 설치 명령 복사 상자 — 측정 누적면적 3.313% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 터미널 UI 9종 — 설치 명령 상자부터 튜토리얼 창까지
이 데모가 속한 글
터미널 UI 9종 — 설치 명령 상자부터 튜토리얼 창까지터미널 UI는 웹 화면 안에 검은 명령창처럼 보이도록 짜 넣는 조각입니다. 여기 아홉 가지는 설치 명령 복사 상자부터 로그 창·문자 진행 막대·크기를 바꾸는 창·튜토리얼 창까지, 붙여 넣으면 바로 도는 형태로 만들어 이 글의 데모 전부 보기


