Skip to content

Conversation

@froggy1014
Copy link
Contributor

@froggy1014 froggy1014 commented May 7, 2025

Changes

Global CSS Variable
Screenshot 2025-05-08 at 00 59 52
--side-??? 와 같이 전역 CSS 변수를 선언했습니다.
Responsive Style
Screen Recording 2025-05-08 at 00 31 58
기기 크기에 따라 mobile, tablet, desktop, wide 등의 브레이크포인트를 정의\n
responsive style은 여기 코드 사실상 가져오긴 했습니다.
export const stack = style(
  responsiveStyle({
    // 모바일 기기에서의 스타일
    mobile: {
      backgroundColor: color.primary,
    },
    // 태블릿 기기에서의 스타일
    tablet: {
      backgroundColor: 'red',
    },
    // 데스크톱 기기에서의 스타일
    desktop: {
      backgroundColor: 'blue',
    },
    // 와이드 스크린에서의 스타일
    wide: {
      backgroundColor: 'yellow',
    },
  }),
);
import { ThemeProvider } from '@sipe-team/theme'

function App = (children) => {
  return (
     <ThemeProvider>
        {children}
     </ThemeProvider>
  )
}

Additional Discussion Points

  • 일단 혹시 몰라서, Dark Mode를 추가해놨습니다.
  • 일단 제가 임의로 토큰값들을 추가했습니다. 추후 넓혀가야 할 것 같습니다.

Summary by CodeRabbit

  • New Features

    • 테마 시스템(@sipe-team/theme) 및 디자인 토큰(@sipe-team/tokens) 패키지 추가로 라이트/다크 모드 전환, 글로벌 CSS 변수, 테마 컨텍스트, 반응형 스타일 유틸리티, 그리드/타이포그래피/컬러 등 다양한 토큰 제공
    • Avatar, Badge, Button, Card 컴포넌트에 vanilla-extract 기반 CSS-in-TypeScript 스타일링 적용 및 타입 안전성 강화
    • Storybook 구성 및 테마 프리뷰, 컴포넌트별 스토리/테스트 확장
  • Refactor

    • 기존 CSS 모듈 및 인라인 스타일 제거, CSS-in-TypeScript로 스타일 일원화
    • 컴포넌트 타입 및 props 구조 개선, 스타일 관련 헬퍼 함수 제거
  • Bug Fixes

    • 스타일 및 토큰 적용 일관성 강화로 색상, 크기, 테마 관련 이슈 개선
  • Documentation

    • 테마 패키지의 README 및 Storybook 문서 추가
  • Chores

    • 패키지 의존성 및 빌드/테스트 도구(vite, tsup, vitest) 구성 업데이트 및 vanilla-extract 플러그인 도입

@changeset-bot
Copy link

changeset-bot bot commented May 7, 2025

⚠️ No Changeset found

Latest commit: f05b2df

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented May 7, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Vanilla Extract 기반의 CSS-in-TypeScript 스타일링 시스템 도입을 위해, 기존 CSS 모듈 및 글로벌 선언을 제거하고, 각 컴포넌트(Button, Badge, Card, Avatar)에 대해 Vanilla Extract 스타일 파일을 추가 및 적용했습니다. 토큰 패키지와 테마 패키지에도 디자인 토큰, 테마 컨트랙트, 유틸리티, 다크/라이트 테마, Storybook 설정 등이 새로 도입되었습니다. 빌드 및 테스트 도구 설정도 Vanilla Extract 플러그인으로 통일되었습니다.

Changes

