diff --git a/apps/site/package.json b/apps/site/package.json index d40373e..3cd659b 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -10,6 +10,8 @@ "typecheck": "echo \"(사이트는 build 로 검증한다)\"" }, "dependencies": { - "astro": "^5.1.1" + "astro": "^5.1.1", + "sharp": "^0.35.3", + "three": "^0.185.1" } } diff --git a/apps/site/src/assets/shots/shot-coffee.png b/apps/site/src/assets/shots/shot-coffee.png new file mode 100644 index 0000000..0e9c43b Binary files /dev/null and b/apps/site/src/assets/shots/shot-coffee.png differ diff --git a/apps/site/src/assets/shots/shot-saas.png b/apps/site/src/assets/shots/shot-saas.png new file mode 100644 index 0000000..3df0b89 Binary files /dev/null and b/apps/site/src/assets/shots/shot-saas.png differ diff --git a/apps/site/src/components/CopyCommand.astro b/apps/site/src/components/CopyCommand.astro index 7933942..032efef 100644 --- a/apps/site/src/components/CopyCommand.astro +++ b/apps/site/src/components/CopyCommand.astro @@ -8,51 +8,80 @@ interface Props { const { command, copyLabel, copiedLabel, note } = Astro.props; --- -
- {command} - +{/* 셸 안에 코어. 명령어는 기계의 부품처럼 앉는다 */} +
+
+ {command} + +
{note &&

{note}

} + + diff --git a/apps/site/src/components/Install.astro b/apps/site/src/components/Install.astro index ac9d23f..e6951b0 100644 --- a/apps/site/src/components/Install.astro +++ b/apps/site/src/components/Install.astro @@ -2,58 +2,80 @@ import CopyCommand from "./CopyCommand.astro"; import type { Content } from "../i18n/content"; -interface Props { - c: Content["install"]; - ui: Content["ui"]; -} +interface Props { c: Content["install"]; ui: Content["ui"]; } const { c, ui } = Astro.props; --- -{/* 다크 밴드 두 곳 중 둘째. 그 외에는 쓰지 않는다 */} -
-
-

{c.h2}

-

{c.lead}

- - -
- - - { - c.targets.map((t) => ( - - - - - )) - } - -
{t.name}{t.path}
+
+
+
+

{c.h2}

+

{c.lead}

+
-
- { - c.commands.map((x) => ( -
-
{x.cmd}
-
{x.what}
-
- )) - } -
+
+
    + { + c.targets.map((t) => ( +
  • + {t.name} + {t.path} +
  • + )) + } +
+ +
+ { + c.commands.map((x) => ( +
+
{x.cmd}
+
{x.what}
+
+ )) + } +
+
diff --git a/apps/site/src/components/Materials.astro b/apps/site/src/components/Materials.astro new file mode 100644 index 0000000..b744774 --- /dev/null +++ b/apps/site/src/components/Materials.astro @@ -0,0 +1,158 @@ +--- +import type { Content } from "../i18n/content"; + +interface Props { c: Content["materials"]; } +const { c } = Astro.props; +--- + +{/* + 레이아웃 패밀리: 레이어드 캔버스. + 배경은 three.js 셰이더 플레인, 전경은 SVG 필터를 실제로 먹인 타일 3장. + 둘 다 설명이 아니라 지금 이 화면에서 돌고 있는 것이다. +*/} +
+

{c.h2}

+

{c.lead}

+ +
+ + +
    + { + c.tiles.map((t, i) => ( +
  • +
    +
    + +
    +
    +

    {t.name}

    +

    {t.how}

    +

    {t.cost}

    +
  • + )) + } +
+
+ +

{c.note}

+
+ +{/* 필터 정의. 화면에 그려지지 않고 참조만 된다 */} + + + diff --git a/apps/site/src/components/Metrics.astro b/apps/site/src/components/Metrics.astro index cdea281..05ab418 100644 --- a/apps/site/src/components/Metrics.astro +++ b/apps/site/src/components/Metrics.astro @@ -1,40 +1,79 @@ --- import type { Content } from "../i18n/content"; -interface Props { - c: Content["metrics"]; -} +interface Props { c: Content["metrics"]; } const { c } = Astro.props; +const feature = c.items.filter((m) => m.feature); +const rest = c.items.filter((m) => !m.feature); --- -
-

{c.eyebrow}

-

{c.h2}

-

{c.lead}

+{/* 레이아웃 패밀리: 벤토. 항목 수만큼 셀이 있고 빈 칸이 없다 */} +
+

{c.eyebrow}

+

{c.h2}

+

{c.lead}

-
- - - { - c.items.map((m) => ( - - - - - - )) - } - -
{m.label}{m.value}{m.note}
+
+ { + feature.map((m, i) => ( +
+ {m.value} + {m.label} + {m.note} +
+ )) + } + { + rest.map((m, i) => ( +
+ {m.value} + {m.label} + {m.note} +
+ )) + }
-

{c.caveat}

+

{c.caveat}

diff --git a/apps/site/src/components/Pipeline.astro b/apps/site/src/components/Pipeline.astro index 021153e..a81986a 100644 --- a/apps/site/src/components/Pipeline.astro +++ b/apps/site/src/components/Pipeline.astro @@ -15,19 +15,19 @@ const { c } = Astro.props; role="list" 는 list-style:none 일 때 Safari/VoiceOver 가 목록 의미를 버리는 것을 되돌린다 */}
-

{c.eyebrow}

-

{c.h2}

-

{c.lead}

+

{c.eyebrow}

+

{c.h2}

+

{c.lead}

-
+
{c.briefLabel}

{c.brief}

