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:
commit
8808c672dc
135 changed files with 38838 additions and 0 deletions
109
research/dogfood-saas/hero/src/components/Hero.jsx
Normal file
109
research/dogfood-saas/hero/src/components/Hero.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
216
research/dogfood-saas/hero/src/styles/hero.css
Normal file
216
research/dogfood-saas/hero/src/styles/hero.css
Normal 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; }
|
||||
}
|
||||
100
research/dogfood-saas/hero/src/styles/tokens.css
Normal file
100
research/dogfood-saas/hero/src/styles/tokens.css
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue