-
Notifications
You must be signed in to change notification settings - Fork 4
feat(theme): implement theme provider #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughVanilla Extract 기반의 CSS-in-TypeScript 스타일링 시스템 도입을 위해, 기존 CSS 모듈 및 글로벌 선언을 제거하고, 각 컴포넌트(Button, Badge, Card, Avatar)에 대해 Vanilla Extract 스타일 파일을 추가 및 적용했습니다. 토큰 패키지와 테마 패키지에도 디자인 토큰, 테마 컨트랙트, 유틸리티, 다크/라이트 테마, Storybook 설정 등이 새로 도입되었습니다. 빌드 및 테스트 도구 설정도 Vanilla Extract 플러그인으로 통일되었습니다. Changes
Changes Table
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
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으로 스타일 적용된 버튼 렌더링
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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에 정상적으로 적용되는지 확인하거나, 필요 시viteFinal에vanillaExtractPlugin()을 추가하여 플러그인을 명시적으로 통합하는 것을 검토해주세요.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: 포괄적인 테마 계약이 체계적으로 구성되었습니다.테마 계약이 다음과 같은 장점을 가지고 있습니다:
- 색상, 간격, 타이포그래피, 경계, 레이아웃 등 다양한 카테고리로 잘 구성되어 있습니다.
- 각 카테고리 내에서 세부 변수들이 논리적으로 구성되어 있습니다.
- 네이밍 컨벤션이 일관되게 적용되어 있습니다.
- z-index 값들이 의미 있는 이름으로 정의되어 있어 가독성이 좋습니다.
이 계약은 애플리케이션 전체에서 일관된 디자인 시스템을 구축하는 데 훌륭한 기반이 될 것입니다. 그러나 몇 가지 개선 사항이 있습니다:
- 색상 섹션에서 기본 색상(primary, black, white)과 기능적 색상(background, text, border)을 구분하는 것이 좋을 수 있습니다.
- 다양한 상태(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 }, });이후
compoundVariants를colors.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 issueweak 변형에 대한 테스트가 실패합니다
파이프라인 실패 로그에 따르면 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를 사용한 것은 좋은 접근 방식입니다. 다음과 같은 장점이 있습니다:
- base 스타일과 variants를 명확하게 분리했습니다.
- 기본 variant를 지정하여 사용성을 높였습니다.
- 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.tsLength 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 issueThemeDisplay 컴포넌트가 포괄적인 테마 시각화를 제공합니다.
이 컴포넌트는 테마 변수들을 시각적으로 확인할 수 있는 좋은 방법을 제공합니다. 다음과 같은 개선 사항을 고려할 수 있습니다:
- 테마 변수 목록이 하드코딩되어 있습니다. 더 많은 변수들을 포함하거나 동적으로 가져올 수 있는 방법을 고려하세요.
- 인라인 스타일이 많아 코드의 가독성을 저하시킵니다. vanilla-extract 스타일을 사용하면 이 문제를 해결할 수 있습니다.
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'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 issueWeak 버전 hover 시 글자·배경 색상이 동일하여 텍스트가 보이지 않는 문제
variant: weak컴파운드 변형에서backgroundColor와color를 동일하게 설정해 버려 글자가 배경에 묻혀 사라집니다. 의도라면 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.tsxLength 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} + /> + );
Changes
--side-???와 같이 전역 CSS 변수를 선언했습니다.Additional Discussion Points
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Documentation
Chores