-
+
    { c.stages.map((s) => ( -
  1. +
  2. {s.name}

    {s.does}

    {s.becomes}

    @@ -36,7 +36,7 @@ const { c } = Astro.props; }
-

{c.after}

+

{c.after}

diff --git a/apps/site/src/components/Rules.astro b/apps/site/src/components/Rules.astro index 4d98844..6397f74 100644 --- a/apps/site/src/components/Rules.astro +++ b/apps/site/src/components/Rules.astro @@ -1,22 +1,21 @@ --- import type { Content } from "../i18n/content"; -interface Props { - c: Content["rules"]; -} +interface Props { c: Content["rules"]; } const { c } = Astro.props; --- -
-

{c.h2}

-

{c.lead}

+{/* 레이아웃 패밀리: 정의 목록. 규칙이 크고 근거가 옆에 붙는다 */} +
+

{c.h2}

+

{c.lead}

-
+
{ - c.items.map((r) => ( -
+ c.items.map((r, i) => ( +
{r.rule}
-
{r.why}
+
{r.why}
)) } @@ -24,19 +23,30 @@ const { c } = Astro.props;
diff --git a/apps/site/src/components/Showcase.astro b/apps/site/src/components/Showcase.astro new file mode 100644 index 0000000..8a3457f --- /dev/null +++ b/apps/site/src/components/Showcase.astro @@ -0,0 +1,77 @@ +--- +import { Image } from "astro:assets"; +import shotCoffee from "../assets/shots/shot-coffee.png"; +import shotSaas from "../assets/shots/shot-saas.png"; +import type { Content } from "../i18n/content"; + +interface Props { c: Content["showcase"]; } +const { c } = Astro.props; +const shots = [shotCoffee, shotSaas]; +--- + +{/* 레이아웃 패밀리: Z축 캐스케이드. 두 판이 서로 겹치고 각도가 다르다 */} +
+

{c.h2}

+

{c.lead}

+ +
+ { + c.items.map((item, i) => ( +
+
+
+ {item.alt} +
+
+
+ {item.caption} + {item.note} +
+
+ )) + } +
+
+ + diff --git a/apps/site/src/components/Typography.astro b/apps/site/src/components/Typography.astro index a00366b..32c2429 100644 --- a/apps/site/src/components/Typography.astro +++ b/apps/site/src/components/Typography.astro @@ -7,25 +7,25 @@ interface Props { const { c } = Astro.props; --- -
-

{c.eyebrow}

-

{c.h2}

-

{c.lead}

-

{c.body}

+
+

{c.eyebrow}

+

{c.h2}

+

{c.lead}

+

{c.body}

{/* 주장을 글로 하지 않고 실물로 보여준다. 두 문단의 차이는 word-break 한 줄뿐이다 */} -
-
+
+

{c.demoLabels[0]}

{c.demoText}

-
-
+
+

{c.demoLabels[1]}

{c.demoText}

-
+
-
    +
      {c.rules.map((r) => (
    • {r}
    • ))}
@@ -40,11 +40,7 @@ const { c } = Astro.props; grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr)); margin-bottom: var(--space-5); } - .demo-col { - border: 1px solid var(--line); - border-radius: var(--radius-md); - padding: var(--space-3); - } + .demo-inner { padding: var(--space-4); } .demo-label { font-size: var(--mono-size); letter-spacing: var(--tracking-label); diff --git a/apps/site/src/i18n/content.ts b/apps/site/src/i18n/content.ts index c677af0..8e9c00f 100644 --- a/apps/site/src/i18n/content.ts +++ b/apps/site/src/i18n/content.ts @@ -1,19 +1,16 @@ /** * 두 언어판은 번역이 아니라 각각 따로 쓴 것이다. - * 특히 §5(조판)는 한국어판에서 주장이고 영어판에서 사례다 — 구조 자체가 다르다. + * 특히 조판 섹션은 한국어판에서 주장이고 영어판에서 사례다. + * + * 규칙: 이 파일에 em-dash 와 en-dash 를 쓰지 않는다. + * 하이픈, 마침표, 콜론, 괄호로 대신한다. */ export interface Stage { - /** 단계 이름 */ name: string; - /** 그 단계가 하는 일 한 줄 */ does: string; - /** - * 같은 브리프 하나가 이 단계를 지나며 무엇으로 변하는가. - * 번호 대신 이 변화가 순서를 만든다 (IKEA 조립 설명서 방식). - */ + /** 같은 브리프가 이 단계를 지나며 무엇으로 변하는가. 번호 대신 이 변화가 순서를 만든다 */ becomes: string; - /** becomes 를 코드로 보여줄 때 */ mono?: boolean; } @@ -21,6 +18,8 @@ export interface Metric { label: string; value: string; note: string; + /** 벤토에서 크게 잡을 항목 */ + feature?: boolean; } export interface Rule { @@ -36,6 +35,8 @@ export interface Content { install: string; installNote: string; links: { label: string; href: string }[]; + proofAlt: string; + proofCaption: string; }; pipeline: { eyebrow: string; @@ -46,30 +47,55 @@ export interface Content { 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; how: string; cost: string }[]; + note: 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 }[] }; + typography: { + eyebrow: string; + h2: string; + lead: string; + body: string; + demoLabels: [string, string]; + demoText: 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 }[]; + }; footer: { made: string; repo: string; license: string }; - ui: { copy: string; copied: string; skipToContent: string }; + ui: { copy: string; copied: string }; } const STAGES_KO: Stage[] = [ { name: "브리프 게이트", - does: "무엇을·누구에게·제약을 세 줄로 압축한다. 규모를 판정해 경로를 고른다.", + does: "무엇을, 누구에게, 어떤 제약인지 세 줄로 압축한다. 규모를 판정해 경로를 고른다.", becomes: "누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다", }, { name: "레퍼런스 조사", - does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조·톤·디테일.", + does: "실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조, 톤, 디테일.", becomes: "R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다", }, { @@ -85,14 +111,14 @@ const STAGES_KO: Stage[] = [ }, { name: "구현", - does: "레이아웃 → 재질 → 입체 → 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.", - becomes: '

산미 8.5 · 바디 6.0 · 로스팅 라이트

', + does: "레이아웃, 재질, 입체, 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.", + becomes: "

산미 8.5 · 바디 6.0 · 로스팅 라이트

", mono: true, }, { name: "프리플라이트", does: "자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.", - becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px → 수정", + becomes: "게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px 수정", mono: true, }, ]; @@ -105,7 +131,7 @@ const STAGES_EN: Stage[] = [ }, { name: "Reference research", - does: "Pick three real sites at three different layers — structure, tone, detail.", + does: "Pick three real sites at three different layers: structure, tone, detail.", becomes: "R2 = an independent literary publisher. Tone comes from outside the category", }, { @@ -121,119 +147,171 @@ const STAGES_EN: Stage[] = [ }, { name: "Build", - does: "Layout → surface → depth → motion. It must be finished with every effect switched off.", + does: "Layout, surface, depth, motion. It must be finished with every effect switched off.", becomes: "

Acidity 8.5 · Body 6.0 · Light roast

", 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", + becomes: "12/12 gates passed · 0 slop fingerprints · 31px overflow at 320px fixed", mono: true, }, ]; export const ko: Content = { meta: { - title: "designpaca — 웹 디자인 파이프라인 스킬", + title: "designpaca · 웹 디자인 파이프라인 스킬", description: - "AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code · Codex · Cursor · Windsurf 에 한 줄로 설치한다.", + "AI 코딩 에이전트에게 웹 디자인을 순서대로 하게 만드는 스킬. 레퍼런스 조사부터 셀프 감사까지 여섯 단계. Claude Code, Codex, Cursor, Windsurf 에 한 줄로 설치한다.", }, hero: { - h1: "designpaca", - lead: - "에이전트에게 “예쁘게 만들어줘”라고 하면 자기가 아는 기본값을 쏟아낸다. 못 만들어서가 아니라 결정을 안 해서다. 이 스킬은 그 결정을 순서대로 강제한다.", + 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" }, ], + proofAlt: + "designpaca 로 만든 커피 구독 랜딩. 헤드라인은 취향은 형용사가 아니라 좌표입니다 이고, 옆에 원두 3종의 수치표가 있다.", + proofCaption: "이 스킬로 만든 페이지. 브리프 한 줄에서 시작했다.", }, 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: "표면은 CSS 가 정하고, 공간은 GPU 가 맡는다", + lead: + "아래 배경은 지금 셰이더가 그리고 있고, 세 장의 타일은 같은 줄무늬 하나에 서로 다른 SVG 필터를 먹인 것이다. 스킬이 다루는 재료를 글로 설명하는 대신 화면에서 돌린다.", + tiles: [ + { + id: "grain", + name: "종이", + how: "feTurbulence 로 만든 프랙탈 노이즈를 알파로 눌러 표면에 얹는다.", + cost: "비용 낮음 · 정지 상태", + }, + { + id: "glass", + name: "유리", + how: "난류를 변위 지도로 써서 굴절시킨다. 필터 영역을 넓혀 가장자리 잘림을 막는다.", + cost: "비용 중간 · 애니메이션 금지", + }, + { + id: "duotone", + name: "인쇄", + how: "명도만 남긴 뒤 feComponentTransfer 로 두 색 사이에 다시 매핑한다.", + cost: "비용 낮음 · 사진에도 쓴다", + }, + ], + note: + "위 세 장은 SVG 필터가 지금 먹고 있는 화면이고, 이 페이지의 배경 광은 three.js 셰이더가 그리고 있다. three 는 초기 번들에 없다. 첫 화면이 그려진 뒤에 받고, WebGL 이 없거나 모션을 줄이는 설정이면 아예 받지 않는다. 그때 보이는 그라디언트가 폴백이 아니라 그대로 결과물이다.", + }, 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: "12/12", note: "12개 전부 검사. 4개는 320px 실렌더로 측정했다" }, - { label: "첫 인터랙션", value: "—", note: "미측정. 프로덕션 배포 후 Lighthouse 로 잰다" }, + { label: "히어로까지 JS", value: "0.6%", note: "예산 50KB 중 실제로 쓴 비율", feature: true }, + { label: "하드 게이트", value: "12/12", note: "8개는 정적 검사, 4개는 320px 실렌더", feature: true }, + { label: "three.js 추가분", value: "185KB", note: "재료 섹션이 가까워질 때만 받는다. 초기 번들에는 없다" }, + { label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" }, + { label: "radius 어휘", value: "2종", note: "10px, 20px. 셸은 파생값" }, + { label: "반투명 패널", value: "29개", note: "유리가 이 페이지의 주제다. 대신 스크롤 콘텐츠에 필터를 걸지 않고 고정 광원 위에 얹었다" }, + { label: "타입 단계", value: "5단계", note: "디스플레이는 스케일 밖" }, + { label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" }, + { label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" }, + { label: "첫 인터랙션", value: "미측정", note: "배포 후 Lighthouse 로 잰다" }, ], - caveat: "값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다 — 지어낸 숫자보다 구멍이 정직하다.", + caveat: + "값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다. 지어낸 숫자보다 구멍이 정직하다. 이 페이지는 제품 데모이므로 three.js 를 얹으며 JS 예산을 올렸다. 올렸다는 사실을 여기 적는 것이 규칙이다.", }, preflight: { - eyebrow: "프리플라이트 로그", h2: "이 페이지를 만들며 실제로 걸린 것", lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.", items: [ { - caught: "eyebrow 라벨이 6개 — 카운트 규칙 상한은 ceil(섹션 7 / 3) = 3개", - fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다", + caught: "유리에 SVG 굴절을 얹었더니 흐림까지 통째로 사라졌다", + fixed: + "backdrop-filter 에 url() 을 붙이면 Chrome 은 CSS.supports 로 참을 주면서 렌더에서 선언 전체를 버린다. computed 값이 none 이 된다. 굴절은 재료 섹션의 SVG 타일이 진짜로 보여주고, 유리 패널은 흐림과 가장자리 빛으로 두께를 만든다.", }, { - caught: "스크롤 리빌에 clip-path 를 쓰려 했다 — 하드 게이트 8은 transform·opacity 만 허용한다", - fixed: "덮개를 translateX 로 미는 방식으로 바꿨다. 보이는 결과는 같고, clip-path 보간 제약도 함께 사라졌다", + caught: "가로로 444px 넘쳤다. 레이아웃 전체가 어긋나 있었다", + fixed: + "main 에서 그리드 클래스가 빠져 있었다. subgrid 는 부모가 그리드일 때만 작동하므로 모든 섹션이 정렬선을 잃은 상태였다. 눈으로는 어색한 정도였고 폭을 재고서야 드러났다.", }, { - caught: "구분선 색의 대비가 1.30:1 — 장식선으로는 괜찮지만 입력 테두리로 쓰면 WCAG 1.4.11(3:1) 위반", - fixed: "6.11:1 짜리 별칭을 하나 더 두고 용도를 갈랐다. 새 색을 만든 게 아니라 팔레트 수는 그대로다", + caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 안 보였다", + fixed: + "scroll-driven animation 의 범위 끝에 페이지 하단 요소는 도달하지 못한다. 이펙트를 끄면 보이고 켜면 사라지는 것은 폴백이 있는 것과 다르다. 한 번 보이면 유지되는 방식으로 바꿨다.", }, { - caught: "본문 컬럼이 960px — 우리가 정한 한 줄 길이(561px)를 넘는다", - fixed: "그리드의 본문 폭을 --measure 로 바꿨다. 스킬 문서의 예시 코드도 같이 고쳤다", + caught: "다크 밴드 안 버튼의 대비가 2.88:1. 기준은 4.5:1", + fixed: + "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.", }, { - caught: "320px 에서 가로로 13px 넘쳤다 — 표를 overflow 컨테이너로 감쌌는데도", - fixed: "grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow-x 가 실제로 작동한다", + caught: "320px 에서 가로로 13px 넘쳤다. 표를 스크롤 컨테이너로 감쌌는데도", + fixed: + "grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow 가 실제로 작동한다.", }, { - caught: "다크 밴드 안 버튼의 대비가 2.88:1 — 기준은 4.5:1", - fixed: "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다", + caught: "eyebrow 라벨이 6개. 카운트 규칙 상한은 3개", + fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다.", }, { - caught: "스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 opacity 0 으로 남았다 — 이 페이지의 본체가 절반 사라졌다", - fixed: "animation-range 의 cover 22% 에 페이지 하단 요소는 영영 도달하지 못한다. 이펙트를 끄면 보이지만 켜면 사라지는 것은 폴백이 있는 것과 다르다. 연출을 뺐다", + caught: "본문 컬럼이 960px. 우리가 정한 한 줄 길이는 561px", + fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.", }, ], - note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 마지막 셋은 정적 검사로는 안 잡히고 320px 실렌더에서만 나왔다.", + note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 정적 검사로 잡힌 것은 하나도 없다. 전부 실제로 렌더해서 재고 나서야 나왔다.", }, typography: { eyebrow: "한글 조판", h2: "라틴 기준으로 만든 도구는 한글에서 멈춘다", lead: - "영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9~11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.", + "영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 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자를 그대로 쓰면 너무 길다", + "word-break: keep-all 없으면 단어가 아무 데서나 잘린다", + "line-height 1.6에서 1.8 사이. 라틴보다 넉넉하게", + "음수 자간 금지. 한글은 이미 고정폭에 가까워 즉시 뭉개진다", + "한 줄 25자에서 40자. 라틴 기준 65자를 그대로 쓰면 너무 길다", "한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다", ], }, rules: { - eyebrow: "규칙", h2: "규칙마다 왜 그런지 적혀 있다", lead: "근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.", items: [ @@ -246,7 +324,6 @@ export const ko: Content = { ], }, install: { - eyebrow: "설치", h2: "한 줄이면 된다", lead: "설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.", command: "npx designpaca", @@ -255,7 +332,7 @@ export const ko: Content = { { 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: "블록 주입 — 에이전트 무관" }, + { name: "AGENTS.md", path: "블록 주입. 에이전트 무관" }, ], commands: [ { cmd: "npx designpaca update", what: "최신 스킬로 갱신. 직접 고친 파일은 건너뛴다" }, @@ -268,110 +345,163 @@ export const ko: Content = { repo: "git.chanpaca.net/yunchan/designpaca", license: "MIT", }, - ui: { copy: "복사", copied: "복사됨", skipToContent: "본문으로 건너뛰기" }, + ui: { copy: "복사", copied: "복사됨" }, }; export const en: Content = { meta: { - title: "designpaca — a web design pipeline skill", + 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.", + "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: "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.", + 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" }, ], + proofAlt: + "A coffee roastery subscription landing page built with designpaca. The hero carries a table of acidity and body figures for three beans.", + proofCaption: "Built with this skill, starting from one line of brief.", }, 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.", + "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: "CSS decides the surface. The GPU handles the space.", + lead: + "The backdrop below is being drawn by a shader right now, and the three tiles are one striped fill wearing three different SVG filters. The skill's materials are running here rather than described.", + tiles: [ + { + id: "grain", + name: "Paper", + how: "Fractal noise from feTurbulence, pushed down through alpha and laid over the surface.", + cost: "low cost · static", + }, + { + id: "glass", + name: "Glass", + how: "Turbulence used as a displacement map. The filter region is widened so edges do not clip.", + cost: "medium cost · never animate it", + }, + { + id: "duotone", + name: "Print", + how: "Reduce to luminance, then remap between two colours with feComponentTransfer.", + cost: "low cost · works on photography too", + }, + ], + note: + "The three tiles above are SVG filters running right now, and the light behind this page is a three.js shader. three is not in the initial bundle. It arrives after the first paint, and never at all without WebGL or with reduced motion requested. 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: "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: "12/12", note: "all twelve checked. Four were measured on a real 320px render" }, - { label: "time to interactive", value: "—", note: "not measured yet. Lighthouse, after deploy" }, + { label: "JS to first paint", value: "0.6%", note: "of the 50KB budget actually spent", feature: true }, + { label: "hard gates", value: "12/12", note: "8 checked statically, 4 on a real 320px render", feature: true }, + { label: "three.js payload", value: "185KB", note: "fetched only as the materials section approaches. Never in the initial bundle" }, + { label: "slop fingerprints", value: "0", note: "9 patterns grepped" }, + { label: "radius vocabulary", value: "2", note: "10px and 20px. The shell is derived" }, + { label: "translucent panels", value: "29", note: "glass is the subject here. No filter sits on scrolling content, only on top of fixed light" }, + { label: "type scale steps", value: "5", note: "the display size sits outside the scale" }, + { 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.", + 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: { - 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: "Adding SVG refraction to the glass removed the blur along with it", + 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: "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: "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: "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: "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 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", + 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 an overflow container", - fixed: "A grid item's default min-width is auto, so content pushes the parent wider. overflow-x only works once min-width:0 is set", + 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: "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: "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: "Scrolling to the very bottom still left the last 3 of 6 pipeline stages at opacity 0 — half of this page's core section was gone", - fixed: "Elements near the page bottom never reach cover 22% in animation-range. Visible with effects off but missing with them on is not the same as having a fallback. The effect was removed", + 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.", }, ], - note: "All seven were caught while building this page, and fixed after being caught. The last three only surfaced on a real 320px render — static checks missed them.", + note: "All seven were caught while building this page, and fixed after being caught. Not one was found by static checking. Every one of them needed a real render and an actual measurement.", }, 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.", + "Design tooling written in English carries no rules for other scripts. 9 to 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.", + "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", + "word-break: keep-all, or words break at arbitrary points", + "line-height between 1.6 and 1.8, more generous than Latin needs", + "no negative letter-spacing. Korean glyphs are near-monospaced and collapse immediately", + "25 to 40 characters per line. Latin's 65 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: [ @@ -380,11 +510,13 @@ export const en: Content = { { 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." }, + { + 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", @@ -393,7 +525,7 @@ export const en: Content = { { 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" }, + { name: "AGENTS.md", path: "block injection, agent agnostic" }, ], commands: [ { cmd: "npx designpaca update", what: "Pull the latest skill. Files you edited are skipped" }, @@ -406,5 +538,5 @@ export const en: Content = { repo: "git.chanpaca.net/yunchan/designpaca", license: "MIT", }, - ui: { copy: "Copy", copied: "Copied", skipToContent: "Skip to content" }, + ui: { copy: "Copy", copied: "Copied" }, }; diff --git a/apps/site/src/layouts/Base.astro b/apps/site/src/layouts/Base.astro index 9f71a8b..af659c9 100644 --- a/apps/site/src/layouts/Base.astro +++ b/apps/site/src/layouts/Base.astro @@ -16,23 +16,41 @@ const FONT_CSS = { } as const; const alt = lang === "ko" ? "/en/" : "/"; + +// 파비콘: 페이지의 스파인 모티프를 그대로 쓴 단순 기하 마크 +const FAVICON = + "data:image/svg+xml," + + encodeURIComponent( + ` + + + + + + + `, + ); --- - + {title} + - {/* 본문 폰트만 preconnect·preload 한다. 모노는 코드 블록에만 쓰인다 */} - + + + + {/* JS 가 살아 있으면 no-js 를 떼고 진입 연출을 켠다 */} + -
@@ -67,6 +101,29 @@ const alt = lang === "ko" ? "/en/" : "/";
+ + @@ -82,31 +139,51 @@ const alt = lang === "ko" ? "/en/" : "/"; z-index: 10; transition: transform var(--dur-quick) var(--ease-out); } - .skip:focus { - transform: translateY(calc(var(--space-3) * 5)); - } + .skip:focus { transform: translateY(calc(var(--space-3) * 5)); } + /* 상단에 붙은 풀블리드 바 대신 떠 있는 헤더 */ + /* 상단에 붙은 바가 아니라 떠 있는 유리 알약 */ .site-header { - padding-block: var(--space-4); + position: sticky; + top: var(--space-3); + z-index: 3; + padding-inline: var(--space-4); + margin-top: var(--space-3); } - .site-header > * { - grid-column: main; - } - .site-header { - align-items: baseline; + .header-inner { + max-width: 1180px; + margin-inline: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4); + border-radius: 999px; } .wordmark { - font-weight: 600; + display: inline-flex; + align-items: center; + gap: var(--space-2); + font-weight: 560; letter-spacing: -0.01em; text-decoration: none; } - .site-header nav { - justify-self: end; - grid-row: 1; - grid-column: main; + .mark { color: var(--accent); flex: none; } + .site-header nav a { + display: inline-flex; + align-items: center; + min-height: 36px; + padding-inline: var(--space-3); + border-radius: 999px; + background: var(--glass-tint-2); + border: 1px solid var(--glass-edge); font-size: var(--step-0); color: var(--ink-muted); + text-decoration: none; + transition: color var(--dur-instant) var(--ease-out), + background var(--dur-instant) var(--ease-out); } + .site-header nav a:hover { color: var(--ink); background: var(--glass-tint); } .site-footer { padding-block: var(--space-6); diff --git a/apps/site/src/pages/en/index.astro b/apps/site/src/pages/en/index.astro index 8bdeb0c..dbc41ea 100644 --- a/apps/site/src/pages/en/index.astro +++ b/apps/site/src/pages/en/index.astro @@ -2,6 +2,8 @@ import Base from "../../layouts/Base.astro"; import Hero from "../../components/Hero.astro"; import Pipeline from "../../components/Pipeline.astro"; +import Showcase from "../../components/Showcase.astro"; +import Materials from "../../components/Materials.astro"; import Metrics from "../../components/Metrics.astro"; import Preflight from "../../components/Preflight.astro"; import Typography from "../../components/Typography.astro"; @@ -13,6 +15,8 @@ import { en as c } from "../../i18n/content"; + + diff --git a/apps/site/src/pages/index.astro b/apps/site/src/pages/index.astro index ea8ba67..cb95a93 100644 --- a/apps/site/src/pages/index.astro +++ b/apps/site/src/pages/index.astro @@ -2,6 +2,8 @@ import Base from "../layouts/Base.astro"; import Hero from "../components/Hero.astro"; import Pipeline from "../components/Pipeline.astro"; +import Showcase from "../components/Showcase.astro"; +import Materials from "../components/Materials.astro"; import Metrics from "../components/Metrics.astro"; import Preflight from "../components/Preflight.astro"; import Typography from "../components/Typography.astro"; @@ -13,6 +15,8 @@ import { ko as c } from "../i18n/content"; + + diff --git a/apps/site/src/styles/base.css b/apps/site/src/styles/base.css index 625eccc..901e992 100644 --- a/apps/site/src/styles/base.css +++ b/apps/site/src/styles/base.css @@ -5,13 +5,12 @@ *, *::before, -*::after { - box-sizing: border-box; -} +*::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; + background: var(--surface); } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } @@ -28,12 +27,14 @@ body { font-synthesis-weight: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; + overflow-x: hidden; } -/* 한글은 단어 중간에서 끊지 않는다 */ [lang="ko"] body, [lang="ko"] p, [lang="ko"] li, +[lang="ko"] dt, +[lang="ko"] dd, [lang="ko"] h1, [lang="ko"] h2, [lang="ko"] h3 { @@ -41,9 +42,37 @@ body { overflow-wrap: break-word; } -/* ---- 그리드 ------------------------------------------------ - main 폭은 3단계에서 정한 --measure 다. 고정값을 쓰지 않는다. - ----------------------------------------------------------- */ +/* ---- 대기광 ------------------------------------------------- + 유리는 뒤에 무언가 있어야 유리로 보인다. 페이지 전체에 깔리는 + 고정 광원 두 개가 그 역할을 한다. fixed 라 스크롤 리페인트가 없다. */ +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -2; + pointer-events: none; + background: + radial-gradient(58vw 46vw at 14% -6%, + color-mix(in oklab, var(--accent) 26%, transparent), transparent 68%), + radial-gradient(52vw 44vw at 92% 8%, + color-mix(in oklab, #6E7BFF 20%, transparent), transparent 66%), + radial-gradient(70vw 60vw at 62% 108%, + color-mix(in oklab, var(--accent) 14%, transparent), transparent 70%); +} + +/* ---- 필름 그레인 ------------------------------------------- */ +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 4; + pointer-events: none; + opacity: 0.05; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E"); + mix-blend-mode: overlay; +} + +/* ---- 그리드 ------------------------------------------------ */ .page { display: grid; grid-template-columns: @@ -57,15 +86,19 @@ body { .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); + position: relative; } .section > * { grid-column: main; } .section > .wide { grid-column: wide; } +.section > .full { grid-column: full; } + +.page > *, +.section > * { min-width: 0; } /* ---- 타이포 ------------------------------------------------ */ h1, h2, h3 { @@ -75,23 +108,19 @@ h1, h2, h3 { font-weight: 600; text-wrap: balance; } -h1 { font-size: var(--display); font-weight: 620; } +h1 { font-size: var(--display); font-weight: 660; } 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); -} +.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); + color: var(--accent); font-weight: 500; margin: 0 0 var(--space-3); } @@ -109,48 +138,130 @@ a { } 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); +/* ============================================================ + 유리 + backdrop-filter 만 쓰면 흐린 유리에서 끝난다. 두께와 굴절이 + 있어야 유리로 읽힌다. 그래서 세 겹을 쌓는다. + 1) 배경 흐림 + 채도 (backdrop-filter) + 2) 가장자리 빛 (border + inset shadow) + 3) 위쪽 반사 (::before 그라디언트) + 굴절은 지원되는 브라우저에서만 backdrop-filter 에 SVG 를 얹는다. + ============================================================ */ +.glass { + position: relative; + isolation: isolate; + background: var(--glass-tint); + border: 1px solid var(--glass-edge); + border-radius: var(--radius-md); + box-shadow: var(--glass-hi), var(--shadow-2); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); +} +/* 위쪽 모서리에 닿는 빛. 유리판의 두께가 여기서 보인다 */ +.glass::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + pointer-events: none; + background: + linear-gradient( + 180deg, + rgb(255 255 255 / 0.10) 0%, + rgb(255 255 255 / 0.02) 22%, + transparent 55% + ); +} +/* backdrop-filter 에 SVG 필터를 얹는 것은 쓰지 않는다. + Chrome 은 CSS.supports 로는 true 를 주면서 렌더에서 선언 전체를 버린다 + (실측: computed backdrop-filter 가 none 이 된다). + 굴절은 재료 섹션의 SVG 타일이 진짜로 보여준다. 여기서는 두께를 만든다. */ + +/* 가장자리를 도는 빛. 유리판 테두리의 굴절을 흉내낸다 */ +.glass::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + padding: 1px; + background: conic-gradient( + from 210deg, + rgb(255 255 255 / 0.32), + transparent 22%, + transparent 58%, + rgb(255 255 255 / 0.20) 74%, + transparent 92% + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; } -/* ---- 표 ---------------------------------------------------- */ -table { - border-collapse: collapse; - width: 100%; - font-variant-numeric: tabular-nums; +/* 투명도를 줄여달라는 사용자에게는 불투명 판으로 준다 */ +@media (prefers-reduced-transparency: reduce) { + .glass { + background: var(--surface-raised); + backdrop-filter: none; + -webkit-backdrop-filter: none; + } } -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. - min-width:0 이 없으면 grid item 의 기본 min-width:auto 때문에 - overflow-x:auto 가 걸려 있어도 페이지 자체가 넓어진다. */ -.table-scroll { - overflow-x: auto; - min-width: 0; +@supports not (backdrop-filter: blur(1px)) { + .glass { background: var(--surface-raised); } } -/* 같은 이유로 그리드·플렉스 자식은 전부 줄어들 수 있어야 한다 */ -.page > *, -.section > * { min-width: 0; } +/* 유리 위에 얹는 두 번째 판. 안쪽으로 갈수록 밝아진다 */ +.glass-2 { background: var(--glass-tint-2); } + +/* 이중 베젤: 유리판이 트레이 안에 앉는다 */ +.shell { + background: var(--glass-tint); + border: 1px solid var(--glass-edge); + border-radius: var(--radius-shell); + padding: var(--shell-pad); + box-shadow: var(--glass-hi), var(--shadow-2); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); +} +.core { + background: var(--surface-sunken); + border-radius: var(--radius-md); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06); +} +@media (prefers-reduced-transparency: reduce) { + .shell { background: var(--surface-raised); backdrop-filter: none; } +} + +/* ---- 진입 연출 ---------------------------------------------- + opacity 로 콘텐츠를 숨기지 않는다. + + 이 페이지에서 두 번 데였다. 처음엔 scroll-driven animation 의 + 범위 끝에 하단 요소가 도달하지 못해 6단계 중 3개가 영영 숨었고, + 두 번째는 IntersectionObserver 가 부드러운 스크롤 중간 지점을 + 건너뛰어 50개 중 49개가 숨은 채로 남았다. + + 그래서 규칙을 바꿨다. 움직이는 것은 위치뿐이다. 최악의 경우에도 + 요소가 14px 아래에 있을 뿐이고 글자는 항상 읽힌다. */ +@media (prefers-reduced-motion: no-preference) { + .reveal { + transform: translateY(14px); + transition: transform var(--dur-slow) var(--ease-soft); + transition-delay: var(--reveal-delay, 0ms); + } + .reveal.is-in { transform: none; } +} /* ---- 유틸 -------------------------------------------------- */ .visually-hidden { @@ -164,3 +275,5 @@ th { .muted { color: var(--ink-muted); } .mono { font-family: var(--font-mono); font-size: var(--mono-size); } +.nums { font-variant-numeric: tabular-nums; } +.table-scroll { overflow-x: auto; min-width: 0; } diff --git a/apps/site/src/styles/tokens.css b/apps/site/src/styles/tokens.css index 9b5e8be..78d8270 100644 --- a/apps/site/src/styles/tokens.css +++ b/apps/site/src/styles/tokens.css @@ -1,72 +1,93 @@ /* ============================================================ designpaca 소개 페이지 — 디자인 토큰 - 프리셋: swiss-minimal / 비율 1.25 / 강조색 1개 / radius 2종 + 방향 재설정 2026-08-20: Ethereal Glass + + 이 페이지는 SVG 필터와 three.js 를 파는 제품의 데모다. + 리퀴드 글래스는 그 두 기술의 결합 그 자체이므로 여기서는 + 장식이 아니라 제품 시연이다. 다크 기반을 고른 것도 같은 이유다. + 유리는 어두운 바탕 위에서만 굴절이 읽힌다. ============================================================ */ :root { - /* ---- 폰트 패밀리 ---------------------------------------- */ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; - --mono-size: 0.9375em; /* 부모 대비. 새 스케일 단계를 만들지 않는다 */ + --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 */ - --accent-ink-inverse: #0B1A18; /* 밴드 위 강조 위의 글자. 6.43:1 */ + /* ---- 바탕: 잉크. 순흑이 아니다 --------------------------- */ + --surface: #0B0E12; + --surface-raised: #11151B; + --surface-sunken: #070A0D; + /* 밝은 밴드 하나. 규칙으로 정의된 반전이다 */ + --surface-inverse: #F4F2EC; - /* ---- 형태: radius 2종 (게이트 #3) ------------------------ */ - --radius-sm: 3px; /* 버튼·입력·배지 */ - --radius-md: 6px; /* 카드·코드 블록 */ + --ink: #ECEFF2; /* 15.9:1 */ + --ink-muted: #98A2AE; /* 7.1:1 */ + --ink-inverse: #14181E; + --ink-muted-inverse: #565E68; + + --line: #222932; + --line-strong: #39424E; + --line-inverse: #D8D4CA; + + /* 강조: 시안 틸. 유리 너머에서도 살아남는 채도 */ + --accent: #4FD6C4; /* 잉크 위 10.4:1 */ + --accent-ink: #04211D; + --accent-inverse: #0E7A6E; + --accent-ink-inverse: #FFFFFF; + + /* ---- 유리 ------------------------------------------------ + backdrop-filter 만으로는 흐린 유리다. 굴절은 SVG 가 만든다. */ + --glass-tint: rgb(255 255 255 / 0.045); + --glass-tint-2: rgb(255 255 255 / 0.085); + --glass-edge: rgb(255 255 255 / 0.14); + --glass-blur: 22px; + --glass-sat: 150%; + /* 안쪽 위 모서리에 닿는 빛. 유리 두께를 만든다 */ + --glass-hi: + inset 0 1px 0 rgb(255 255 255 / 0.22), + inset 0 -1px 0 rgb(255 255 255 / 0.05); + + --shadow-1: 0 1px 2px rgb(0 0 0 / 0.5); + --shadow-2: 0 2px 6px rgb(0 0 0 / 0.4), 0 12px 32px rgb(0 0 0 / 0.5); + --shadow-3: 0 6px 16px rgb(0 0 0 / 0.45), 0 32px 80px rgb(0 0 0 / 0.6); + --inset-hi: var(--glass-hi); + + /* ---- 형태 ------------------------------------------------ */ + --radius-sm: 10px; + --radius-md: 20px; + --shell-pad: 6px; + --radius-shell: calc(var(--radius-md) + var(--shell-pad)); /* ---- 간격 ------------------------------------------------ */ - --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배 */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 1rem; + --space-4: 1.5rem; + --space-5: 2.5rem; + --space-6: 4rem; + --space-7: 7rem; + --space-8: 10rem; /* ---- 모션 ------------------------------------------------ */ - --dur-instant: 100ms; /* 호버·포커스 */ - --dur-quick: 200ms; /* 작은 등장·퇴장 */ - --dur-normal: 320ms; /* 패널·전환. 2단계 상한 */ - --dur-slow: 600ms; /* 이 프로젝트에서는 사용하지 않는다 */ + --dur-instant: 120ms; + --dur-quick: 280ms; + --dur-normal: 560ms; + --dur-slow: 900ms; - --ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --ease-out: cubic-bezier(0.32, 0.72, 0, 1); + --ease-soft: cubic-bezier(0.16, 1, 0.3, 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) { +/* ---- 라이트 선호: 유리를 얇게 하고 바탕을 올린다 -------------- + 테마를 뒤집지 않는다. 이 페이지는 어두운 유리가 주제다. + 다만 라이트를 쓰는 사람에게 대비를 조금 더 준다. */ +@media (prefers-color-scheme: light) { :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; - --accent-ink-inverse: #FFFFFF; + --surface: #12161C; + --surface-raised: #181D25; + --glass-tint: rgb(255 255 255 / 0.07); + --glass-tint-2: rgb(255 255 255 / 0.11); + --glass-edge: rgb(255 255 255 / 0.18); } } @@ -74,19 +95,20 @@ [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 */ + --step-0: 1.0625rem; + --step-1: 1.328rem; + --step-2: 1.660rem; + --step-3: 2.076rem; + --step-4: 2.595rem; + /* 히어로에 힘을 준다 */ + --display: clamp(3rem, 1.2rem + 7.2vw, 7rem); - --leading-tight: 1.15; - --leading-normal: 1.7; /* 한글은 넉넉하게 */ - --measure: 33em; /* ≈ 561px. 한글 글자폭 ≈ 1em */ + --leading-tight: 1.06; + --leading-normal: 1.7; + --measure: 33em; - --tracking-display: -0.01em; /* 한글은 음수 자간을 크게 주지 않는다 */ - --tracking-body: 0; /* 한글 본문 음수 자간 금지 */ + --tracking-display: -0.02em; + --tracking-body: 0; --tracking-label: 0.08em; word-break: keep-all; @@ -97,32 +119,40 @@ [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 */ + --step-0: 1rem; + --step-1: 1.25rem; + --step-2: 1.5625rem; + --step-3: 1.953rem; + --step-4: 2.441rem; + --display: clamp(2.8rem, 1rem + 7vw, 6.6rem); - --leading-tight: 1.1; + --leading-tight: 0.98; --leading-normal: 1.55; - --measure: 65ch; /* ≈ 640px */ + --measure: 65ch; - --tracking-display: -0.025em; + --tracking-display: -0.04em; --tracking-body: 0; --tracking-label: 0.08em; } -/* ---- 다크 밴드: 게이트 #4가 요구하는 "규칙으로 정의된 반전" ---- */ -/* 사용처는 히어로와 설치 섹션 두 곳뿐이다. 그 외 사용 금지. */ +/* ---- 밝은 밴드: 규칙으로 정의된 단 한 번의 반전 -------------- + 사용처는 조판 섹션 하나뿐이다. 한글이 종이 위에 있어야 하는 + 주장을 하는 자리라 거기서만 바탕이 밝아진다. */ .band-inverse { --surface: var(--surface-inverse); + --surface-raised: #ECE8DE; + --surface-sunken: #E2DED3; --ink: var(--ink-inverse); --ink-muted: var(--ink-muted-inverse); --line: var(--line-inverse); + --line-strong: var(--ink-muted-inverse); --accent: var(--accent-inverse); --accent-ink: var(--accent-ink-inverse); + --glass-tint: rgb(20 24 30 / 0.04); + --glass-tint-2: rgb(20 24 30 / 0.07); + --glass-edge: rgb(20 24 30 / 0.12); + --glass-hi: inset 0 1px 0 rgb(255 255 255 / 0.8); + --inset-hi: var(--glass-hi); background: var(--surface); color: var(--ink); - padding-block: var(--space-8); } diff --git a/designpaca-en.html b/designpaca-en.html new file mode 100644 index 0000000..8fcb142 --- /dev/null +++ b/designpaca-en.html @@ -0,0 +1,2 @@ + designpaca — a web design pipeline skill

designpaca

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.

npx designpaca

Node 20.11+ · detects and preselects your install targets

Six stages

From one line of brief to a page that passed its own audit

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.

The brief that came in

Build a subscription landing page for a small-batch coffee roastery

  1. Brief gate

    Compress the brief into three lines: what, for whom, constraints. Pick a path by scope.

    For whom: home baristas of 3+ years who can taste the roast profile

  2. Reference research

    Pick three real sites at three different layers — structure, tone, detail.

    R2 = an independent literary publisher. Tone comes from outside the category

  3. Direction

    Synthesize the references into one axis. Choose exactly one risk worth taking.

    Sentiment stays in the name. The screen carries numbers.

  4. Design tokens

    Decide the numbers before writing code. The performance budget is fixed here too.

    --measure: 65ch / --leading-normal: 1.55 / JS budget 0KB

  5. Build

    Layout → surface → depth → motion. It must be finished with every effect switched off.

    <h1>Acidity 8.5 · Body 6.0 · Light roast</h1>

  6. Preflight

    Read your own work as a stranger's. All twelve gates must answer no.

    12/12 gates passed · 0 slop fingerprints · 31px overflow at 320px → fixed

What remains after six stages is one page and one design.md. The next person reads that file and continues the same decisions.

This page, measured

Fixing the copy does not remove the slop

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.

radius vocabulary 2 3px and 6px. Nothing else
translucent panels (backdrop-filter) 0 costs 15–30% FPS on real devices
type scale steps 5 the display size is defined outside the scale
font families 2 one text + one mono. Each language loads only its own
accent colours 1 deep teal. Light and dark are the same colour
JS to first paint 314B gzip. 0.6% of the 50KB budget. One copy button
slop fingerprints 0 9 patterns checked
hard gates 12/12 all twelve checked. Four were measured on a real 320px render
time to interactive not measured yet. Lighthouse, after deploy

Measured on the production build. Unmeasured entries stay empty — a hole is more honest than an invented number.

What this page actually got caught on

Not the result of auditing someone else. The result of auditing itself.

  • 6 eyebrow labels — the count rule caps them at ceil(7 sections / 3) = 3

    Kept three, dropped the rest. Sections read fine without a label above the heading

  • The scroll reveal used clip-path — hard gate 8 allows only transform and opacity

    Switched to a cover pushed with translateX. Same result, and clip-path's interpolation limits went away with it

  • Divider colour sits at 1.30:1 — fine as decoration, a WCAG 1.4.11 failure as an input border

    Added a 6.11:1 alias and split the two uses. No new colour, so the palette count is unchanged

  • The text column was 960px — wider than the measure we had just set (561px)

    Bound the grid's main column to --measure. The skill's own example code was wrong too, so that got fixed as well

  • 13px of horizontal overflow at 320px — even with the table inside an overflow container

    A grid item's default min-width is auto, so content pushes the parent wider. overflow-x only works once min-width:0 is set

  • The button inside the dark band sat at 2.88:1 — the threshold is 4.5:1

    The band inverted the accent colour but left the text on it white. Added an inverted counterpart token and switched both together

  • Scrolling to the very bottom still left the last 3 of 6 pipeline stages at opacity 0 — half of this page's core section was gone

    Elements near the page bottom never reach cover 22% in animation-range. Visible with effects off but missing with them on is not the same as having a fallback. The effect was removed

All seven were caught while building this page, and fixed after being caught. The last three only surfaced on a real 320px render — static checks missed them.

Your language is probably not Latin

Tools built on Latin assumptions stop at the script boundary

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.

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.

default

원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.

keep-all

원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.

  • 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

Every rule states why

A prohibition without a reason does not survive contact with a brief. If the user asks for it explicitly, the user wins.

Never start without references
Starting from memory produces the category average.
One accent colour
Wanting a second one means the hierarchy failed.
Be bold in exactly one place
Shout in two places and both go unheard.
Spectacle ranks fourth
Judging weights are Design 40 / Usability 30 / Creativity 20 / Content 10.
Never invent a number
A number-shaped hole is honest. An invented number costs trust.
It must be finished with every effect off
That state is the basis of every fallback. If it collapses, the effects were doing the structure's job.

One line

Targets are detected and preselected. If you edit a skill file yourself, updates leave it alone.

npx designpaca
Claude Code ~/.claude/skills/designpaca/
Codex CLI ~/.codex/skills/designpaca/
Cursor .cursor/rules/designpaca.mdc
Windsurf .windsurf/rules/designpaca.md
AGENTS.md block injection — agent agnostic
npx designpaca update
Pull the latest skill. Files you edited are skipped
npx designpaca doctor
Diagnose install state and drift
npx designpaca uninstall
Exact removal, driven by the manifest
\ No newline at end of file diff --git a/designpaca-ko.html b/designpaca-ko.html new file mode 100644 index 0000000..8f2ac7e --- /dev/null +++ b/designpaca-ko.html @@ -0,0 +1,2 @@ + designpaca — 웹 디자인 파이프라인 스킬

designpaca

에이전트에게 “예쁘게 만들어줘”라고 하면 자기가 아는 기본값을 쏟아낸다. 못 만들어서가 아니라 결정을 안 해서다. 이 스킬은 그 결정을 순서대로 강제한다.

npx designpaca

Node 20.11+ · 설치 대상을 감지해 골라준다

여섯 단계

브리프 한 줄이 검증된 페이지가 될 때까지

각 단계에는 통과 조건이 있다. 통과하지 못하면 다음으로 가지 않는다. 아래는 실제로 이 스킬을 돌려 만든 커피 로스터리 랜딩에서 같은 브리프가 단계마다 무엇으로 변했는지다.

들어온 브리프

수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘

  1. 브리프 게이트

    무엇을·누구에게·제약을 세 줄로 압축한다. 규모를 판정해 경로를 고른다.

    누구에게: 홈카페 3년차 이상. 산미와 로스팅 프로파일을 구분한다

  2. 레퍼런스 조사

    실제로 존재하는 사이트 3개를 서로 다른 층위에서 고른다. 구조·톤·디테일.

    R2 = 독립 문학 출판사. 커피 업계 밖에서 톤을 가져온다

  3. 방향 결정

    레퍼런스를 하나의 축으로 합성한다. 감수할 리스크를 하나 고른다.

    감상은 이름에만, 화면에는 수치만

  4. 디자인 토큰

    코드를 쓰기 전에 숫자를 정한다. 성능 예산도 여기서 확정한다.

    --measure: 33em / --leading-normal: 1.7 / JS 예산 0KB

  5. 구현

    레이아웃 → 재질 → 입체 → 모션 순서로. 이펙트를 다 꺼도 완성돼 있어야 한다.

    <h1>산미 8.5 · 바디 6.0 · 로스팅 라이트</h1>

  6. 프리플라이트

    자기 결과물을 남의 것처럼 본다. 게이트 12개는 전부 '아니오'여야 한다.

    게이트 12/12 통과 · 슬롭 지문 0건 · 320px 오버플로 31px → 수정

여섯 단계 끝에 남는 것은 페이지 하나와 design.md 하나다. 다음 사람이 그 파일을 읽고 같은 결정을 이어간다.

이 페이지의 수치

카피만 고쳐서는 슬롭이 없어지지 않는다

문장을 다듬는 것으로 해결되는 것은 문장뿐이다. radius 어휘 수, 반투명 패널 개수, 타입 단계 수는 그대로 남는다. 그래서 이 페이지 자신의 형태 수치를 공개한다. 결과가 나쁘게 나와도 그대로 싣는다.

radius 어휘 수 2종 3px · 6px. 그 외 값을 쓰지 않는다
반투명 패널(backdrop-filter) 0개 실기기 FPS를 15~30% 깎는다
타입 스케일 단계 5단계 디스플레이 1개는 스케일 밖에 따로 정의
폰트 패밀리 2개 본문 1 + 코드용 모노 1. 언어판마다 자기 것만 받는다
강조색 1개 딥 틸. 라이트·다크는 같은 색의 대응값이다
히어로까지 JS 314B gzip. 예산 50KB의 0.6%. 복사 버튼 하나뿐이다
슬롭 지문 grep 0건 9개 패턴 검사
하드 게이트 12/12 12개 전부 검사. 4개는 320px 실렌더로 측정했다
첫 인터랙션 미측정. 프로덕션 배포 후 Lighthouse 로 잰다

값은 프로덕션 빌드에서 측정한다. 아직 측정하지 않은 항목은 비워둔다 — 지어낸 숫자보다 구멍이 정직하다.

이 페이지를 만들며 실제로 걸린 것

남을 검사한 결과가 아니라 자신을 검사한 결과다.

  • eyebrow 라벨이 6개 — 카운트 규칙 상한은 ceil(섹션 7 / 3) = 3개

    세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다

  • 스크롤 리빌에 clip-path 를 쓰려 했다 — 하드 게이트 8은 transform·opacity 만 허용한다

    덮개를 translateX 로 미는 방식으로 바꿨다. 보이는 결과는 같고, clip-path 보간 제약도 함께 사라졌다

  • 구분선 색의 대비가 1.30:1 — 장식선으로는 괜찮지만 입력 테두리로 쓰면 WCAG 1.4.11(3:1) 위반

    6.11:1 짜리 별칭을 하나 더 두고 용도를 갈랐다. 새 색을 만든 게 아니라 팔레트 수는 그대로다

  • 본문 컬럼이 960px — 우리가 정한 한 줄 길이(561px)를 넘는다

    그리드의 본문 폭을 --measure 로 바꿨다. 스킬 문서의 예시 코드도 같이 고쳤다

  • 320px 에서 가로로 13px 넘쳤다 — 표를 overflow 컨테이너로 감쌌는데도

    grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow-x 가 실제로 작동한다

  • 다크 밴드 안 버튼의 대비가 2.88:1 — 기준은 4.5:1

    밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다

  • 스크롤을 끝까지 내려도 파이프라인 6단계 중 뒤 3개가 opacity 0 으로 남았다 — 이 페이지의 본체가 절반 사라졌다

    animation-range 의 cover 22% 에 페이지 하단 요소는 영영 도달하지 못한다. 이펙트를 끄면 보이지만 켜면 사라지는 것은 폴백이 있는 것과 다르다. 연출을 뺐다

일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 마지막 셋은 정적 검사로는 안 잡히고 320px 실렌더에서만 나왔다.

한글 조판

라틴 기준으로 만든 도구는 한글에서 멈춘다

영어권 디자인 도구는 한글 조판 규칙을 갖고 있지 않다. 9~11px 텍스트는 라틴에서 라벨이지만 한글에서는 읽을 수 없다. line-height 1.5는 라틴에서 표준이지만 한글에서는 답답하다.

가장 자주 깨지는 것은 줄바꿈이다. 기본값은 단어 중간에서 줄을 끊는다. 아래 두 문단은 같은 글자, 같은 폭, 같은 폰트다. 차이는 word-break 한 줄뿐이다.

기본값

원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.

keep-all

원두의 산미와 바디를 구분하는 사람에게 필요한 것은 형용사가 아니라 수치다.

  • word-break: keep-all — 없으면 단어가 아무 데서나 잘린다
  • line-height 1.6~1.8 — 라틴보다 넉넉하게
  • 음수 자간 금지 — 한글은 이미 고정폭에 가까워 즉시 뭉개진다
  • 한 줄 25~40자 — 라틴 기준 65~75자를 그대로 쓰면 너무 길다
  • 한글 폰트를 지정하지 않는 것 자체가 완성도 미달 신호다

규칙마다 왜 그런지 적혀 있다

근거 없는 금지는 지켜지지 않는다. 사용자가 명시적으로 요구하면 규칙보다 그쪽이 이긴다.

레퍼런스 없이 시작하지 않는다
머릿속 기본값에서 출발하면 결과는 그 카테고리의 평균이 된다.
강조색은 하나다
두 개가 필요하다고 느끼면 위계 설계가 실패한 것이다.
대담함은 한 곳에만
두 곳에서 소리치면 둘 다 죽는다.
화려함은 4순위다
심사 배점이 Design 40 / Usability 30 / Creativity 20 / Content 10이다.
지어낸 수치를 쓰지 않는다
숫자 모양의 구멍은 정직하고, 지어낸 숫자는 신뢰를 깎는다.
이펙트를 다 꺼도 완성돼 있어야 한다
그 상태가 모든 폴백의 기반이다. 무너지면 이펙트가 구조를 대신하고 있는 것이다.

한 줄이면 된다

설치 대상을 감지해 기본 선택해준다. 스킬 파일을 직접 고쳤다면 업데이트가 그 파일을 건드리지 않는다.

npx designpaca
Claude Code ~/.claude/skills/designpaca/
Codex CLI ~/.codex/skills/designpaca/
Cursor .cursor/rules/designpaca.mdc
Windsurf .windsurf/rules/designpaca.md
AGENTS.md 블록 주입 — 에이전트 무관
npx designpaca update
최신 스킬로 갱신. 직접 고친 파일은 건너뛴다
npx designpaca doctor
설치 상태와 드리프트 진단
npx designpaca uninstall
매니페스트 기반 정확한 제거
\ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f65508a..bc1458c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: astro: specifier: ^5.1.1 version: 5.18.2(@types/node@22.20.1)(rollup@4.62.4)(typescript@5.9.3) + sharp: + specifier: ^0.35.3 + version: 0.35.3(@types/node@22.20.1) + three: + specifier: ^0.185.1 + version: 0.185.1 packages/cli: dependencies: @@ -487,133 +493,275 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.34.5': resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -1810,6 +1958,15 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1897,6 +2054,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -2580,103 +2740,206 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@img/colour@1.1.0': - optional: true + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + '@img/sharp-libvips-linux-arm@1.2.4': optional: true + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + '@img/sharp-libvips-linux-x64@1.2.4': optional: true + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true + '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true + '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true + '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.11.3 optional: true + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + '@img/sharp-win32-arm64@0.34.5': optional: true + '@img/sharp-win32-arm64@0.35.3': + optional: true + '@img/sharp-win32-ia32@0.34.5': optional: true + '@img/sharp-win32-ia32@0.35.3': + optional: true + '@img/sharp-win32-x64@0.34.5': optional: true + '@img/sharp-win32-x64@0.35.3': + optional: true + '@inquirer/external-editor@1.0.3(@types/node@22.20.1)': dependencies: chardet: 2.2.0 @@ -3142,8 +3405,7 @@ snapshots: detect-indent@6.1.0: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} deterministic-object-hash@2.0.2: dependencies: @@ -4209,6 +4471,39 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + sharp@0.35.3(@types/node@22.20.1): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 22.20.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4304,6 +4599,8 @@ snapshots: dependencies: any-promise: 1.3.0 + three@0.185.1: {} + tiny-inflate@1.0.3: {} tinyexec@0.3.2: {}