파일/경로 그룹 변경 요약
package.json, pnpm-workspace.yaml, tsup.config.ts, vite.config.ts, vitest.config.ts Vanilla Extract 및 관련 플러그인, Vite, esbuild, tsup, vitest 설정 추가 및 통합
packages/button/global.d.ts, packages/avatar/global.d.ts CSS 모듈 글로벌 타입 선언 제거
packages/button/src/Button.module.css, packages/badge/src/Badge.module.css, packages/card/src/Card.module.css, packages/avatar/src/Avatar.module.css 기존 CSS 모듈 파일 삭제
packages/button/src/Button.css.ts, packages/badge/src/Badge.css.ts, packages/card/src/Card.css.ts, packages/avatar/src/Avatar.css.ts Vanilla Extract 기반 스타일 파일 신규 추가 및 스타일 시스템 전환
packages/button/src/Button.tsx, packages/badge/src/Badge.tsx, packages/card/src/Card.tsx, packages/avatar/src/Avatar.tsx 스타일 적용 방식을 CSS 모듈/인라인 → Vanilla Extract 스타일로 리팩터링, 타입 및 props 구조 일부 변경
packages/button/package.json, packages/badge/package.json, packages/card/package.json, packages/avatar/package.json devDependencies에 Vanilla Extract 패키지 추가, 파일 배열 포맷 단일화, 일부 의존성 수정
packages/badge/src/Badge.stories.tsx, packages/card/src/Card.stories.tsx, packages/theme/src/ThemeProvider.stories.tsx 스토리북 스토리 확장 및 스타일/테마 관련 스토리 추가
packages/badge/src/Badge.test.tsx, packages/avatar/src/Avatar.test.tsx 토큰 기반 스타일 값 사용 및 import 정리
packages/card/src/index.ts, packages/avatar/src/index.ts index 파일에서 CSS 및 컴포넌트 모듈 재구성 및 export 확장
packages/card/src/Card.module.css, packages/card/src/Card.stories.tsx Card 컴포넌트 스토리 확장 및 기존 CSS 모듈 삭제
packages/avatar/tsup.config.ts, packages/avatar/vite.config.ts, packages/avatar/vitest.config.ts 공통 빌드/테스트 설정 공유 및 간소화
packages/tokens/src/* border, breakpoints, grid, opacity, radius, shadows, spacing, zIndex 등 디자인 토큰 신규 추가 및 색상 팔레트 업데이트, index에서 토큰 일괄 export
packages/theme/* 테마 패키지 신규 추가: 테마 컨트랙트, 다크/라이트 글로벌 테마, ThemeProvider, useTheme 훅, Storybook 설정, README 등 도입

Changes Table

파일/경로 그룹 변경 요약
package.json, pnpm-workspace.yaml, tsup.config.ts, vite.config.ts, vitest.config.ts Vanilla Extract 및 관련 플러그인, Vite, esbuild, tsup, vitest 설정 추가 및 통합
packages/button/global.d.ts, packages/avatar/global.d.ts CSS 모듈 글로벌 타입 선언 제거
packages/button/src/Button.module.css, packages/badge/src/Badge.module.css, packages/card/src/Card.module.css, packages/avatar/src/Avatar.module.css 기존 CSS 모듈 파일 삭제
packages/button/src/Button.css.ts, packages/badge/src/Badge.css.ts, packages/card/src/Card.css.ts, packages/avatar/src/Avatar.css.ts Vanilla Extract 기반 스타일 파일 신규 추가 및 스타일 시스템 전환
packages/button/src/Button.tsx, packages/badge/src/Badge.tsx, packages/card/src/Card.tsx, packages/avatar/src/Avatar.tsx 스타일 적용 방식을 CSS 모듈/인라인 → Vanilla Extract 스타일로 리팩터링, 타입 및 props 구조 일부 변경
packages/button/package.json, packages/badge/package.json, packages/card/package.json, packages/avatar/package.json devDependencies에 Vanilla Extract 패키지 추가, 파일 배열 포맷 단일화, 일부 의존성 수정
packages/badge/src/Badge.stories.tsx, packages/card/src/Card.stories.tsx, packages/theme/src/ThemeProvider.stories.tsx 스토리북 스토리 확장 및 스타일/테마 관련 스토리 추가
packages/badge/src/Badge.test.tsx, packages/avatar/src/Avatar.test.tsx 토큰 기반 스타일 값 사용 및 import 정리
packages/card/src/index.ts, packages/avatar/src/index.ts index 파일에서 CSS 및 컴포넌트 모듈 재구성 및 export 확장
packages/card/src/Card.module.css, packages/card/src/Card.stories.tsx Card 컴포넌트 스토리 확장 및 기존 CSS 모듈 삭제
packages/avatar/tsup.config.ts, packages/avatar/vite.config.ts, packages/avatar/vitest.config.ts 공통 빌드/테스트 설정 공유 및 간소화
packages/tokens/src/* border, breakpoints, grid, opacity, radius, shadows, spacing, zIndex 등 디자인 토큰 신규 추가 및 색상 팔레트 업데이트, index에서 토큰 일괄 export
packages/theme/* 테마 패키지 신규 추가: 테마 컨트랙트, 다크/라이트 글로벌 테마, ThemeProvider, useTheme 훅, Storybook 설정, README 등 도입

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant ThemeProvider
    participant useTheme
    participant ThemeContext

    App->>ThemeProvider: ThemeProvider({ defaultMode, children })
    ThemeProvider->>ThemeContext: Provide { mode, setMode, toggleMode }
    useTheme->>ThemeContext: useContext(ThemeContext)
    useTheme-->>App: { mode, setMode, toggleMode }
    App-->>ThemeProvider: Render children
Loading
sequenceDiagram
    participant Button
    participant Button.css.ts

    Button->>Button.css.ts: import { button, disabled }
    Button-->>Button: button({ color, variant })로 className 생성
    Button-->>Button: disabled prop에 따라 styles.disabled 적용
    Button-->>DOM: className으로 스타일 적용된 버튼 렌더링
Loading

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • kimdaeyeobbb
  • hy57in
  • developerjhp
  • bae-sh
  • heeji289
  • yeojini
  • SEMIN-97
  • jiji-hoon96
  • noahluftyang

Poem

🍃 바닐라 향 솔솔~
CSS 모듈은 안녕, 타입스크립트와 춤을!
버튼, 뱃지, 카드, 아바타
스타일 한 입에 쏙-
테마도 다크/라이트,
토큰은 든든,
SIPE 토끼는 기분이 좋아요! 🐰✨


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@froggy1014 froggy1014 changed the base branch from main to release/v1 May 7, 2025 16:06
@froggy1014 froggy1014 closed this May 7, 2025
@froggy1014 froggy1014 reopened this May 7, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 6

🧹 Nitpick comments (17)
vite.config.ts (1)

1-6: Vite 플러그인 설정 검토 필요
vanillaExtractPlugin()을 plugins 배열에 추가했으나 기본 옵션을 사용합니다. CSS 클래스 네이밍 규칙(identifierName), sourcemap, esbuild loader 설정 등 프로젝트 요구사항에 맞추어 추가 옵션이 필요한지 검토해주세요.

packages/theme/.storybook/main.ts (1)

1-10: Storybook Vite 설정 및 Vanilla Extract 통합 검증 요청
@storybook/react-vite 빌더로 stories와 addons 구성이 적절해 보입니다. 다만, Vanilla Extract 스타일이 Storybook에 정상적으로 적용되는지 확인하거나, 필요 시 viteFinalvanillaExtractPlugin()을 추가하여 플러그인을 명시적으로 통합하는 것을 검토해주세요.

packages/avatar/src/Avatar.css.ts (2)

6-6: 하드코딩된 색상 값 대신 테마 토큰을 사용하는 것이 좋습니다.

배경색에 하드코딩된 값(#e2e8f0)을 사용하고 있습니다. 테마 시스템과의 일관성을 위해 @sipe-team/tokens에서 제공하는 색상 토큰을 사용하는 것이 좋습니다.

- backgroundColor: '#e2e8f0',
+ backgroundColor: colorToken.gray100, // 또는 적절한 토큰 값

53-57: 폴백 스타일에 하드코딩된 색상값이 있습니다.

폴백 텍스트 색상에 하드코딩된 값(#2d3748)을 사용하고 있습니다. 테마 시스템과의 일관성을 위해 @sipe-team/tokens에서 제공하는 색상 토큰을 사용하는 것이 좋습니다.

- color: '#2d3748',
+ color: colorToken.gray800, // 또는 적절한 토큰 값
packages/card/src/Card.stories.tsx (2)

25-42: UI 시각화를 위한 도우미 컴포넌트가 잘 구현되었습니다.

RatioVisualizer 컴포넌트는 카드 비율을 직관적으로 보여주는 좋은 접근법입니다. 이는 사용자가 다양한 비율 옵션의 효과를 시각적으로 이해하는 데 도움이 됩니다.

다만, 인라인 스타일 대신 vanilla-extract를 사용하여 스타일을 정의하는 것이 프로젝트의 일관성을 유지하는 데 도움이 될 수 있습니다.


54-97: Filled 변형의 모든 비율을 효과적으로 보여주는 스토리입니다.

다양한 비율 옵션을 그리드 레이아웃으로 시각화한 방식이 좋습니다. 각 카드 아래에 설명 텍스트를 추가하여 사용자 이해도를 높였습니다.

하지만 반복적인 코드 구조가 있습니다. 이 부분은 배열과 map을 사용하여 리팩토링할 수 있습니다.

-export const FilledVariant: Story = {
-  render: () => (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '40px', maxWidth: '1000px' }}>
-      <h3>Filled Variant - All Ratios</h3>
-      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '30px' }}>
-        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
-          <Card variant="filled" ratio="rectangle" style={{ width: '250px' }}>
-            <RatioVisualizer label="Rectangle" ratio="16:9" />
-          </Card>
-          <div>Rectangle (16:9)</div>
-        </div>
-
-        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
-          <Card variant="filled" ratio="square" style={{ width: '250px' }}>
-            <RatioVisualizer label="Square" ratio="1:1" />
-          </Card>
-          <div>Square (1:1)</div>
-        </div>
-
-        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
-          <Card variant="filled" ratio="wide" style={{ width: '250px' }}>
-            <RatioVisualizer label="Wide" ratio="21:9" />
-          </Card>
-          <div>Wide (21:9)</div>
-        </div>
-
-        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
-          <Card variant="filled" ratio="portrait" style={{ width: '200px' }}>
-            <RatioVisualizer label="Portrait" ratio="3:4" />
-          </Card>
-          <div>Portrait (3:4)</div>
-        </div>
-
-        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
-          <Card variant="filled" ratio="auto" style={{ width: '250px', height: '150px' }}>
-            <RatioVisualizer label="Auto" ratio="Custom" />
-          </Card>
-          <div>Auto (Custom Size)</div>
-        </div>
-      </div>
-    </div>
-  ),
-};
+export const FilledVariant: Story = {
+  render: () => {
+    const ratios = [
+      { name: 'Rectangle', ratio: 'rectangle', display: '16:9', width: '250px' },
+      { name: 'Square', ratio: 'square', display: '1:1', width: '250px' },
+      { name: 'Wide', ratio: 'wide', display: '21:9', width: '250px' },
+      { name: 'Portrait', ratio: 'portrait', display: '3:4', width: '200px' },
+      { name: 'Auto', ratio: 'auto', display: 'Custom', width: '250px', height: '150px' },
+    ];
+    
+    return (
+      <div style={{ display: 'flex', flexDirection: 'column', gap: '40px', maxWidth: '1000px' }}>
+        <h3>Filled Variant - All Ratios</h3>
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '30px' }}>
+          {ratios.map((item) => (
+            <div key={item.ratio} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
+              <Card 
+                variant="filled" 
+                ratio={item.ratio} 
+                style={{ width: item.width, ...(item.height ? { height: item.height } : {}) }}
+              >
+                <RatioVisualizer label={item.name} ratio={item.display} />
+              </Card>
+              <div>{item.name} ({item.display})</div>
+            </div>
+          ))}
+        </div>
+      </div>
+    );
+  },
+};
packages/theme/src/themeContract.css.ts (1)

5-115: 포괄적인 테마 계약이 체계적으로 구성되었습니다.

테마 계약이 다음과 같은 장점을 가지고 있습니다:

  1. 색상, 간격, 타이포그래피, 경계, 레이아웃 등 다양한 카테고리로 잘 구성되어 있습니다.
  2. 각 카테고리 내에서 세부 변수들이 논리적으로 구성되어 있습니다.
  3. 네이밍 컨벤션이 일관되게 적용되어 있습니다.
  4. z-index 값들이 의미 있는 이름으로 정의되어 있어 가독성이 좋습니다.

이 계약은 애플리케이션 전체에서 일관된 디자인 시스템을 구축하는 데 훌륭한 기반이 될 것입니다. 그러나 몇 가지 개선 사항이 있습니다:

  1. 색상 섹션에서 기본 색상(primary, black, white)과 기능적 색상(background, text, border)을 구분하는 것이 좋을 수 있습니다.
  2. 다양한 상태(error, warning, success, info 등)에 대한 색상이 없습니다.
 export const vars = createGlobalThemeContract(
   {
     color: {
+      // 기본 색상
       primary: 'color-primary',
       black: 'color-black',
       white: 'color-white',
+      // 기능적 색상
       background: 'color-background',
       text: 'color-text',
       border: 'color-border',
+      // 상태 색상
+      error: 'color-error',
+      warning: 'color-warning',
+      success: 'color-success',
+      info: 'color-info',
     },
     // 나머지 코드는 동일하게 유지
packages/theme/src/ThemeProvider.stories.tsx (1)

23-39: 테마를 활용한 CustomButton 컴포넌트가 구현되었습니다.

테마 변수를 사용하여 스타일링된 버튼 컴포넌트가 잘 구현되었습니다. 다만, 인라인 스타일 대신 vanilla-extract를 사용하여 스타일을 정의하는 것이 일관성 있는 접근 방식이 될 것입니다.

+import { style } from '@vanilla-extract/css';
+
+const customButtonStyle = style({
+  padding: '8px 16px',
+  backgroundColor: theme.color.primary,
+  color: theme.color.black,
+  border: 'none',
+  borderRadius: '4px',
+  cursor: 'pointer',
+  fontWeight: theme.typography.fontWeight.medium,
+});
+
 const CustomButton = ({ onClick, children }: { onClick: () => void; children: React.ReactNode }) => (
   <button
     type="button"
     onClick={onClick}
-    style={{
-      padding: '8px 16px',
-      backgroundColor: theme.color.primary,
-      color: theme.color.black,
-      border: 'none',
-      borderRadius: '4px',
-      cursor: 'pointer',
-      fontWeight: theme.typography.fontWeight.medium,
-    }}
+    className={customButtonStyle}
   >
     {children}
   </button>
 );
packages/theme/src/ThemeProvider.tsx (2)

30-46: 테마 모드 관리 로직이 잘 구현되었습니다만, 로컬 스토리지 지원을 추가하는 것이 좋을 것 같습니다.

테마 전환 로직이 잘 구현되어 있지만, 페이지 새로고침 시에도 사용자의
테마 선택을 유지하기 위해 localStorage를 사용하는 것이 좋을 것 같습니다.

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultMode = 'light' }) => {
-  const [mode, setMode] = useState<ThemeMode>(defaultMode);
+  const [mode, setMode] = useState<ThemeMode>(() => {
+    // 로컬 스토리지에서 테마 모드 불러오기
+    const savedMode = localStorage.getItem('theme-mode');
+    return (savedMode === 'light' || savedMode === 'dark') ? savedMode : defaultMode;
+  });

  // Apply theme mode to HTML element
  useEffect(() => {
    const htmlElement = document.documentElement;

    if (mode === 'dark') {
      htmlElement.setAttribute('data-theme', 'dark');
    } else {
      htmlElement.removeAttribute('data-theme');
    }
+    
+    // 로컬 스토리지에 테마 모드 저장
+    localStorage.setItem('theme-mode', mode);
  }, [mode]);

30-46: 시스템 테마 설정 감지 기능 추가를 고려해보세요.

사용자 경험을 향상시키기 위해 시스템 테마 설정(라이트/다크 모드)을 감지하고 초기 테마를 설정하는 기능을 추가하는 것이 좋을 것 같습니다.

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultMode = 'light' }) => {
-  const [mode, setMode] = useState<ThemeMode>(defaultMode);
+  const [mode, setMode] = useState<ThemeMode>(() => {
+    // 로컬 스토리지에서 저장된 테마 확인
+    const savedMode = localStorage.getItem('theme-mode');
+    if (savedMode === 'light' || savedMode === 'dark') {
+      return savedMode as ThemeMode;
+    }
+    
+    // 시스템 테마 설정 확인
+    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+      return 'dark';
+    }
+    
+    return defaultMode;
+  });
+
+  // 시스템 테마 변경 감지
+  useEffect(() => {
+    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+    
+    const handleChange = (e: MediaQueryListEvent) => {
+      const savedMode = localStorage.getItem('theme-mode');
+      // 사용자가 명시적으로 테마를 설정하지 않은 경우에만 시스템 테마를 따름
+      if (!savedMode) {
+        setMode(e.matches ? 'dark' : 'light');
+      }
+    };
+    
+    mediaQuery.addEventListener('change', handleChange);
+    
+    return () => mediaQuery.removeEventListener('change', handleChange);
+  }, []);
packages/badge/src/Badge.css.ts (4)

17-23: 베이스 스타일이 간결하게 정의되었습니다만, 토큰 사용을 권장합니다.

root 스타일에서 border-radius 값을 하드코딩하는 대신 디자인 시스템의 radius 토큰을 사용하면 더 일관된 디자인을 유지할 수 있습니다.

// Base styles for the badge
export const root = style({
-  borderRadius: 8,
+  borderRadius: theme.radius.md, // 디자인 시스템의 radius 토큰 사용
  display: 'inline-flex',
  alignItems: 'center',
  justifyContent: 'center',
});

25-36: 사이즈 변형이 잘 정의되어 있습니다만, 토큰 사용을 고려해보세요.

패딩 값을 하드코딩하는 대신 spacing 토큰을 사용하면 디자인 시스템과의 일관성을 높일 수 있습니다.

// Size variants
export const size = styleVariants({
  [BadgeSize.small]: {
-    padding: '4px 8px',
+    padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
  },
  [BadgeSize.medium]: {
-    padding: '8px 16px',
+    padding: `${theme.spacing.sm} ${theme.spacing.md}`,
  },
  [BadgeSize.large]: {
-    padding: '12px 24px',
+    padding: `${theme.spacing.md} ${theme.spacing.lg}`,
  },
});

51-65: 변형 스타일이 토큰을 잘 활용하고 있습니다.

색상에 디자인 토큰을 사용하여 일관성을 유지하고 있습니다. 다만, border 두께에도 토큰을 사용하면 더 좋을 것 같습니다.

export const variant = styleVariants({
  [BadgeVariant.filled]: {
    backgroundColor: colorToken.cyan900,
    border: 'none',
  },
  [BadgeVariant.outline]: {
    backgroundColor: 'transparent',
-    border: `2px solid ${colorToken.cyan900}`,
+    border: `${theme.border.width.medium} ${theme.border.style.solid} ${colorToken.cyan900}`,
  },
  [BadgeVariant.weak]: {
    backgroundColor: colorToken.gray200,
    border: 'none',
  },
});

67-71: 텍스트 스타일이 잘 정의되어 있습니다만, 폰트 웨이트에 토큰 사용을 권장합니다.

폰트 웨이트 값을 하드코딩하는 대신 타이포그래피 토큰을 사용하면 더 일관된 디자인을 유지할 수 있습니다.

export const text = style({
  color: colorToken.cyan300,
-  fontWeight: 600,
+  fontWeight: theme.typography.fontWeight.medium, // 디자인 시스템의 fontWeight 토큰 사용
});
packages/button/src/Button.css.ts (2)

5-9: 하드코드된 색상 상수를 디자인 토큰으로 치환하는 것을 고려해 주세요
primaryColor, blackColor, whiteColor처럼 직접 선언된 값들은 테마 시스템·토큰 시스템과 분리돼 있어 유지보수가 어렵습니다. 이미 packages/theme에서 색상 토큰을 정의하고 있으므로, 해당 토큰을 import해서 재사용하면 전체 프로젝트의 일관성을 높이고 다크모드 대응도 수월해집니다.
예) import { color } from '@sipe-team/tokens';primaryColor = color.cyan300


52-176: 컴파운드 변형 중복 정의를 함수형 헬퍼로 줄일 수 있습니다
color-variant 조합이 총 9개(3 × 3)인데, 각 조합마다 유사한 코드가 반복됩니다. 매트릭스 형태의 스타일은 헬퍼를 만들어 동적으로 생성하면 유지보수성이 높아집니다. 예를 들어:

const filledStyle = (bg: string, fg: string) => ({
  backgroundColor: bg,
  color: fg,
  ':hover': { backgroundColor: darken(bg), color: fg },
});

const outlineStyle = (border: string, fg: string) => ({
  border: `1px solid ${border}`,
  color: fg,
  ':hover': { backgroundColor: border, color: fgOnHover },
});

이후 compoundVariantscolors.map(...).flat() 방식으로 자동생성하면 중복을 크게 줄일 수 있습니다.

packages/theme/src/themes.css.ts (1)

121-130: 색상 팔레트가 최소한의 기본 색상만 포함돼 있습니다
primary, background, text, border, black, white만 지정돼 있어 보조 컬러(secondary, danger, success 등) 사용 시 토큰 불일치가 발생할 수 있습니다. 추후 컴포넌트 전반에서 색상 변형을 안정적으로 사용하려면, 토큰 레이어에 정의된 주요 팔레트를 모두 매핑해 두는 편이 좋습니다.

🛑 Comments failed to post (6)
packages/badge/src/Badge.test.tsx (1)

38-43: ⚠️ Potential issue

weak 변형에 대한 테스트가 실패합니다

파이프라인 실패 로그에 따르면 weak 변형에 대한 테스트가 실패하고 있습니다. 테스트는 배경색이 #e4e4e7일 것으로 예상하지만, 실제로는 다른 값을 받고 있습니다. 이는 Badge 컴포넌트의 스타일 정의와 테스트 기대값 사이에 불일치가 있음을 나타냅니다.

-test('variant가 weak인 경우 배경색 gray200로 형태를 적용한다.', () => {
+test(`variant가 weak인 경우 배경색 ${colorToken.gray200}로 형태를 적용한다.`, () => {
  render(<Badge variant="weak">테스트</Badge>);

  expect(screen.getByRole('status')).toHaveStyle({
-    backgroundColor: '#e4e4e7',
+    backgroundColor: colorToken.gray200,
  });
});

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: coverage

[failure] 41-41: src/Badge.test.tsx > variant가 weak인 경우 배경색 gray200로 형태를 적용한다.
Error: expect(element).toHaveStyle()

  • Expected
  • Received
  • backgroundColor: #e4e4e7;
    ❯ src/Badge.test.tsx:41:38
🪛 GitHub Actions: Run tests and upload coverage

[error] 41-43: Test failure: expect(element).toHaveStyle() assertion failed. Expected backgroundColor: #e4e4e7 but received a different value.

🪛 GitHub Actions: CI

[error] 41-41: Test failure: expect(element).toHaveStyle() failed. Expected backgroundColor: #e4e4e7 but received a different value.

packages/card/src/Card.css.ts (1)

20-61: 💡 Verification agent

🧩 Analysis chain

recipe 패턴을 활용한 스타일 적용이 효과적으로 구현되었습니다.

Card 컴포넌트의 스타일링을 위해 @vanilla-extract/recipes를 사용한 것은 좋은 접근 방식입니다. 다음과 같은 장점이 있습니다:

  1. base 스타일과 variants를 명확하게 분리했습니다.
  2. 기본 variant를 지정하여 사용성을 높였습니다.
  3. aspect-ratio를 활용한 다양한 카드 비율 지원이 잘 구현되어 있습니다.

단, 접근성을 높이기 위해 border 스타일에 충분한 대비를 제공하는지 확인하는 것이 좋습니다.


🏁 Script executed:

#!/bin/bash
# Description: Check color contrast between border and background colors
# Since we can't perform actual WCAG contrast calculations in this environment,
# this script shows the colors used for borders and backgrounds

echo "Checking color values used in Card.css.ts:"
echo "Background colors:"
grep -A 1 "backgroundColor:" packages/card/src/Card.css.ts
echo "Border colors:"
grep -A 1 "border:" packages/card/src/Card.css.ts

# Also check the color values in the tokens file
echo "Color token definitions:"
grep -A 5 "gray50\|gray100\|gray200\|cyan300" packages/tokens/src/colors.ts

Length of output: 1089


카드 컴포넌트 테두리 색상 대비 개선 필요

테두리(border) 색상과 배경(background) 색상의 대비 비율이 WCAG AA 기준(최소 3:1)을 충족하지 않습니다. 현황을 확인한 결과:

  • Filled variant: background #f3f4f6(gray100) / border #e5e7eb(gray200) → 대비 약 1.1:1
  • Outline variant: background #f9fafb(gray50) / border #00ffff(cyan300) → 대비 약 1.2:1

아래 파일의 해당 부분을 더 어두운 색상으로 교체해 접근성 기준을 맞춰주세요.

• packages/card/src/Card.css.ts

  • [CardVariant.filled] border: color.gray200 → 예: color.gray500 이상
  • [CardVariant.outline] border: color.cyan300 → 예: color.cyan600 이상

예시 변경(diff):

@@ packages/card/src/Card.css.ts
   variants: {
     variant: {
       [CardVariant.filled]: {
         backgroundColor: color.gray100,
-        border: `1px solid ${color.gray200}`,
+        border: `1px solid ${color.gray500}`,
       },
       [CardVariant.outline]: {
         backgroundColor: color.gray50,
-        border: `1px solid ${color.cyan300}`,
+        border: `1px solid ${color.cyan600}`,
       },
     },

위와 같이 대비 비율을 높이면 시각장애를 가진 사용자도 테두리를 명확히 인지할 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const card = recipe({
  base: {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: '12px',
    padding: '20px',
  },
  variants: {
    variant: {
      [CardVariant.filled]: {
        backgroundColor: color.gray100,
        border: `1px solid ${color.gray500}`,
      },
      [CardVariant.outline]: {
        backgroundColor: color.gray50,
        border: `1px solid ${color.cyan600}`,
      },
    },
    ratio: {
      [CardRatio.square]: {
        aspectRatio: '1 / 1',
      },
      [CardRatio.rectangle]: {
        aspectRatio: '16 / 9',
      },
      [CardRatio.wide]: {
        aspectRatio: '21 / 9',
      },
      [CardRatio.portrait]: {
        aspectRatio: '3 / 4',
      },
      [CardRatio.auto]: {
        aspectRatio: 'auto',
      },
    },
  },
  defaultVariants: {
    variant: CardVariant.filled,
    ratio: CardRatio.rectangle,
  },
});
packages/card/src/Card.stories.tsx (1)

99-142: 🛠️ Refactor suggestion

Outline 변형 스토리도 효과적으로 구현되었지만 코드 중복이 있습니다.

이 스토리는 앞서 제안한 것과 같은 방식으로 배열과 map을 사용하여 리팩토링할 수 있습니다. 또한, FilledVariant와 OutlineVariant 사이에 중복이 많아 이를 공통 함수로 추출할 수도 있습니다.

+// 공통 컴포넌트 생성하여 코드 중복 제거
+const RatioCards = ({ variant }: { variant: 'filled' | 'outline' }) => {
+  const ratios = [
+    { name: 'Rectangle', ratio: 'rectangle', display: '16:9', width: '250px' },
+    { name: 'Square', ratio: 'square', display: '1:1', width: '250px' },
+    { name: 'Wide', ratio: 'wide', display: '21:9', width: '250px' },
+    { name: 'Portrait', ratio: 'portrait', display: '3:4', width: '200px' },
+    { name: 'Auto', ratio: 'auto', display: 'Custom', width: '250px', height: '150px' },
+  ];
+  
+  return (
+    <div style={{ display: 'flex', flexDirection: 'column', gap: '40px', maxWidth: '1000px' }}>
+      <h3>{variant === 'filled' ? 'Filled' : 'Outline'} Variant - All Ratios</h3>
+      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '30px' }}>
+        {ratios.map((item) => (
+          <div key={item.ratio} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
+            <Card 
+              variant={variant} 
+              ratio={item.ratio} 
+              style={{ width: item.width, ...(item.height ? { height: item.height } : {}) }}
+            >
+              <RatioVisualizer label={item.name} ratio={item.display} />
+            </Card>
+            <div>{item.name} ({item.display})</div>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export const FilledVariant: Story = {
+  render: () => <RatioCards variant="filled" />,
+};
+
+export const OutlineVariant: Story = {
+  render: () => <RatioCards variant="outline" />,
+};

Committable suggestion skipped: line range outside the PR's diff.

packages/theme/src/ThemeProvider.stories.tsx (1)

47-164: 🛠️ Refactor suggestion

⚠️ Potential issue

ThemeDisplay 컴포넌트가 포괄적인 테마 시각화를 제공합니다.

이 컴포넌트는 테마 변수들을 시각적으로 확인할 수 있는 좋은 방법을 제공합니다. 다음과 같은 개선 사항을 고려할 수 있습니다:

  1. 테마 변수 목록이 하드코딩되어 있습니다. 더 많은 변수들을 포함하거나 동적으로 가져올 수 있는 방법을 고려하세요.
  2. 인라인 스타일이 많아 코드의 가독성을 저하시킵니다. vanilla-extract 스타일을 사용하면 이 문제를 해결할 수 있습니다.
  3. getComputedStyle을 사용하는 부분은 서버 사이드 렌더링 환경에서 오류를 발생시킬 수 있습니다. 이 부분을 useEffect 내에서 처리하는 것이 좋습니다.

또한, 브라우저 환경에서만 동작하는 코드가 포함되어 있어 SSR(Server-Side Rendering) 시 문제가 발생할 수 있습니다.

 const ThemeDisplay = () => {
   const { mode } = useTheme();
+  const [computedVars, setComputedVars] = React.useState<Record<string, string>>({});
 
   const themeVars = [
     {
       category: 'Colors',
       vars: ['color-primary', 'color-background', 'color-text', 'color-border', 'color-black', 'color-white'],
     },
   ];
 
+  React.useEffect(() => {
+    const newComputedVars: Record<string, string> = {};
+    themeVars.forEach(({ vars }) => {
+      vars.forEach((varName) => {
+        const fullVarName = `--side-${varName}`;
+        newComputedVars[varName] = getComputedStyle(document.documentElement).getPropertyValue(fullVarName);
+      });
+    });
+    setComputedVars(newComputedVars);
+  }, [mode]); // mode가 변경될 때마다 다시 계산
+
   return (
     /* 기존 JSX 부분... */

     {themeVars.map(({ category, vars }) => (
       <div key={category} style={{ marginBottom: theme.spacing.md }}>
         <h2>{category}</h2>
         <div
           style={{
             display: 'grid',
             gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
             gap: theme.spacing.sm,
           }}
         >
           {vars.map((varName) => {
             const fullVarName = `--side-${varName}`;
-            const value = getComputedStyle(document.documentElement).getPropertyValue(fullVarName);
+            const value = computedVars[varName] || '';

             return (
               /* 기존 JSX 부분... */
             );
           })}
         </div>
       </div>
     ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const ThemeDisplay = () => {
  const { mode } = useTheme();
+  const [computedVars, setComputedVars] = React.useState<Record<string, string>>({});

  const themeVars = [
    {
      category: 'Colors',
      vars: [
        'color-primary',
        'color-background',
        'color-text',
        'color-border',
        'color-black',
        'color-white',
      ],
    },
  ];

+  React.useEffect(() => {
+    const newComputedVars: Record<string, string> = {};
+    themeVars.forEach(({ vars }) => {
+      vars.forEach((varName) => {
+        const fullVarName = `--side-${varName}`;
+        newComputedVars[varName] =
+          getComputedStyle(document.documentElement).getPropertyValue(fullVarName);
+      });
+    });
+    setComputedVars(newComputedVars);
+  }, [mode]);

  return (
    <div
      style={{
        maxWidth: '800px',
        color: theme.color.text,
        backgroundColor: theme.color.background,
        padding: theme.spacing.md,
        borderRadius: '8px',
        transition: 'all 0.3s ease',
        fontFamily: theme.typography.fontFamily,
      }}
    >
      <h1 style={{ marginBottom: theme.spacing.md }}>Current Theme: {mode}</h1>

      <ThemeToggler />

      <div style={{ marginTop: theme.spacing.xl }}>
        <h2>Theme Preview</h2>
        <div
          style={{
            padding: theme.spacing.md,
            border: `1px solid ${theme.color.border}`,
            borderRadius: '8px',
          }}
        >
          <p style={{ color: theme.color.text, fontSize: theme.typography.fontSize.medium }}>
            This is regular text using the theme&apos;s text color and medium font size.
          </p>
          <p
            style={{
              color: theme.color.primary,
              fontSize: theme.typography.fontSize.large,
              fontWeight: theme.typography.fontWeight.bold,
            }}
          >
            This is primary-colored text with large font size and bold weight.
          </p>
          <div
            style={{
              backgroundColor: theme.color.primary,
              color: mode === 'dark' ? theme.color.black : theme.color.white,
              padding: theme.spacing.sm,
              borderRadius: '4px',
              marginTop: theme.spacing.sm,
              fontWeight: theme.typography.fontWeight.medium,
            }}
          >
            This is a primary background container
          </div>
        </div>
      </div>

      <div style={{ marginTop: theme.spacing.md }}>
        {themeVars.map(({ category, vars }) => (
          <div key={category} style={{ marginBottom: theme.spacing.md }}>
            <h2>{category}</h2>
            <div
              style={{
                display: 'grid',
                gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
                gap: theme.spacing.sm,
              }}
            >
              {vars.map((varName) => {
                const fullVarName = `--side-${varName}`;
-               const value = getComputedStyle(document.documentElement).getPropertyValue(fullVarName);
+               const value = computedVars[varName] || '';

                return (
                  <div
                    key={varName}
                    style={{
                      padding: theme.spacing.sm,
                      border: `1px solid ${theme.color.border}`,
                      borderRadius: '4px',
                      marginBottom: theme.spacing.xs,
                    }}
                  >
                    <div style={{ fontWeight: theme.typography.fontWeight.bold }}>
                      {fullVarName}
                    </div>
                    <div
                      style={{
                        display: 'flex',
                        alignItems: 'center',
                        gap: theme.spacing.xs,
                      }}
                    >
                      {varName.includes('color') && !varName.includes('typography') && (
                        <div
                          style={{
                            width: '20px',
                            height: '20px',
                            backgroundColor: value,
                            border: '1px solid #ccc',
                            borderRadius: '4px',
                          }}
                        />
                      )}
                      <code>{value}</code>
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};
packages/button/src/Button.css.ts (1)

82-93: ⚠️ Potential issue

Weak 버전 hover 시 글자·배경 색상이 동일하여 텍스트가 보이지 않는 문제
variant: weak 컴파운드 변형에서 backgroundColorcolor를 동일하게 설정해 버려 글자가 배경에 묻혀 사라집니다. 의도라면 opacity 등으로 대비를 주어야 하고, 아니라면 텍스트 색상을 대비되는 색상(예: black / white)으로 수정해야 합니다.

-        ':hover': {
-          backgroundColor: primaryColor,
-          color: primaryColor,
+        ':hover': {
+          backgroundColor: primaryColor,
+          color: blackColor,          // 또는 theme token 사용
         },

동일 문제가 black/white weak 변형에도 반복됩니다. 한 번에 수정하거나 공통 로직으로 추출하는 것을 권장합니다.

Also applies to: 124-133, 168-174

packages/button/src/Button.tsx (1)

37-40: 💡 Verification agent

🧩 Analysis chain

asChild 사용 시 disabled 속성 전달을 조건부로 변경할 필요가 있습니다
Slot으로 감싸면 내부 엘리먼트가 <a>disabled를 지원하지 않는 태그일 수 있습니다. 현재는 무조건 전달되어 콘솔 경고가 발생할 수 있으니 아래처럼 조건부로 처리해 주세요.

-  return <Comp ref={ref} className={className} disabled={disabled} {...props} />;
+  return (
+    <Comp
+      ref={ref}
+      className={className}
+      {...(asChild ? {} : { disabled })}
+      {...props}
+    />
+  );

🏁 Script executed:

#!/bin/bash
sed -n '1,50p' packages/button/src/Button.tsx

Length of output: 1221


asChild 사용 시 disabled 속성 조건부 전파 적용 필요

asChild가 true일 때 Slot이 감싸는 엘리먼트가 <a> 등 disabled를 지원하지 않는 태그일 수 있으므로, disabled 속성을 조건부로 전달하도록 수정해주세요.

• 파일: packages/button/src/Button.tsx
• 위치: return 구문

-  return <Comp ref={ref} className={className} disabled={disabled} {...props} />;
+  return (
+    <Comp
+      ref={ref}
+      className={className}
+      {...(asChild ? {} : { disabled })}
+      {...props}
+    />
+  );

@froggy1014 froggy1014 linked an issue May 16, 2025 that may be closed by this pull request
@froggy1014 froggy1014 merged commit 141020b into release/v1 May 17, 2025
3 checks passed
@froggy1014 froggy1014 deleted the feat/theme-token branch May 17, 2025 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] theme 기능 제공

3 participants