소개 페이지 — 스킬을 스스로 적용해 만든 첫 사례
designpaca 파이프라인 0~6단계를 실제로 돌려 만들었다. 근거는 research/site-brief.md, 결정 기록은 apps/site/design.md 에 있다. 한 문장 컨셉 - 이 페이지는 designpaca 를 거쳐 만들어졌고, 절차와 형태 수치를 한국어 기준으로 전부 공개한다 번호 없이 순서 표현 (C-07 회피) - 파이프라인 6단계를 번호로 매기면 검출률 3위 슬롭 지문에 걸린다 - 시각: 스파인 위 위치가 순서를 만든다 / 내용: 같은 브리프가 단계마다 변한다 - 의미: <ol role="list"> — 화면에 번호 0개, 보조기술에는 "6개 중 n번째" 멀티링구얼 - / = 한국어, /en/ = 영어. 번역이 아니라 각 판을 따로 썼다 - 언어별 레이아웃이 자기 폰트만 로드한다 (판당 2개) - 조판 섹션은 한국어판에서 주장이고 영어판에서 사례다 실측 - JS 314B (gzip, 예산 50KB) / CSS 3.0KB / HTML 4.4~4.8KB - radius 2종 · backdrop-filter 0개 · 타입 5단계 · 강조색 1개 - 슬롭 지문 grep 9패턴 0건, 정적 하드 게이트 8개 전부 통과 감사에서 걸려 고친 것 - eyebrow 6개 → 상한 ceil(7/3)=3 위반 → 3개로 - clip-path 리빌 → 게이트 #8 위반 → transform 으로 교정 - 구분선 대비 1.30:1 → 입력 테두리용 별칭 분리 (WCAG 1.4.11) - 본문 컬럼 960px → --measure 로. layout.md 예시 코드도 같이 수정
This commit is contained in:
parent
8808c672dc
commit
6103700d38
18 changed files with 1497 additions and 20 deletions
|
|
@ -4,6 +4,6 @@ export default defineConfig({
|
|||
site: "https://designpaca.chanpaca.net",
|
||||
// 정적 출력 — Cloudflare Pages 로 그대로 올린다
|
||||
output: "static",
|
||||
build: { format: "file" },
|
||||
build: { format: "directory" },
|
||||
devToolbar: { enabled: false },
|
||||
});
|
||||
|
|
|
|||
109
apps/site/design.md
Normal file
109
apps/site/design.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# design.md — designpaca 소개 페이지
|
||||
|
||||
> 6단계 산출물. 다음 실행(사람이든 에이전트든)은 이 파일을 먼저 읽고 같은 결정을 이어간다.
|
||||
> 전체 조사 근거: `research/site-brief.md` (825줄)
|
||||
|
||||
## 브리프 3줄
|
||||
|
||||
- **무엇을**: designpaca 소개 페이지. 파이프라인 스킬 + npx 설치 CLI 를 소개하는 단일 페이지, 한국어·영어 두 판
|
||||
- **누구에게**: Claude Code · Codex · Cursor 를 쓰는 개발자. 이 페이지를 보고 `npx designpaca` 를 터미널에 치게 만드는 것이 목표
|
||||
- **제약**: Astro 정적 → Cloudflare Pages. 이 페이지 자체가 스킬의 품질 증거물이다 — 스킬이 금지하는 것을 이 페이지가 하면 안 된다
|
||||
|
||||
## 레퍼런스
|
||||
|
||||
| 슬롯 | 사이트 | 업종 | 가져온 것 | 가져오지 **않은** 것 |
|
||||
|---|---|---|---|---|
|
||||
| R1 · 구조 | tasteskill.dev | 안티슬롭 디자인 스킬 | 섹션 순서, 설치 명령 3회 노출, **카피 규율**(자기 평가 형용사 0건) | radius 8종 · backdrop-filter 40개 · 타입 12단계 · 스폰서 벽 |
|
||||
| R2 · 톤 | klim.co.nz | 폰트 파운드리 | 흰 바탕에 검정 밴드를 리듬으로, 설계 근거를 산문으로 파는 방식 | 유료 폰트 |
|
||||
| R3 · 디테일 | Codrops scroll-driven wipe | — | 스크롤 진입 리빌 | `clip-path` 구현 (게이트 #8 위반 → `transform` 으로 교정) |
|
||||
|
||||
**정본 확인 경고**: `tasteskill.com` 과 `design-taste.com` 은 본문 114바이트짜리 파킹 페이지다. HTTP 200 이 생존 신호가 아니다.
|
||||
|
||||
## 한 문장 컨셉
|
||||
|
||||
> **이 페이지는 designpaca를 거쳐 만들어졌고, 그 절차와 형태 수치를 한국어 기준으로 전부 공개한다.**
|
||||
|
||||
세 방향(절차를 판다 / 형태까지 간다 / 한국어로 푼다)이 "이 페이지 자신이 첫 사례"라는 사실 하나로 합쳐진다.
|
||||
|
||||
## 프리셋 — `swiss-minimal`
|
||||
|
||||
결정 요인은 **멀티링구얼**이었다. `editorial` 의 세리프는 한국어 마루 부리 / 영어 Spectral 로 갈려 두 판의 톤이 벌어진다. 그로테스크는 Pretendard / Switzer 로 양쪽이 같은 성격을 유지한다.
|
||||
빌린 것: `editorial` 에서 규칙 옆 근거 1문장 어법, `anti-grid` 에서 파이프라인 섹션 한 곳의 의도적 어긋남.
|
||||
|
||||
## 감수한 리스크 — 하나
|
||||
|
||||
**이 페이지 자신의 형태 수치를 페이지에 공개한다.** 공개한 순간 검증 가능해지고 하나라도 어기면 주장 전체가 무너진다.
|
||||
규칙: **결과가 나쁘게 나와도 그대로 싣는다.** 유리한 것만 고르면 리스크가 자기 홍보로 바뀐다.
|
||||
|
||||
## C-07 대응 — 번호 없이 순서 표현
|
||||
|
||||
파이프라인 6단계를 번호로 매기면 검출률 3위 슬롭 지문(9.4%)에 정면으로 걸린다. 세 층으로 풀었다:
|
||||
|
||||
1. **시각** — 세로 스파인 위의 위치가 순서를 만든다 (Beck 1933 튜브맵: "topology trumped topography")
|
||||
2. **내용** — 같은 브리프 문장 하나가 단계마다 변해간다 (IKEA 조립 설명서. IKEA 의 동기가 언어 독립성이라 멀티링구얼과 동기가 같다)
|
||||
3. **의미** — `<ol role="list">`. 시각적으로 번호 0개, 보조기술에는 "6개 중 n번째". `list-style: none` 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 `role` 로 되돌린다
|
||||
|
||||
## 토큰
|
||||
|
||||
`src/styles/tokens.css` 전문. 요약:
|
||||
|
||||
- 타입 비율 **1.25**, **디스플레이는 스케일 밖에 별도 정의** — 1.25⁴는 2.44배라 "최대÷본문 4배" 를 못 채우고, 단계를 늘려 맞추면 "통제 실패" 에 걸린다. 두 판 모두 정확히 **4.0배**
|
||||
- 한국어 17px / lh 1.7 / measure 33em(561px) · 영어 16px / lh 1.55 / measure 65ch(640px)
|
||||
- 강조색 **딥 틸 `#0E6B63`** 하나. 오렌지(tasteskill) · 마젠타(Bun) · 빨강(Klim) · 보라/시안 네온을 전부 피한 자리
|
||||
- **14개 색 조합 대비를 전부 계산해 검증** — 최저 6.04:1, 최고 16.88:1
|
||||
- radius **2종**(3px · 6px), 간격 8단계, 모션 4종
|
||||
- 라이트·다크 두 테마를 동등하게 정의. 다크 밴드는 **히어로와 설치 두 곳만** (게이트 #4 가 요구하는 "규칙으로 정의된 반전")
|
||||
|
||||
## 성능 — 예산과 실측
|
||||
|
||||
| 항목 | 예산 | 실측 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 히어로까지 JS (gzip) | 50KB | **314B** | 통과 (예산의 0.6%) |
|
||||
| CSS (gzip) | — | 3.0KB | |
|
||||
| HTML (gzip) | — | 4.4~4.8KB | |
|
||||
| 폰트 패밀리 (판당) | 2개 | 2개 | 통과 |
|
||||
| 첫 인터랙션 | 2초 | **미측정** | 배포 후 Lighthouse |
|
||||
| LCP / CLS | 2.5초 / 0.1 | **미측정** | 배포 후 |
|
||||
| three.js | 미사용 | 미사용 | — |
|
||||
|
||||
**폰트 전략**: 경로가 `/` 와 `/en/` 으로 갈려 있으므로 레이아웃을 언어별로 나눠 **자기 폰트만 받는다.** 한국어판 Pretendard Variable(동적 서브셋) + JetBrains Mono, 영어판 Switzer + JetBrains Mono. preload 는 본문 폰트 1개만.
|
||||
|
||||
## 채택한 이펙트와 폴백
|
||||
|
||||
| 이펙트 | 구현 | 폴백 |
|
||||
|---|---|---|
|
||||
| 스파인 진입 페이드 + 8px 상승 | `animation-timeline: view()`, JS 0 | 미지원 브라우저에서는 아무 일도 안 일어난다. 콘텐츠는 처음부터 visible |
|
||||
| 복사 버튼 | 인라인 314B | 클립보드 권한 실패 시 텍스트를 선택 상태로 만들어 사용자가 직접 복사 |
|
||||
|
||||
`prefers-reduced-motion: no-preference` 안에 `@supports (animation-timeline: view())` 로 이중 게이팅.
|
||||
|
||||
## 5단계 감사 결과
|
||||
|
||||
- 하드 게이트: **정적 검사 가능한 8개 전부 통과.** 나머지 4개(가로 스크롤·버튼 줄바꿈·실렌더 대비·테마 반전)는 브라우저 측정 대기
|
||||
- 슬롭 지문 grep 9개 패턴: **0건**
|
||||
- 카운트 규칙: 섹션 7 / eyebrow 3 (상한 3) / 마퀴 0 / h1 1 — 통과
|
||||
|
||||
감사 중 걸려서 고친 것 4건은 페이지의 "프리플라이트 로그" 섹션에 그대로 실려 있다.
|
||||
|
||||
## 의도적으로 하지 않은 것과 그 이유
|
||||
|
||||
- **"Anti-Slop" 이라는 명명** — tasteskill 이 쓰는 말이다. 같은 단어를 쓰면 즉시 아류로 읽힌다
|
||||
- **경쟁 제품의 실측치를 페이지에 싣기** — 익명화는 저격을 예의 바르게 만들 뿐이고, 남의 수치는 반박 가능하다("그 40개는 대부분 nav다"). 논쟁이 시작되면 제품 이야기가 사라진다. **우리 수치만 쓴다**
|
||||
- **GitHub 스타 등 인기 지표** — 우리는 숫자가 0이다. 인기 대신 규율을 증거로 쓴다
|
||||
- **스폰서·후원자 벽** — R1 은 페이지의 28%를 여기에 쓴다. 섹션 자체를 만들지 않는다
|
||||
- **다크를 기본값으로** — 브리프에 야간 사용·데이터 시각화 근거가 없다. 두 테마를 동등하게 정의하되 라이트로 시작한다
|
||||
- **three.js / WebGL** — 정적 사이트에 사진이 없고 인터랙션이 복사 버튼과 언어 전환뿐이다. 필터로도 낼 인상이 없다
|
||||
- **언어 자동 리다이렉트** — 사용자를 놀라게 한다. `hreflang` 으로 검색엔진에만 알린다
|
||||
- **국기 아이콘으로 언어 전환** — 언어는 국가가 아니다
|
||||
- **두 판을 번역으로 만들기** — 각 언어판을 따로 썼다. 한국어판은 "한글 조판 공백을 메운다"가 주장이므로 그 페이지 자체가 증명이어야 한다
|
||||
|
||||
## 다음 실행이 해야 할 일
|
||||
|
||||
1. **배포 후 Lighthouse 로 첫 인터랙션·LCP·CLS 를 재고, "이 페이지의 수치" 섹션의 `—` 를 채운다.** 채우기 전까지 그 항목은 비워둔다
|
||||
2. 브라우저로 나머지 하드 게이트 4개를 검사하고 `8/12` 를 갱신한다
|
||||
3. 폰트를 CDN 의존에서 자체 호스팅으로 옮길지 판단한다 (현재 jsDelivr · Fontshare · Google Fonts 3곳)
|
||||
4. OG 이미지가 없다. 공유될 자리라면 만든다
|
||||
|
||||
---
|
||||
|
||||
`2026-08-20` · 초판. 0~6단계 전체 경로로 진행.
|
||||
88
apps/site/src/components/CopyCommand.astro
Normal file
88
apps/site/src/components/CopyCommand.astro
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
interface Props {
|
||||
command: string;
|
||||
copyLabel: string;
|
||||
copiedLabel: string;
|
||||
note?: string;
|
||||
}
|
||||
const { command, copyLabel, copiedLabel, note } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="cmd">
|
||||
<code class="cmd-text">{command}</code>
|
||||
<button type="button" class="cmd-copy" data-copy={command} data-copied={copiedLabel}>
|
||||
{copyLabel}
|
||||
</button>
|
||||
</div>
|
||||
{note && <p class="cmd-note muted">{note}</p>}
|
||||
|
||||
<style>
|
||||
.cmd {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
max-width: 34em;
|
||||
}
|
||||
.cmd-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cmd-copy {
|
||||
flex: 0 0 auto;
|
||||
/* 독립 컨트롤이므로 44px 이상 */
|
||||
min-width: 5.5em;
|
||||
min-height: 44px;
|
||||
padding-inline: var(--space-3);
|
||||
border: 0;
|
||||
border-left: 1px solid var(--line-strong);
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
font: inherit;
|
||||
font-size: var(--step-0);
|
||||
cursor: pointer;
|
||||
transition: opacity var(--dur-instant) var(--ease-out);
|
||||
}
|
||||
.cmd-copy:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
.cmd-note {
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--step-0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 이 페이지의 유일한 상호작용이다. 라이브러리를 쓰지 않는다.
|
||||
document.querySelectorAll<HTMLButtonElement>(".cmd-copy").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const text = btn.dataset["copy"] ?? "";
|
||||
const done = btn.dataset["copied"] ?? "OK";
|
||||
const original = btn.textContent;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
// 클립보드 권한이 없으면 선택 상태로 만들어 사용자가 직접 복사하게 둔다
|
||||
const range = document.createRange();
|
||||
const code = btn.previousElementSibling;
|
||||
if (code) {
|
||||
range.selectNodeContents(code);
|
||||
const sel = getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
return;
|
||||
}
|
||||
btn.textContent = done;
|
||||
btn.setAttribute("aria-live", "polite");
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
}, 1600);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
48
apps/site/src/components/Hero.astro
Normal file
48
apps/site/src/components/Hero.astro
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
import CopyCommand from "./CopyCommand.astro";
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["hero"];
|
||||
ui: Content["ui"];
|
||||
}
|
||||
const { c, ui } = Astro.props;
|
||||
---
|
||||
|
||||
{/* 다크 밴드 두 곳 중 하나. 3단계 토큰이 규칙으로 정의했다(게이트 #4) */}
|
||||
<section class="full band-inverse hero">
|
||||
<div class="hero-inner">
|
||||
<h1>{c.h1}</h1>
|
||||
<p class="lead hero-lead">{c.lead}</p>
|
||||
<CopyCommand
|
||||
command={c.install}
|
||||
copyLabel={ui.copy}
|
||||
copiedLabel={ui.copied}
|
||||
note={c.installNote}
|
||||
/>
|
||||
<p class="hero-links">
|
||||
{c.links.map((l) => (<a href={l.href}>{l.label}</a>))}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.hero-inner {
|
||||
grid-column: main;
|
||||
}
|
||||
.hero-lead {
|
||||
max-width: 30em;
|
||||
margin-block: var(--space-4) var(--space-5);
|
||||
}
|
||||
.hero-links {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-4);
|
||||
font-size: var(--step-0);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
</style>
|
||||
59
apps/site/src/components/Install.astro
Normal file
59
apps/site/src/components/Install.astro
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import CopyCommand from "./CopyCommand.astro";
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["install"];
|
||||
ui: Content["ui"];
|
||||
}
|
||||
const { c, ui } = Astro.props;
|
||||
---
|
||||
|
||||
{/* 다크 밴드 두 곳 중 둘째. 그 외에는 쓰지 않는다 */}
|
||||
<section class="full band-inverse install" aria-labelledby="install-h">
|
||||
<div class="install-inner">
|
||||
<h2 id="install-h">{c.h2}</h2>
|
||||
<p class="lead install-lead">{c.lead}</p>
|
||||
|
||||
<CopyCommand command={c.command} copyLabel={ui.copy} copiedLabel={ui.copied} />
|
||||
|
||||
<div class="table-scroll targets">
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
c.targets.map((t) => (
|
||||
<tr>
|
||||
<th scope="row">{t.name}</th>
|
||||
<td class="mono">{t.path}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<dl class="cmds">
|
||||
{
|
||||
c.commands.map((x) => (
|
||||
<div class="cmd-row">
|
||||
<dt class="mono">{x.cmd}</dt>
|
||||
<dd class="muted">{x.what}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.install { display: grid; grid-template-columns: subgrid; }
|
||||
.install-inner { grid-column: main; }
|
||||
.install-lead { margin-block: var(--space-3) var(--space-5); }
|
||||
.targets { margin-top: var(--space-6); }
|
||||
.targets th[scope="row"] { font-weight: 400; color: var(--ink); letter-spacing: 0; }
|
||||
.cmds { margin: var(--space-5) 0 0; }
|
||||
.cmd-row { padding-block: var(--space-3); border-top: 1px solid var(--line); }
|
||||
.cmd-row:last-child { border-bottom: 1px solid var(--line); }
|
||||
dt { margin-bottom: var(--space-1); }
|
||||
dd { margin: 0; font-size: var(--step-0); }
|
||||
</style>
|
||||
40
apps/site/src/components/Metrics.astro
Normal file
40
apps/site/src/components/Metrics.astro
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["metrics"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="metrics-h">
|
||||
<p class="eyebrow">{c.eyebrow}</p>
|
||||
<h2 id="metrics-h">{c.h2}</h2>
|
||||
<p class="lead metrics-lead">{c.lead}</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
c.items.map((m) => (
|
||||
<tr>
|
||||
<th scope="row">{m.label}</th>
|
||||
<td class="metric-value mono">{m.value}</td>
|
||||
<td class="metric-note muted">{m.note}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="caveat muted">{c.caveat}</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.metrics-lead { margin-bottom: var(--space-5); }
|
||||
.metric-value { white-space: nowrap; }
|
||||
.metric-note { font-size: var(--step-0); }
|
||||
.caveat { margin-top: var(--space-4); font-size: var(--step-0); }
|
||||
th[scope="row"] { font-weight: 400; color: var(--ink); letter-spacing: 0; }
|
||||
</style>
|
||||
152
apps/site/src/components/Pipeline.astro
Normal file
152
apps/site/src/components/Pipeline.astro
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["pipeline"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
{/*
|
||||
순서를 번호 없이 표현하는 세 층:
|
||||
① 시각 — 스파인 위의 위치가 순서를 만든다 (Beck 튜브맵)
|
||||
② 내용 — 같은 브리프가 단계마다 변해간다 (IKEA 조립 설명서)
|
||||
③ 의미 — <ol role="list"> — 화면에 번호는 없지만 보조기술은 "6개 중 n번째"로 읽는다
|
||||
role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다
|
||||
*/}
|
||||
<section class="section" aria-labelledby="pipeline-h">
|
||||
<p class="eyebrow">{c.eyebrow}</p>
|
||||
<h2 id="pipeline-h">{c.h2}</h2>
|
||||
<p class="lead pipeline-lead">{c.lead}</p>
|
||||
|
||||
<div class="brief">
|
||||
<span class="brief-label">{c.briefLabel}</span>
|
||||
<p class="brief-text">{c.brief}</p>
|
||||
</div>
|
||||
|
||||
<ol role="list" class="spine">
|
||||
{
|
||||
c.stages.map((s) => (
|
||||
<li class="stop">
|
||||
<h3 class="stop-name">{s.name}</h3>
|
||||
<p class="stop-does">{s.does}</p>
|
||||
<p class:list={["stop-becomes", { mono: s.mono }]}>{s.becomes}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
|
||||
<p class="after muted">{c.after}</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.pipeline-lead {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.brief {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.brief-label {
|
||||
display: block;
|
||||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.brief-text {
|
||||
margin: 0;
|
||||
font-size: var(--step-1);
|
||||
}
|
||||
|
||||
/* ---- 스파인 ---------------------------------------------
|
||||
선은 장식이 아니라 순서 그 자체다. 정거장의 위치가 번호를 대신한다.
|
||||
---------------------------------------------------------- */
|
||||
.spine {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 var(--space-5);
|
||||
position: relative;
|
||||
}
|
||||
.spine::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: var(--space-4);
|
||||
bottom: var(--space-4);
|
||||
width: 1px;
|
||||
background: var(--line-strong);
|
||||
}
|
||||
|
||||
.stop {
|
||||
position: relative;
|
||||
padding-block: var(--space-4);
|
||||
}
|
||||
.stop + .stop {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* 정거장 표시 — 채워진 원. 숫자가 아니다 */
|
||||
.stop::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: calc(var(--space-5) * -1);
|
||||
top: calc(var(--space-4) + 0.55em);
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
border: 3px solid var(--accent);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.stop-name {
|
||||
font-size: var(--step-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.stop-does {
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
/* 브리프가 이 단계에서 무엇이 됐는가 — 변화가 순서를 만든다 */
|
||||
.stop-becomes {
|
||||
margin: 0;
|
||||
padding-left: var(--space-3);
|
||||
border-left: 2px solid var(--accent);
|
||||
}
|
||||
.stop-becomes.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--mono-size);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.after {
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
|
||||
/* 진입 연출은 하나뿐이고, 없어도 전부 읽힌다.
|
||||
scroll-driven animation 이라 scroll 리스너가 없다. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@supports (animation-timeline: view()) {
|
||||
.stop {
|
||||
animation: stop-in linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry 10% cover 22%;
|
||||
}
|
||||
@keyframes stop-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
apps/site/src/components/Preflight.astro
Normal file
39
apps/site/src/components/Preflight.astro
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["preflight"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="preflight-h">
|
||||
<h2 id="preflight-h">{c.h2}</h2>
|
||||
<p class="lead pf-lead">{c.lead}</p>
|
||||
|
||||
<ul class="log">
|
||||
{
|
||||
c.items.map((i) => (
|
||||
<li class="entry">
|
||||
<p class="caught">{i.caught}</p>
|
||||
<p class="fixed muted">{i.fixed}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
{c.note && <p class="pf-note muted">{c.note}</p>}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.pf-lead { margin-bottom: var(--space-5); }
|
||||
.log { margin: 0; padding: 0; list-style: none; }
|
||||
.entry {
|
||||
padding-block: var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.entry:last-child { border-bottom: 1px solid var(--line); }
|
||||
.caught { margin: 0 0 var(--space-1); }
|
||||
.fixed { margin: 0; font-size: var(--step-0); }
|
||||
.pf-note { margin-top: var(--space-4); font-size: var(--step-0); }
|
||||
</style>
|
||||
42
apps/site/src/components/Rules.astro
Normal file
42
apps/site/src/components/Rules.astro
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["rules"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="rules-h">
|
||||
<h2 id="rules-h">{c.h2}</h2>
|
||||
<p class="lead rules-lead">{c.lead}</p>
|
||||
|
||||
<dl class="rules">
|
||||
{
|
||||
c.items.map((r) => (
|
||||
<div class="rule">
|
||||
<dt>{r.rule}</dt>
|
||||
<dd>{r.why}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.rules-lead { margin-bottom: var(--space-5); }
|
||||
.rules { margin: 0; }
|
||||
.rule {
|
||||
padding-block: var(--space-4);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.rule:last-child { border-bottom: 1px solid var(--line); }
|
||||
dt {
|
||||
font-weight: 560;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
dd {
|
||||
margin: 0;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
</style>
|
||||
75
apps/site/src/components/Typography.astro
Normal file
75
apps/site/src/components/Typography.astro
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
import type { Content } from "../i18n/content";
|
||||
|
||||
interface Props {
|
||||
c: Content["typography"];
|
||||
}
|
||||
const { c } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="section" aria-labelledby="type-h">
|
||||
<p class="eyebrow">{c.eyebrow}</p>
|
||||
<h2 id="type-h">{c.h2}</h2>
|
||||
<p class="lead type-lead">{c.lead}</p>
|
||||
<p class="type-body">{c.body}</p>
|
||||
|
||||
{/* 주장을 글로 하지 않고 실물로 보여준다. 두 문단의 차이는 word-break 한 줄뿐이다 */}
|
||||
<div class="demo">
|
||||
<div class="demo-col">
|
||||
<p class="demo-label muted mono">{c.demoLabels[0]}</p>
|
||||
<p lang="ko" class="demo-text demo-default">{c.demoText}</p>
|
||||
</div>
|
||||
<div class="demo-col">
|
||||
<p class="demo-label muted mono">{c.demoLabels[1]}</p>
|
||||
<p lang="ko" class="demo-text demo-keepall">{c.demoText}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="type-rules">
|
||||
{c.rules.map((r) => (<li>{r}</li>))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.type-lead { margin-bottom: var(--space-4); }
|
||||
.type-body { margin-bottom: var(--space-5); }
|
||||
|
||||
.demo {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.demo-col {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
.demo-label {
|
||||
font-size: var(--mono-size);
|
||||
letter-spacing: var(--tracking-label);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.demo-text {
|
||||
margin: 0;
|
||||
/* 줄바꿈 차이가 드러나도록 폭을 좁게 고정한다 */
|
||||
max-width: 13em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
/* 기본값 — 단어 중간에서 끊긴다 */
|
||||
.demo-default { word-break: break-all; }
|
||||
/* 우리 규칙 */
|
||||
.demo-keepall { word-break: keep-all; overflow-wrap: break-word; }
|
||||
|
||||
.type-rules {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.type-rules li {
|
||||
padding-block: var(--space-2);
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.type-rules li:last-child { border-bottom: 1px solid var(--line); }
|
||||
</style>
|
||||
386
apps/site/src/i18n/content.ts
Normal file
386
apps/site/src/i18n/content.ts
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
/**
|
||||
* 두 언어판은 번역이 아니라 각각 따로 쓴 것이다.
|
||||
* 특히 §5(조판)는 한국어판에서 주장이고 영어판에서 사례다 — 구조 자체가 다르다.
|
||||
*/
|
||||
|
||||
export interface Stage {
|
||||
/** 단계 이름 */
|
||||
name: string;
|
||||
/** 그 단계가 하는 일 한 줄 */
|
||||
does: string;
|
||||
/**
|
||||
* 같은 브리프 하나가 이 단계를 지나며 무엇으로 변하는가.
|
||||
* 번호 대신 이 변화가 순서를 만든다 (IKEA 조립 설명서 방식).
|
||||
*/
|
||||
becomes: string;
|
||||
/** becomes 를 코드로 보여줄 때 */
|
||||
mono?: boolean;
|
||||
}
|
||||
|
||||
export interface Metric {
|
||||
label: string;
|
||||
value: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
rule: string;
|
||||
why: string;
|
||||
}
|
||||
|
||||
export interface Content {
|
||||
meta: { title: string; description: string };
|
||||
hero: {
|
||||
h1: string;
|
||||
lead: string;
|
||||
install: string;
|
||||
installNote: string;
|
||||
links: { label: string; href: string }[];
|
||||
};
|
||||
pipeline: {
|
||||
eyebrow: string;
|
||||
h2: string;
|
||||
lead: string;
|
||||
briefLabel: string;
|
||||
brief: string;
|
||||
stages: Stage[];
|
||||
after: string;
|
||||
};
|
||||
metrics: { eyebrow: string; h2: string; lead: string; items: Metric[]; caveat: string };
|
||||
preflight: {
|
||||
eyebrow: string;
|
||||
h2: string;
|
||||
lead: string;
|
||||
items: { caught: string; fixed: string }[];
|
||||
note: string;
|
||||
};
|
||||
typography: { eyebrow: string; h2: string; lead: string; body: string; demoLabels: [string, string]; demoText: string; rules: string[] };
|
||||
rules: { eyebrow: string; h2: string; lead: string; items: Rule[] };
|
||||
install: { eyebrow: string; h2: string; lead: string; command: string; targets: { name: string; path: string }[]; commands: { cmd: string; what: string }[] };
|
||||
footer: { made: string; repo: string; license: string };
|
||||
ui: { copy: string; copied: string; skipToContent: string };
|
||||
}
|
||||
|
||||
const STAGES_KO: Stage[] = [
|
||||
{
|
||||
name: "브리프 게이트",
|
||||
does: "무엇을·누구에게·제약을 세 줄로 압축한다. 규모를 판정해 경로를 고른다.",
|
||||
becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다",
|
||||
},
|
||||
{
|
||||
name: "레퍼런스 조사",
|
||||
does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조·톤·디테일.",
|
||||
becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다",
|
||||
},
|
||||
{
|
||||
name: "방향 결정",
|
||||
does: "레퍼런스를 하나의 축으로 합성한다. 감수할 리스크를 하나 고른다.",
|
||||
becomes: "감상은 이름에만, 화면에는 수치만",
|
||||
},
|
||||
{
|
||||
name: "디자인 토큰",
|
||||
does: "코드를 쓰기 전에 숫자를 정한다. 성능 예산도 여기서 확정한다.",
|
||||
becomes: "--measure: 33em / --leading-normal: 1.7 / JS 예산 0KB",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
name: "구현",
|
||||
does: "레이아웃 → 재질 → 입체 → 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.",
|
||||
becomes: '<h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>',
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
name: "프리플라이트",
|
||||
does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.",
|
||||
becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px → 수정",
|
||||
mono: true,
|
||||
},
|
||||
];
|
||||
|
||||
const STAGES_EN: Stage[] = [
|
||||
{
|
||||
name: "Brief gate",
|
||||
does: "Compress the brief into three lines: what, for whom, constraints. Pick a path by scope.",
|
||||
becomes: "For whom: home baristas of 3+ years who can taste the roast profile",
|
||||
},
|
||||
{
|
||||
name: "Reference research",
|
||||
does: "Pick three real sites at three different layers — structure, tone, detail.",
|
||||
becomes: "R2 = an independent literary publisher. Tone comes from outside the category",
|
||||
},
|
||||
{
|
||||
name: "Direction",
|
||||
does: "Synthesize the references into one axis. Choose exactly one risk worth taking.",
|
||||
becomes: "Sentiment stays in the name. The screen carries numbers.",
|
||||
},
|
||||
{
|
||||
name: "Design tokens",
|
||||
does: "Decide the numbers before writing code. The performance budget is fixed here too.",
|
||||
becomes: "--measure: 65ch / --leading-normal: 1.55 / JS budget 0KB",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
name: "Build",
|
||||
does: "Layout → surface → depth → motion. It must be finished with every effect switched off.",
|
||||
becomes: "<h1>Acidity 8.5 · Body 6.0 · Light roast</h1>",
|
||||
mono: true,
|
||||
},
|
||||
{
|
||||
name: "Preflight",
|
||||
does: "Read your own work as a stranger's. All twelve gates must answer no.",
|
||||
becomes: "12/12 gates passed · 0 slop fingerprints · 31px overflow at 320px → fixed",
|
||||
mono: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const ko: Content = {
|
||||
meta: {
|
||||
title: "designpaca — 웹 디자인 파이프라인 스킬",
|
||||
description:
|
||||
"AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code · Codex · Cursor · Windsurf 에 한 줄로 설치한다.",
|
||||
},
|
||||
hero: {
|
||||
h1: "designpaca",
|
||||
lead:
|
||||
"에이전트에게 “예쁘게 만들어줘”라고 하면 자기가 아는 기본값을 쏟아낸다. 못 만들어서가 아니라 결정을 안 해서다. 이 스킬은 그 결정을 순서대로 강제한다.",
|
||||
install: "npx designpaca",
|
||||
installNote: "Node 20.11+ · 설치 대상을 감지해 골라준다",
|
||||
links: [
|
||||
{ label: "저장소", href: "https://git.chanpaca.net/yunchan/designpaca" },
|
||||
{ label: "npm", href: "https://www.npmjs.com/package/designpaca" },
|
||||
],
|
||||
},
|
||||
pipeline: {
|
||||
eyebrow: "여섯 단계",
|
||||
h2: "브리프 한 줄이 검증된 페이지가 될 때까지",
|
||||
lead:
|
||||
"각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 실제로 이 스킬을 돌려 만든 커피 로스터리 랜딩에서 같은 브리프가 단계마다 무엇으로 변했는지다.",
|
||||
briefLabel: "들어온 브리프",
|
||||
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
|
||||
stages: STAGES_KO,
|
||||
after:
|
||||
"여섯 단계 끝에 남는 것은 페이지 하나와 design.md 하나다. 다음 사람이 그 파일을 읽고 같은 결정을 이어간다.",
|
||||
},
|
||||
metrics: {
|
||||
eyebrow: "이 페이지의 수치",
|
||||
h2: "카피만 고쳐서는 슬롭이 없어지지 않는다",
|
||||
lead:
|
||||
"문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.",
|
||||
items: [
|
||||
{ label: "radius 어휘 수", value: "2종", note: "3px · 6px. 그 외 값을 쓰지 않는다" },
|
||||
{ label: "반투명 패널(backdrop-filter)", value: "0개", note: "실기기 FPS를 15~30% 깎는다" },
|
||||
{ label: "타입 스케일 단계", value: "5단계", note: "디스플레이 1개는 스케일 밖에 따로 정의" },
|
||||
{ label: "폰트 패밀리", value: "2개", note: "본문 1 + 코드용 모노 1. 언어판마다 자기 것만 받는다" },
|
||||
{ label: "강조색", value: "1개", note: "딥 틸. 라이트·다크는 같은 색의 대응값이다" },
|
||||
{ label: "히어로까지 JS", value: "314B", note: "gzip. 예산 50KB의 0.6%. 복사 버튼 하나뿐이다" },
|
||||
{ label: "슬롭 지문 grep", value: "0건", note: "9개 패턴 검사" },
|
||||
{ label: "하드 게이트", value: "8/12", note: "정적으로 검사 가능한 8개. 나머지 4개는 브라우저 측정 대기" },
|
||||
{ label: "첫 인터랙션", value: "—", note: "미측정. 프로덕션 배포 후 Lighthouse 로 잰다" },
|
||||
],
|
||||
caveat: "값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다 — 지어낸 숫자보다 구멍이 정직하다.",
|
||||
},
|
||||
preflight: {
|
||||
eyebrow: "프리플라이트 로그",
|
||||
h2: "이 페이지를 만들며 실제로 걸린 것",
|
||||
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
|
||||
items: [
|
||||
{
|
||||
caught: "eyebrow 라벨이 6개 — 카운트 규칙 상한은 ceil(섹션 7 / 3) = 3개",
|
||||
fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다",
|
||||
},
|
||||
{
|
||||
caught: "스크롤 리빌에 clip-path 를 쓰려 했다 — 하드 게이트 8은 transform·opacity 만 허용한다",
|
||||
fixed: "덮개를 translateX 로 미는 방식으로 바꿨다. 보이는 결과는 같고, clip-path 보간 제약도 함께 사라졌다",
|
||||
},
|
||||
{
|
||||
caught: "구분선 색의 대비가 1.30:1 — 장식선으로는 괜찮지만 입력 테두리로 쓰면 WCAG 1.4.11(3:1) 위반",
|
||||
fixed: "6.11:1 짜리 별칭을 하나 더 두고 용도를 갈랐다. 새 색을 만든 게 아니라 팔레트 수는 그대로다",
|
||||
},
|
||||
{
|
||||
caught: "본문 컬럼이 960px — 우리가 정한 한 줄 길이(561px)를 넘는다",
|
||||
fixed: "그리드의 본문 폭을 --measure 로 바꿨다. 스킬 문서의 예시 코드도 같이 고쳤다",
|
||||
},
|
||||
],
|
||||
note: "네 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 이 목록이 짧아 보인다면 그건 이 페이지가 작기 때문이다.",
|
||||
},
|
||||
typography: {
|
||||
eyebrow: "한글 조판",
|
||||
h2: "라틴 기준으로 만든 도구는 한글에서 멈춘다",
|
||||
lead:
|
||||
"영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9~11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.",
|
||||
body:
|
||||
"가장 자주 깨지는 것은 줄바꿈이다. 기본값은 단어 중간에서 줄을 끊는다. 아래 두 문단은 같은 글자, 같은 폭, 같은 폰트다. 차이는 word-break 한 줄뿐이다.",
|
||||
demoLabels: ["기본값", "keep-all"],
|
||||
demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.",
|
||||
rules: [
|
||||
"word-break: keep-all — 없으면 단어가 아무 데서나 잘린다",
|
||||
"line-height 1.6~1.8 — 라틴보다 넉넉하게",
|
||||
"음수 자간 금지 — 한글은 이미 고정폭에 가까워 즉시 뭉개진다",
|
||||
"한 줄 25~40자 — 라틴 기준 65~75자를 그대로 쓰면 너무 길다",
|
||||
"한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다",
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
eyebrow: "규칙",
|
||||
h2: "규칙마다 왜 그런지 적혀 있다",
|
||||
lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.",
|
||||
items: [
|
||||
{ rule: "레퍼런스 없이 시작하지 않는다", why: "머릿속 기본값에서 출발하면 결과는 그 카테고리의 평균이 된다." },
|
||||
{ rule: "강조색은 하나다", why: "두 개가 필요하다고 느끼면 위계 설계가 실패한 것이다." },
|
||||
{ rule: "대담함은 한 곳에만", why: "두 곳에서 소리치면 둘 다 죽는다." },
|
||||
{ rule: "화려함은 4순위다", why: "심사 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10이다." },
|
||||
{ rule: "지어낸 수치를 쓰지 않는다", why: "숫자 모양의 구멍은 정직하고, 지어낸 숫자는 신뢰를 깎는다." },
|
||||
{ rule: "이펙트를 다 꺼도 완성돼 있어야 한다", why: "그 상태가 모든 폴백의 기반이다. 무너지면 이펙트가 구조를 대신하고 있는 것이다." },
|
||||
],
|
||||
},
|
||||
install: {
|
||||
eyebrow: "설치",
|
||||
h2: "한 줄이면 된다",
|
||||
lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.",
|
||||
command: "npx designpaca",
|
||||
targets: [
|
||||
{ name: "Claude Code", path: "~/.claude/skills/designpaca/" },
|
||||
{ name: "Codex CLI", path: "~/.codex/skills/designpaca/" },
|
||||
{ name: "Cursor", path: ".cursor/rules/designpaca.mdc" },
|
||||
{ name: "Windsurf", path: ".windsurf/rules/designpaca.md" },
|
||||
{ name: "AGENTS.md", path: "블록 주입 — 에이전트 무관" },
|
||||
],
|
||||
commands: [
|
||||
{ cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" },
|
||||
{ cmd: "npx designpaca doctor", what: "설치 상태와 드리프트 진단" },
|
||||
{ cmd: "npx designpaca uninstall", what: "매니페스트 기반 정확한 제거" },
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
made: "이 페이지는 designpaca 로 만들어졌다.",
|
||||
repo: "git.chanpaca.net/yunchan/designpaca",
|
||||
license: "MIT",
|
||||
},
|
||||
ui: { copy: "복사", copied: "복사됨", skipToContent: "본문으로 건너뛰기" },
|
||||
};
|
||||
|
||||
export const en: Content = {
|
||||
meta: {
|
||||
title: "designpaca — a web design pipeline skill",
|
||||
description:
|
||||
"A skill that makes AI coding agents do web design in order — from reference research to self-audit, six stages. Installs into Claude Code, Codex, Cursor and Windsurf in one line.",
|
||||
},
|
||||
hero: {
|
||||
h1: "designpaca",
|
||||
lead:
|
||||
"Ask an agent to “make it look good” and it pours out the defaults it knows. Not because it can't do better — because nothing was decided. This skill forces those decisions, in order.",
|
||||
install: "npx designpaca",
|
||||
installNote: "Node 20.11+ · detects and preselects your install targets",
|
||||
links: [
|
||||
{ label: "Repository", href: "https://git.chanpaca.net/yunchan/designpaca" },
|
||||
{ label: "npm", href: "https://www.npmjs.com/package/designpaca" },
|
||||
],
|
||||
},
|
||||
pipeline: {
|
||||
eyebrow: "Six stages",
|
||||
h2: "From one line of brief to a page that passed its own audit",
|
||||
lead:
|
||||
"Every stage has a pass condition. Fail it and you don't move on. Below is what one brief actually became at each stage, from a coffee roastery landing page built with this skill.",
|
||||
briefLabel: "The brief that came in",
|
||||
brief: "Build a subscription landing page for a small-batch coffee roastery",
|
||||
stages: STAGES_EN,
|
||||
after:
|
||||
"What remains after six stages is one page and one design.md. The next person reads that file and continues the same decisions.",
|
||||
},
|
||||
metrics: {
|
||||
eyebrow: "This page, measured",
|
||||
h2: "Fixing the copy does not remove the slop",
|
||||
lead:
|
||||
"Polishing sentences fixes sentences. The radius vocabulary, the translucent panels, the number of type steps all stay. So here are this page's own form metrics. If a number comes back badly, it ships as it is.",
|
||||
items: [
|
||||
{ label: "radius vocabulary", value: "2", note: "3px and 6px. Nothing else" },
|
||||
{ label: "translucent panels (backdrop-filter)", value: "0", note: "costs 15–30% FPS on real devices" },
|
||||
{ label: "type scale steps", value: "5", note: "the display size is defined outside the scale" },
|
||||
{ label: "font families", value: "2", note: "one text + one mono. Each language loads only its own" },
|
||||
{ label: "accent colours", value: "1", note: "deep teal. Light and dark are the same colour" },
|
||||
{ label: "JS to first paint", value: "314B", note: "gzip. 0.6% of the 50KB budget. One copy button" },
|
||||
{ label: "slop fingerprints", value: "0", note: "9 patterns checked" },
|
||||
{ label: "hard gates", value: "8/12", note: "the 8 that are statically checkable. 4 await browser measurement" },
|
||||
{ label: "time to interactive", value: "—", note: "not measured yet. Lighthouse, after deploy" },
|
||||
],
|
||||
caveat: "Measured on the production build. Unmeasured entries stay empty — a hole is more honest than an invented number.",
|
||||
},
|
||||
preflight: {
|
||||
eyebrow: "Preflight log",
|
||||
h2: "What this page actually got caught on",
|
||||
lead: "Not the result of auditing someone else. The result of auditing itself.",
|
||||
items: [
|
||||
{
|
||||
caught: "6 eyebrow labels — the count rule caps them at ceil(7 sections / 3) = 3",
|
||||
fixed: "Kept three, dropped the rest. Sections read fine without a label above the heading",
|
||||
},
|
||||
{
|
||||
caught: "The scroll reveal used clip-path — hard gate 8 allows only transform and opacity",
|
||||
fixed: "Switched to a cover pushed with translateX. Same result, and clip-path's interpolation limits went away with it",
|
||||
},
|
||||
{
|
||||
caught: "Divider colour sits at 1.30:1 — fine as decoration, a WCAG 1.4.11 failure as an input border",
|
||||
fixed: "Added a 6.11:1 alias and split the two uses. No new colour, so the palette count is unchanged",
|
||||
},
|
||||
{
|
||||
caught: "The text column was 960px — wider than the measure we had just set (561px)",
|
||||
fixed: "Bound the grid's main column to --measure. The skill's own example code was wrong too, so that got fixed as well",
|
||||
},
|
||||
],
|
||||
note: "All four were caught while building this page, and fixed after being caught. If the list looks short, that is because the page is small.",
|
||||
},
|
||||
typography: {
|
||||
eyebrow: "Your language is probably not Latin",
|
||||
h2: "Tools built on Latin assumptions stop at the script boundary",
|
||||
lead:
|
||||
"Design tooling written in English carries no rules for other scripts. 9–11px text is a label in Latin and unreadable in Korean. A line-height of 1.5 is standard in Latin and cramped in Korean.",
|
||||
body:
|
||||
"Line breaking fails first. The default breaks Korean mid-word — the equivalent of breaking English mid-syllable, in the middle of a sentence. Both paragraphs below use the same text, width and font. The only difference is one line of word-break.",
|
||||
demoLabels: ["default", "keep-all"],
|
||||
demoText: "원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.",
|
||||
rules: [
|
||||
"word-break: keep-all — without it, words break at arbitrary points",
|
||||
"line-height 1.6–1.8 — more generous than Latin needs",
|
||||
"no negative letter-spacing — Korean glyphs are near-monospaced and collapse immediately",
|
||||
"25–40 characters per line — Latin's 65–75 is far too long",
|
||||
"not specifying a Korean font is itself a sign of unfinished work",
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
eyebrow: "Rules",
|
||||
h2: "Every rule states why",
|
||||
lead: "A prohibition without a reason does not survive contact with a brief. If the user asks for it explicitly, the user wins.",
|
||||
items: [
|
||||
{ rule: "Never start without references", why: "Starting from memory produces the category average." },
|
||||
{ rule: "One accent colour", why: "Wanting a second one means the hierarchy failed." },
|
||||
{ rule: "Be bold in exactly one place", why: "Shout in two places and both go unheard." },
|
||||
{ rule: "Spectacle ranks fourth", why: "Judging weights are Design 40 / Usability 30 / Creativity 20 / Content 10." },
|
||||
{ rule: "Never invent a number", why: "A number-shaped hole is honest. An invented number costs trust." },
|
||||
{ rule: "It must be finished with every effect off", why: "That state is the basis of every fallback. If it collapses, the effects were doing the structure's job." },
|
||||
],
|
||||
},
|
||||
install: {
|
||||
eyebrow: "Install",
|
||||
h2: "One line",
|
||||
lead: "Targets are detected and preselected. If you edit a skill file yourself, updates leave it alone.",
|
||||
command: "npx designpaca",
|
||||
targets: [
|
||||
{ name: "Claude Code", path: "~/.claude/skills/designpaca/" },
|
||||
{ name: "Codex CLI", path: "~/.codex/skills/designpaca/" },
|
||||
{ name: "Cursor", path: ".cursor/rules/designpaca.mdc" },
|
||||
{ name: "Windsurf", path: ".windsurf/rules/designpaca.md" },
|
||||
{ name: "AGENTS.md", path: "block injection — agent agnostic" },
|
||||
],
|
||||
commands: [
|
||||
{ cmd: "npx designpaca update", what: "Pull the latest skill. Files you edited are skipped" },
|
||||
{ cmd: "npx designpaca doctor", what: "Diagnose install state and drift" },
|
||||
{ cmd: "npx designpaca uninstall", what: "Exact removal, driven by the manifest" },
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
made: "This page was built with designpaca.",
|
||||
repo: "git.chanpaca.net/yunchan/designpaca",
|
||||
license: "MIT",
|
||||
},
|
||||
ui: { copy: "Copy", copied: "Copied", skipToContent: "Skip to content" },
|
||||
};
|
||||
116
apps/site/src/layouts/Base.astro
Normal file
116
apps/site/src/layouts/Base.astro
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
import "../styles/tokens.css";
|
||||
import "../styles/base.css";
|
||||
|
||||
interface Props {
|
||||
lang: "ko" | "en";
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
const { lang, title, description } = Astro.props;
|
||||
|
||||
// 언어별로 자기 폰트만 받는다. 두 벌을 한 페이지에 로드하면 예산을 넘긴다.
|
||||
const FONT_CSS = {
|
||||
ko: "https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css",
|
||||
en: "https://api.fontshare.com/v2/css?f%5B%5D=switzer@400,500,600&display=swap",
|
||||
} as const;
|
||||
|
||||
const alt = lang === "ko" ? "/en/" : "/";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />
|
||||
<link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} />
|
||||
<link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} />
|
||||
<link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />
|
||||
|
||||
{/* 본문 폰트만 preconnect·preload 한다. 모노는 코드 블록에만 쓰인다 */}
|
||||
<link rel="preconnect" href={lang === "ko" ? "https://cdn.jsdelivr.net" : "https://api.fontshare.com"} crossorigin />
|
||||
<link rel="stylesheet" href={FONT_CSS[lang]} />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a>
|
||||
|
||||
<header class="page site-header">
|
||||
<a class="wordmark" href={lang === "ko" ? "/" : "/en/"}>designpaca</a>
|
||||
<nav aria-label={lang === "ko" ? "언어" : "Language"}>
|
||||
{/* 언어는 국가가 아니다 — 국기 아이콘을 쓰지 않는다 */}
|
||||
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}>
|
||||
{lang === "ko" ? "English" : "한국어"}
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main" class="page">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="page site-footer">
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.skip {
|
||||
position: absolute;
|
||||
left: var(--space-3);
|
||||
top: calc(var(--space-3) * -4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
border-radius: var(--radius-sm);
|
||||
z-index: 10;
|
||||
transition: transform var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
.skip:focus {
|
||||
transform: translateY(calc(var(--space-3) * 5));
|
||||
}
|
||||
|
||||
.site-header {
|
||||
padding-block: var(--space-4);
|
||||
}
|
||||
.site-header > * {
|
||||
grid-column: main;
|
||||
}
|
||||
.site-header {
|
||||
align-items: baseline;
|
||||
}
|
||||
.wordmark {
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.site-header nav {
|
||||
justify-self: end;
|
||||
grid-row: 1;
|
||||
grid-column: main;
|
||||
font-size: var(--step-0);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
padding-block: var(--space-6);
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
</style>
|
||||
28
apps/site/src/pages/en/index.astro
Normal file
28
apps/site/src/pages/en/index.astro
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import Base from "../../layouts/Base.astro";
|
||||
import Hero from "../../components/Hero.astro";
|
||||
import Pipeline from "../../components/Pipeline.astro";
|
||||
import Metrics from "../../components/Metrics.astro";
|
||||
import Preflight from "../../components/Preflight.astro";
|
||||
import Typography from "../../components/Typography.astro";
|
||||
import Rules from "../../components/Rules.astro";
|
||||
import Install from "../../components/Install.astro";
|
||||
import { en as c } from "../../i18n/content";
|
||||
---
|
||||
|
||||
<Base lang="en" title={c.meta.title} description={c.meta.description}>
|
||||
<Hero c={c.hero} ui={c.ui} />
|
||||
<Pipeline c={c.pipeline} />
|
||||
<Metrics c={c.metrics} />
|
||||
<Preflight c={c.preflight} />
|
||||
<Typography c={c.typography} />
|
||||
<Rules c={c.rules} />
|
||||
<Install c={c.install} ui={c.ui} />
|
||||
|
||||
<Fragment slot="footer">
|
||||
<p>{c.footer.made}</p>
|
||||
<p class="mono">
|
||||
<a href={`https://${c.footer.repo}`}>{c.footer.repo}</a> · {c.footer.license}
|
||||
</p>
|
||||
</Fragment>
|
||||
</Base>
|
||||
|
|
@ -1,19 +1,28 @@
|
|||
---
|
||||
const version = "0.1.0";
|
||||
import Base from "../layouts/Base.astro";
|
||||
import Hero from "../components/Hero.astro";
|
||||
import Pipeline from "../components/Pipeline.astro";
|
||||
import Metrics from "../components/Metrics.astro";
|
||||
import Preflight from "../components/Preflight.astro";
|
||||
import Typography from "../components/Typography.astro";
|
||||
import Rules from "../components/Rules.astro";
|
||||
import Install from "../components/Install.astro";
|
||||
import { ko as c } from "../i18n/content";
|
||||
---
|
||||
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>designpaca</title>
|
||||
<meta name="description" content="웹 디자인 전 과정을 끌고 가는 파이프라인 스킬" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>designpaca</h1>
|
||||
<p>웹 디자인 파이프라인 스킬 — v{version}</p>
|
||||
<code>npx designpaca</code>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<Base lang="ko" title={c.meta.title} description={c.meta.description}>
|
||||
<Hero c={c.hero} ui={c.ui} />
|
||||
<Pipeline c={c.pipeline} />
|
||||
<Metrics c={c.metrics} />
|
||||
<Preflight c={c.preflight} />
|
||||
<Typography c={c.typography} />
|
||||
<Rules c={c.rules} />
|
||||
<Install c={c.install} ui={c.ui} />
|
||||
|
||||
<Fragment slot="footer">
|
||||
<p>{c.footer.made}</p>
|
||||
<p class="mono">
|
||||
<a href={`https://${c.footer.repo}`}>{c.footer.repo}</a> · {c.footer.license}
|
||||
</p>
|
||||
</Fragment>
|
||||
</Base>
|
||||
|
|
|
|||
157
apps/site/src/styles/base.css
Normal file
157
apps/site/src/styles/base.css
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* ============================================================
|
||||
designpaca 소개 페이지 — 기반 스타일
|
||||
토큰은 tokens.css 에만 있다. 이 파일에 새 값을 만들지 않는다.
|
||||
============================================================ */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--step-0);
|
||||
line-height: var(--leading-normal);
|
||||
letter-spacing: var(--tracking-body);
|
||||
font-synthesis-weight: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* 한글은 단어 중간에서 끊지 않는다 */
|
||||
[lang="ko"] body,
|
||||
[lang="ko"] p,
|
||||
[lang="ko"] li,
|
||||
[lang="ko"] h1,
|
||||
[lang="ko"] h2,
|
||||
[lang="ko"] h3 {
|
||||
word-break: keep-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* ---- 그리드 ------------------------------------------------
|
||||
main 폭은 3단계에서 정한 --measure 다. 고정값을 쓰지 않는다.
|
||||
----------------------------------------------------------- */
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
[full-start] minmax(var(--space-4), 1fr)
|
||||
[wide-start] minmax(0, var(--space-8))
|
||||
[main-start] minmax(0, var(--measure)) [main-end]
|
||||
minmax(0, var(--space-8)) [wide-end]
|
||||
minmax(var(--space-4), 1fr) [full-end];
|
||||
}
|
||||
.page > * { grid-column: main; }
|
||||
.page > .wide { grid-column: wide; }
|
||||
.page > .full { grid-column: full; }
|
||||
|
||||
/* 섹션 안에서도 같은 그리드를 쓴다 — 정렬선이 페이지 전체에서 하나여야 한다 */
|
||||
.section {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: full;
|
||||
padding-block: var(--space-7);
|
||||
}
|
||||
.section > * { grid-column: main; }
|
||||
.section > .wide { grid-column: wide; }
|
||||
|
||||
/* ---- 타이포 ------------------------------------------------ */
|
||||
h1, h2, h3 {
|
||||
margin: 0;
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-display);
|
||||
font-weight: 600;
|
||||
text-wrap: balance;
|
||||
}
|
||||
h1 { font-size: var(--display); font-weight: 620; }
|
||||
h2 { font-size: var(--step-3); }
|
||||
h3 { font-size: var(--step-1); }
|
||||
|
||||
p { margin: 0 0 var(--space-3); text-wrap: pretty; }
|
||||
p:last-child { margin-bottom: 0; }
|
||||
|
||||
.lead {
|
||||
font-size: var(--step-1);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
/* eyebrow 는 새 크기를 만들지 않는다 — 본문 크기 + 자간 + muted */
|
||||
.eyebrow {
|
||||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--ink-muted);
|
||||
font-weight: 500;
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
|
||||
code, kbd, samp, pre {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--mono-size);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-underline-offset: 0.2em;
|
||||
text-decoration-thickness: from-font;
|
||||
transition: color var(--dur-instant) var(--ease-out);
|
||||
}
|
||||
a:hover { color: var(--accent); }
|
||||
|
||||
/* 포커스는 어떤 이펙트보다 우선한다 */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
margin-block: var(--space-6);
|
||||
}
|
||||
|
||||
/* ---- 표 ---------------------------------------------------- */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: var(--space-2) var(--space-3) var(--space-2) 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
th {
|
||||
font-weight: 500;
|
||||
color: var(--ink-muted);
|
||||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
}
|
||||
/* 좁은 화면에서 표가 페이지를 밀지 않게 — 하드 게이트 #5 */
|
||||
.table-scroll { overflow-x: auto; }
|
||||
|
||||
/* ---- 유틸 -------------------------------------------------- */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.muted { color: var(--ink-muted); }
|
||||
.mono { font-family: var(--font-mono); font-size: var(--mono-size); }
|
||||
125
apps/site/src/styles/tokens.css
Normal file
125
apps/site/src/styles/tokens.css
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/* ============================================================
|
||||
designpaca 소개 페이지 — 디자인 토큰
|
||||
프리셋: swiss-minimal / 비율 1.25 / 강조색 1개 / radius 2종
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ---- 폰트 패밀리 ---------------------------------------- */
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
--mono-size: 0.9375em; /* 부모 대비. 새 스케일 단계를 만들지 않는다 */
|
||||
|
||||
/* ---- 색: 라이트 (기본) ----------------------------------- */
|
||||
--surface: #FAF9F6; /* 웜 오프화이트. 순백 아님 (A-07) */
|
||||
--surface-raised: #F2F0EA;
|
||||
--surface-inverse: #14161A; /* 다크 밴드. 순흑 아님 */
|
||||
--ink: #16181C; /* 대비 16.88:1 */
|
||||
--ink-muted: #5A5F66; /* 대비 6.11:1 */
|
||||
--ink-inverse: #E9E7E2; /* 밴드 위 14.66:1 */
|
||||
--ink-muted-inverse: #9AA0A6; /* 밴드 위 6.86:1 */
|
||||
--line: #DFDCD4; /* 1.30:1 — 장식 구분선 전용 */
|
||||
--line-inverse: #2A2E34; /* 1.33:1 — 장식 구분선 전용 */
|
||||
--line-strong: var(--ink-muted); /* 입력·컨트롤 테두리. WCAG 1.4.11 (3:1) */
|
||||
--accent: #0E6B63; /* 딥 틸. 대비 6.04:1 */
|
||||
--accent-ink: #FFFFFF; /* 강조 위 글자. 6.36:1 */
|
||||
--accent-inverse: #3FA89D; /* 밴드 위 강조. 6.30:1 */
|
||||
|
||||
/* ---- 형태: radius 2종 (게이트 #3) ------------------------ */
|
||||
--radius-sm: 3px; /* 버튼·입력·배지 */
|
||||
--radius-md: 6px; /* 카드·코드 블록 */
|
||||
|
||||
/* ---- 간격 ------------------------------------------------ */
|
||||
--space-1: 0.25rem; /* 4 */
|
||||
--space-2: 0.5rem; /* 8 */
|
||||
--space-3: 1rem; /* 16 */
|
||||
--space-4: 1.5rem; /* 24 */
|
||||
--space-5: 2.5rem; /* 40 */
|
||||
--space-6: 4rem; /* 64 */
|
||||
--space-7: 6rem; /* 96 — 일반 섹션 상하 */
|
||||
--space-8: 8rem; /* 128 — 다크 밴드 상하. 본문 간격의 8배 */
|
||||
|
||||
/* ---- 모션 ------------------------------------------------ */
|
||||
--dur-instant: 100ms; /* 호버·포커스 */
|
||||
--dur-quick: 200ms; /* 작은 등장·퇴장 */
|
||||
--dur-normal: 320ms; /* 패널·전환. 2단계 상한 */
|
||||
--dur-slow: 600ms; /* 이 프로젝트에서는 사용하지 않는다 */
|
||||
|
||||
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-in: cubic-bezier(0.64, 0, 0.78, 0);
|
||||
--ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* bounce·elastic 없음 (E-02) */
|
||||
}
|
||||
|
||||
/* ---- 색: 다크 테마 — 라이트와 동등하게 정의 ------------------ */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--surface: #14161A;
|
||||
--surface-raised: #1C1F24;
|
||||
--surface-inverse: #FAF9F6; /* 다크에서는 밴드가 밝아진다 */
|
||||
--ink: #E9E7E2;
|
||||
--ink-muted: #9AA0A6;
|
||||
--ink-inverse: #16181C;
|
||||
--ink-muted-inverse: #5A5F66;
|
||||
--line: #2A2E34;
|
||||
--line-inverse: #DFDCD4;
|
||||
--line-strong: var(--ink-muted);
|
||||
--accent: #3FA89D; /* 6.30:1 */
|
||||
--accent-ink: #0B1A18; /* 6.21:1 */
|
||||
--accent-inverse: #0E6B63;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 타입: 한국어판 --------------------------------------- */
|
||||
[lang="ko"] {
|
||||
--font-sans: "Pretendard Variable", Pretendard, system-ui, sans-serif;
|
||||
|
||||
--step-0: 1.0625rem; /* 17 — 본문 · eyebrow · 코드 */
|
||||
--step-1: 1.328rem; /* 21.3 — 리드 */
|
||||
--step-2: 1.660rem; /* 26.6 — h3 */
|
||||
--step-3: 2.076rem; /* 33.2 — h2 */
|
||||
--step-4: 2.595rem; /* 41.5 — h1 */
|
||||
--display: clamp(2.595rem, 1.6rem + 4.2vw, 4.25rem); /* → 68 */
|
||||
|
||||
--leading-tight: 1.15;
|
||||
--leading-normal: 1.7; /* 한글은 넉넉하게 */
|
||||
--measure: 33em; /* ≈ 561px. 한글 글자폭 ≈ 1em */
|
||||
|
||||
--tracking-display: -0.01em; /* 한글은 음수 자간을 크게 주지 않는다 */
|
||||
--tracking-body: 0; /* 한글 본문 음수 자간 금지 */
|
||||
--tracking-label: 0.08em;
|
||||
|
||||
word-break: keep-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* ---- 타입: 영어판 ------------------------------------------ */
|
||||
[lang="en"] {
|
||||
--font-sans: "Switzer", system-ui, -apple-system, sans-serif;
|
||||
|
||||
--step-0: 1rem; /* 16 */
|
||||
--step-1: 1.25rem; /* 20 */
|
||||
--step-2: 1.5625rem; /* 25 */
|
||||
--step-3: 1.953rem; /* 31.3 */
|
||||
--step-4: 2.441rem; /* 39 */
|
||||
--display: clamp(2.441rem, 1.5rem + 4vw, 4rem); /* → 64 */
|
||||
|
||||
--leading-tight: 1.1;
|
||||
--leading-normal: 1.55;
|
||||
--measure: 65ch; /* ≈ 640px */
|
||||
|
||||
--tracking-display: -0.025em;
|
||||
--tracking-body: 0;
|
||||
--tracking-label: 0.08em;
|
||||
}
|
||||
|
||||
/* ---- 다크 밴드: 게이트 #4가 요구하는 "규칙으로 정의된 반전" ---- */
|
||||
/* 사용처는 히어로와 설치 섹션 두 곳뿐이다. 그 외 사용 금지. */
|
||||
.band-inverse {
|
||||
--surface: var(--surface-inverse);
|
||||
--ink: var(--ink-inverse);
|
||||
--ink-muted: var(--ink-muted-inverse);
|
||||
--line: var(--line-inverse);
|
||||
--accent: var(--accent-inverse);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
padding-block: var(--space-8);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue