designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인

웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포.

스킬 (packages/skill)
- SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로
  본문은 절차와 인덱스만, 지식은 references/ 로 분리
- 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기
- 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외
- 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙,
  SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침

설치 CLI (packages/cli, packages/core)
- npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md
- 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다
- 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성
- AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입
- Windsurf 는 12,000자 상한 초과 시 설치를 차단

배포 (build/ci, .forgejo/workflows)
- 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러
  → draft 릴리스 → Cloudflare Pages. 재실행 멱등

근거 (research/)
- 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다

테스트 22개 통과 (core 16 · cli 6)
This commit is contained in:
Yun Chan 2026-08-20 10:48:00 +09:00
commit 8808c672dc
135 changed files with 38838 additions and 0 deletions

View file

@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pulsegate — hero (preflight target)</title>
<!-- Hero.jsx 와 동일한 CSS. 마크업만 감사용으로 복제했다. -->
<link rel="stylesheet" href="./src/styles/tokens.css" />
<link rel="stylesheet" href="./src/styles/hero.css" />
</head>
<body>
<div class="page">
<nav class="nav" aria-label="Primary">
<p class="nav__mark">Pulsegate</p>
<a class="nav__link" href="#pricing">Pricing</a>
<a class="nav__link" href="#docs">Docs</a>
<button type="button" class="theme-toggle" id="theme" aria-pressed="false">Light</button>
</nav>
<header class="hero">
<div>
<h1 class="hero__headline">Add every host you want. You pay for checks.</h1>
<p class="hero__sub">Pulsegate watches your endpoints from {{REGION_COUNT}} regions and pages you when latency drifts. No agents to install.</p>
<div class="hero__actions">
<a class="cta" href="#signup">Start the free trial</a>
<a class="link-quiet" href="#pricing">See how billing is calculated</a>
</div>
</div>
<figure style="margin:0">
<img class="shot" src="/product-checks.png" width="960" height="720"
alt="Pulsegate 체크 목록 화면. 엔드포인트 이름, 최근 응답 시간, 상태가 표 형태로 정렬되어 있다."
loading="eager" decoding="async" />
<figcaption class="shot-caption">checks/ — the only screen you need during an incident</figcaption>
</figure>
</header>
<div class="install">
<span class="install__prompt" aria-hidden="true">$</span>
<code class="install__cmd">npx pulsegate init</code>
<button type="button" class="install__copy" id="copy">Copy</button>
<span role="status" aria-live="polite" class="visually-hidden" id="copied"></span>
</div>
</div>
<script>
const root = document.documentElement
const btn = document.getElementById('theme')
const resolved = () => root.getAttribute('data-theme')
?? (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
const sync = () => {
const t = resolved()
btn.textContent = t === 'dark' ? 'Light' : 'Dark'
btn.setAttribute('aria-pressed', String(t === 'light'))
}
btn.addEventListener('click', () => {
root.setAttribute('data-theme', resolved() === 'dark' ? 'light' : 'dark')
sync()
})
sync()
const copyBtn = document.getElementById('copy')
const status = document.getElementById('copied')
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText('npx pulsegate init')
copyBtn.textContent = 'Copied'
status.textContent = 'Command copied to clipboard'
setTimeout(() => { copyBtn.textContent = 'Copy'; status.textContent = '' }, 1600)
} catch {}
})
</script>
</body>
</html>

View file

