designpaca/apps/site/src/i18n/content.ts
Yun Chan e1e6002bee
All checks were successful
ci / build (push) Successful in 25s
backdrop-filter 오판을 정정하고 리퀴드 글래스 기법을 기록한다
'backdrop-filter 에 SVG 를 얹으면 Chrome 이 선언 전체를 버린다' 고
적어두고 페이지 프리플라이트 로그에까지 실었다. 사실이 아니었다.

Chrome 151 에서 재측정
  computed 값이 none 이 아니라 url(#id) 로 유지된다
  대비 높은 배경 위에서 실제로 굴절된다(줄무늬가 가장자리에서 압축됨)

진짜 원인은 feImage 의 width/height 를 요소 크기와 맞추지 않은 것이었다.
backdrop-filter 는 필터 이미지를 자동으로 조정하지 않는다.

검색도 다시 했다. feTurbulence grain 에 갇혀 2019년 글만 보고 있었다.
liquid glass 계열 최신 자료를 보니 변위 지도는 노이즈가 아니라
물리 기반 사전계산 이미지였다 — R=X변위, G=Y변위, 128 이 중립,
감쇠는 Squircle 곡선. 캔버스로 즉석 생성하면 240x160 이 2.7KB 다.

스킬 svg-filters.md 에 '리퀴드 글래스' 절을 넣었다.
구조 · 변위 지도 만드는 법 · 함정 넷 · @supports 폴백.
오판 정정도 문서 첫 줄에 남겼다.

한 번 실패한 것을 '안 되는 것' 으로 적으면 그 문장이 다음 사람의
상한선이 된다. 페이지 로그 항목도 그 내용으로 바꿨다.
2026-08-20 22:09:22 +09:00

682 lines
35 KiB
TypeScript

/**
* 두 언어판은 번역이 아니라 각각 따로 쓴 것이다.
* 특히 조판 섹션은 한국어판에서 주장이고 영어판에서 사례다.
*
* 규칙: 이 파일에 em-dash 와 en-dash 를 쓰지 않는다.
* 하이픈, 마침표, 콜론, 괄호로 대신한다.
*/
export interface Stage {
name: string;
does: string;
/** 같은 브리프가 이 단계를 지나며 무엇으로 변하는가. 번호 대신 이 변화가 순서를 만든다 */
becomes: string;
mono?: boolean;
}
export interface Metric {
label: string;
value: string;
note: string;
/** 벤토에서 크게 잡을 항목 */
feature?: boolean;
}
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 }[];
stage: {
alt: string;
caption: string;
gate: string;
pass: string;
};
};
pipeline: {
eyebrow: string;
h2: string;
lead: string;
briefLabel: string;
brief: string;
stages: Stage[];
after: string;
};
showcase: {
h2: string;
lead: string;
items: { alt: string; caption: string; note: string }[];
};
materials: {
eyebrow: string;
h2: string;
lead: string;
tiles: { id: "grain" | "glass" | "duotone"; name: string; use: string; how: string; cost: string }[];
note: string;
};
metrics: { eyebrow: string; h2: string; lead: string; items: Metric[]; caveat: string };
preflight: {
h2: string;
lead: string;
items: { caught: string; fixed: string }[];
note: string;
};
typography: {
eyebrow: string;
h2: string;
lead: string;
sample: string;
demos: {
id: "break" | "leading" | "size";
title: string;
badLabel: string;
goodLabel: string;
note: string;
}[];
rules: string[];
};
rules: { h2: string; lead: string; items: Rule[] };
install: {
h2: string;
lead: string;
command: string;
targets: { name: string; path: string }[];
commands: { cmd: string; what: string }[];
colophon: { made: string; repo: string; license: string };
};
nav: { items: { label: string; href: string }[]; menu: string };
ui: { copy: string; copied: 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: "결정하지 않으면 기본값이 나온다",
lead: "AI가 만든 티는 못 만들어서가 아니라 결정을 안 해서 난다. 그 결정을 여섯 단계로 강제하는 스킬.",
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" },
],
stage: {
alt: "유리판 여섯 장이 깊이 방향으로 나란히 서 있다. 스크롤하면 카메라가 판을 하나씩 통과하고, 통과한 판은 뒤로 물러난다.",
caption: "판 하나가 단계 하나다. 통과 조건을 채우지 못하면 다음 판으로 가지 않는다.",
gate: "게이트",
pass: "통과",
},
},
pipeline: {
eyebrow: "여섯 단계",
h2: "브리프 한 줄이 검증된 페이지가 될 때까지",
lead:
"각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 같은 브리프가 단계마다 무엇으로 변했는지다.",
briefLabel: "들어온 브리프",
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
stages: STAGES_KO,
after:
"여섯 단계 끝에 남는 것은 페이지 하나와 design.md 하나다. 다음 사람이 그 파일을 읽고 같은 결정을 이어간다.",
},
showcase: {
h2: "결과물은 서로 닮지 않는다",
lead:
"같은 파이프라인을 두 브리프에 돌린 결과다. 1단계에서 톤을 다른 업종에서 가져오게 강제하기 때문에 같은 도구를 써도 같은 화면이 나오지 않는다. 둘 다 수치 자리가 비어 있는 것도 우연이 아니다.",
items: [
{
alt: "무월 원두 구독 랜딩. 오프화이트 배경에 굵은 한글 헤드라인, 오른쪽에 원두 3종의 산미와 바디 표가 있다.",
caption: "수제 커피 로스터리 · 구독 전환",
note: "프리셋 quiet-commerce. 사진이 없다는 제약을 수치표로 뒤집었다. 표의 값은 아직 비어 있다. 로스터에게 받기 전까지 채우지 않는다.",
},
{
alt: "Pulsegate 모니터링 SaaS 히어로. 밝은 배경에 큰 세리프 헤드라인과 무료 체험 버튼이 있다.",
caption: "API 모니터링 SaaS · 무료 체험 가입",
note: "개발자 도구라고 자동으로 다크로 가지 않았다. 브리프에 근거가 없었기 때문이다. 리전 수는 아직 중괄호 자리로 남아 있다.",
},
],
},
materials: {
eyebrow: "재료",
h2: "텍스처 200KB 를 300바이트가 대신한다",
lead:
"필터는 장식이 아니라 재료다. 셋 다 실제로 쓰이는 자리에 걸어 뒀다 — 그레인은 매끈한 면에, 굴절은 유리 뒤 글자에, 듀오톤은 사진에. 배경은 지금 three.js 셰이더가 그리고 있다.",
tiles: [
{
id: "grain",
name: "그레인",
use: "매끈한 그라디언트가 싸구려로 보일 때",
how: "feTurbulence 가 만든 프랙탈 노이즈를 알파로 눌러 표면에 얹는다. 이미지 요청이 한 건도 늘지 않는다.",
cost: "300바이트 · 정지 상태",
},
{
id: "glass",
name: "굴절",
use: "유리 뒤의 글자가 실제로 휘어야 할 때",
how: "난류를 변위 지도로 써서 픽셀을 민다. 필터 영역을 넓혀야 가장자리가 잘리지 않는다.",
cost: "중간 · 애니메이션 금지",
},
{
id: "duotone",
name: "듀오톤",
use: "출처가 제각각인 사진을 한 팔레트로 묶을 때",
how: "명도만 남긴 뒤 feComponentTransfer 로 두 색 사이에 다시 매핑한다. 원본을 보정하지 않는다.",
cost: "낮음 · 사진에 쓴다",
},
],
note:
"이 셋은 SVG 필터가 지금 먹고 있는 화면이고, 그 뒤 배경은 three.js 셰이더가 그리고 있다. three 는 초기 번들에 없다 — 화면에 가까워질 때만 받고, WebGL 이 없거나 모션을 줄이는 설정이면 아예 받지 않는다. 그때 보이는 그라디언트는 폴백이 아니라 그대로 결과물이다."
},
metrics: {
eyebrow: "이 페이지의 수치",
h2: "카피만 고쳐서는 슬롭이 없어지지 않는다",
lead:
"문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.",
items: [
{ label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", feature: true },
{ label: "하드 게이트", value: "12/12", note: "정적 검사와 320px · 1440px 실렌더 양쪽", feature: true },
{ label: "three.js 추가분", value: "184KB", note: "검사대가 화면에 들어올 때만 받는다. 초기 번들에는 없다" },
{ label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
{ label: "radius 어휘", value: "2종", note: "3px 과 14px. 알약 모양을 없앴다" },
{ label: "반투명 패널", value: "2개", note: "29개까지 늘렸다가 걷어냈다. 유리는 무언가를 담는 액자에만 남겼다" },
{ label: "타입 단계", value: "6단계", note: "토큰에 정의된 수. 디스플레이는 스케일 밖이고, 최대 글자는 본문의 5.6배" },
{ label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" },
{ label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
{ label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잰다" },
],
caveat:
"값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.",
},
preflight: {
h2: "이 페이지를 만들며 실제로 걸린 것",
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
items: [
{
caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었다",
fixed:
"웹폰트가 오기 전에는 시스템 폰트로 그려진다. 맑은 고딕은 Pretendard 보다 13% 넓어서 교체되는 순간 줄바꿈이 다시 잡힌다. 폴백 폰트에 size-adjust 로 치수를 맞췄다. 실측 차이 0.0%. OS 마다 시스템 폰트가 다르므로 폴백도 따로 정의했다.",
},
{
caught: "유리 카드를 29개 뿌렸다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였다",
fixed:
"우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨다. 유리는 5개만 남았다.",
},
{
caught: "“backdrop-filter 에 SVG 를 얹으면 Chrome 이 버린다” 고 적었는데 사실이 아니었다",
fixed:
"한 번 실패하고 브라우저 탓으로 적어뒀다. 다시 재보니 Chrome 151 에서 computed 값이 none 이 아니라 url(#id) 로 살아 있고 배경도 실제로 굴절된다. 진짜 원인은 feImage 의 width/height 를 요소 크기와 맞추지 않은 것이었다 — backdrop-filter 는 필터 이미지를 자동으로 조정하지 않는다. 한 번 실패한 것을 ‘안 되는 것’ 으로 적으면 그 문장이 다음 사람의 상한선이 된다.",
},
{
caught: "가로로 444px 넘쳤다. 레이아웃 전체가 어긋나 있었다",
fixed:
"main 에서 그리드 클래스가 빠져 있었다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.",
},
{
caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였다",
fixed:
"scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못한다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨다.",
},
{
caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1",
fixed:
"밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.",
},
{
caught: "320px 에서 가로로 13px 넘쳤다. 표를 스크롤 컨테이너로 감쌌는데도",
fixed:
"grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow 가 실제로 작동한다.",
},
{
caught: "eyebrow 라벨이 6개. 카운트 규칙 상한은 3개",
fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다.",
},
{
caught: "본문 컬럼이 960px. 우리가 정한 한 줄 길이는 561px",
fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.",
},
{
caught: "방향을 바꾸면서 이 스킬의 1단계를 건너뛰었다",
fixed:
"재질을 유리로 바꾸라는 요청은 2단계 방향 재결정이고, 그때는 전체 경로로 올려 레퍼런스부터 다시 봐야 한다. 그러지 않고 구현으로 바로 들어갔다. 결과는 이전 방향의 뼈대에 유리를 바른 페이지였고 design.md 는 옛 결정을 그대로 담고 있었다. 0단계부터 다시 돌았다.",
},
{
caught: "유리판이 정면으로 서 있어 프레넬이 구조적으로 0이었다",
fixed:
"평면이 카메라를 정면으로 보면 법선과 시선의 내적이 1이라 가장자리 반사가 계산되지 않는다. 유리로 읽히게 하는 유일한 신호가 죽어 있었고, 그래서 판 여섯 장이 흐린 사각형으로 보였다. 법선을 미세하게 굽히고 테두리 광을 UV 경계에서 따로 만들었다.",
},
{
caught: "페이지의 본체인 파이프라인이 화면의 43%만 쓰고 있었다",
fixed:
"1440px 중 625px. 아홉 섹션에서 가장 좁으면서 높이는 1637px 로 가장 길었다. 단계 설명과 산출물을 두 열로 열어 1113px 로 넓혔다.",
},
{
caught: "대비를 재는 스크립트가 반투명 배경을 불투명으로 계산했다",
fixed:
"\"English 1.15:1\" 이라는 틀린 값이 나왔다. 알파를 아래에서 위로 합성하도록 고치자 실패 0건이었다. 검사 도구가 틀리면 통과도 실패도 의미가 없다.",
},
],
note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못한다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.",
},
typography: {
eyebrow: "한글 조판",
h2: "라틴 기본값은 한글에서 세 군데가 깨진다",
lead:
"영어권 디자인 도구에는 한글 조판 규칙이 없다. 그래서 기본값을 그대로 쓰면 아래 셋이 동시에 무너진다. 왼쪽이 기본값, 오른쪽이 이 스킬이 정한 값이다. 글자도 폭도 폰트도 같다.",
sample: "스페셜티커피의 로스팅프로파일을 구분하려면 산미와 바디를 수치로 봐야 한다.",
demos: [
{
id: "break",
title: "줄바꿈",
badLabel: "기본값",
goodLabel: "word-break: keep-all",
note: "기본값은 어절 한가운데를 끊는다. 왼쪽 마지막 줄을 보라 — 한 단어가 두 줄에 걸쳐 있다.",
},
{
id: "leading",
title: "행간",
badLabel: "line-height 1.45",
goodLabel: "line-height 1.75",
note: "라틴 표준 1.4~1.5 는 받침이 있는 글자에 좁다. 줄이 서로 붙어 보인다.",
},
{
id: "size",
title: "크기",
badLabel: "11px",
goodLabel: "15px",
note: "라틴에서 11px 은 라벨로 쓴다. 한글은 획이 많아 같은 크기에서 뭉갠다.",
},
],
rules: [
"word-break: keep-all 없으면 단어가 아무 데서나 잘린다",
"line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게",
"음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개진다",
"한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 길다",
"한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다",
],
},
rules: {
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: {
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: "매니페스트 기반 정확한 제거" },
],
colophon: {
made: "designpaca 로 만들었다",
repo: "git.chanpaca.net/yunchan/designpaca",
license: "MIT",
},
},
nav: {
items: [
{ label: "파이프라인", href: "#pipeline" },
{ label: "사례", href: "#showcase" },
{ label: "수치", href: "#metrics" },
{ label: "설치", href: "#install" },
],
menu: "메뉴",
},
ui: { copy: "복사", copied: "복사됨" },
};
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: reference research through self-audit, six stages. Installs into Claude Code, Codex, Cursor and Windsurf in one line.",
},
hero: {
h1: "Undecided design ships as defaults",
lead: "The AI look is not a skill problem. It is a decision problem. This skill forces the 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" },
],
stage: {
alt: "Six glass panes stand in a row along the depth axis. Scrolling moves the camera through them one at a time, and each pane recedes once passed.",
caption: "One pane is one stage. Fail its condition and you do not reach the next.",
gate: "GATE",
pass: "PASS",
},
},
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 do not move on. Below is what one brief became at each stage.",
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.",
},
showcase: {
h2: "The outputs do not resemble each other",
lead:
"Two briefs through the same pipeline. Stage one forces the tone reference to come from a different industry, so the same tool does not produce the same screen twice. Both still have empty number slots, and that is not an accident either.",
items: [
{
alt: "A coffee subscription landing. Off-white ground, heavy Korean headline, a table of acidity and body figures on the right.",
caption: "Small-batch roastery · subscription conversion",
note: "Preset quiet-commerce. Having no photography became the argument, not the limit. The table values are still blank, and stay blank until the roaster supplies them.",
},
{
alt: "Pulsegate monitoring SaaS hero. Light ground, large serif headline, a free trial button.",
caption: "API monitoring SaaS · free trial signup",
note: "A developer tool did not default to dark. The brief carried no reason for it. The region count is still sitting in braces.",
},
],
},
materials: {
eyebrow: "Materials",
h2: "300 bytes replaces a 200KB texture",
lead:
"Filters are material, not decoration. Each one sits where it is actually used — grain on a flat gradient, refraction behind glass over type, duotone on a photograph. The backdrop is a three.js shader, running now.",
tiles: [
{
id: "grain",
name: "Grain",
use: "when a smooth gradient reads as cheap",
how: "feTurbulence generates fractal noise, pushed down through alpha onto the surface. Not one extra image request.",
cost: "300 bytes · static",
},
{
id: "glass",
name: "Refraction",
use: "when type behind glass has to actually bend",
how: "Turbulence drives a displacement map that pushes pixels. Widen the filter region or the edges clip.",
cost: "medium · never animate",
},
{
id: "duotone",
name: "Duotone",
use: "when photos from different sources need one palette",
how: "Keep luminance only, then remap between two colours with feComponentTransfer. The source file is untouched.",
cost: "low · use it on photos",
},
],
note:
"These three are SVG filters running on live pixels, and the field behind them is a three.js shader. three is not in the initial bundle — it arrives only as the section approaches, and never at all when WebGL is missing or motion is reduced. What you see then is not a fallback; it is the result."
},
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: "JS to first paint", value: "0.6%", note: "of the 50KB budget actually spent", feature: true },
{ label: "hard gates", value: "12/12", note: "static checks plus real renders at 320px and 1440px", feature: true },
{ label: "three.js payload", value: "184KB", note: "fetched only when the inspection frame enters view. Never in the initial bundle" },
{ label: "slop fingerprints", value: "0", note: "9 patterns grepped" },
{ label: "radius vocabulary", value: "2", note: "3px and 14px. The pill shape is gone" },
{ label: "translucent panels", value: "2", note: "grew to 29 before being pulled back. Glass stayed only on frames that hold something" },
{ label: "type scale steps", value: "6", note: "as defined in tokens. Display sits outside the scale, and the largest type is 6x the body" },
{ label: "font families", value: "2", note: "each language loads only its own" },
{ label: "accent colours", value: "1", note: "light and dark are the same colour" },
{ label: "time to interactive", value: "not yet", note: "measured with Lighthouse after deploy" },
],
caveat:
"Measured on the production build. Unmeasured entries stay empty. A hole is more honest than an invented number. This page is a product demo, so the JS budget was raised to carry three.js. Saying so here is the rule.",
},
preflight: {
h2: "What this page actually got caught on",
lead: "Not the result of auditing someone else. The result of auditing itself.",
items: [
{
caught: "Text width jumped 13% at the moment the font swapped",
fixed:
"Before the web font arrives the system font draws instead, and Malgun Gothic runs 13% wider than Pretendard, so line breaks are recalculated on swap. Matched the fallback with size-adjust. Measured difference is now 0.0%. System fonts differ per OS, so each gets its own fallback.",
},
{
caught: "29 glass cards. Only the material had changed. The structure was still a row of cards",
fixed:
"Our own rule says cards are for when elevation carries real hierarchy. Figures became typography, rules became an offset list, the audit became a log. Five panels of glass remain.",
},
{
caught: "We wrote down that Chrome drops backdrop-filter with SVG. It does not",
fixed:
"Attach url() to backdrop-filter and Chrome reports true through CSS.supports while discarding the whole declaration at render time. The computed value becomes none. Refraction is demonstrated for real by the SVG tiles in the materials section, and the panels build thickness from blur and edge light instead.",
},
{
caught: "444px of horizontal overflow. The entire layout was off its rails",
fixed:
"The grid class had gone missing from main. Subgrid only works when the parent is a grid, so every section had lost its alignment. It merely looked slightly off until the width was actually measured.",
},
{
caught: "Scrolling to the very bottom still left the last 3 of 6 pipeline stages invisible",
fixed:
"Elements near the page bottom never reach the end of a scroll-driven animation range. Visible with effects off but missing with them on is not the same as having a fallback. Rebuilt so that once revealed, it stays revealed.",
},
{
caught: "The button inside the dark band sat at 2.88:1. The threshold is 4.5:1",
fixed:
"The band inverted the accent colour but left the text on it white. Added an inverted counterpart token and switched both together.",
},
{
caught: "13px of horizontal overflow at 320px, even with the table inside a scroll container",
fixed:
"A grid item's default min-width is auto, so content pushes the parent wider. Overflow only works once min-width:0 is set.",
},
{
caught: "6 eyebrow labels. The count rule caps them at 3",
fixed: "Kept three, dropped the rest. Sections read fine without a label above the heading.",
},
{
caught: "The text column was 960px. The measure we had just set was 561px",
fixed:
"Bound the grid's main column to the token. The skill's own example code was wrong too, so that got fixed as well.",
},
{
caught: "We skipped this skill's own stage 1 when the direction changed",
fixed:
"Switching the material to glass is a stage-2 decision, and that means going back to references first. We jumped straight to implementation instead. The result was glass painted onto the previous direction's skeleton, while design.md still described the old decisions. We re-ran the whole path from stage 0.",
},
{
caught: "The glass panes faced the camera head-on, so Fresnel was structurally zero",
fixed:
"When a flat plane faces the camera, the dot product of normal and view is 1, so no edge reflection is computed. The one signal that makes a surface read as glass was dead, which is why six panes looked like blurry rectangles. We bend the normal slightly and derive the rim light from the UV edge instead.",
},
{
caught: "The pipeline, the spine of this page, used only 43% of the viewport",
fixed:
"625px out of 1440. Narrowest of the nine sections while being the tallest at 1637px. Opening each stage into two columns brought it to 1113px.",
},
{
caught: "The contrast script treated translucent backgrounds as opaque",
fixed:
"It reported \"English 1.15:1\", which was wrong. Compositing alpha bottom-up gave zero failures. A checker that is wrong makes both its passes and its failures meaningless.",
},
],
note: "All thirteen were caught while building this page, and fixed after being caught. Three came from a person rather than a checker: the cards look generic, the typography is not handled well, and did we actually use our own skill.",
},
typography: {
eyebrow: "Your language is probably not Latin",
h2: "Latin defaults break Korean in three places",
lead:
"Western design tools carry no Korean typesetting rules. Leave the defaults in place and all three of these fail at once. Left is the default, right is what this skill sets. Same text, same width, same font.",
sample: "스페셜티커피의 로스팅프로파일을 구분하려면 산미와 바디를 수치로 봐야 한다.",
demos: [
{
id: "break",
title: "Line breaking",
badLabel: "default",
goodLabel: "word-break: keep-all",
note: "The default breaks mid-word. Look at the last line on the left — one word straddles two lines.",
},
{
id: "leading",
title: "Leading",
badLabel: "line-height 1.45",
goodLabel: "line-height 1.75",
note: "The Latin standard of 1.4-1.5 is tight for syllable blocks with a final consonant.",
},
{
id: "size",
title: "Size",
badLabel: "11px",
goodLabel: "15px",
note: "11px is a label size in Latin. Korean glyphs carry more strokes and smear at it.",
},
],
rules: [
"Without word-break: keep-all, words break at arbitrary points",
"line-height between 1.6 and 1.8 — looser than Latin",
"Never use negative tracking. Korean is already near-monospaced",
"25 to 40 characters per line. The Latin 65 is far too long",
"Shipping without a Korean font stack is itself a tell",
],
},
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: {
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" },
],
colophon: {
made: "Built with designpaca",
repo: "git.chanpaca.net/yunchan/designpaca",
license: "MIT",
},
},
nav: {
items: [
{ label: "Pipeline", href: "#pipeline" },
{ label: "Work", href: "#showcase" },
{ label: "Numbers", href: "#metrics" },
{ label: "Install", href: "#install" },
],
menu: "Menu",
},
ui: { copy: "Copy", copied: "Copied" },
};