From 38faf109a3d8c81dcf803b3b3f385629e30c5ed5 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 27 Apr 2026 23:36:21 +0900 Subject: [PATCH 1/9] Release (#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: KYBee Co-authored-by: Jinho Yeom <81306489+Yeom-JinHo@users.noreply.github.com> Co-authored-by: 이원주 <101818687+3o14@users.noreply.github.com> Co-authored-by: YOUNGBEEN, KIM <62539910+KYBee@users.noreply.github.com> Co-authored-by: 오소현 <53892427+osohyun0224@users.noreply.github.com> Co-authored-by: Minji Jeon <105787441+minji0214@users.noreply.github.com> --- .changeset/spicy-wasps-clap.md | 20 + .changeset/wet-pants-train.md | 5 + .github/CONTRIBUTING.md | 38 +- .github/decisions/token-architecture.md | 331 +++ .github/decisions/token-naming-convention.md | 420 ++++ .github/decisions/v1-audit.md | 292 +++ .github/workflows/ci.yaml | 8 +- .gitignore | 5 +- .husky/pre-push | 29 +- .mise.toml | 2 +- .templates/component/.storybook/main.ts | 10 - .templates/component/.storybook/preview.ts | 5 - .templates/component/package.json | 14 +- .vscode/extensions.json | 2 +- AGENTS.md | 2 +- CLAUDE.md | 12 +- LICENSE | 2 +- README.md | 4 +- commitlint.config.ts | 44 +- package.json | 15 +- packages/accordion/.storybook/main.ts | 10 - packages/accordion/.storybook/preview.ts | 8 - packages/accordion/package.json | 17 +- packages/avatar/.storybook/main.ts | 10 - packages/avatar/.storybook/preview.ts | 5 - packages/avatar/package.json | 14 +- packages/avatar/vite.config.ts | 1 - packages/badge/.storybook/main.ts | 10 - packages/badge/.storybook/preview.ts | 8 - packages/badge/package.json | 18 +- packages/button/.storybook/main.ts | 10 - packages/button/.storybook/preview.ts | 5 - packages/button/package.json | 18 +- packages/button/src/Button.css.ts | 4 + packages/button/src/Button.test.tsx | 18 + packages/button/src/Button.tsx | 3 +- packages/card/.storybook/main.ts | 10 - packages/card/.storybook/preview.ts | 8 - packages/card/package.json | 18 +- packages/checkbox/.storybook/main.ts | 10 - packages/checkbox/.storybook/preview.ts | 5 - packages/checkbox/package.json | 18 +- packages/chip/package.json | 4 +- packages/divider/.storybook/main.ts | 10 - packages/divider/.storybook/preview.ts | 5 - packages/divider/package.json | 18 +- packages/flex/.storybook/main.ts | 10 - packages/flex/.storybook/preview.ts | 5 - packages/flex/package.json | 18 +- packages/flex/src/Flex.stories.tsx | 9 +- packages/flex/src/Flex.test.tsx | 298 ++- packages/grid/.storybook/main.ts | 10 - packages/grid/.storybook/preview.ts | 5 - packages/grid/package.json | 18 +- packages/icon/.storybook/main.ts | 10 - packages/icon/.storybook/preview.ts | 5 - packages/icon/package.json | 14 +- packages/input/.storybook/main.ts | 10 - packages/input/.storybook/preview.ts | 5 - packages/input/package.json | 18 +- packages/plugin-figma-codegen/manifest.json | 9 - packages/plugin-figma-codegen/package.json | 40 - packages/plugin-figma-codegen/src/index.ts | 22 - packages/plugin-figma-codegen/tsconfig.json | 6 - packages/plugin-figma-codegen/tsup.config.ts | 8 - packages/radio/.storybook/main.ts | 10 - packages/radio/.storybook/preview.ts | 5 - packages/radio/package.json | 18 +- packages/reset/package.json | 5 +- packages/side/package.json | 10 +- packages/skeleton/.storybook/main.ts | 10 - packages/skeleton/.storybook/preview.ts | 5 - packages/skeleton/package.json | 18 +- packages/switch/.storybook/main.ts | 16 - packages/switch/.storybook/preview.ts | 5 - packages/switch/package.json | 18 +- packages/theme/.storybook/main.ts | 10 - packages/theme/.storybook/preview.ts | 5 - packages/theme/package.json | 10 +- packages/tokens/.storybook/main.ts | 10 - packages/tokens/.storybook/preview.ts | 5 - packages/tokens/package.json | 10 +- packages/tooltip/.storybook/main.ts | 10 - packages/tooltip/.storybook/preview.ts | 5 - packages/tooltip/package.json | 19 +- packages/tooltip/src/Tooltip.css.ts | 214 +- packages/tooltip/src/Tooltip.test.tsx | 196 +- packages/tooltip/src/Tooltip.tsx | 47 +- .../src/hooks/useTooltip/useTooltip.tsx | 17 +- packages/typography/.storybook/main.ts | 10 - packages/typography/.storybook/preview.ts | 5 - packages/typography/package.json | 18 +- pnpm-lock.yaml | 2057 +++++------------ pnpm-workspace.yaml | 3 + tokens/primitive/color.json | 136 ++ tokens/primitive/radius.json | 9 + tokens/primitive/spacing.json | 14 + tokens/primitive/typography.json | 29 + tsconfig.json | 2 +- types/index.d.ts | 15 - 100 files changed, 2465 insertions(+), 2556 deletions(-) create mode 100644 .changeset/spicy-wasps-clap.md create mode 100644 .changeset/wet-pants-train.md create mode 100644 .github/decisions/token-architecture.md create mode 100644 .github/decisions/token-naming-convention.md create mode 100644 .github/decisions/v1-audit.md delete mode 100644 .templates/component/.storybook/main.ts delete mode 100644 .templates/component/.storybook/preview.ts delete mode 100644 packages/accordion/.storybook/main.ts delete mode 100644 packages/accordion/.storybook/preview.ts delete mode 100644 packages/avatar/.storybook/main.ts delete mode 100644 packages/avatar/.storybook/preview.ts delete mode 100644 packages/avatar/vite.config.ts delete mode 100644 packages/badge/.storybook/main.ts delete mode 100644 packages/badge/.storybook/preview.ts delete mode 100644 packages/button/.storybook/main.ts delete mode 100644 packages/button/.storybook/preview.ts delete mode 100644 packages/card/.storybook/main.ts delete mode 100644 packages/card/.storybook/preview.ts delete mode 100644 packages/checkbox/.storybook/main.ts delete mode 100644 packages/checkbox/.storybook/preview.ts delete mode 100644 packages/divider/.storybook/main.ts delete mode 100644 packages/divider/.storybook/preview.ts delete mode 100644 packages/flex/.storybook/main.ts delete mode 100644 packages/flex/.storybook/preview.ts delete mode 100644 packages/grid/.storybook/main.ts delete mode 100644 packages/grid/.storybook/preview.ts delete mode 100644 packages/icon/.storybook/main.ts delete mode 100644 packages/icon/.storybook/preview.ts delete mode 100644 packages/input/.storybook/main.ts delete mode 100644 packages/input/.storybook/preview.ts delete mode 100644 packages/plugin-figma-codegen/manifest.json delete mode 100644 packages/plugin-figma-codegen/package.json delete mode 100644 packages/plugin-figma-codegen/src/index.ts delete mode 100644 packages/plugin-figma-codegen/tsconfig.json delete mode 100644 packages/plugin-figma-codegen/tsup.config.ts delete mode 100644 packages/radio/.storybook/main.ts delete mode 100644 packages/radio/.storybook/preview.ts delete mode 100644 packages/skeleton/.storybook/main.ts delete mode 100644 packages/skeleton/.storybook/preview.ts delete mode 100644 packages/switch/.storybook/main.ts delete mode 100644 packages/switch/.storybook/preview.ts delete mode 100644 packages/theme/.storybook/main.ts delete mode 100644 packages/theme/.storybook/preview.ts delete mode 100644 packages/tokens/.storybook/main.ts delete mode 100644 packages/tokens/.storybook/preview.ts delete mode 100644 packages/tooltip/.storybook/main.ts delete mode 100644 packages/tooltip/.storybook/preview.ts delete mode 100644 packages/typography/.storybook/main.ts delete mode 100644 packages/typography/.storybook/preview.ts create mode 100644 tokens/primitive/color.json create mode 100644 tokens/primitive/radius.json create mode 100644 tokens/primitive/spacing.json create mode 100644 tokens/primitive/typography.json delete mode 100644 types/index.d.ts diff --git a/.changeset/spicy-wasps-clap.md b/.changeset/spicy-wasps-clap.md new file mode 100644 index 00000000..45be6409 --- /dev/null +++ b/.changeset/spicy-wasps-clap.md @@ -0,0 +1,20 @@ +--- +"@sipe-team/accordion": patch +"@sipe-team/checkbox": patch +"@sipe-team/skeleton": patch +"@sipe-team/divider": patch +"@sipe-team/tooltip": patch +"@sipe-team/button": patch +"@sipe-team/switch": patch +"@sipe-team/badge": patch +"@sipe-team/input": patch +"@sipe-team/radio": patch +"@sipe-team/reset": patch +"@sipe-team/card": patch +"@sipe-team/flex": patch +"@sipe-team/grid": patch +"@sipe-team/side": patch +"@sipe-team/typography": patch +--- + +Preserve CSS imports in `sideEffects` so consumer bundlers don't tree-shake `./styles.css`. diff --git a/.changeset/wet-pants-train.md b/.changeset/wet-pants-train.md new file mode 100644 index 00000000..76211d3c --- /dev/null +++ b/.changeset/wet-pants-train.md @@ -0,0 +1,5 @@ +--- +"@sipe-team/tooltip": patch +--- + +improve accessibility and refactor styles for Tooltip diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index cd78cfab..019ed607 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -100,36 +100,36 @@ Before proposing changes, please **open an issue** to discuss the bug or feature ### Create a Pull Request 1. **Create a Branch** - Follow the branch naming convention: `/-` (`ISSUENUMBER` is optional) + Follow the branch naming convention: `/-` + - `USERNAME` is required (at least one hyphen must be present) ```bash - git checkout -b your-branch + git checkout -b feature/button-john ``` - | **Category** | **Description** | - |--------------|-----------------------------------------------------| - | **feat** | Developing a new feature | - | **fix** | Fixing a bug | - | **hotfix** | Emergency fixes for immediate release | - | **chore** | Maintenance tasks or minor updates | - | **refactor** | Code refactoring without functional changes | - | **release** | Preparing for a new release version | - | **test** | Writing or modifying test cases | - | **docs** | Documentation updates or additions | - | **ci** | CI/CD pipeline updates | - | **build** | Changes to the build system or dependencies | + | **Category** | **Description** | + |---------------|-----------------------------------------------------| + | **feature** | Developing a new feature | + | **fix** | Fixing a bug | + | **docs** | Documentation updates or additions | + | **style** | Code style changes (formatting, no logic change) | + | **refactor** | Code refactoring without functional changes | + | **test** | Writing or modifying test cases | + | **deploy** | Deployment-related changes | + | **chore** | Maintenance tasks or minor updates | + | **settings** | Configuration or settings changes | 2. **Commit Changes** Write meaningful commit messages using the [Conventional Commits](https://www.conventionalcommits.org/) format: ```bash git commit -m "(): " ``` - We recommend following the Conventional Commits standard for clear and consistent commit messages. Below is the suggested structure: - + We follow the [Conventional Commits](https://www.conventionalcommits.org/) standard with [`@commitlint/config-conventional`](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional): + | Element | Requirement | Description | |--------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `` | **Required** | Describes the purpose of the commit. Examples: `feat`, `fix`, `docs`, `style`, `refactor`, `test` | - | `` | **Optional** | Specifies the affected module, file, or functionality. Limited to **20 characters** (e.g., `auth`, `header`). | - | `` | **Required** | A concise summary of the changes
- Starts with a lowercase letter
- Avoid ending with a period (.)
- Limited to **50 characters**
- Must contain only English characters, numbers, and basic punctuation (!@#$%^&*(),.?":{}|<>_-)
- Cannot be empty | + | `` | **Required** | Describes the purpose of the commit: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test` | + | `` | **Optional** | Specifies the affected module, file, or functionality (e.g., `auth`, `header`). | + | `` | **Required** | A concise summary of the changes
- Starts with a lowercase letter
- Avoid ending with a period (.)
- Cannot be empty | diff --git a/.github/decisions/token-architecture.md b/.github/decisions/token-architecture.md new file mode 100644 index 00000000..e3c46a53 --- /dev/null +++ b/.github/decisions/token-architecture.md @@ -0,0 +1,331 @@ +# SIDE v2 토큰 아키텍처 + +## 1. 레퍼런스 디자인 시스템 비교 + +| 시스템 | 계층 수 | Primitive 키 | Semantic 키 예시 | 다크모드 전환 | 비고 | +|---|---|---|---|---|---| +| **Toss TDS** | 2 (Primitive → Semantic) | `blue-500` | `color-background-primary` | `data-tds-theme` attribute | Semantic만 CSS var 노출 | +| **Atlassian** | 3 (Base → Semantic → Component) | `color.blue.500` | `color.text.accent.blue` | `data-color-mode` attribute | Component 계층이 방대해 유지보수 부담 | +| **Adobe Spectrum** | 3 (Global → Alias → Component) | `--spectrum-blue-500` | `--spectrum-accent-color-default` | CSS selector 교체 | Alias = Semantic, 가장 세분화 | +| **shadcn/ui** ⚠️ | 2 (Scale → Semantic) | 직접 노출 안 함 | `--background`, `--primary` | `.dark` class | 컴포넌트 모음이지 디자인 시스템이 아님 — 토큰 구조 레퍼런스로만 참고 | +| **Radix Colors** ⚠️ | 1 (Primitive scale 제공) | `--blue-9` (12단계) | Semantic은 소비자가 정의 | 별도 dark scale 제공 | 색상 팔레트 도구 — 디자인 시스템 아님. 단계별 의미 고정(9=solid, 11=text)이 Primitive 설계에 참고할 만함 | +| **11번가** | 2 (Brand → Semantic) | `Gray_01`, `11STREET_Red` | `text-primary`, `icon-disabled` | 없음 (라이트 전용) | 브랜드 색상 중심; CSS var 기반 테마 없음 | +| **쏘카 SOCARFRAME 2.0** | 3 (Primitive → Semantic → Component) | `tw-blue-100` | `tw-text-primary-strong` | `data-theme` attribute | Tailwind 기반 — VE 미사용; Figma Code Connect 연동 | +| **G마켓 GDS** | 2 (Core Palette → Semantic) | `Green-500`, `Gray-900` | `text-primary`, `bg-white` | 없음 (라이트 전용) | 2021년 v1.0; WCAG 2.0 기반 색상 스케일 | +| **라인 LDSM** | 2+ (Primitive → Semantic 확인) | 미공개 (SPA 렌더링) | `line-semantic-colors` | 미확인 | Semantic 계층 존재 확인; 컴포넌트 라이브러리 규모 큼 | +| **카카오스타일 지그재그** ✅ | 2 (Primitive → Semantic) | 미공개 | — | `data-theme` attribute | **VE `createGlobalThemeContract` 사용** — 현 프로젝트와 구조 가장 유사 | + +- **Toss**: Component 계층은 컴포넌트 `.css.ts` 내부 변수로 처리해서 2계층으로 충분함 +- **Atlassian**: Component 계층을 토큰 패키지에 넣으면 컴포넌트 변경마다 토큰 패키지 릴리스가 강제되는 결합이 생김 +- **Radix Colors**: 단계별 의미를 고정한 방식(9=solid bg, 11=text)이 Semantic 색상 역할 분리에 참고할 만함 +- **shadcn/ui**: HSL 분리 대신 opacity 변형이 필요한 색상은 별도 토큰으로 명시하는 게 VE 방식에 더 맞음 +- **카카오스타일**: VE 기반 한국 디자인 시스템 중 가장 직접적인 레퍼런스 — [기술 블로그](https://devblog.kakaostyle.com/ko/2024-12-13-1-rebuilding-frontend-design-system/) 참고 + +--- + +## 2. 계층 구조 결정 + +``` +┌──────────────────────────────────────────────────┐ +│ Semantic Layer (목적·의미 기반 alias) │ +│ vars.color.text.primary │ +│ vars.color.bg.surface │ +│ CSS: --side-color-text-primary │ +└────────────────────┬─────────────────────────────┘ + │ alias only +┌────────────────────▼─────────────────────────────┐ +│ Primitive Layer (원시값 척도) │ +│ vars.color.blue[500] │ +│ vars.spacing[4] │ +│ CSS: --side-color-blue-500 │ +└──────────────────────────────────────────────────┘ +``` + +## 3. 계층별 허용 범위 원칙 + +### Primitive Layer + +- hex, px, rem, ms, 숫자 스케일 값 등 원시값만 허용 +- 다른 토큰 참조, 의미론적 이름 사용은 하지 않음 +- 라이트/다크 모드에 따라 값이 바뀌지 않는다 — 순수 팔레트 + +```json +// 올바른 예 +{ "color": { "blue": { "500": "#3b82f6" } } } + +// 금지 예 — Primitive에 Semantic 명명을 쓰는 것 +{ "color": { "primary": "#3b82f6" } } +``` + +### Semantic Layer + +- Primitive token에 대한 alias만 허용 +- hex, px 등 원시값 직접 사용 금지 +- 모드(light/dark)마다 **동일 키에 다른 Primitive를 할당**하는 방식으로 구현 + +```json +// 올바른 예 — Primitive alias +{ "color": { "text": { "primary": "{color.gray.950}" } } } + +// 금지 예 — Primitive 값 직접 기입 +{ "color": { "text": { "primary": "#131518" } } } +``` + +--- + +## 4. Vanilla Extract 파일 역할 정의 + +### 파일 목록 + +``` +packages/tokens/src/ +├── primitive/ +│ └── theme.css.ts # createGlobalTheme — 단일 구현, contract 불필요 +├── semantic/ +│ ├── contract.css.ts # createGlobalThemeContract (prefix 적용) — Semantic CSS var 계약 선언 +│ ├── _values.ts # light/dark 순수 값 객체 (CSS 생성 없음) +│ ├── light.css.ts # createGlobalTheme(':root', ...) — 라이트 모드 값 +│ └── dark.css.ts # createGlobalTheme + globalStyle @media — 다크 모드 값 +└── index.ts # vars (semantic)만 re-export +``` + +### `primitive/theme.css.ts` + +구현체가 하나뿐이므로 contract 없이 `createGlobalTheme`만으로 충분하다. 반환값 `primitiveVars`는 패키지 내부(`semantic/`)에서만 참조하며 외부로 노출하지 않는다. + +```ts +import { createGlobalTheme } from '@vanilla-extract/css'; + +// 단일 전역 주입 — 모드와 무관하게 항상 동일한 값 +// primitiveVars는 semantic 계층 내부 전용 — index.ts에서 export하지 않는다 +export const primitiveVars = createGlobalTheme(':root', { + color: { + blue: { '500': '#3b82f6', '600': '#2563eb' }, + gray: { '50': '#fafafa', '950': '#111111' }, + }, + spacing: { '1': '4px', '2': '8px' }, + // ... +}); +``` + +### `semantic/contract.css.ts` + +```ts +import { createGlobalThemeContract } from '@vanilla-extract/css'; + +export const vars = createGlobalThemeContract( + { + color: { + text: { primary: null, secondary: null, disabled: null, inverse: null, /* ... */ }, + bg: { base: null, surface: null, overlay: null, /* ... */ }, + border: { default: null, focused: null, error: null, /* ... */ }, + icon: { default: null, secondary: null, disabled: null }, + status: { success: null, warning: null, error: null, info: null }, + // 테마 브랜드 색상 + brand: { primary: null, secondary: null, gradient: null }, + }, + // Semantic spacing/radius는 Primitive를 그대로 alias + // 컴포넌트에서 vars.radius.md 처럼 쓰기 위해 재노출 + radius: { none: null, sm: null, md: null, lg: null, xl: null, full: null }, + }, + (_, path) => `side-${path.join('-')}`, +); +``` + +### `semantic/_values.ts` + +CSS를 생성하지 않는 순수 값 객체. `light.css.ts`와 `dark.css.ts` 두 파일이 동일한 값 객체를 각각 참조해야 하므로 별도 분리한다. + +```ts +// _values.ts +import { primitiveVars } from '../primitive/theme.css'; + +export const lightValues = { + color: { + text: { + primary: primitiveVars.color.gray['950'], + secondary: primitiveVars.color.gray['600'], + disabled: primitiveVars.color.gray['400'], + inverse: primitiveVars.color.gray['50'], + }, + bg: { + base: primitiveVars.color.gray['50'], + surface: primitiveVars.color.white, + }, + brand: { + primary: '#f4a1a0', // 예외: 브랜드 색은 테마별 고유값 + }, + // ... + }, + radius: primitiveVars.radius, + // ... +}; + +export const darkValues = { + color: { + text: { + primary: primitiveVars.color.gray['50'], + secondary: primitiveVars.color.gray['400'], + disabled: primitiveVars.color.gray['600'], + inverse: primitiveVars.color.gray['950'], + }, + bg: { + base: primitiveVars.color.gray['950'], + surface: primitiveVars.color.gray['900'], + }, + // ... + }, + // ... +}; +``` + +### `semantic/dark.css.ts` + +다크 모드가 기본값이므로 `:root`에 darkValues를 주입한다. + +```ts +import { createGlobalTheme } from '@vanilla-extract/css'; +import { vars } from './contract.css'; +import { darkValues } from './_values'; + +// 기본값: 다크 모드 +createGlobalTheme(':root', vars, darkValues); +``` + +### `semantic/light.css.ts` + +`createGlobalTheme`은 빌드타임 함수라 미디어 쿼리 안에 직접 넣을 수 없다. `data-mode="light"` 단독으로는 `prefers-color-scheme: light` 유저를 JS 토글 없이 커버할 수 없으므로 `globalStyle`로 시스템 라이트모드를 별도 처리한다. + +우선순위: `[data-mode="light"]` selector specificity > `@media` → `data-mode`가 명시된 경우 항상 우선된다. 단, 사용자가 시스템을 라이트로 설정했는데 `data-mode="dark"`가 명시된 경우 `@media`가 이겨버리는 충돌이 생기므로 이를 방어하는 `[data-mode="dark"]` override를 추가한다. + +```ts +import { assignVars, createGlobalTheme, globalStyle } from '@vanilla-extract/css'; +import { vars } from './contract.css'; +import { darkValues, lightValues } from './_values'; + +// ① JS 토글 방식 (data-mode attribute 기반) +createGlobalTheme('[data-mode="light"]', vars, lightValues); + +// ② 시스템 라이트모드 (JS 토글 없이도 동작) +globalStyle('body', { + '@media': { + '(prefers-color-scheme: light)': { + vars: assignVars(vars, lightValues), + }, + }, +}); + +// ③ data-mode="dark" 명시 시 @media를 덮어씀 (우선순위 방어) +globalStyle('[data-mode="dark"]', { + vars: assignVars(vars, darkValues), +}); +``` + +> **설계 원칙**: `light.css.ts` / `dark.css.ts`는 **codegen 대상**이다. `tokens/**/*.json`에서 자동 생성되며, 수동 편집하지 않는다. `contract.css.ts`는 타입 계약이므로 JSON 스키마 변경 시 함께 갱신된다. + +--- + +## 5. 다크 모드 확장성 + +### 파일 구조 + +``` +tokens/ +├── primitive/ +│ ├── color.json # 불변 팔레트 (모드 무관) +│ ├── spacing.json +│ ├── radius.json +│ └── typography.json +└── semantic/ + ├── light/ + │ └── color.json # 라이트 모드 alias 맵 + ├── dark/ + │ └── color.json # 다크 모드 alias 맵 + └── brand/ + ├── 1st.json # 각 SIPE 기수별 브랜드 색 + ├── 2nd.json + ├── 3rd.json + └── 4th.json +``` + +### Theme Switching 메커니즘 + +``` + + → vars가 semantic/light + brand/4th 조합으로 결정됨 + + + → vars가 semantic/dark + brand/4th 조합으로 결정됨 +``` + +CSS 우선순위: +1. `:root` — 기본 다크 모드 (4th 테마) +2. `@media (prefers-color-scheme: light)` — 시스템 라이트모드 (JS 토글 없이 동작) +3. `[data-mode="light"]` — 라이트 모드 명시 override (②보다 specificity 높음) +4. `[data-mode="dark"]` — 다크 모드 명시 override (②의 @media 충돌 방어) +5. `[data-theme="Nth"]` — 브랜드 색상 override (primary, secondary, gradient) + +### Tokens Studio Sets 구조 + +``` +Sets: + - primitive/color (global, always active) + - primitive/spacing (global, always active) + - primitive/radius (global, always active) + - primitive/typography (global, always active) + - semantic/light/color (theme set: mode=light) + - semantic/dark/color (theme set: mode=dark) + - brand/1st (theme set: theme=1st) + - brand/2nd (theme set: theme=2nd) + - brand/3rd (theme set: theme=3rd) + - brand/4th (theme set: theme=4th) +``` + +--- + +## 6. Vanilla Extract Contract 패턴 + +### createGlobalThemeContract의 역할 + +`createGlobalThemeContract`는 **타입의 근거**다: +- CSS 변수명을 매핑 함수 하나로 일관되게 결정한다. +- TypeScript 타입을 생성해 잘못된 토큰 참조를 컴파일 타임에 잡는다. +- `createGlobalTheme`에 값 주입 시 계약 키를 모두 채웠는지 타입으로 강제한다. + +```ts +// contract가 있으면 이 코드는 타입 에러 +createGlobalTheme(':root', vars, { + color: { text: { primary: '#000' } } + // TS Error: color.text.secondary 누락 +}); +``` + +### 컴포넌트에서의 사용 + +```ts +// packages/button/src/Button.css.ts +import { vars } from '@sipe-team/tokens'; // Semantic vars만 import + +export const buttonRecipe = recipe({ + base: { + backgroundColor: vars.color.bg.surface, + color: vars.color.text.primary, + borderRadius: vars.radius.md, + }, + variants: { + variant: { + primary: { + backgroundColor: vars.color.brand.primary, + }, + }, + }, +}); +``` + +컴포넌트는 **Semantic vars만** import한다. `primitiveVars`는 `semantic/` 내부에서만 참조하며, `index.ts`에서 export하지 않는다. 소비자가 `primitiveVars.color.blue['500']`을 컴포넌트에 직접 사용하기 시작하면 semantic 계층이 형식적으로 전락하므로 노출 자체를 차단한다. + +```ts +// index.ts +export { vars } from './semantic/contract.css'; // 소비자용 +// primitiveVars는 export하지 않는다 +``` diff --git a/.github/decisions/token-naming-convention.md b/.github/decisions/token-naming-convention.md new file mode 100644 index 00000000..765f6ddf --- /dev/null +++ b/.github/decisions/token-naming-convention.md @@ -0,0 +1,420 @@ +# SIDE v2 토큰 네이밍 컨벤션 + +> 이 문서의 규칙은 `tokens/**/*.json` → codegen → `contract.css.ts` / `themes.css.ts` 파이프라인 전반에 적용된다. + +--- + +## 1. 구분자 결정 + +세 가지 표현 형식에 서로 다른 구분자를 사용하며, 변환 규칙은 고정이다. + +| 형식 | 구분자 | 예시 | +|---|---|---| +| **JSON 키** | `.` (점, 중첩 객체) | `color.blue.500` | +| **VE vars 경로** | `.` (객체 접근) + `[]` (숫자 키) | `vars.color.blue[500]` | +| **CSS 변수명** | `-` (하이픈) | `--side-color-blue-500` | + +### 변환 규칙 + +``` +JSON key path: color.blue.500 + ↓ join('-') +CSS var name: --side-color-blue-500 + ↓ TS 객체 경로 +VE vars path: vars.color.blue['500'] +``` + +> **camelCase 금지**: CSS 변수명에는 하이픈만 사용한다. `--side-fontWeight-bold`가 아니라 `--side-font-weight-bold`. + +--- + +## 2. Primitive Layer 네이밍 + +### 2.1 색상 (color) + +``` +color.. + +예시: + color.gray.50 → --side-color-gray-50 + color.blue.500 → --side-color-blue-500 + color.red.950 → --side-color-red-950 + color.black → --side-color-black + color.white → --side-color-white +``` + +- ``: `gray | red | orange | yellow | green | teal | blue | cyan | purple | pink | black | white` +- ``: `50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950` +- `black`, `white`는 step 없이 단일 값 +- v1의 `gray50` 형태(camelCase + 숫자 붙이기)는 **폐기** — `.` 분리가 표준 + +### 2.2 간격 (spacing) + +``` +spacing. + +예시: + spacing.1 → --side-spacing-1 (= 4px) + spacing.2 → --side-spacing-2 (= 8px) + spacing.4 → --side-spacing-4 (= 16px) +``` + +- 값은 `multiplier × 4px` 규칙으로 계산 +- v1의 키(0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24)를 유지하되 JSON에서 문자열 키로 저장 (`"1"`, `"2"`) +- `spacing.0`은 `0px`, 토큰으로 정의하되 사용 빈도는 낮다 + +### 2.3 둥근 모서리 (radius) + +``` +radius. + +예시: + radius.none → --side-radius-none (= 0) + radius.sm → --side-radius-sm (= 2px) + radius.md → --side-radius-md (= 4px) + radius.lg → --side-radius-lg (= 8px) + radius.xl → --side-radius-xl (= 12px) + radius.full → --side-radius-full (= 9999px) +``` + +- 스케일: `none | sm | md | lg | xl | full` (v1 동일 유지) + +### 2.4 타이포그래피 (typography) + +``` +typography.fontSize. +typography.fontWeight. +typography.lineHeight. +typography.fontFamily. + +예시: + typography.fontSize.12 → --side-typography-font-size-12 + typography.fontWeight.semiBold → --side-typography-font-weight-semi-bold + typography.lineHeight.regular → --side-typography-line-height-regular +``` + +- `fontSize`: 숫자 값을 키로 사용 (`12 | 14 | 16 | 18 | 20 | 24 | 28 | 32 | 36 | 48`) +- `fontWeight`: 시맨틱 이름 (`regular | medium | semiBold | bold`) +- CSS 변수명 변환 시 camelCase → kebab-case (`semiBold` → `semi-bold`) + +### 2.5 그림자 (shadow) + +``` +shadow. + +예시: + shadow.none → --side-shadow-none + shadow.sm → --side-shadow-sm + shadow.md → --side-shadow-md + shadow.lg → --side-shadow-lg + shadow.xl → --side-shadow-xl + shadow.2xl → --side-shadow-2xl +``` + +### 2.6 Z축 (z) + +``` +z. + +예시: + z.hide → --side-z-hide (= -1) + z.base → --side-z-base (= 0) + z.dropdown → --side-z-dropdown (= 1000) + z.sticky → --side-z-sticky (= 1100) + z.modal → --side-z-modal (= 1400) + z.toast → --side-z-toast (= 1600) + z.tooltip → --side-z-tooltip (= 1700) +``` + +- v1 `zIndex` 키 이름을 `z`로 단축 (CSS var에서 `z-index`와 혼동 없음) + +### 2.7 애니메이션 (motion) — v2 신규 + +``` +motion.duration. +motion.easing. + +예시: + motion.duration.fast → --side-motion-duration-fast (= 100ms) + motion.duration.normal → --side-motion-duration-normal (= 200ms) + motion.duration.slow → --side-motion-duration-slow (= 300ms) + motion.duration.slower → --side-motion-duration-slower (= 500ms) + + motion.easing.default → --side-motion-easing-default (= ease-in-out) + motion.easing.decelerate → --side-motion-easing-decelerate (= cubic-bezier(0,0,0.2,1)) + motion.easing.accelerate → --side-motion-easing-accelerate (= cubic-bezier(0.4,0,1,1)) + motion.easing.spring → --side-motion-easing-spring (= cubic-bezier(0.4,0,0.2,1)) +``` + +> v1 감사에서 확인된 분산된 duration 값 (`0.15s, 0.2s, 0.3s, 1.2s, 1.5s, 2s`)을 `fast/normal/slow/slower` + 별도 animation 전용 키로 통합 + +### 2.8 불투명도 (opacity) + +``` +opacity. + +예시: + opacity.0 → --side-opacity-0 (= 0) + opacity.50 → --side-opacity-50 (= 0.5) + opacity.100 → --side-opacity-100 (= 1) +``` + +- step: `0 | 5 | 10 | 20 | 25 | 30 | 40 | 50 | 60 | 70 | 75 | 80 | 90 | 95 | 100` +- 퍼센트 단위로 표현 (0.5 → `50`) + +### 2.9 테두리 (border) + +``` +border.width. +border.style. + +예시: + border.width.none → --side-border-width-none (= 0) + border.width.thin → --side-border-width-thin (= 1px) + border.width.medium → --side-border-width-medium (= 2px) + border.width.thick → --side-border-width-thick (= 4px) +``` + +--- + +## 3. Semantic Layer 네이밍 + +### 3.1 색상 카테고리 (color) + +Semantic 색상은 **역할(role)** 기반으로 명명한다. Primitive에 있는 색상 이름을 Semantic에 쓰지 않는다. + +``` +color..[.] + +카테고리: + text — 텍스트 + bg — 배경 + border — 경계선 + icon — 아이콘 + status — 상태 (success/warning/error/info) + brand — 테마 브랜드 색 +``` + +**text** + +| 키 | 용도 | +|---|---| +| `color.text.primary` | 본문 기본 텍스트 | +| `color.text.secondary` | 보조 텍스트, 설명 | +| `color.text.tertiary` | 더 약한 힌트, placeholder | +| `color.text.disabled` | 비활성 텍스트 | +| `color.text.inverse` | 어두운 배경 위 밝은 텍스트 | +| `color.text.link` | 링크 텍스트 | +| `color.text.onAccent` | accent 배경 위 텍스트 | + +**bg** + +| 키 | 용도 | +|---|---| +| `color.bg.base` | 페이지 최상위 배경 | +| `color.bg.surface` | 카드·패널 배경 | +| `color.bg.overlay` | 모달·드로어 배경 | +| `color.bg.subtle` | 구분선 없이 영역 구분할 때 | +| `color.bg.accent` | 강조 배경 (brand primary) | +| `color.bg.disabled` | 비활성 입력 배경 | + +**border** + +| 키 | 용도 | +|---|---| +| `color.border.default` | 일반 경계선 | +| `color.border.subtle` | 미세 구분선 | +| `color.border.focused` | 포커스 링 | +| `color.border.error` | 에러 상태 경계선 | +| `color.border.disabled` | 비활성 경계선 | + +**icon** + +| 키 | 용도 | +|---|---| +| `color.icon.default` | 기본 아이콘 | +| `color.icon.secondary` | 보조 아이콘 | +| `color.icon.disabled` | 비활성 아이콘 | +| `color.icon.onAccent` | accent 배경 위 아이콘 | + +**status** + +| 키 | 용도 | +|---|---| +| `color.status.success` | 성공·완료 | +| `color.status.warning` | 경고 | +| `color.status.error` | 오류 (v1 `danger` → `error`로 변경) | +| `color.status.info` | 정보 (v1 `positive` → `info`로 변경) | +| `color.status.success.bg` | 성공 상태 배경 | +| `color.status.error.bg` | 오류 상태 배경 | + +> v1 `danger` → `error`, `positive` → `info` 로 명칭 변경. 더 범용적이고 산업 표준에 가깝다. + +**brand** + +| 키 | 용도 | +|---|---| +| `color.brand.primary` | 테마 주 색상 (SIPE 기수별 상이) | +| `color.brand.secondary` | 테마 보조 색상 | +| `color.brand.gradient` | 그라디언트 (CSS value 전체) | +| `color.brand.onPrimary` | primary 배경 위 텍스트 | + +### 3.2 상태 접미사 (state suffix) + +상태를 가지는 토큰은 마지막 세그먼트에 상태를 붙인다. 기본 상태(default)는 접미사를 생략한다. + +| 접미사 | 의미 | 예시 | +|---|---|---| +| _(없음)_ | 기본 상태 | `color.text.primary` | +| `.hover` | 마우스 오버 | `color.bg.accent.hover` | +| `.active` | 클릭/누름 | `color.bg.accent.active` | +| `.disabled` | 비활성 | `color.text.disabled` | +| `.focus` | 키보드 포커스 | `color.border.focused` (별도 카테고리로 분리) | +| `.selected` | 선택됨 | `color.bg.surface.selected` | + +> `focus`는 `color.border.focused`처럼 `border` 카테고리 안에서 표현하는 것을 우선한다. 단독 `color.bg.xxx.focus`는 복합 상태에서만 사용. + +--- + +## 4. 계열 접두사 규칙 요약 + +| 카테고리 | Primitive 접두사 | Semantic 접두사 | VE vars 경로 | +|---|---|---|---| +| 색상 | `color..` | `color..*` | `vars.color.*` | +| 간격 | `spacing.` | — (Primitive 직접 사용) | `vars.spacing[n]` | +| 둥근 모서리 | `radius.` | — (Primitive 직접 alias) | `vars.radius.*` | +| 그림자 | `shadow.` | `shadow.` | `vars.shadow.*` | +| Z축 | `z.` | — (Primitive 직접 사용) | `vars.z.*` | +| 타이포그래피 | `typography.*` | — (Primitive 직접 사용) | `vars.typography.*` | +| 애니메이션 | `motion.duration.*` / `motion.easing.*` | — | `vars.motion.*` | +| 불투명도 | `opacity.` | — | `vars.opacity[n]` | +| 테두리 두께 | `border.width.*` | — | `vars.border.width.*` | + +Semantic이 없는 카테고리(spacing, radius 등)는 컴포넌트에서 Primitive vars를 직접 참조한다. + +--- + +## 5. VE Contract 키 규칙 + +### 숫자 키 처리 + +TypeScript 객체 키는 숫자로 시작할 수 없다. 숫자 키는 **문자열 키**로 선언한다. + +```ts +// 올바른 예 +const primitiveVars = createGlobalThemeContract({ + color: { + gray: { + '50': null, // 문자열 키 + '500': null, + '950': null, + }, + }, + spacing: { + '1': null, + '4': null, + }, +}); + +// 접근 시 +vars.color.gray['500'] // bracket notation +vars.spacing['4'] +``` + +### camelCase → kebab-case 변환 + +Contract 매핑 함수가 자동으로 변환한다: + +```ts +createGlobalThemeContract( + { typography: { fontWeight: { semiBold: null } } }, + (_, path) => `side-${path.map(toKebab).join('-')}`, + // → --side-typography-font-weight-semi-bold +); + +function toKebab(str: string) { + return str.replace(/([A-Z])/g, '-$1').toLowerCase(); +} +``` + +--- + +## 6. JSON 파일 키 예시 + +### `tokens/primitive/color.json` + +```json +{ + "color": { + "black": { "value": "#131518", "type": "color" }, + "white": { "value": "#ffffff", "type": "color" }, + "gray": { + "50": { "value": "#fafafa", "type": "color" }, + "500": { "value": "#71717a", "type": "color" }, + "950": { "value": "#111111", "type": "color" } + }, + "blue": { + "500": { "value": "#3b82f6", "type": "color" } + } + } +} +``` + +### `tokens/semantic/light/color.json` + +```json +{ + "color": { + "text": { + "primary": { "value": "{color.gray.950}", "type": "color" }, + "secondary": { "value": "{color.gray.600}", "type": "color" }, + "disabled": { "value": "{color.gray.400}", "type": "color" }, + "inverse": { "value": "{color.white}", "type": "color" } + }, + "bg": { + "base": { "value": "{color.gray.50}", "type": "color" }, + "surface": { "value": "{color.white}", "type": "color" } + }, + "status": { + "success": { "value": "{color.green.500}", "type": "color" }, + "warning": { "value": "{color.orange.400}", "type": "color" }, + "error": { "value": "{color.red.500}", "type": "color" }, + "info": { "value": "{color.blue.400}", "type": "color" } + } + } +} +``` + +### `tokens/semantic/dark/color.json` + +```json +{ + "color": { + "text": { + "primary": { "value": "{color.gray.50}", "type": "color" }, + "secondary": { "value": "{color.gray.400}", "type": "color" }, + "disabled": { "value": "{color.gray.600}", "type": "color" }, + "inverse": { "value": "{color.gray.950}", "type": "color" } + }, + "bg": { + "base": { "value": "{color.gray.950}", "type": "color" }, + "surface": { "value": "{color.gray.900}", "type": "color" } + } + } +} +``` + +--- + +## 7. 금지 패턴 + +| 금지 패턴 | 이유 | 대안 | +|---|---|---| +| Semantic에 hex 직접 사용 | 다크모드 전환 불가 | Primitive alias `{color.gray.950}` | +| Primitive에 의미론 이름 | 역할이 고정되어 확장 어려움 | `color.primary` → `color.brand.primary` (Semantic에) | +| 컴포넌트에서 `primitiveVars` import | Primitive는 내부 구현 세부사항 | `vars` (Semantic) 만 import | +| CSS var에 camelCase | CSS 관례 위반 | `--side-font-weight-semi-bold` | +| `danger`, `positive` 이름 | 비표준, 혼동 유발 | `error`, `info` | +| `vars.spacing.xs` 같은 t-shirt size | 범위가 불명확, 확장 어려움 | `vars.spacing['2']` (4px 단위 scale) | + +> v1의 `vars.spacing.xs/sm/md/lg/xl` 패턴은 **v2에서 폐기**. 숫자 스케일이 컴포넌트 조합 시 훨씬 예측 가능하다. diff --git a/.github/decisions/v1-audit.md b/.github/decisions/v1-audit.md new file mode 100644 index 00000000..c0e2de3a --- /dev/null +++ b/.github/decisions/v1-audit.md @@ -0,0 +1,292 @@ +# v1 토큰 사용 현황 감사 + +> 작성일: 2026-04-17 +> 목적: v2 설계 의사결정을 위한 v1 토큰 정의·사용 현황 파악 + +--- + +## 1. 토큰 정의 목록 (`packages/tokens`) + +### 1.1 Color + +**Primitive (팔레트)** + +| 그룹 | 범위 | 예시 | +|------|------|------| +| black/white | 2개 | `#131518`, `#ffffff` | +| gray | 50–950 (11단계) | `#fafafa` ~ `#111111` | +| red | 50–950 (11단계) | `#fef2f2` ~ `#1f0808` | +| orange | 50–950 (11단계) | `#fff7ed` ~ `#220a04` | +| yellow | 50–950 (11단계) | `#fefce8` ~ `#281304` | +| green | 50–950 (11단계) | `#f0fdf4` ~ `#03190c` | +| teal | 50–950 (11단계) | `#f0fdfa` ~ `#021716` | +| blue | 50–950 (11단계) | `#eff6ff` ~ `#0c142e` | +| cyan | 50–950 (11단계) | `#ecfeff` ~ `#051b24` | +| purple | 50–950 (11단계) | `#faf5ff` ~ `#1a032e` | +| pink | 50–950 (11단계) | `#fdf2f8` ~ `#2c0514` | + +**Semantic** + +| 키 | 참조 | 실제 값 | +|---|---|---| +| success | green500 | `#22c55e` | +| warning | orange400 | `#fb923c` | +| danger | red500 | `#ef4444` | +| positive | blue400 | `#60a5fa` | + +**Theme (4개 테마)** + +| 테마 | primary | secondary | background | +|------|---------|-----------|------------| +| theme1st | `#01fe13` | `#01fe13` | `#000000` | +| theme2nd | `#03ff31` | `#06ffe3` | `#131518` | +| theme3rd | `#00ffff` | `#00ff99` | `#0d0d0d` | +| theme4th | `#f4a1a0` | `#f4a1a0` | `#0f1010` | + +--- + +### 1.2 Typography + +| 카테고리 | 값 | +|---------|---| +| fontSize | 12, 14, 16, 18, 20, 24, 28, 32, 36, 48 | +| fontWeight | regular(400), medium(500), semiBold(600), bold(700) | +| lineHeight | regular(1.5), compact(1.3) | + +--- + +### 1.3 Effects + +**Border Radius** + +| 키 | 값 | +|---|---| +| none | 0 | +| sm | 2px | +| md | 4px | +| lg | 8px | +| xl | 12px | +| full | 9999px | + +**Shadows** + +| 키 | 값 | +|---|---| +| none | none | +| sm | `0 1px 2px 0 rgba(0,0,0,0.05)` | +| md | `0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06)` | +| lg | `0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05)` | +| xl | `0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04)` | +| 2xl | `0 25px 50px -12px rgba(0,0,0,0.25)` | + +**Z-Index** + +| 키 | 값 | +|---|---| +| hide | -1 | +| base | 0 | +| dropdown | 1000 | +| sticky | 1100 | +| fixed | 1200 | +| overlay | 1300 | +| modal | 1400 | +| popover | 1500 | +| toast | 1600 | +| tooltip | 1700 | + +**Opacity**: 0, 0.05, 0.1, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 0.95, 1 (15단계) + +**Border Width**: none(0), thin(1px), medium(2px), thick(4px) + +**Border Style**: solid, dashed, dotted + +--- + +### 1.4 Layout + +**Spacing** + +| 키 | 값 | +|---|---| +| 0 | 0px | +| 1 | 4px | +| 2 | 8px | +| 3 | 12px | +| 4 | 16px | +| 5 | 20px | +| 6 | 24px | +| 8 | 32px | +| 10 | 40px | +| 12 | 48px | +| 16 | 64px | +| 20 | 80px | +| 24 | 96px | + +**Breakpoints**: sm(0), md(780px), lg(1060px) + +**Grid**: 12컬럼, gutter sm(8)/md(16)/lg(24) + +--- + +### 1.5 Theme Contract Vars (`vars`) + +`packages/tokens/src/theme/contract.css.ts` 에서 export되는 CSS 변수 계약: + +- `vars.color`: primary, secondary, background, text, gradient +- `vars.spacing`: xs, sm, md, lg, xl +- `vars.typography.fontSize`: 050–900 +- `vars.typography.lineHeight`: regular, compact +- `vars.typography.fontWeight`: regular, medium, semiBold, bold +- `vars.typography.fontFamily` +- `vars.radius`: none, sm, md, lg, xl, full +- `vars.shadows`: none, sm, md, lg, xl, 2xl +- `vars.mode`, `vars.theme` + +--- + +## 2. 컴포넌트별 토큰 사용 매트릭스 + +| 패키지 | `@sipe-team/tokens` import | `vars` 사용 | 하드코딩 | +|-------|---------------------------|-------------|---------| +| button | ✅ `vars` | ✅ | ✗ | +| input | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| card | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| badge | ⚠️ `color`, `fontSize` (raw) | ✗ | ✅ | +| chip | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| checkbox | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| radio | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| switch | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| accordion | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| avatar | ✗ | ✗ | ✅ | +| skeleton | ⚠️ `color`, `radius` (raw) | ✅ (radius만) | ✅ | +| tooltip | ✗ | ✗ | ✅ | +| divider | ⚠️ `color` (raw 팔레트) | ✗ | ✅ | +| flex | ✗ | ✗ | ✗ (레이아웃 유틸) | +| grid | ✗ | ✗ | ✗ (레이아웃 유틸) | +| typography | ✅ `fontSize`, `fontWeight`, `lineHeight` | ✅ | ✗ | + +> **요약**: `button`, `typography` 만 `vars`를 올바르게 사용. 대부분의 컴포넌트는 raw 팔레트 값(`color.grayXXX`)을 직접 참조하거나 hex/px를 하드코딩. + +--- + +## 3. 누락 토큰 유형 + +| 카테고리 | 상태 | 비고 | +|---------|------|------| +| Animation Duration | ❌ 미정의 | 0.15s, 0.2s, 0.3s, 1.5s, 2s 등 분산 사용 | +| Animation Easing | ❌ 미정의 | ease-in-out, cubic-bezier(0.4,0,0.2,1), ease 등 분산 사용 | +| Component Dimensions | ❌ 미정의 | 버튼 height, 아바타 size, 칩 height 등 컴포넌트별 직접 기입 | +| Spacing (px단위 직접) | ⚠️ 부분 정의 | `vars.spacing` xs–xl 정의되어 있으나 컴포넌트에서 미사용 | +| Box Shadow (커스텀) | ⚠️ 부분 정의 | shadows 토큰 존재하나 컴포넌트에서 미사용 | +| Z-Index | ⚠️ 부분 정의 | 토큰 정의되어 있으나 Tooltip에서 `zIndex: 1000` 직접 기입 | +| Border Width | ⚠️ 부분 정의 | 토큰 정의되어 있으나 `1px` 직접 기입 | +| Outline / Focus Ring | ❌ 미정의 | outline-offset(2px, 3px), outline-color 직접 기입 | + +--- + +## 4. 컴포넌트별 하드코딩 값 수집 + +### 4.1 Border-Radius + +| 값 | 사용 컴포넌트 | 대응 토큰 | +|---|---|---| +| `2px` | skeleton (radius.sm 사용) | `vars.radius.sm` ✅ | +| `4px` | checkbox, avatar, tooltip, switch | `vars.radius.md` (미사용) | +| `8px` | card, accordion, tooltip, input | `vars.radius.lg` (미사용) | +| `12px` | card, accordion | `vars.radius.xl` (미사용) | +| `50%` / `100px` / `100px` | radio, avatar, chip | `vars.radius.full` (미사용) | +| `9999px` | chip | `vars.radius.full` (미사용) | + +### 4.2 Transition / Animation + +| 값 | 사용 컴포넌트 | +|---|---| +| `0.15s ease-in-out` | checkbox, radio | +| `0.2s ease-in-out` | button, chip | +| `0.3s ease-in-out` | tooltip | +| `0.3s cubic-bezier(0.4, 0, 0.2, 1)` | accordion (height) | +| `0.3s ease` | accordion (transform), skeleton | +| `150ms ease-in-out` | switch | +| `1.2s ease-in-out infinite` | skeleton | +| `1.5s ease-in-out infinite` | skeleton | +| `2s ease-in-out infinite` | skeleton | +| `2s infinite` | skeleton | + +### 4.3 Spacing (padding / margin / gap) + +| 값 | 사용 컴포넌트 | +|---|---| +| `4px` | chip, badge | +| `6px` | checkbox (container padding), radio (container gap) | +| `8px` | input, tooltip, radio, switch, badge, chip, checkbox | +| `10px` | checkbox, radio (lg size) | +| `12px` | input, badge, accordion, chip | +| `16px` | input, accordion, badge, chip | +| `20px` | card, accordion, chip | +| `24px` | badge | +| `-1px` | switch (thumb 위치 계산) | + +### 4.4 색상 하드코딩 (토큰 미참조) + +| 값 | 사용 컴포넌트 | 비고 | +|---|---|---| +| `#3B82F6` | checkbox | checked 상태 배경 (blue500에 해당) | +| `#1a202c` | accordion | 배경색 | +| `#2d3748` | accordion, avatar | 배경색 / 텍스트색 | +| `#e2e8f0` | avatar | 배경색 | +| `#000000` | tooltip | CSS 변수 fallback | +| `#ffffff` | tooltip | 텍스트색 | +| `rgba(0, 0, 0, 0.2)` | tooltip | box-shadow | +| `#ccc`, `#f9f9f9`, `#e6e6e6` | tooltip | 경계·배경 | + +### 4.5 Component Dimensions (height / width) + +| 컴포넌트 | 값 | +|---------|---| +| button | height: `32px`(sm), `48px`(lg) | +| input | defaultActionSize: `24px` | +| avatar | size: 24, 32, 40, 70, 96px | +| chip | height: `24px`, `32px`, `40px` | +| checkbox | inputSize: `16px`, `20px`, `24px` | +| radio | inputSize: `12px`, `16px`, `20px`; `::after` 4/6/8px | +| switch | 계산값: width 32–48px, height 16–24px | +| tooltip | maxWidth: `250px` | + +### 4.6 Typography 하드코딩 + +| 값 | 사용 컴포넌트 | 비고 | +|---|---|---| +| fontSize: 12, 14, 16px | chip, input, switch, tooltip, avatar | `0.8rem` 포함 | +| fontWeight: 600 | badge, chip | semiBold 토큰 미사용 | +| lineHeight: `16px`, `20px`, `24px` | chip | compact/regular 토큰 미사용 | + +--- + +## 5. 요약 및 v2 설계 시사점 + +### 토큰 정의 완성도 + +| 카테고리 | 정의 | `vars` 사용 | 평가 | +|---------|------|-------------|------| +| Color (팔레트) | ✅ 150+ | ⚠️ 선택적 | raw 팔레트 직접 참조가 주류 | +| Color (semantic) | ✅ 4개 | ❌ | 미사용 | +| FontSize | ✅ 10개 | ⚠️ typography만 | 대부분 px 문자열 직접 기입 | +| FontWeight | ✅ 4개 | ⚠️ typography만 | badge, chip 하드코딩 | +| LineHeight | ✅ 2개 | ⚠️ 부분 | chip은 px 직접 기입 | +| BorderRadius | ✅ 6개 | ⚠️ skeleton만 | 7개 컴포넌트 하드코딩 | +| Shadows | ✅ 6개 | ❌ | 전혀 미사용 | +| ZIndex | ✅ 10개 | ❌ | tooltip 하드코딩 | +| Opacity | ✅ 15개 | ❌ | 전혀 미사용 | +| Spacing | ✅ 13개 | ❌ | `vars.spacing` 미사용 | +| BorderWidth | ✅ 4개 | ❌ | `1px` 하드코딩 | +| **Animation Duration** | ❌ | ❌ | v2 신규 필요 | +| **Animation Easing** | ❌ | ❌ | v2 신규 필요 | +| **Component Dimensions** | ❌ | ❌ | v2 신규 필요 | + +### 핵심 문제 + +1. **토큰이 있어도 안 쓴다** — radius, shadows, zIndex, spacing 모두 정의되어 있지만 대부분 컴포넌트에서 미사용 +2. **raw 팔레트 직접 참조** — `vars` 대신 `color.grayXXX`를 직접 import하는 패턴이 주류 (button, typography 제외) +3. **animation 토큰 전무** — duration/easing이 컴포넌트마다 제각각 (5가지 이상의 서로 다른 duration 혼재) +4. **치수(dimension) 토큰 전무** — 컴포넌트 height/width/size를 직접 기입하는 방식 +5. **완전 미연동 컴포넌트** — avatar, tooltip은 토큰 import 자체 없음 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8d7ff66..355b0381 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,10 +16,10 @@ jobs: - name: Install dependencies run: pnpm install - name: Lint - run: pnpm --filter="...[origin/${{ github.base_ref }}]" lint + run: pnpm --filter="...[origin/${{ github.base_ref }}]" --if-present lint - name: Test - run: pnpm --filter="...[origin/${{ github.base_ref }}]" test + run: pnpm --filter="...[origin/${{ github.base_ref }}]" --if-present test - name: Type check - run: pnpm --filter="...[origin/${{ github.base_ref }}]" typecheck + run: pnpm --filter="...[origin/${{ github.base_ref }}]" --if-present typecheck - name: Build - run: pnpm --filter="...[origin/${{ github.base_ref }}]" build + run: pnpm --filter="...[origin/${{ github.base_ref }}]" --if-present build diff --git a/.gitignore b/.gitignore index 90d53faa..4f3006f7 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,7 @@ storybook-static # Docusaurus build/ -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json + +# oh-my-claudecode local state +.omc/ \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index f99a9212..296eb3f8 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,17 +1,32 @@ # Get the current branch name BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) -BRANCH_REGEX='^(feat|fix|hotfix|chore|refactor|release|test|docs|ci|build)\/[a-z0-9-]+$' + +# Special branches that skip naming rules +SPECIAL_REGEX='^(main|dev\/.+)$' + +# Standard branch naming: /- +# At least one hyphen required (to enforce username suffix) +BRANCH_REGEX='^(feature|fix|docs|style|refactor|test|deploy|chore|settings)\/[a-zA-Z0-9]+(-[a-zA-Z0-9]+)+$' + +# Allow special branches to bypass the naming convention +if [[ $BRANCH_NAME =~ $SPECIAL_REGEX ]]; then + exit 0 +fi # Check if the branch name matches the defined regex if ! [[ $BRANCH_NAME =~ $BRANCH_REGEX ]]; then echo "Error: Invalid branch name format." - echo + echo echo "Please rename your branch using:" - echo "git branch -m / or git branch -m /-" - echo - echo "CATEGORY: feat, fix, hotfix, chore, refactor, release, test, docs, ci, build" - echo "SUBJECT: Use only lowercase letters(a-z), numbers(0-9), and hyphens(-)" - echo "ISSUENUMBER: Use only numbers(0-9)" + echo "git branch -m /-" + echo + echo "CATEGORY: feature, fix, docs, style, refactor, test, deploy, chore, settings" + echo "SUBJECT: Letters(a-zA-Z), numbers(0-9), and hyphens(-)" + echo "USERNAME: Your name (e.g., john)" + echo + echo "Examples:" + echo " feature/button-john" + echo " fix/233-login-bug-john" echo exit 1 fi diff --git a/.mise.toml b/.mise.toml index 97f4f456..625d7472 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,3 +1,3 @@ [tools] node = "22.22.2" -pnpm = "9.7.1" +pnpm = "10.33.0" diff --git a/.templates/component/.storybook/main.ts b/.templates/component/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/.templates/component/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/.templates/component/.storybook/preview.ts b/.templates/component/.storybook/preview.ts deleted file mode 100644 index 82ec7ed0..00000000 --- a/.templates/component/.storybook/preview.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/.templates/component/package.json b/.templates/component/package.json index 5e2cb05f..fd02dc70 100644 --- a/.templates/component/package.json +++ b/.templates/component/package.json @@ -14,28 +14,18 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "dev:storybook": "storybook dev -p 6006", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", "prepack": "pnpm run build" }, "devDependencies": { - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "catalog:react", "happy-dom": "catalog:", "react": "catalog:react", "react-dom": "catalog:react", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -61,6 +51,8 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false, + "sideEffects": [ + "**/*.css" + ], "private": true } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 72f42207..322d7ab5 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["biomejs.biome", "github.vscode-github-actions", "hverlin.mise-vscode",] + "recommendations": ["biomejs.biome", "github.vscode-github-actions", "hverlin.mise-vscode"] } diff --git a/AGENTS.md b/AGENTS.md index 5a88ceaa..9900f2b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,4 +24,4 @@ strict TypeScript를 사용하고, 각 패키지의 `src/index.ts`에서 공개 ## 커밋 및 PR 가이드라인 -`feat(chip): add removable variant`와 같은 Conventional Commits를 사용하세요. scope는 짧게, subject는 영어로, `commitlint.config.ts`를 만족하도록 50자 이내로 작성하세요. PR은 `.github/PULL_REQUEST_TEMPLATE.md`를 따르세요: 변경 사항 요약, UI 변경 시 시각 자료 첨부, 스펙과 테스트 추가 여부 확인. 배포 패키지에 영향을 주는 변경이면 `.changeset` 항목을 포함하고 PR 설명에 릴리스 영향을 명시하세요. +[Conventional Commits](https://www.conventionalcommits.org/) 사용. 커밋 타입: `build | chore | ci | docs | feat | fix | perf | refactor | revert | style | test` ([`config-conventional`](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) 기준). 브랜치: `/-` (하이픈 최소 1개). 카테고리: `feature | fix | docs | style | refactor | test | deploy | chore | settings`. 특수 브랜치 `main`, `dev/*`는 규칙 제외. PR은 `.github/PULL_REQUEST_TEMPLATE.md` 준수. 배포 패키지 변경 시 `.changeset` 항목 포함. diff --git a/CLAUDE.md b/CLAUDE.md index b41e5bb6..af367bca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,8 +9,8 @@ ## 명령어 ```bash -mise install # Node v22.22.2 + pnpm 9.7.1 설치 (.mise.toml 기반) -pnpm install # 의존성 설치 (pnpm 9.7.1, Node v22.22.2) +mise install # Node v22.22.2 + pnpm 10.33.0 설치 (.mise.toml 기반) +pnpm install # 의존성 설치 (pnpm 10.33.0, Node v22.22.2) pnpm dev:storybook # Storybook 개발 서버 실행 (:6006) pnpm build:storybook # Storybook 빌드 pnpm lint # Biome 린트 + 수정 (변경된 패키지) @@ -68,12 +68,14 @@ export type ButtonSize = (typeof ButtonSize)[keyof typeof ButtonSize]; ## 커밋 컨벤션 -형식: `type(scope): subject` — 영어만, subject 최대 50자, scope 최대 20자. -타입: feat, fix, hotfix, chore, refactor, release, test, docs, ci, build. +`type(scope): subject` — [conventional commits](https://www.conventionalcommits.org/) + [`config-conventional`](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional). +타입: `build | chore | ci | docs | feat | fix | perf | refactor | revert | style | test` ## 브랜치 네이밍 -`/-` (이슈 번호는 선택) +`/-` — 하이픈 최소 1개 필수. +카테고리: `feature | fix | docs | style | refactor | test | deploy | chore | settings` +특수: `main`, `dev/*` ## 배포 diff --git a/LICENSE b/LICENSE index 726334b1..fd2defc6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 SIPE +Copyright (c) 2024-2026 SIPE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index da54a0d8..ef29f818 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![](./public/og-image.png) # Sipe Design System -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/sipe-team/side/blob/main/LICENSE) ![Tool Manager](https://img.shields.io/badge/mise-latest-purple?logo=mise) ![Package Manager](https://img.shields.io/badge/pnpm-9.7.1-orange?logo=pnpm) [![Storybook](https://img.shields.io/badge/Storybook-8.4.7-ff4785?logo=storybook)](https://storybook.sipe.team/?path=/docs/what-is-side--docs) ![Tests](https://img.shields.io/badge/Vitest-2.1.4-green?logo=vitest) [![codecov](https://codecov.io/gh/sipe-team/side/branch/changeset-release%2Fmain/graph/badge.svg?token=1TNLVUFPXC)](https://codecov.io/gh/sipe-team/side) Github Stars +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/sipe-team/side/blob/main/LICENSE) ![Tool Manager](https://img.shields.io/badge/mise-latest-purple?logo=mise) ![Package Manager](https://img.shields.io/badge/pnpm-10.33.0-orange?logo=pnpm) [![Storybook](https://img.shields.io/badge/Storybook-8.4.7-ff4785?logo=storybook)](https://storybook.sipe.team/?path=/docs/what-is-side--docs) ![Tests](https://img.shields.io/badge/Vitest-2.1.8-green?logo=vitest) [![codecov](https://codecov.io/gh/sipe-team/side/branch/changeset-release%2Fmain/graph/badge.svg?token=1TNLVUFPXC)](https://codecov.io/gh/sipe-team/side) Github Stars Sipe Design System is a monorepo-based component library built to modernize and standardize the official Sipe website. Drawing inspiration from our existing design patterns, we're creating a robust, type-safe, and accessible component system that can be used across all Sipe projects. @@ -72,6 +72,6 @@ We warmly welcome contributions from the community, whether you're a Sipe team m ## License -Copyright (c) 2026 SIPE, Inc. See [LICENSE](./LICENSE) for details. +Copyright (c) 2024-2026 SIPE, Inc. See [LICENSE](./LICENSE) for details. diff --git a/commitlint.config.ts b/commitlint.config.ts index 4f0cec6d..79065713 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -1,40 +1,8 @@ -const englishOnly = /^[A-Za-z0-9\s!@#$%^&*(),.?":{}|<>_-]+$/; - -// https://commitlint.js.org/reference/configuration.html#typescript-configuration -import { RuleConfigSeverity, type UserConfig } from '@commitlint/types'; - -const Configuration: UserConfig = { +// https://www.conventionalcommits.org +// https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional +// +// @commitlint/config-conventional 허용 타입: +// build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test +export default { extends: ['@commitlint/config-conventional'], - // parserPreset: '', - // formatter: '', - // https://commitlint.js.org/reference/plugins.html#working-with-plugins - plugins: [ - { - rules: { - 'subject-english-only': ({ subject }) => { - if (!subject) return [true, '']; - const valid = englishOnly.test(subject); - return [valid, 'Commit subject must contain only English characters']; - }, - }, - }, - ], - rules: { - // https://commitlint.js.org/reference/rules.html - 'type-empty': [RuleConfigSeverity.Error, 'never'], - 'subject-empty': [RuleConfigSeverity.Error, 'never'], - 'subject-max-length': [RuleConfigSeverity.Error, 'always', 50], - 'scope-max-length': [RuleConfigSeverity.Error, 'always', 20], - - // custom rules - 'subject-english-only': [RuleConfigSeverity.Error, 'always'], - }, - - prompt: { - settings: {}, - messages: {}, - questions: {}, - }, }; - -export default Configuration; diff --git a/package.json b/package.json index 4a17ca6b..3379c28d 100644 --- a/package.json +++ b/package.json @@ -12,17 +12,19 @@ "build:storybook": "storybook build public", "serve:storybook": "serve storybook-static -p 6006", "create:component": "tsx scripts/createComponent.ts create", + "build": "pnpm --filter './packages/*' -r build", + "typecheck": "pnpm --filter './packages/*' -r typecheck", "test": "vitest", + "test:coverage": "vitest --coverage", "clean": "pnpm --filter './www' clean && pnpm --filter './packages/*' clean" }, "devDependencies": { "@biomejs/biome": "^2.4.10", "@changesets/cli": "^2.27.9", "@clack/prompts": "^0.9.0", - "@commitlint/cli": "^19.6.1", - "@commitlint/config-conventional": "^19.6.0", - "@commitlint/cz-commitlint": "^19.6.1", - "@commitlint/types": "^19.5.0", + "@commitlint/cli": "^20.5.0", + "@commitlint/config-conventional": "^20.5.0", + "@commitlint/cz-commitlint": "^20.5.1", "@storybook/addon-docs": "^8.4.7", "@storybook/addon-essentials": "catalog:", "@storybook/addon-interactions": "catalog:", @@ -43,6 +45,7 @@ "chromatic": "^11.19.0", "clipanion": "4.0.0-rc.4", "commitizen": "^4.3.1", + "inquirer": "^9.3.8", "globals": "^15.14.0", "husky": "^9.1.7", "knip": "catalog:", @@ -55,10 +58,10 @@ "vite": "catalog:", "vitest": "catalog:" }, - "packageManager": "pnpm@9.7.1", + "packageManager": "pnpm@10.33.0", "engines": { "node": ">=22.22.2 <23", - "pnpm": "9.7.1" + "pnpm": "10.33.0" }, "lint-staged": { "*.{ts,tsx,css,js}": [ diff --git a/packages/accordion/.storybook/main.ts b/packages/accordion/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/packages/accordion/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/accordion/.storybook/preview.ts b/packages/accordion/.storybook/preview.ts deleted file mode 100644 index 6c95c9f1..00000000 --- a/packages/accordion/.storybook/preview.ts +++ /dev/null @@ -1,8 +0,0 @@ -import 'sanitize.css'; -import 'sanitize.css/typography.css'; - -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/accordion/package.json b/packages/accordion/package.json index f89f41f5..3373e2b1 100644 --- a/packages/accordion/package.json +++ b/packages/accordion/package.json @@ -14,10 +14,9 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", "dev:storybook": "storybook dev -p 6006", - "lint:biome": "pnpm exec biome lint", - "lint:eslint": "pnpm exec eslint", + "build:storybook": "storybook build", + "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", "prepack": "pnpm run build" @@ -32,20 +31,12 @@ }, "devDependencies": { "@sipe-team/typography": "workspace:*", - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "^18.3.12", "happy-dom": "catalog:", "react": "^18.3.1", "react-dom": "^18.3.1", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -71,5 +62,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/avatar/.storybook/main.ts b/packages/avatar/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/packages/avatar/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/avatar/.storybook/preview.ts b/packages/avatar/.storybook/preview.ts deleted file mode 100644 index 82ec7ed0..00000000 --- a/packages/avatar/.storybook/preview.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/avatar/package.json b/packages/avatar/package.json index f172ac61..b8fd38ca 100644 --- a/packages/avatar/package.json +++ b/packages/avatar/package.json @@ -14,13 +14,11 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", - "dev:storybook": "storybook dev -p 6006", "lint": "biome lint .", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "@radix-ui/react-slot": "catalog:", @@ -30,13 +28,6 @@ }, "devDependencies": { "@faker-js/faker": "catalog:", - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@types/react": "catalog:react", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", @@ -44,7 +35,6 @@ "happy-dom": "catalog:", "react": "catalog:react", "react-dom": "catalog:react", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" diff --git a/packages/avatar/vite.config.ts b/packages/avatar/vite.config.ts deleted file mode 100644 index 2484cb4e..00000000 --- a/packages/avatar/vite.config.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../../vite.config'; diff --git a/packages/badge/.storybook/main.ts b/packages/badge/.storybook/main.ts deleted file mode 100644 index 3724f800..00000000 --- a/packages/badge/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-links'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/badge/.storybook/preview.ts b/packages/badge/.storybook/preview.ts deleted file mode 100644 index 6c95c9f1..00000000 --- a/packages/badge/.storybook/preview.ts +++ /dev/null @@ -1,8 +0,0 @@ -import 'sanitize.css'; -import 'sanitize.css/typography.css'; - -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/badge/package.json b/packages/badge/package.json index 5811f1ec..ef3dd148 100644 --- a/packages/badge/package.json +++ b/packages/badge/package.json @@ -14,13 +14,13 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", "dev:storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "@sipe-team/tokens": "workspace:*", @@ -28,13 +28,6 @@ "clsx": "catalog:" }, "devDependencies": { - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "catalog:react", @@ -43,7 +36,6 @@ "react": "catalog:react", "react-dom": "catalog:react", "sanitize.css": "^13.0.0", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -69,5 +61,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/button/.storybook/main.ts b/packages/button/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/packages/button/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/button/.storybook/preview.ts b/packages/button/.storybook/preview.ts deleted file mode 100644 index 82ec7ed0..00000000 --- a/packages/button/.storybook/preview.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/button/package.json b/packages/button/package.json index d1c3bc05..54b54db9 100644 --- a/packages/button/package.json +++ b/packages/button/package.json @@ -14,13 +14,13 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", "dev:storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "@radix-ui/react-slot": "catalog:", @@ -30,13 +30,6 @@ "clsx": "catalog:" }, "devDependencies": { - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "catalog:react", @@ -45,7 +38,6 @@ "react": "catalog:react", "react-dom": "catalog:react", "sanitize.css": "^13.0.0", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -71,5 +63,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/button/src/Button.css.ts b/packages/button/src/Button.css.ts index d6cbe620..b0f73c58 100644 --- a/packages/button/src/Button.css.ts +++ b/packages/button/src/Button.css.ts @@ -22,6 +22,10 @@ export const button = recipe({ transition: 'all 0.2s ease-in-out', border: 'none', fontFamily: vars.typography.fontFamily, + ':focus-visible': { + outline: `2px solid ${vars.color.primary}`, + outlineOffset: '2px', + }, }, variants: { variant: { diff --git a/packages/button/src/Button.test.tsx b/packages/button/src/Button.test.tsx index 4c513c63..7530a64d 100644 --- a/packages/button/src/Button.test.tsx +++ b/packages/button/src/Button.test.tsx @@ -78,3 +78,21 @@ test('large size works correctly', () => { expect(button).toBeInTheDocument(); expect(button.className).toBeTruthy(); }); + +test('type="button" is used as default when type is not provided', () => { + render(); + + const button = screen.getByRole('button'); + + // Should default to type="button" + expect(button).toHaveAttribute('type', 'button'); +}); + +test('type="submit" works correctly', () => { + render(); + + const button = screen.getByRole('button'); + + // Should allow overriding type + expect(button).toHaveAttribute('type', 'submit'); +}); diff --git a/packages/button/src/Button.tsx b/packages/button/src/Button.tsx index f5082d93..98a343ac 100644 --- a/packages/button/src/Button.tsx +++ b/packages/button/src/Button.tsx @@ -29,6 +29,7 @@ export const Button = forwardRef(function Button( { variant = ButtonVariant.filled, size = ButtonSize.lg, + type = 'button', asChild, disabled, className: _className, @@ -39,5 +40,5 @@ export const Button = forwardRef(function Button( const Comp = asChild ? Slot : 'button'; const className = cx(styles.button({ variant, size }), { [styles.disabled]: disabled }, _className); - return ; + return ; }); diff --git a/packages/card/.storybook/main.ts b/packages/card/.storybook/main.ts deleted file mode 100644 index 3724f800..00000000 --- a/packages/card/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-links'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/card/.storybook/preview.ts b/packages/card/.storybook/preview.ts deleted file mode 100644 index 6c95c9f1..00000000 --- a/packages/card/.storybook/preview.ts +++ /dev/null @@ -1,8 +0,0 @@ -import 'sanitize.css'; -import 'sanitize.css/typography.css'; - -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/card/package.json b/packages/card/package.json index ddffe184..443e22cf 100644 --- a/packages/card/package.json +++ b/packages/card/package.json @@ -14,13 +14,13 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", "dev:storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "@radix-ui/react-slot": "catalog:", @@ -28,13 +28,6 @@ "clsx": "catalog:" }, "devDependencies": { - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "catalog:react", @@ -44,7 +37,6 @@ "react": "catalog:react", "react-dom": "catalog:react", "sanitize.css": "^13.0.0", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -70,5 +62,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/checkbox/.storybook/main.ts b/packages/checkbox/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/packages/checkbox/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/checkbox/.storybook/preview.ts b/packages/checkbox/.storybook/preview.ts deleted file mode 100644 index 82ec7ed0..00000000 --- a/packages/checkbox/.storybook/preview.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/checkbox/package.json b/packages/checkbox/package.json index cc2e8576..5aa03c5b 100644 --- a/packages/checkbox/package.json +++ b/packages/checkbox/package.json @@ -14,13 +14,13 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", "dev:storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "@sipe-team/tokens": "workspace:*", @@ -28,13 +28,6 @@ "@vanilla-extract/recipes": "catalog:" }, "devDependencies": { - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@testing-library/user-event": "catalog:", @@ -43,7 +36,6 @@ "happy-dom": "catalog:", "react": "catalog:react", "react-dom": "catalog:react", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -69,5 +61,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/chip/package.json b/packages/chip/package.json index a67cbde6..e1868eca 100644 --- a/packages/chip/package.json +++ b/packages/chip/package.json @@ -19,10 +19,12 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "lint": "pnpm exec biome lint", "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "prepack": "pnpm run build" }, "dependencies": { "@radix-ui/react-slot": "catalog:", diff --git a/packages/divider/.storybook/main.ts b/packages/divider/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/packages/divider/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/divider/.storybook/preview.ts b/packages/divider/.storybook/preview.ts deleted file mode 100644 index 82ec7ed0..00000000 --- a/packages/divider/.storybook/preview.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/divider/package.json b/packages/divider/package.json index f30a34a4..184c0589 100644 --- a/packages/divider/package.json +++ b/packages/divider/package.json @@ -14,13 +14,13 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", "dev:storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "clsx": "catalog:", @@ -28,13 +28,6 @@ }, "devDependencies": { "@sipe-team/typography": "workspace:*", - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "catalog:react", @@ -42,7 +35,6 @@ "happy-dom": "catalog:", "react": "catalog:react", "react-dom": "catalog:react", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -67,5 +59,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/flex/.storybook/main.ts b/packages/flex/.storybook/main.ts deleted file mode 100644 index e7f301d3..00000000 --- a/packages/flex/.storybook/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { StorybookConfig } from '@storybook/react-vite'; - -export default { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, -} satisfies StorybookConfig; diff --git a/packages/flex/.storybook/preview.ts b/packages/flex/.storybook/preview.ts deleted file mode 100644 index 82ec7ed0..00000000 --- a/packages/flex/.storybook/preview.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Preview } from '@storybook/react'; - -export default { - tags: ['autodocs'], -} satisfies Preview; diff --git a/packages/flex/package.json b/packages/flex/package.json index 3d350f5e..ae466dbb 100644 --- a/packages/flex/package.json +++ b/packages/flex/package.json @@ -14,13 +14,13 @@ ], "scripts": { "build": "tsup", - "build:storybook": "storybook build", - "clean": "rm -rf node_modules dist", "dev:storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", "lint": "pnpm exec biome lint", "test": "vitest", "typecheck": "tsc", - "prepack": "pnpm run build" + "prepack": "pnpm run build", + "clean": "rm -rf node_modules dist" }, "dependencies": { "@radix-ui/react-slot": "catalog:", @@ -28,13 +28,6 @@ }, "devDependencies": { "@faker-js/faker": "catalog:", - "@storybook/addon-essentials": "catalog:", - "@storybook/addon-interactions": "catalog:", - "@storybook/addon-links": "catalog:", - "@storybook/blocks": "catalog:", - "@storybook/react": "catalog:", - "@storybook/react-vite": "catalog:", - "@storybook/test": "catalog:", "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@types/react": "catalog:react", @@ -42,7 +35,6 @@ "happy-dom": "catalog:", "react": "catalog:react", "react-dom": "catalog:react", - "storybook": "catalog:", "tsup": "catalog:", "typescript": "catalog:", "vitest": "catalog:" @@ -68,5 +60,7 @@ "./styles.css": "./dist/index.css" } }, - "sideEffects": false + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/flex/src/Flex.stories.tsx b/packages/flex/src/Flex.stories.tsx index 5df52165..9e522be8 100644 --- a/packages/flex/src/Flex.stories.tsx +++ b/packages/flex/src/Flex.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Flex } from './Flex'; +import { FLEX_ALIGNS, FLEX_DIRECTIONS, FLEX_JUSTIFY_CONTENTS, FLEX_WRAPS } from './constants'; const meta = { title: 'Components/Flex', @@ -9,22 +10,22 @@ const meta = { argTypes: { direction: { control: 'select', - options: ['row', 'column', 'row-reverse', 'column-reverse'], + options: FLEX_DIRECTIONS, description: 'Flex direction', }, align: { control: 'select', - options: ['flex-start', 'flex-end', 'center', 'stretch', 'baseline', 'normal'], + options: FLEX_ALIGNS, description: 'Align items', }, justify: { control: 'select', - options: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly', 'normal'], + options: FLEX_JUSTIFY_CONTENTS, description: 'Justify content', }, wrap: { control: 'select', - options: ['nowrap', 'wrap', 'wrap-reverse'], + options: FLEX_WRAPS, description: 'Flex wrap', }, gap: { diff --git a/packages/flex/src/Flex.test.tsx b/packages/flex/src/Flex.test.tsx index eb71a4bd..6382395f 100644 --- a/packages/flex/src/Flex.test.tsx +++ b/packages/flex/src/Flex.test.tsx @@ -1,13 +1,14 @@ -import { type CSSProperties, createElement } from 'react'; +import { type CSSProperties, createElement, createRef } from 'react'; import { faker } from '@faker-js/faker'; import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { Flex } from './Flex'; +import { FLEX_ALIGNS, FLEX_DIRECTIONS, FLEX_JUSTIFY_CONTENTS, FLEX_WRAPS } from './constants'; describe('Flex', () => { - it('flex 컴포넌트는 props를 전달하지 않으면 flex의 기본값으로 설정된다.', () => { + it('uses the default flex styles when no props are provided', () => { render(
item 1
@@ -26,24 +27,17 @@ describe('Flex', () => { }); }); - it('flex 컴포넌트에 className을 주입하면 추가로 전달한다.', () => { + it('passes through a custom className', () => { const customClassName = faker.word.noun(); render(); expect(screen.getByTestId('flex-container')).toHaveClass(customClassName); }); - describe('flex 속성', () => { + describe('flex props', () => { describe('justify', () => { - it.each([ - { justifyContent: 'flex-start' }, - { justifyContent: 'flex-end' }, - { justifyContent: 'center' }, - { justifyContent: 'space-between' }, - { justifyContent: 'space-around' }, - { justifyContent: 'space-evenly' }, - ] satisfies Array<{ - justifyContent: CSSProperties['justifyContent']; - }>)('flex의 justify prop에 $justifyContent 속성을 주입하면 해당 속성을 적용한다.', ({ justifyContent }) => { + it.each( + FLEX_JUSTIFY_CONTENTS.map((justifyContent) => ({ justifyContent })), + )('applies justifyContent: $justifyContent when justify is provided', ({ justifyContent }) => { render(
item 1
@@ -57,15 +51,9 @@ describe('Flex', () => { }); describe('align', () => { - it.each([ - { alignItems: 'flex-start' }, - { alignItems: 'flex-end' }, - { alignItems: 'center' }, - { alignItems: 'baseline' }, - { alignItems: 'stretch' }, - ] satisfies Array<{ - alignItems: CSSProperties['alignItems']; - }>)('flex의 align prop에 $alignItems 속성을 주입하면 해당 속성을 적용한다.', ({ alignItems }) => { + it.each( + FLEX_ALIGNS.map((alignItems) => ({ alignItems })), + )('applies alignItems: $alignItems when align is provided', ({ alignItems }) => { render(
item 1
@@ -79,9 +67,9 @@ describe('Flex', () => { }); describe('wrap', () => { - it.each([{ wrap: 'wrap' }, { wrap: 'nowrap' }, { wrap: 'wrap-reverse' }] satisfies Array<{ - wrap: CSSProperties['flexWrap']; - }>)('flex의 wrap prop에 $wrap 속성을 주입하면 해당 속성을 적용한다.', ({ wrap }) => { + it.each( + FLEX_WRAPS.map((wrap) => ({ wrap })), + )('applies flexWrap: $wrap when wrap is provided', ({ wrap }) => { render(
item 1
@@ -95,15 +83,9 @@ describe('Flex', () => { }); describe('direction', () => { - it.each([ - { direction: 'row' }, - { direction: 'column' }, - { direction: 'row-reverse' }, - { direction: 'column-reverse' }, - { direction: 'column-reverse' }, - ] satisfies Array<{ - direction: CSSProperties['flexDirection']; - }>)('flex의 direction prop에 $direction 속성을 주입하면 해당 속성을 적용한다.', ({ direction }) => { + it.each( + FLEX_DIRECTIONS.map((direction) => ({ direction })), + )('applies flexDirection: $direction when direction is provided', ({ direction }) => { render(
item 1
@@ -114,89 +96,89 @@ describe('Flex', () => { const flexContainer = screen.getByTestId('flex-container'); expect(flexContainer).toHaveStyle({ flexDirection: direction }); }); + }); + + describe('basis', () => { + it.each([ + { basis: '100px' }, + { basis: '100%' }, + { basis: 'auto' }, + { basis: '10rem' }, + { basis: 'content' }, + ] satisfies Array<{ + basis: CSSProperties['flexBasis']; + }>)('applies flexBasis: $basis when basis is provided', ({ basis }) => { + render( + +
item 1
+
item 2
+
, + ); - describe('basis', () => { - it.each([ - { basis: '100px' }, - { basis: '100%' }, - { basis: 'auto' }, - { basis: '10rem' }, - { basis: 'content' }, - ] satisfies Array<{ - basis: CSSProperties['flexBasis']; - }>)('flex의 basis prop에 $basis 속성을 주입하면 해당 속성을 적용한다.', ({ basis }) => { - render( - -
item 1
-
item 2
-
, - ); - - const flexContainer = screen.getByTestId('flex-container'); - expect(flexContainer).toHaveStyle({ flexBasis: basis }); - }); + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ flexBasis: basis }); }); + }); + + describe('grow', () => { + it.each([{ grow: 0 }, { grow: 1 }, { grow: 2 }] satisfies Array<{ + grow: CSSProperties['flexGrow']; + }>)('applies flexGrow: $grow when grow is provided', ({ grow }) => { + render( + +
item 1
+
item 2
+
, + ); - describe('grow', () => { - it.each([{ grow: 0 }, { grow: 1 }, { grow: 2 }] satisfies Array<{ - grow: CSSProperties['flexGrow']; - }>)('flex의 grow prop에 $grow 속성을 주입하면 해당 속성을 적용한다.', ({ grow }) => { - render( - -
item 1
-
item 2
-
, - ); - - const flexContainer = screen.getByTestId('flex-container'); - expect(flexContainer).toHaveStyle({ flexGrow: grow }); - }); + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ flexGrow: grow }); }); + }); + + describe('shrink', () => { + it.each([{ shrink: 0 }, { shrink: 1 }, { shrink: 2 }] satisfies Array<{ + shrink: CSSProperties['flexShrink']; + }>)('applies flexShrink: $shrink when shrink is provided', ({ shrink }) => { + render( + +
item 1
+
item 2
+
, + ); - describe('shrink', () => { - it.each([{ shrink: 0 }, { shrink: 1 }, { shrink: 2 }] satisfies Array<{ - shrink: CSSProperties['flexShrink']; - }>)('flex의 shrink prop에 $shrink 속성을 주입하면 해당 속성을 적용한다.', ({ shrink }) => { - render( - -
item 1
-
item 2
-
, - ); - - const flexContainer = screen.getByTestId('flex-container'); - expect(flexContainer).toHaveStyle({ flexShrink: shrink }); - }); + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ flexShrink: shrink }); }); + }); + + describe('inline', () => { + it('renders with inline-flex display when inline is true', () => { + render( + +
item 1
+
item 2
+
, + ); - describe('inline', () => { - it('flex의 inline prop에 true 속성을 주입하면 해당 속성을 적용한다.', () => { - render( - -
item 1
-
item 2
-
, - ); - - const flexContainer = screen.getByTestId('flex-container'); - expect(flexContainer).toHaveStyle({ display: 'inline-flex' }); - }); + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ display: 'inline-flex' }); }); + }); + + describe('gap', () => { + it.each([{ gap: '10px' }, { gap: '1rem' }] satisfies Array<{ + gap: CSSProperties['gap']; + }>)('applies gap: $gap when gap is provided', ({ gap }) => { + render( + +
item 1
+
item 2
+
, + ); - describe('gap', () => { - it.each([{ gap: '10px' }, { gap: '1rem' }] satisfies Array<{ - gap: CSSProperties['gap']; - }>)('flex의 gap prop에 $gap 속성을 주입하면 해당 속성을 적용한다.', ({ gap }) => { - render( - -
item 1
-
item 2
-
, - ); - - const flexContainer = screen.getByTestId('flex-container'); - expect(flexContainer).toHaveStyle({ gap }); - }); + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ gap }); }); }); }); @@ -207,7 +189,7 @@ describe('Flex', () => { { style: { alignItems: 'center' } }, { style: { flexWrap: 'wrap' } }, { style: { flexDirection: 'column' } }, - ] satisfies Array<{ style: CSSProperties }>)('flex의 style prop에 $style 속성을 주입하면 해당 속성을 적용한다.', ({ + ] satisfies Array<{ style: CSSProperties }>)('applies style overrides from the style prop: $style', ({ style, }) => { render( @@ -220,6 +202,61 @@ describe('Flex', () => { const flexContainer = screen.getByTestId('flex-container'); expect(flexContainer).toHaveStyle(style); }); + + it('allows the style prop to override inline style values for the same CSS properties', () => { + render( + +
item 1
+
item 2
+
, + ); + + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ + flexBasis: '50%', + gap: '16px', + }); + }); + }); + + describe('consumer usage patterns', () => { + it('preserves the expected layout styles when direction, align, and gap are combined', () => { + render( + +
item 1
+
item 2
+
, + ); + + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '12px', + }); + }); + + it('preserves the expected layout styles when inline, justify, and gap are combined', () => { + render( + +
item 1
+
item 2
+
, + ); + + const flexContainer = screen.getByTestId('flex-container'); + expect(flexContainer).toHaveStyle({ + display: 'inline-flex', + justifyContent: 'center', + gap: '8px', + }); + }); }); describe('polymorphic', () => { @@ -230,7 +267,7 @@ describe('Flex', () => { 'input', 'label', 'div', - ])('flex의 asChild prop에 true 속성을 주입하면 자식으로 %s 엘리먼트가 전달되면 해당 엘리먼트의 태그로 렌더링된다', (element) => { + ])('renders as the child %s element when asChild is true', (element) => { render( {createElement(element)} @@ -241,5 +278,44 @@ describe('Flex', () => { expect(flexContainer).toBeInTheDocument(); expect(flexContainer.tagName.toLowerCase()).toBe(element); }); + + it('passes className, style, and rest props to the child element when asChild is used', () => { + render( + +