designpaca/apps/site/src/components/CopyCommand.astro
Yun Chan 9c9ab2de7e 디자인 SSOT 를 세우고 3D 를 배경 재질로 되돌린다
지적받은 것: 모바일 마진·패딩, 폰트 웨이트, 버튼 스타일 불일치, SSOT,
스크롤바, 모바일 메뉴, 그리고 3D 를 왜 상자에 넣었는지.

3D 의 역할을 잘못 잡았다
  lusion.co 의 "3D 는 액자 안"은 3D 자체가 상품일 때의 규칙이다.
  우리는 절차를 파는데 액자에 넣을 상품이 없어 "저 상자는 뭐냐"가 됐고,
  의미를 게이트 판독기가 자막으로 설명하고 있었다.
  이 씬의 역할은 재질이다. 배경으로 되돌리고 값을 전부 낮췄다 —
  테두리 광 0.85->0.42, 알파 0.72->0.34, UV 경계 0.05->0.17, 이동 60%
  ResizeObserver 로 캔버스를 따라가게 했다. window.resize 만 듣는 동안
  히어로 높이 변화를 놓쳐 배경이 화면의 3분의 2만 덮었다

SSOT
  웨이트 6종(400/500/560/600/620/660) -> 3종. 셋은 토큰에 없던 값이었다
  .btn + btn-solid/btn-quiet. 두 버튼이 높이 36 vs 44, 패딩 16 vs 8,
    테두리 1px vs 0, 웨이트 400 vs 500 이었다
  --header-h. 간격을 clamp 로 바꾸자 관계가 끊겨 h1 이 헤더 뒤로 들어갔다
  --space-5~8 과 --gutter 를 clamp 로. 고정이면 모바일 여백이 화면의 26%
  scrollbar-color 지정

모바일
  좌우 여백 24 -> 32px, 섹션 상하 112 -> 56px
  헤더에 섹션 앵커 4개. 900px 미만은 details 기반 메뉴(JS 0바이트)

패럴랙스는 섹션마다 다른 말을 한다
  히어로   글자 상승 / 배경 하강 — 반대 방향이라야 시차가 보인다
  파이프라인 단계명 고정, 산출물만 지연 — 인과
  재료     타일이 서로 다른 깊이 — 표면은 한 겹이 아니다
  프리플라이트 뒤 항목일수록 늦게 — 로그는 흐른다
  수치     없음. 의도적 정지 — 계기판의 바늘은 떨지 않는다

스킬
  three.md  §0-A 를 역할별로 갈랐다. "액자 안"만 적어둔 것이 잘못된
            일반화였고 내가 거기 그대로 걸렸다. 재질일 때의 값 교정을 실측으로 남김
  tokens.md 웨이트·컨트롤·헤더 높이·반응형 간격 — 토큰이 아예 없던 네 축
  motion.md 섹션별 패럴랙스 배정표. 움직이지 않는 것도 결정이다

390px 실렌더: 가로 스크롤 0, 대비 실패 0, 웨이트 3종, radius 2종. 테스트 22개 통과
2026-08-20 14:15:30 +09:00

103 lines
3.2 KiB
Text

---
interface Props {
command: string;
copyLabel: string;
copiedLabel: string;
note?: string;
}
const { command, copyLabel, copiedLabel, note } = Astro.props;
---
{/* 명령어를 담는 액자. 유리를 쓰는 세 자리 중 하나다 */}
<div class="cmd-shell frame">
<div class="cmd">
<code class="cmd-text">{command}</code>
<button type="button" class="btn btn-solid cmd-copy" data-copy={command} data-copied={copiedLabel}>
<span class="cmd-copy-label">{copyLabel}</span>
<span class="cmd-copy-icon" aria-hidden="true">
<svg viewBox="0 0 16 16" width="12" height="12" fill="none">
<path d="M5.5 10.5L10.5 5.5M10.5 5.5H6.5M10.5 5.5V9.5"
stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</button>
</div>
</div>
{note && <p class="cmd-note muted">{note}</p>}
<style>
.cmd-shell { max-width: 30em; min-width: 0; }
.cmd {
display: flex;
min-width: 0;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
}
.cmd-text {
flex: 1 1 0;
min-width: 0;
overflow-x: auto;
white-space: nowrap;
color: var(--ink);
}
.cmd-text::before {
content: "$ ";
color: var(--ink-muted);
}
/* 높이·패딩·웨이트·전환은 .btn 이 정한다. 여기서는 이 자리에만
필요한 것만 더한다 — 아이콘이 오른쪽 안쪽에 붙어야 해서 그쪽만 좁다 */
.cmd-copy {
flex: 0 0 auto;
padding-right: var(--space-2);
font-family: inherit;
}
.cmd-copy-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: var(--radius-sm);
background: color-mix(in oklab, var(--accent-ink) 18%, transparent);
transition: transform var(--dur-quick) var(--ease-out);
}
.cmd-copy:hover .cmd-copy-icon { transform: translate(2px, -2px); }
.cmd-note {
margin-top: var(--space-2);
font-size: var(--step-0);
}
</style>
<script>
// 이 페이지의 유일한 상호작용이다. 라이브러리를 쓰지 않는다.
document.querySelectorAll<HTMLButtonElement>(".cmd-copy").forEach((btn) => {
const label = btn.querySelector<HTMLSpanElement>(".cmd-copy-label");
if (!label) return;
btn.addEventListener("click", async () => {
const text = btn.dataset["copy"] ?? "";
const done = btn.dataset["copied"] ?? "OK";
const original = label.textContent;
try {
await navigator.clipboard.writeText(text);
} catch {
// 클립보드 권한이 없으면 선택 상태로 만들어 사용자가 직접 복사하게 둔다
const code = btn.parentElement?.querySelector(".cmd-text");
if (code) {
const range = document.createRange();
range.selectNodeContents(code);
const sel = getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
return;
}
label.textContent = done;
label.setAttribute("aria-live", "polite");
setTimeout(() => { label.textContent = original; }, 1600);
});
});
</script>