# 시작하기 (/docs) `@coldsurfers/surf-ui` 은 토큰 값 · 컴파일된 CSS · React 컴포넌트를 한 패키지에 담습니다. 설치하고 import 두 줄이면 COLDSURF 의 색 · 타이포 · 컴포넌트가 그대로 붙습니다. **범용 UI 킷은 아닙니다.** 오픈소스로 공개하지만 COLDSURF 서비스에 맞춘 기본값(accent · paper · cover 톤)이 처음부터 들어 있습니다 — 그 결이 맞으면 그대로 쓰고, 아니면 토큰만 갈아끼웁니다. 목표가 "누구나 쓸 수 있게 범용화" 가 아니라 **"재활용 가능한 것을 잘 옮기는 것"** 이라서입니다. ## 설치 [#설치] `@coldsurfers` 스코프는 GitHub Packages 에 있습니다. `.npmrc` 에 레지스트리를 적습니다. ```ini title=".npmrc" @coldsurfers:registry=https://npm.pkg.github.com ``` ```bash pnpm add @coldsurfers/surf-ui ``` ## 쓰기 [#쓰기] CSS 배선은 **없습니다**. 진입점이 `styles.css` 를 직접 물고 오고, `.css.ts` 는 이 레포에서 컴파일해 내보내므로 소비자 쪽에 vanilla-extract 번들러 플러그인도 필요 없습니다. ```ts title="app/entry.tsx" import { vars } from '@coldsurfers/surf-ui' import { spacing } from '@coldsurfers/surf-ui/tokens' import { Button } from '@coldsurfers/surf-ui/primitives' ``` `styles.css` 는 한 장입니다(`cssCodeSplit: false`). `Button` 하나를 쓰는 소비자도 DS CSS 전량을 뭅니다 — 배선 없이 끝나는 대신 치르는 값입니다. ## 진입점 [#진입점] `exports` 맵에 있는 경로가 곧 API 입니다. 여기 없는 경로는 열리지 않습니다. | 진입점 | 무엇 | | ---------------------------------- | ------------------------------------------------ | | `@coldsurfers/surf-ui` | `vars` 계약 · `@layer` 이름과 순서 · `media` | | `.../tokens` | 토큰 **값**. React 도 CSS 도 물지 않는 순수 데이터 | | `.../primitives` | React 컴포넌트 — Button · Chip · Field · Modal · … | | `.../cards` | 카드 조립체 — ConcertCard · ArticleCard · LeadFeature | | `.../layout` | Page · Container · PageBanner | | `.../sprinkles` | 원자 유틸. **JS 38 kB** 라 배럴에서 갈라 두었다 | | `.../style-utils` | `.css.ts` 저작 헬퍼 | | `.../motion` | 모션 유틸 클래스 | | `.../native` · `.../tokens/native` | React Native 표면 | | `.../styles.css` | 컴파일된 CSS 한 장 | ## 미리보기는 실물이다 [#미리보기는-실물이다] 문서의 미리보기는 **실제로 발행되는 패키지를 import 해서** 그립니다. 그림도, 복사한 코드도 아닙니다. `exports` 맵에 없는 경로는 이 사이트도 열지 못하므로, 사이트가 빌드된다는 것 자체가 계약이 살아 있다는 증거입니다. --- # 에이전트로 쓰기 (/docs/mcp) 이 사이트의 모든 페이지는 HTML 과 함께 **평문 사본**을 굽습니다. 사본에는 미리보기 대신 예제 원본이, props 표 대신 DS 소스 타입에서 뽑은 표가, 토큰 표 대신 발행된 값이 그대로 펼쳐져 있습니다. 에이전트가 읽는 것과 사람이 보는 것이 같은 데이터입니다. ## 한 줄로 시작 [#한-줄로-시작] 에이전트에게 이 단락을 그대로 붙여 넣습니다. ```txt https://design.coldsurf.io/llms.txt 를 읽고 UI 는 @coldsurfers/surf-ui 로 짜줘. 컴포넌트를 쓰기 전에 그 페이지의 .txt 에서 props 와 예제를 확인해. ``` ## 무엇을 읽히나 [#무엇을-읽히나] | 주소 | 무엇 | 언제 | | ---------------------------------- | ----------------------- | -------------------- | | [`/llms.txt`](/llms.txt) | 색인. 링크가 아래 `.txt` 로 갑니다 | 에이전트가 링크를 따라갈 수 있을 때 | | [`/llms-full.txt`](/llms-full.txt) | 사이트 전체를 한 파일로 | 컨텍스트에 통째로 넣을 때 | | `/llms/components/button.txt` | 페이지 하나 | 지금 쓰는 컴포넌트만 필요할 때 | 페이지 머리의 **복사** 버튼이 그 페이지의 사본을 클립보드에 담고, `Open ▾` 이 같은 사본을 ChatGPT · Claude 에 열어 줍니다. ## MCP [#mcp] `@coldsurfers/design-system-mcp` 는 위 사본에 **목록 · 검색 · 경로 검증**을 얹은 [MCP](https://modelcontextprotocol.io) 서버입니다. 다른 데이터가 아닙니다. 레지스트리 설정은 [시작하기](/docs) 와 같습니다. ```bash claude mcp add coldsurf-ds -- npx -y @coldsurfers/design-system-mcp ``` ```json title="mcp.json" { "mcpServers": { "coldsurf-ds": { "command": "npx", "args": ["-y", "@coldsurfers/design-system-mcp"] } } } ``` | 툴 | 하는 일 | | --------------- | --------------------------------------- | | `discover_docs` | 카테고리 구조와 문서 수. 먼저 부릅니다 | | `list_docs` | 제목 · 경로 · 한 줄 설명. `category` 로 좁힙니다 | | `get_doc` | 본문 하나 — 예제 코드 · props 표 · 토큰 값이 들어 있습니다 | 패키지는 문서 사본을 들고 있지 않고 이 사이트를 읽어옵니다. 번들했다면 발행 시점에 얼어붙어 소비자가 옛 문서를 최신인 줄 알고 읽습니다 — **틀린 문서보다 나쁜 건 틀린 줄 모르는 문서입니다.** ## 소비 레포의 AGENTS.md [#소비-레포의-agentsmd] 매번 프롬프트를 붙이지 않으려면 소비 레포의 `AGENTS.md`(또는 `CLAUDE.md`)에 적어 둡니다. ```md title="AGENTS.md" ## UI - 컴포넌트 · 토큰은 `@coldsurfers/surf-ui` 를 쓴다. 문서: https://design.coldsurf.io/llms.txt - 컴포넌트를 쓰기 전에 `/llms/<경로>.txt` 에서 props 와 예제를 확인한다. 추측하지 않는다. - 색 · 여백은 리터럴 대신 토큰(`vars` · `--surf-*`)을 쓴다. ``` --- # Tailwind CSS (/docs/tailwind) `@coldsurfers/tailwind4-theme` 는 토큰을 Tailwind v4 의 `@theme` 로 옮긴 **CSS 한 장**입니다. JS 는 없고, Tailwind 를 안 쓰는 소비자는 설치를 안 해 0 바이트입니다 — DS 의 `exports` 에 올리지 않고 패키지를 가른 이유입니다. ## 설치 [#설치] `@coldsurfers` 스코프는 GitHub Packages 에 있습니다. 레지스트리 설정은 [시작하기](/docs) 와 같습니다. ```bash pnpm add -D @coldsurfers/tailwind4-theme ``` `tailwindcss@4` 와 `@coldsurfers/surf-ui` 이 peer 입니다. **값의 정본은 DS 쪽**이고 이 패키지는 그 값을 `var()` 로 참조만 하므로, 변수 시트를 반드시 같이 물어야 합니다. ## 배선 [#배선] Tailwind 로 화면을 짜면서 색 · 간격만 우리 값으로 맞출 때입니다. ```css title="app/global.css" @import "tailwindcss"; @import "@coldsurfers/surf-ui/tokens.css"; @import "@coldsurfers/tailwind4-theme"; ``` `tokens.css` 는 무레이어 `:root` 한 장입니다 — 리셋도 컴포넌트 CSS 도 들어 있지 않습니다. 레이어 밖이라 캐스케이드 다툼도 없고, 그래서 이 조합은 **import 순서를 타지 않습니다**. `Button` · `ConcertCard` 같은 primitives 를 같이 쓸 때입니다. ```css title="app/global.css" @import "@coldsurfers/surf-ui/styles.css"; @import "tailwindcss"; @import "@coldsurfers/tailwind4-theme"; ``` `styles.css` 가 `tokens.css` 의 이름과 값을 전부 품고 있으므로 둘을 같이 물 필요는 없습니다. **`styles.css` 가 `tailwindcss` 보다 먼저여야 합니다.** 레이어 순서는 이름이 *처음 등장한* 순서로 굳고, 나중 선언이 그걸 되돌리지 못합니다. 실측(v4.3.3): ``` tailwind 먼저 → base · components · utilities · ds-reset · ds-tokens · ds-components · ds-utilities DS 먼저 → base · ds-reset · ds-tokens · ds-components · components · ds-utilities · utilities ``` 앞쪽에서는 `ds-components` 가 Tailwind `utilities` 를 이깁니다 — `p-4` 로 컴포넌트 여백을 덮으려 해도 먹지 않습니다. `@layer …;` 선언을 손으로 적지 않습니다. `styles.css` 의 첫 줄이 [순서 계약](/docs/foundations/layers)을 직접 발행합니다 — 적어야 할 건 import 순서 하나뿐입니다. ## 무엇이 열리는가 [#무엇이-열리는가] `@theme` 는 Tailwind 네임스페이스에 이름을 얹을 뿐입니다. **값은 여기 없습니다** — 아래 표의 "정본" 이 값을 갖습니다. | 네임스페이스 | 유틸 | 정본 | | --------------------------------- | ------------------------------------------ | ----------------------------------- | | `--color-*` | `bg-bg` · `text-heading` · `border-border` | [색](/docs/foundations/colors) | | `--color-cover-*` | `bg-cover-forest` · `text-cover-plum` | [색](/docs/foundations/colors) | | `--color-paper-*` | `bg-paper-warm` | [색](/docs/foundations/colors) | | `--radius-*` | `rounded-lg` · `rounded-full` | [색](/docs/foundations/colors) | | `--spacing-*` | `p-4` · `gap-2.5` · `mt-14` | [여백](/docs/foundations/spacing) | | `--text-*` | `text-sm` · `text-3xs` | [타이포](/docs/foundations/typography) | | `--text-{그룹}-{크기}` | `text-display-md` · `text-eyebrow-sm` | [타이포](/docs/foundations/typography) | | `--leading-*` · `--font-weight-*` | `leading-relaxed` · `font-regular` | [타이포](/docs/foundations/typography) | | `--font-*` | `font-sans` · `font-serif` · `font-mono` | [타이포](/docs/foundations/typography) | | `--breakpoint-*` | `tablet:p-8` · `desktop:p-10` | [계약과 레이어](/docs/foundations/layers) | ```tsx