@ -0,0 +1,109 @@
import { useEffect, useState } from 'react'
/**
* 브리프가 수치를 주지 않았다. SKILL.md 0단계 "가정은 소리 내서 말해라" 따라
* 지어내지 않고 placeholder 두고, design.md 미확정 항목으로 적는다.
* 하드게이트 #11(사용자가 주지 않은 수치) 회피.
*/
const COPY = {
headline: 'Add every host you want. You pay for checks.',
sub: 'Pulsegate watches your endpoints from {{REGION_COUNT}} regions and pages you when latency drifts. No agents to install.',
cta: 'Start the free trial',
secondary: 'See how billing is calculated',
command: 'npx pulsegate init',
shotAlt:
'Pulsegate 체크 목록 화면. 엔드포인트 이름, 최근 응답 시간, 상태가 표 형태로 정렬되어 있다.',
shotCaption: 'checks/ — the only screen you need during an incident',
}
function useTheme() {
const [theme, setTheme] = useState(null) // null = OS
useEffect(() => {
const root = document.documentElement
if (theme === null) root.removeAttribute('data-theme')
else root.setAttribute('data-theme', theme)
}, [theme])
const resolved =
theme ??
(window.matchMedia?.('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
return [resolved, () => setTheme(resolved === 'dark' ? 'light' : 'dark')]
}
function CopyCommand({ command }) {
const [copied, setCopied] = useState(false)
async function copy() {
try {
await navigator.clipboard.writeText(command)
setCopied(true)
setTimeout(() => setCopied(false), 1600)
} catch {
setCopied(false)
}
}
return (
<div className="install">
<span className="install__prompt" aria-hidden="true">$</span>
<code className="install__cmd">{command}</code>
<button type="button" className="install__copy" onClick={copy}>
{copied ? 'Copied' : 'Copy'}
</button>
{/* 상태 변화를 스크린리더에도 알린다 */}
<span role="status" aria-live="polite" className="visually-hidden">
{copied ? 'Command copied to clipboard' : ''}
</span>
</div>
)
}
export default function Hero() {
const [theme, toggleTheme] = useTheme()
return (
<div className="page">
<nav className="nav" aria-label="Primary">
<p className="nav__mark">Pulsegate</p>
<a className="nav__link" href="#pricing">Pricing</a>
<a className="nav__link" href="#docs">Docs</a>
<button
type="button"
className="theme-toggle"
onClick={toggleTheme}
aria-pressed={theme === 'light'}
>
{theme === 'dark' ? 'Light' : 'Dark'}
</button>
</nav>
<header className="hero">
<div>
<h1 className="hero__headline">{COPY.headline}</h1>
<p className="hero__sub">{COPY.sub}</p>
<div className="hero__actions">
<a className="cta" href="#signup">{COPY.cta}</a>
<a className="link-quiet" href="#pricing">{COPY.secondary}</a>
</div>
</div>
<figure style={{ margin: 0 }}>
<img
className="shot"
src="/product-checks.png"
width="960"
height="720"
alt={COPY.shotAlt}
loading="eager"
decoding="async"
/>
<figcaption className="shot-caption">{COPY.shotCaption}</figcaption>
</figure>
</header>
<CopyCommand command={COPY.command} />
</div>
)
}

View file

@ -0,0 +1,216 @@
/* 히어로. 값은 전부 tokens.css 에서 온다. */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--surface);
color: var(--ink);
font-family: var(--font-body);
font-size: var(--step-0);
line-height: var(--leading-normal);
letter-spacing: var(--tracking-body);
font-variant-numeric: tabular-nums; /* dark-instrument: 수치가 흔들리면 계기판이 아니다 */
}
/* layout.md §1 — 하나의 그리드에서 전부 나온다 */
.page {
display: grid;
grid-template-columns:
[full-start] minmax(var(--gutter), 1fr)
[main-start] minmax(0, var(--container)) [main-end]
minmax(var(--gutter), 1fr) [full-end];
}
.page > * { grid-column: main; }
.page > .full { grid-column: full; }
/* ---------------------------------------------------------------- 내비 */
.nav {
display: flex;
flex-wrap: wrap; /* 320px 에서 밀려 가로 스크롤이 생겼다 */
align-items: center;
gap: var(--space-2) var(--space-4);
padding-block: var(--space-4);
}
.nav__mark {
font-family: var(--font-display);
font-size: var(--step-2);
letter-spacing: var(--tracking-display);
margin: 0;
margin-inline-end: auto;
}
.nav__link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px; /* 터치 타깃 44×44 */
min-width: 44px;
color: var(--ink-muted);
font-size: var(--step--1);
text-decoration: none;
border-bottom: 1px solid transparent;
}
.nav__link:hover { color: var(--ink); border-bottom-color: var(--line); }
/* ---------------------------------------------------------------- 히어로 */
.hero {
/* 100vh 금지(하드게이트 #9). dvh 를 쓰고 min-height 로만 잡는다 */
min-height: min(84dvh, 46rem);
display: grid;
/* 비대칭 7:5. layout.md — 중앙 정렬 히어로는 쓰지 않는다 */
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
gap: var(--space-6) var(--space-7);
align-items: start;
padding-block: var(--space-7) var(--space-8);
}
.hero__headline {
font-family: var(--font-display);
font-size: var(--display-1);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-display);
font-weight: 400;
margin: 0 0 var(--space-4);
text-wrap: balance;
}
.hero__sub {
max-width: var(--measure);
color: var(--ink-muted);
font-size: var(--step-1);
margin: 0 0 var(--space-5);
}
.hero__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-4);
}
/* 주 CTA — 감수한 리스크: 강조색을 쓰지 않고 잉크 반전으로만 만든다 */
.cta {
display: inline-block;
background: var(--cta-bg);
color: var(--cta-ink);
font-size: var(--step-0);
font-weight: 600;
line-height: 1.2;
text-decoration: none;
padding: var(--space-3) var(--space-5);
border: 0;
border-radius: var(--radius-md);
white-space: nowrap; /* 하드게이트 #6 — 라벨이 2줄로 접히지 않는다 */
/* 하드게이트 #8 — transform/opacity 만 트랜지션한다 */
transition: transform var(--dur-instant) var(--ease-out);
}
.cta:hover { transform: translateY(-1px); }
.cta:active { transform: translateY(0); }
.link-quiet {
display: inline-flex;
align-items: center;
min-height: 44px; /* 터치 타깃 */
color: var(--accent); /* 강조색은 오직 글자에만 */
font-size: var(--step-0);
text-decoration: underline; /* 색만으로 구분하지 않는다 */
text-underline-offset: 0.25em;
}
/* ---------------------------------------------------------------- 제품 화면
* 하드게이트 #12 div 만든 가짜 스크린샷 금지.
* 브리프가 실제 스크린샷을 제공하므로 <img> 쓴다.
* aspect-ratio 자리를 미리 잡아 CLS 막는다.
*/
.shot {
display: block;
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
object-position: left top;
background: var(--surface-raised); /* 로드 전/실패 시의 자리 */
border-radius: var(--radius-md);
}
.shot-caption {
display: block;
margin-top: var(--space-2);
color: var(--ink-muted);
font-size: var(--step--1);
font-family: var(--font-mono);
}
/* ---------------------------------------------------------------- 설치 명령 */
.install {
grid-column: main;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
background: var(--surface-raised);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
font-family: var(--font-mono);
font-size: var(--step-0);
}
.install__prompt { color: var(--ink-muted); user-select: none; }
.install__cmd { color: var(--ink); }
.install__copy {
margin-inline-start: auto;
min-height: 44px;
min-width: 44px;
background: transparent;
color: var(--ink-muted);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
font: inherit;
font-size: var(--step--1);
padding: var(--space-1) var(--space-3);
cursor: pointer;
transition: transform var(--dur-instant) var(--ease-out);
}
.install__copy:hover { color: var(--ink); transform: translateY(-1px); }
/* ---------------------------------------------------------------- 테마 토글 */
.theme-toggle {
min-height: 44px;
min-width: 44px;
background: transparent;
color: var(--ink-muted);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
font: inherit;
font-size: var(--step--1);
padding: var(--space-1) var(--space-3);
cursor: pointer;
}
.theme-toggle:hover { color: var(--ink); }
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* ---------------------------------------------------------------- 포커스 */
:where(a, button):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
/* ---------------------------------------------------------------- 반응형 */
/* 콘텐츠가 깨지는 지점에서만. 기기 크기를 가정하지 않는다 */
@media (max-width: 54rem) {
.hero { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

View file

@ -0,0 +1,100 @@
/* 3단계에서 확정한 토큰. 하드코딩된 값은 이 파일 밖에 존재하지 않는다. */
:root {
/* ---- 타입: 본문 스케일 (비율 1.250, 5단계) ---- */
--step--1: 0.8125rem;
--step-0: 1rem;
--step-1: 1.25rem;
--step-2: 1.5625rem;
--step-3: 1.9531rem;
/* ---- 타입: 디스플레이 (스케일 밖. 위계를 만드는 유일한 장치) ---- */
--display-2: clamp(2rem, 1.4rem + 3vw, 2.5rem);
--display-1: clamp(2.75rem, 1.8rem + 4.8vw, 4rem);
--leading-tight: 1.05;
--leading-snug: 1.3;
--leading-normal: 1.55;
--measure: 68ch;
--font-display: 'Newsreader', Georgia, 'Times New Roman', serif;
--font-body: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--tracking-display: -0.025em;
--tracking-body: 0;
/* ---- 간격: 4px 그리드 ---- */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2.5rem;
--space-6: 4rem;
--space-7: 6rem;
--space-8: 10rem;
--container: 70rem;
--gutter: clamp(1rem, 4vw, 5rem);
/* ---- 형태: 어휘 2종 ---- */
--radius-sm: 2px;
--radius-md: 4px;
/* ---- 모션 ---- */
--dur-instant: 100ms;
--dur-quick: 200ms;
--dur-normal: 350ms;
--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);
/* ---- 색: 다크가 저작 기준 (2단계에서 정당화) ---- */
color-scheme: dark light;
--surface: #0D0F12;
--surface-raised: #151A20;
--line: #262E37;
--ink: #E6E8E6;
--ink-muted: #9BA6B2;
--accent: #7FA6FF;
--accent-ink: #0D0F12;
--cta-bg: #E6E8E6;
--cta-ink: #0D0F12;
--ok: #4FA97A;
--warn: #C9A227;
--fail: #D46A55;
}
/* OS가 라이트를 선호하면 라이트. 토글이 명시적으로 다크를 잡으면 무시한다. */
@media (prefers-color-scheme: light) {
:root:not([data-theme='dark']) {
--surface: #F7F5F0;
--surface-raised: #FDFCF9;
--line: #DCD8CF;
--ink: #16181A;
--ink-muted: #5C646E;
--accent: #1B4FD8;
--accent-ink: #FAF9F6;
--cta-bg: #16181A;
--cta-ink: #FAF9F6;
--ok: #2F7D57;
--warn: #8A6D14;
--fail: #A8432E;
}
}
/* 토글이 라이트를 잡으면 OS와 무관하게 라이트 */
:root[data-theme='light'] {
--surface: #F7F5F0;
--surface-raised: #FDFCF9;
--line: #DCD8CF;
--ink: #16181A;
--ink-muted: #5C646E;
--accent: #1B4FD8;
--accent-ink: #FAF9F6;
--cta-bg: #16181A;
--cta-ink: #FAF9F6;
--ok: #2F7D57;
--warn: #8A6D14;
--fail: #A8432E;
}