now playing

Slowdive

Souvlaki, 1993

``` `uppercase` 를 직접 적은 게 실수가 아닙니다 — 아래 "안 넘어오는 것" 을 보세요. ## 좌표계가 둘이 됩니다 [#좌표계가-둘이-됩니다] Tailwind 기본 스케일은 지워지지 않습니다. 우리 이름이 **덮어쓴 자리만** 바뀌고 나머지는 그대로 살아 있어서, 같은 뜻의 이름이 둘씩 생깁니다. * `sm:` · `md:` · `lg:` 가 그대로입니다. `tablet:`(768px)과 `md:`(48rem)은 값이 같지만 이름이 둘입니다 — 팀에서 하나로 정해 씁니다 * 스케일 밖 값도 조용히 컴파일됩니다. `p-9` 는 `calc(var(--spacing) * 9)` 로 떨어집니다 — 4px 격자를 벗어난 걸 Tailwind 가 막아주지 않습니다 * `font-bold`(700)는 Tailwind 기본값입니다. 토큰의 무게는 `light` · `regular` · `medium` · `semibold` 넷뿐입니다 ## 안 넘어오는 것 [#안-넘어오는-것] * **`textTransform`.** eyebrow 는 대문자 변환이 스타일의 일부지만 `--text-*` 네임스페이스로는 크기 · 행간 · 자간까지만 표현됩니다. `uppercase` 는 직접 적습니다 * **`sprinkles` · `vars` · 컴포넌트 recipe.** JS 쪽 계약이라 CSS 한 장에 담기지 않습니다. `.css.ts` 를 직접 쓰는 소비자는 [계약과 레이어](/docs/foundations/layers)로 갑니다 ## 이름에 `4` 가 붙은 이유 [#이름에-4-가-붙은-이유] Tailwind major 마다 `@theme` 규약이 갈립니다. 이름에 major 를 박아 두면 v5 용을 **나란히** 낼 수 있습니다 — 기존 소비자를 깨지 않고. (선례: `@seed-design/tailwind4-theme`) --- # Button (/docs/components/button) ```tsx import { Button } from '@coldsurfers/surf-ui/primitives' export default function Example() { return ( <> ) } ``` variant 는 여섯입니다. `primary` 는 ink 필, `accent` 는 주 행동 파랑, `secondary` 는 회색 면(저장 · 프로필), `outline` 은 흰 배경 + 테두리, `ghost` 는 무테 텍스트 액션, `danger` 는 statusDanger 필입니다. 시안 v3 `v3/Button` 과는 이렇게 맞춥니다. `Kind=primary` 는 `accent`, `Kind=secondary` 는 `secondary`, `Kind=ink` 는 `primary` 이고 셋 다 `size="lg"` 입니다. `Kind=google` 은 `primary` 에 `leadingIcon` 으로 G 마크를 넣은 것이라 variant 를 따로 두지 않습니다. **`danger` 는 색이 아니라 뜻으로 고릅니다.** 되돌릴 수 없는 액션 — 탈퇴 · 영구 삭제 — 의 자리입니다. 눈에 띄게 하려고 고르는 것이 아니라 *되돌릴 수 없음*을 말하려고 고릅니다. 그래서 주 액션인 `accent` 와 나란히 서지 않습니다. 한 화면에 둘이 같이 필요하다면 둘 중 하나는 주 액션이 아닙니다. ## 크기와 아이콘 [#크기와-아이콘] ```tsx import { Button } from '@coldsurfers/surf-ui/primitives' export default function Example() { return ( <> ) } ``` **size 는 넷입니다 — 더 늘리지 않습니다.** `lg` 는 시안 v3 의 알약 48(굵게)입니다. 시안 컨트롤 높이는 다섯 종이지만 그대로 받으면 `size` 가 7종이 됩니다. 그건 추상화가 아니라 목록입니다. variant 를 늘리기 전에 *이 축이 이 컴포넌트의 것인가* 를 먼저 묻습니다. ## 모서리 · 앞 아이콘 · 비활성 [#모서리--앞-아이콘--비활성] ```tsx import { Button } from '@coldsurfers/surf-ui/primitives' export default function Example() { return ( <> ) } ``` `shape="pill"` 은 크기와 상관없이 알약 모서리입니다. 높이와 모서리는 따로 움직이므로 `size` 에 섞지 않습니다. `leadingIcon` 은 라벨 앞, `trailingIcon` 은 라벨 뒤 슬롯입니다. `disabled` 는 모든 variant 에서 흐려지고(`opacity: 0.4`) hover 가 걸리지 않습니다. **"이미 됐다"를 보여주는 자리에 `disabled` 를 쓰지 않습니다** — 저장됨 같은 상태는 흐리지 않고 선명하게 서야 합니다. 그 자리의 누름 가드는 **소비처가 합니다.** `Button` 은 `aria-disabled` 를 따로 읽지 않아서, 붙여도 클릭이 그대로 들어오고 hover 도 걸립니다. `onClick` 을 넘기지 않거나 핸들러 안에서 막습니다. ## 라우팅 CTA 는 `asChild` [#라우팅-cta-는-aschild] 버튼처럼 보이지만 실제로는 이동이면 ` ``` ## Props [#props] | prop | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | `variant?` | `'primary' \| 'secondary' \| 'ghost' \| 'accent' \| 'outline' \| 'danger'` | — | — | | `size?` | `'sm' \| 'md' \| 'cta' \| 'lg'` | — | — | | `shape?` | `'rounded' \| 'pill'` | — | — | | `leadingIcon?` | `ReactNode` | — | — | | `trailingIcon?` | `ReactNode` | — | — | | `asChild?` | `boolean` | — | — | 그 밖의 `ButtonHTMLAttributes` 속성은 루트 엘리먼트로 그대로 통과한다. --- # Callout (/docs/components/callout) ```tsx import { Callout } from '@coldsurfers/surf-ui/primitives' export default function Example() { return (
현행 보기 →}>지난 버전을 보고 있습니다. 대학로를 내 동네로 정하려면 로그인이 필요합니다. 로그인하면 대학로 페이지로 돌아와 바로 저장합니다. 저장했습니다. 예매 마감이 한 시간 남았습니다. 로그인에 실패했습니다.
) } ``` ## Toast 와 갈리는 지점은 수명이다 [#toast-와-갈리는-지점은-수명이다] 토스트는 1.6초 뒤 사라지는 **액션 결과**고, Callout 은 조건이 유지되는 동안 지면에 서 있는 **상태**입니다. 둘을 한 축으로 묶지 않습니다. ## `role` 을 붙여주지 않는다 [#role-을-붙여주지-않는다] 같은 상자가 스크린리더에 읽혀야 할 때(소셜 로그인 콜백 실패)와 읽히면 방해일 때(정적 안내)가 있는데, 그건 DS 가 알 수 없는 맥락입니다. `...rest` 로 통과만 시킵니다. ```tsx 로그인에 실패했습니다 ``` ## 앞 아이콘은 `icon` [#앞-아이콘은-icon] `icon` 은 본문과 한 덩어리입니다. 본문이 두 줄로 넘어가도 아이콘은 첫 줄 옆에 서고, 액션과 따로 접히지 않습니다. 장식이라 스크린리더에서는 숨깁니다. ## `margin` 을 소유하지 않는다 [#margin-을-소유하지-않는다] 상자가 앞뒤 무엇과 서는지는 지면이 압니다. 바깥 여백은 소비처가 줍니다. ## Props [#props] | prop | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | `tone?` | `'accent' \| 'success' \| 'warning' \| 'danger'` | — | 기본 `accent`. status 3종은 글자까지 톤색이 되고 `accent` 만 중립 글자다(#34 D-4). | | `icon?` | `ReactNode` | — | 본문 앞 아이콘. 장식이라 스크린리더에서 숨긴다. | | `action?` | `ReactNode` | — | 오른쪽 끝 액션(링크·버튼). 폭이 모자라면 본문 아래로 접힌다. | 그 밖의 `HTMLAttributes` 속성은 루트 엘리먼트로 그대로 통과한다. --- # Checkbox (/docs/components/checkbox) ```tsx import { Checkbox } from '@coldsurfers/surf-ui/primitives' import { useState } from 'react' export default function Example() { const [agreed, setAgreed] = useState(true) return (
setAgreed(e.target.checked)}> 이용약관에 동의합니다 마케팅 수신 (비활성)
) } ``` 네이티브 `` 를 시각적으로만 감춥니다. 접근성·키보드·폼 제출· react-hook-form 은 전부 네이티브에 그대로 위임하고, 칠하는 것만 가져옵니다. `ref` 는 내부 실제 `` 을 노출합니다 — `{ focus, blur }` 만 노출하면 RHF 의 `defaultValues` 가 깨집니다. 라벨 텍스트 색은 강제하지 않고 **부모에서 상속**합니다. 라이트 폼이든 다크 밴드든 감싼 문맥을 따릅니다. ## Props [#props] | prop | 타입 | 기본값 | 설명 | | --- | --- | --- | --- | | `children?` | `ReactNode` | — | 체크박스 우측 라벨/설명 슬롯. | | `containerClassName?` | `string` | — | 감싸는 `