@kimdw-rtk/ui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.babelrc +12 -0
  2. package/.turbo/turbo-check-types.log +2 -0
  3. package/.turbo/turbo-lint.log +12 -0
  4. package/.turbo/turbo-test.log +4084 -0
  5. package/.vscode/settings.json +4 -0
  6. package/eslint.config.mjs +4 -0
  7. package/jest.config.json +10 -0
  8. package/jest.setup.js +2 -0
  9. package/package.json +53 -0
  10. package/src/components/Accordion/Accordion.css.ts +29 -0
  11. package/src/components/Accordion/Accordion.spec.tsx +6 -0
  12. package/src/components/Accordion/Accordion.tsx +44 -0
  13. package/src/components/Accordion/AccordionContent.css.ts +29 -0
  14. package/src/components/Accordion/AccordionContent.tsx +87 -0
  15. package/src/components/Accordion/AccordionContext.ts +9 -0
  16. package/src/components/Accordion/AccordionTrigger.css.ts +46 -0
  17. package/src/components/Accordion/AccordionTrigger.tsx +41 -0
  18. package/src/components/Accordion/index.ts +3 -0
  19. package/src/components/Alert/index.tsx +25 -0
  20. package/src/components/Box/Box.css.ts +18 -0
  21. package/src/components/Box/Box.spec.tsx +6 -0
  22. package/src/components/Box/index.tsx +41 -0
  23. package/src/components/Button/Button.css.ts +241 -0
  24. package/src/components/Button/Button.spec.tsx +30 -0
  25. package/src/components/Button/index.tsx +60 -0
  26. package/src/components/Card/Card.css.ts +93 -0
  27. package/src/components/Card/Card.spec.tsx +24 -0
  28. package/src/components/Card/Card.tsx +41 -0
  29. package/src/components/Card/CardContent.css.ts +8 -0
  30. package/src/components/Card/CardContent.tsx +23 -0
  31. package/src/components/Card/CardInteraction.css.ts +11 -0
  32. package/src/components/Card/CardInteraction.tsx +36 -0
  33. package/src/components/Card/CardThumbnail.css.ts +6 -0
  34. package/src/components/Card/CardThumbnail.tsx +23 -0
  35. package/src/components/Card/index.ts +4 -0
  36. package/src/components/Chip/Chip.css.ts +75 -0
  37. package/src/components/Chip/Chip.spec.tsx +6 -0
  38. package/src/components/Chip/Chip.tsx +37 -0
  39. package/src/components/Chip/index.ts +1 -0
  40. package/src/components/Confirm/index.tsx +44 -0
  41. package/src/components/Dialog/Dialog.css.ts +25 -0
  42. package/src/components/Dialog/Dialog.spec.tsx +26 -0
  43. package/src/components/Dialog/Dialog.tsx +30 -0
  44. package/src/components/Dialog/DialogContent.css.ts +16 -0
  45. package/src/components/Dialog/DialogContent.tsx +26 -0
  46. package/src/components/Dialog/DialogFooter.css.ts +20 -0
  47. package/src/components/Dialog/DialogFooter.tsx +26 -0
  48. package/src/components/Dialog/DialogHeader.css.ts +31 -0
  49. package/src/components/Dialog/DialogHeader.tsx +37 -0
  50. package/src/components/Dialog/index.ts +4 -0
  51. package/src/components/Navigation/Navigation.spec.tsx +19 -0
  52. package/src/components/Navigation/NavigationAside.css.ts +7 -0
  53. package/src/components/Navigation/NavigationAside.tsx +23 -0
  54. package/src/components/Navigation/NavigationBar.css.ts +42 -0
  55. package/src/components/Navigation/NavigationBar.tsx +25 -0
  56. package/src/components/Navigation/NavigationContainer.css.ts +11 -0
  57. package/src/components/Navigation/NavigationContainer.tsx +26 -0
  58. package/src/components/Navigation/NavigationDrawer.css.ts +61 -0
  59. package/src/components/Navigation/NavigationDrawer.tsx +67 -0
  60. package/src/components/Navigation/NavigationItem.css.ts +43 -0
  61. package/src/components/Navigation/NavigationItem.tsx +24 -0
  62. package/src/components/Navigation/NavigationLogo.css.ts +5 -0
  63. package/src/components/Navigation/NavigationLogo.tsx +28 -0
  64. package/src/components/Navigation/NavigationMenu.css.ts +23 -0
  65. package/src/components/Navigation/NavigationMenu.tsx +25 -0
  66. package/src/components/Navigation/index.ts +7 -0
  67. package/src/components/Range/Range.css.ts +132 -0
  68. package/src/components/Range/Range.spec.tsx +6 -0
  69. package/src/components/Range/Range.tsx +90 -0
  70. package/src/components/Range/index.ts +1 -0
  71. package/src/components/ScrollArea/ScrollArea.css.ts +40 -0
  72. package/src/components/ScrollArea/ScrollArea.spec.tsx +6 -0
  73. package/src/components/ScrollArea/ScrollArea.tsx +68 -0
  74. package/src/components/ScrollArea/index.ts +1 -0
  75. package/src/components/Select/Select.css.ts +22 -0
  76. package/src/components/Select/Select.spec.tsx +65 -0
  77. package/src/components/Select/Select.tsx +111 -0
  78. package/src/components/Select/SelectContext.ts +59 -0
  79. package/src/components/Select/SelectOption.css.ts +14 -0
  80. package/src/components/Select/SelectOption.tsx +40 -0
  81. package/src/components/Select/SelectOptionList.css.ts +68 -0
  82. package/src/components/Select/SelectOptionList.tsx +59 -0
  83. package/src/components/Select/SelectTrigger.css.ts +73 -0
  84. package/src/components/Select/SelectTrigger.tsx +49 -0
  85. package/src/components/Select/index.tsx +2 -0
  86. package/src/components/Skeleton/Skeleton.css.ts +26 -0
  87. package/src/components/Skeleton/Skeleton.spec.tsx +6 -0
  88. package/src/components/Skeleton/index.tsx +27 -0
  89. package/src/components/Table/Table.css.ts +10 -0
  90. package/src/components/Table/Table.spec.tsx +12 -0
  91. package/src/components/Table/Table.tsx +27 -0
  92. package/src/components/Table/TableBody.tsx +14 -0
  93. package/src/components/Table/TableCell.css.ts +43 -0
  94. package/src/components/Table/TableCell.tsx +30 -0
  95. package/src/components/Table/TableHead.css.ts +10 -0
  96. package/src/components/Table/TableHead.tsx +30 -0
  97. package/src/components/Table/TableHeader.tsx +14 -0
  98. package/src/components/Table/TableRow.css.ts +3 -0
  99. package/src/components/Table/TableRow.tsx +24 -0
  100. package/src/components/Table/index.ts +6 -0
  101. package/src/components/Tabs/Tabs.spec.tsx +46 -0
  102. package/src/components/Tabs/Tabs.tsx +34 -0
  103. package/src/components/Tabs/TabsContent.tsx +32 -0
  104. package/src/components/Tabs/TabsList.css.ts +11 -0
  105. package/src/components/Tabs/TabsList.tsx +25 -0
  106. package/src/components/Tabs/TabsProvider.tsx +17 -0
  107. package/src/components/Tabs/TabsTrigger.css.ts +38 -0
  108. package/src/components/Tabs/TabsTrigger.tsx +43 -0
  109. package/src/components/Tabs/index.ts +4 -0
  110. package/src/components/TextField/TextField.css.ts +81 -0
  111. package/src/components/TextField/TextField.spec.tsx +6 -0
  112. package/src/components/TextField/index.tsx +38 -0
  113. package/src/components/Toast/Toast.css.ts +79 -0
  114. package/src/components/Toast/Toast.spec.tsx +6 -0
  115. package/src/components/Toast/index.tsx +48 -0
  116. package/src/components/Typography/Typography.css.ts +17 -0
  117. package/src/components/Typography/Typography.spec.tsx +35 -0
  118. package/src/components/Typography/index.tsx +57 -0
  119. package/src/components/index.ts +18 -0
  120. package/src/contexts/UIProvider.tsx +30 -0
  121. package/src/contexts/index.ts +1 -0
  122. package/src/hooks/index.ts +5 -0
  123. package/src/hooks/useDialog/index.tsx +51 -0
  124. package/src/hooks/useDialog/useDialog.spec.tsx +80 -0
  125. package/src/hooks/useMouseScroll/index.ts +63 -0
  126. package/src/hooks/usePointerSlider/index.ts +79 -0
  127. package/src/hooks/useRipple/index.tsx +152 -0
  128. package/src/hooks/useRipple/ripple.css.ts +40 -0
  129. package/src/hooks/useToast/ToastContainer.css.ts +12 -0
  130. package/src/hooks/useToast/ToastContainer.tsx +11 -0
  131. package/src/hooks/useToast/ToastProvider.tsx +131 -0
  132. package/src/hooks/useToast/index.ts +15 -0
  133. package/src/index.ts +8 -0
  134. package/src/styles/globalStyle.css.ts +36 -0
  135. package/src/styles/index.ts +4 -0
  136. package/src/styles/layers.css.ts +4 -0
  137. package/src/styles/overlay.css.ts +40 -0
  138. package/src/styles/sprinkles.css.ts +149 -0
  139. package/src/styles/sx.ts +13 -0
  140. package/src/tests/uiTest.tsx +54 -0
  141. package/src/themes/darkTheme.css.ts +30 -0
  142. package/src/themes/index.ts +3 -0
  143. package/src/themes/lightTheme.css.ts +30 -0
  144. package/src/themes/theme.css.ts +32 -0
  145. package/src/tokens/index.ts +5 -0
  146. package/src/tokens/scale/color.ts +604 -0
  147. package/src/tokens/semantic/breakpoint.ts +6 -0
  148. package/src/tokens/semantic/color.ts +10 -0
  149. package/src/tokens/semantic/spacing.ts +9 -0
  150. package/src/tokens/semantic/typography.ts +32 -0
  151. package/src/types/index.ts +1 -0
  152. package/src/types/ui.types.ts +26 -0
  153. package/src/utils/index.ts +1 -0
  154. package/src/utils/sprinklesUtils.ts +28 -0
  155. package/src/utils/styleUtils.css.ts +109 -0
  156. package/tsconfig.json +11 -0
  157. package/turbo/generators/config.ts +30 -0
  158. package/turbo/generators/templates/component.hbs +8 -0
@@ -0,0 +1,241 @@
1
+ import { createVar, globalStyle, keyframes, style } from '@vanilla-extract/css';
2
+
3
+ import { recipeWithLayer, styleWithLayer } from '#styleUtils';
4
+ import { theme } from '#themes';
5
+ import { semanticColor } from '#tokens';
6
+
7
+ import { SCALE_COLOR, type ScaleColor } from '../../tokens/scale/color';
8
+
9
+ const backgroundVar = createVar();
10
+ const foregroundVar = createVar();
11
+
12
+ const semanticColors = semanticColor.reduce(
13
+ (prev, color) => ({
14
+ ...prev,
15
+ [color]: styleWithLayer({
16
+ vars: {
17
+ [backgroundVar]: theme.color[color],
18
+ [foregroundVar]: theme.color[`${color}-foreground`],
19
+ },
20
+ }),
21
+ }),
22
+ {} as Record<(typeof semanticColor)[number], string>,
23
+ );
24
+
25
+ const scaleColors = SCALE_COLOR.reduce(
26
+ (prev, value) => ({
27
+ ...prev,
28
+ [value]: styleWithLayer({
29
+ vars: {
30
+ [backgroundVar]: theme.color[value][500],
31
+ [foregroundVar]: theme.color.background,
32
+ },
33
+ }),
34
+ }),
35
+ {} as Record<ScaleColor, string>,
36
+ );
37
+
38
+ const pulse = keyframes({
39
+ '0%': {
40
+ backgroundPosition: '-300% 0',
41
+ },
42
+
43
+ '100%': {
44
+ backgroundPosition: '300% 0',
45
+ },
46
+ });
47
+
48
+ export const span = recipeWithLayer({
49
+ base: {
50
+ lineHeight: '0',
51
+ },
52
+ variants: {
53
+ size: {
54
+ sm: {
55
+ fontSize: '0.875em',
56
+ },
57
+
58
+ md: {
59
+ fontSize: '1em',
60
+ },
61
+
62
+ lg: {
63
+ fontSize: '1.125em',
64
+ },
65
+
66
+ 'icon-sm': {
67
+ fontSize: '1em',
68
+ },
69
+
70
+ 'icon-md': {
71
+ fontSize: '1.25em',
72
+ },
73
+
74
+ 'icon-lg': {
75
+ fontSize: '1.5em',
76
+ },
77
+ },
78
+ },
79
+ });
80
+
81
+ export const button = recipeWithLayer({
82
+ base: {
83
+ display: 'inline-flex',
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ position: 'relative',
87
+ overflow: 'hidden',
88
+
89
+ border: '0',
90
+ borderRadius: theme.borderRadius,
91
+
92
+ fontSize: '1em',
93
+
94
+ transition: 'background-color 0.2s ease, color 0.2s ease',
95
+
96
+ cursor: 'pointer',
97
+ userSelect: 'none',
98
+ },
99
+
100
+ variants: {
101
+ hasIcon: {
102
+ true: {
103
+ gap: '0.5em',
104
+ },
105
+ },
106
+
107
+ color: {
108
+ ...semanticColors,
109
+ ...scaleColors,
110
+ },
111
+
112
+ size: {
113
+ sm: {
114
+ height: '2.25em',
115
+ padding: '0 0.75em',
116
+ },
117
+
118
+ md: {
119
+ height: '2.5em',
120
+ padding: '0 0.875em',
121
+ },
122
+
123
+ lg: {
124
+ height: '2.75em',
125
+ padding: '0 1em',
126
+ },
127
+
128
+ 'icon-sm': {
129
+ width: '2em',
130
+ height: '2em',
131
+ },
132
+
133
+ 'icon-md': {
134
+ width: '2.5em',
135
+ height: '2.5em',
136
+ },
137
+
138
+ 'icon-lg': {
139
+ width: '2.75em',
140
+ height: '2.75em',
141
+ },
142
+ },
143
+
144
+ variant: {
145
+ contained: {
146
+ boxShadow: `inset 0 0 0 1px rgba(255, 255, 255, 0.08)`,
147
+ backgroundColor: `rgb(${backgroundVar})`,
148
+ color: `rgb(${foregroundVar})`,
149
+
150
+ ':disabled': {
151
+ backgroundColor: `rgb(${theme.color.muted})`,
152
+
153
+ color: `rgb(${theme.color['muted-foreground']})`,
154
+
155
+ cursor: 'default',
156
+ },
157
+ },
158
+
159
+ outlined: {
160
+ boxShadow: `inset 0 0 0 1px rgb(${backgroundVar})`,
161
+ backgroundColor: `rgb(${theme.color.background})`,
162
+ color: `rgb(${theme.color.foreground})`,
163
+
164
+ ':disabled': {
165
+ backgroundColor: `rgb(${theme.color.muted})`,
166
+
167
+ color: `rgb(${theme.color['muted-foreground']})`,
168
+
169
+ cursor: 'default',
170
+ },
171
+ },
172
+
173
+ ghost: {
174
+ background: 'transparent',
175
+
176
+ color: `rgb(${theme.color.foreground})`,
177
+
178
+ ':hover': {
179
+ backgroundColor: `rgba(${backgroundVar}, 0.8)`,
180
+
181
+ color: `rgb(${foregroundVar})`,
182
+ },
183
+
184
+ ':disabled': {
185
+ color: `rgb(${theme.color['muted-foreground']})`,
186
+
187
+ cursor: 'default',
188
+ },
189
+ },
190
+
191
+ glass: {
192
+ boxShadow: `inset 0 0 0 1px rgba(255, 255, 255, 0.08)`,
193
+ backgroundColor: `rgba(${backgroundVar}, 0.75)`,
194
+ backdropFilter: 'blur(1rem)',
195
+
196
+ color: `rgb(${theme.color.foreground})`,
197
+
198
+ ':disabled': {
199
+ backgroundColor: `rgb(${theme.color.muted})`,
200
+
201
+ color: `rgb(${theme.color['muted-foreground']})`,
202
+
203
+ cursor: 'default',
204
+ },
205
+ },
206
+ },
207
+
208
+ pulse: {
209
+ true: {
210
+ '::after': {
211
+ content: '',
212
+ position: 'absolute',
213
+ inset: '0',
214
+
215
+ background:
216
+ 'linear-gradient(90deg, transparent 30%, #ffffff33 65%, transparent 100%)',
217
+ backgroundSize: '300% 100%',
218
+
219
+ animation: `${pulse} 5s linear 0s infinite`,
220
+ },
221
+ },
222
+ },
223
+ },
224
+ });
225
+
226
+ export const icon = style({
227
+ lineHeight: '0',
228
+ });
229
+
230
+ globalStyle(`${icon} > *`, {
231
+ width: '1em',
232
+ height: '1em',
233
+ lineHeight: '0',
234
+
235
+ pointerEvents: 'none',
236
+ });
237
+
238
+ globalStyle(`${button.classNames.base} svg`, {
239
+ width: '1em',
240
+ height: '1em',
241
+ });
@@ -0,0 +1,30 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+
3
+ import { uiTest } from '../../tests/uiTest';
4
+ import { Button } from './';
5
+
6
+ describe('Button 컴포넌트', () => {
7
+ uiTest(Button, 'Button');
8
+
9
+ it('Button을 클릭하면 onClick이 호출되어야 한다', () => {
10
+ const handleClick = jest.fn();
11
+ render(<Button onClick={handleClick}>Click</Button>);
12
+
13
+ fireEvent.click(screen.getByRole('button', { name: 'Click' }));
14
+
15
+ expect(handleClick).toHaveBeenCalledTimes(1);
16
+ });
17
+
18
+ it('disabled Button을 클릭하면 onClick이 호출되지 않아야 한다', () => {
19
+ const handleClick = jest.fn();
20
+ render(
21
+ <Button onClick={handleClick} disabled>
22
+ Click
23
+ </Button>,
24
+ );
25
+
26
+ fireEvent.click(screen.getByRole('button', { name: 'Click' }));
27
+
28
+ expect(handleClick).not.toHaveBeenCalledTimes(1);
29
+ });
30
+ });
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, type ReactElement } from 'react';
4
+
5
+ import { clsx } from 'clsx';
6
+
7
+ import { useRipple } from '#hooks';
8
+ import { sx } from '#styles';
9
+ import type { UIComponent } from '#types';
10
+
11
+ import * as s from './Button.css';
12
+
13
+ interface ButtonProps
14
+ extends Omit<UIComponent<'button', typeof s.button>, 'hasIcon'> {
15
+ icon?: ReactElement;
16
+ }
17
+
18
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
19
+ (
20
+ {
21
+ children,
22
+ color = 'primary',
23
+ size = 'md',
24
+ variant = 'contained',
25
+ pulse = false,
26
+ className,
27
+ sx: propSx,
28
+ icon,
29
+ ...props
30
+ },
31
+ ref,
32
+ ) => {
33
+ const { ripple } = useRipple<HTMLButtonElement>(ref);
34
+
35
+ return (
36
+ <button
37
+ ref={ref}
38
+ className={clsx(
39
+ className,
40
+ s.button({
41
+ color,
42
+ size,
43
+ variant,
44
+ pulse,
45
+ hasIcon: icon !== undefined,
46
+ }),
47
+ sx(propSx),
48
+ )}
49
+ {...props}
50
+ >
51
+ {icon !== undefined && <span className={s.icon}>{icon}</span>}
52
+ <span className={s.span({ size })}>{children}</span>
53
+ {ripple}
54
+ </button>
55
+ );
56
+ },
57
+ );
58
+ Button.displayName = 'Button';
59
+
60
+ export { s as buttonCss };
@@ -0,0 +1,93 @@
1
+ import { recipeWithLayer, styleWithLayer } from '#styleUtils';
2
+ import { theme } from '#themes';
3
+ import { semanticColor } from '#tokens';
4
+
5
+ import { SCALE_COLOR, type ScaleColor } from '../../tokens/scale/color';
6
+ import { cardInteraction } from './CardInteraction.css';
7
+
8
+ const semanticColors = semanticColor.reduce(
9
+ (prev, color) => ({
10
+ ...prev,
11
+ [color]: styleWithLayer({
12
+ backgroundColor: `rgb(${theme.color[color]})`,
13
+ }),
14
+ }),
15
+ {} as Record<(typeof semanticColor)[number], string>,
16
+ );
17
+
18
+ const scaleColors = SCALE_COLOR.reduce(
19
+ (prev, value) => ({
20
+ ...prev,
21
+ [value]: styleWithLayer({
22
+ backgroundColor: `color-mix(in srgb, rgb(${theme.color[value][500]}) 20%, rgb(${theme.color.background}) 80%)`,
23
+ }),
24
+ }),
25
+ {} as Record<ScaleColor, string>,
26
+ );
27
+
28
+ export const card = recipeWithLayer({
29
+ base: {
30
+ display: 'flex',
31
+ flexDirection: 'column',
32
+ overflow: 'hidden',
33
+ position: 'relative',
34
+
35
+ borderRadius: theme.borderRadius,
36
+
37
+ color: `rgb(${theme.color.foreground})`,
38
+
39
+ transition: 'border-color 0.2s ease, transform 0.4s ease',
40
+
41
+ selectors: {
42
+ [`&:has(${cardInteraction}:hover)`]: {
43
+ borderColor: `color-mix(in srgb, rgb(${theme.color.primary}) 10%, rgb(${theme.color.border}) 90%)`,
44
+ },
45
+
46
+ [`&:has(${cardInteraction}:active)`]: {
47
+ borderColor: `color-mix(in srgb, rgb(${theme.color.primary}) 40%, rgb(${theme.color.border}) 60%)`,
48
+ },
49
+ },
50
+ },
51
+ variants: {
52
+ variant: {
53
+ contained: {},
54
+
55
+ outlined: {
56
+ border: '1px solid',
57
+ borderColor: `rgb(${theme.color.border})`,
58
+ },
59
+
60
+ glass: {
61
+ border: `1px solid rgb(${theme.color['border.weak']})`,
62
+ borderRadius: '0.75rem',
63
+
64
+ background: `linear-gradient(rgba(${theme.color['card.gradient']}, 0.06) 0%, rgba(${theme.color['card.gradient']}, 0.02) 100%)`,
65
+ backdropFilter: 'blur(1rem)',
66
+
67
+ '::before': {
68
+ position: 'absolute',
69
+ inset: '0',
70
+
71
+ background:
72
+ 'linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.05) 100%)',
73
+
74
+ opacity: '0',
75
+ transition: 'opacity 0.3s ease',
76
+
77
+ content: '',
78
+ },
79
+
80
+ selectors: {
81
+ '&:hover::before': {
82
+ opacity: '1',
83
+ },
84
+ },
85
+ },
86
+ },
87
+
88
+ color: {
89
+ ...semanticColors,
90
+ ...scaleColors,
91
+ },
92
+ },
93
+ });
@@ -0,0 +1,24 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+
3
+ import { uiTest } from '../../tests/uiTest';
4
+ import { Card, CardContent, CardInteraction, CardThumbnail } from './';
5
+
6
+ describe('Card 컴포넌트', () => {
7
+ uiTest(Card, 'Card');
8
+ uiTest(CardContent, 'CardContent');
9
+ uiTest(CardInteraction, 'CardInteraction');
10
+ uiTest(CardThumbnail, 'CardThumbnail');
11
+
12
+ it('CardInteraction을 클릭하면 onClick이 호출되어야 한다', () => {
13
+ const handleClick = jest.fn();
14
+ render(
15
+ <Card onClick={handleClick}>
16
+ <CardInteraction>Click</CardInteraction>
17
+ </Card>,
18
+ );
19
+
20
+ fireEvent.click(screen.getByText('Click'));
21
+
22
+ expect(handleClick).toHaveBeenCalledTimes(1);
23
+ });
24
+ });
@@ -0,0 +1,41 @@
1
+ import { forwardRef, type CSSProperties } from 'react';
2
+
3
+ import clsx from 'clsx';
4
+
5
+ import { sx } from '#styles';
6
+ import type { UIComponent } from '#types';
7
+
8
+ import * as s from './Card.css';
9
+
10
+ interface CardProps extends UIComponent<'div', typeof s.card> {
11
+ width?: CSSProperties['width'];
12
+ height?: CSSProperties['height'];
13
+ }
14
+
15
+ export const Card = forwardRef<HTMLDivElement, CardProps>(
16
+ (
17
+ {
18
+ width,
19
+ height,
20
+ color = 'card',
21
+ variant = 'outlined',
22
+ className,
23
+ style,
24
+ sx: propSx,
25
+ ...props
26
+ },
27
+ ref,
28
+ ) => {
29
+ return (
30
+ <div
31
+ ref={ref}
32
+ className={clsx(s.card({ color, variant }), className, sx(propSx))}
33
+ style={{ width, height, ...style }}
34
+ {...props}
35
+ />
36
+ );
37
+ },
38
+ );
39
+ Card.displayName = 'Card';
40
+
41
+ export { s as cardCss };
@@ -0,0 +1,8 @@
1
+ import { styleWithLayer } from '#styleUtils';
2
+ import { spacing } from '#tokens';
3
+
4
+ export const cardContent = styleWithLayer({
5
+ flex: '1 1 auto',
6
+
7
+ padding: spacing.md,
8
+ });
@@ -0,0 +1,23 @@
1
+ import { forwardRef } from 'react';
2
+
3
+ import { clsx } from 'clsx';
4
+
5
+ import { sx } from '#styles';
6
+ import type { UIComponent } from '#types';
7
+
8
+ import * as s from './CardContent.css';
9
+
10
+ type CardContentProps = UIComponent<'div'>;
11
+
12
+ export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
13
+ ({ className, sx: propSx, ...props }, ref) => {
14
+ return (
15
+ <div
16
+ ref={ref}
17
+ className={clsx(s.cardContent, className, sx(propSx))}
18
+ {...props}
19
+ />
20
+ );
21
+ },
22
+ );
23
+ CardContent.displayName = 'CardContent';
@@ -0,0 +1,11 @@
1
+ import { styleWithLayer } from '#styleUtils';
2
+
3
+ export const cardInteraction = styleWithLayer({
4
+ position: 'relative',
5
+ overflow: 'hidden',
6
+
7
+ height: '100%',
8
+
9
+ cursor: 'pointer',
10
+ userSelect: 'none',
11
+ });
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { forwardRef } from 'react';
4
+
5
+ import clsx from 'clsx';
6
+
7
+ import { useRipple } from '#hooks';
8
+ import { sx } from '#styles';
9
+ import type { UIComponent } from '#types';
10
+
11
+ import * as s from './CardInteraction.css';
12
+
13
+ type CardInteractionProps = UIComponent<'div'>;
14
+
15
+ export const CardInteraction = forwardRef<HTMLDivElement, CardInteractionProps>(
16
+ ({ children, className, sx: propSx, ...props }, ref) => {
17
+ const { ripple } = useRipple<HTMLDivElement>(ref);
18
+
19
+ return (
20
+ <div
21
+ ref={ref}
22
+ className={clsx(
23
+ className,
24
+ s.cardInteraction,
25
+ s.cardInteraction,
26
+ sx(propSx),
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ {ripple}
32
+ </div>
33
+ );
34
+ },
35
+ );
36
+ CardInteraction.displayName = 'CardInteraction';
@@ -0,0 +1,6 @@
1
+ import { styleWithLayer } from '#styleUtils';
2
+
3
+ export const thumbnail = styleWithLayer({
4
+ width: 'auto',
5
+ maxWidth: '100%',
6
+ });
@@ -0,0 +1,23 @@
1
+ import { forwardRef } from 'react';
2
+
3
+ import clsx from 'clsx';
4
+
5
+ import { sx } from '#styles';
6
+ import type { UIComponent } from '#types';
7
+
8
+ import * as s from './CardThumbnail.css';
9
+
10
+ type CardThumbnailProps = UIComponent<'img'>;
11
+
12
+ export const CardThumbnail = forwardRef<HTMLImageElement, CardThumbnailProps>(
13
+ ({ className, sx: propSx, ...props }, ref) => {
14
+ return (
15
+ <img
16
+ ref={ref}
17
+ className={clsx(s.thumbnail, className, sx(propSx))}
18
+ {...props}
19
+ />
20
+ );
21
+ },
22
+ );
23
+ CardThumbnail.displayName = 'CardThumbnail';
@@ -0,0 +1,4 @@
1
+ export * from './Card';
2
+ export * from './CardContent';
3
+ export * from './CardInteraction';
4
+ export * from './CardThumbnail';
@@ -0,0 +1,75 @@
1
+ import { createVar } from '@vanilla-extract/css';
2
+
3
+ import { recipeWithLayer, styleWithLayer } from '#styleUtils';
4
+ import { theme } from '#themes';
5
+ import { semanticColor } from '#tokens';
6
+
7
+ import { SCALE_COLOR, type ScaleColor } from '../../tokens/scale/color';
8
+
9
+ const backgroundVar = createVar();
10
+ const foregroundVar = createVar();
11
+
12
+ const semanticColors = semanticColor.reduce(
13
+ (prev, color) => ({
14
+ ...prev,
15
+ [color]: styleWithLayer({
16
+ vars: {
17
+ [backgroundVar]: theme.color[color],
18
+ [foregroundVar]: theme.color[`${color}-foreground`],
19
+ },
20
+ }),
21
+ }),
22
+ {} as Record<(typeof semanticColor)[number], string>,
23
+ );
24
+
25
+ const scaleColors = SCALE_COLOR.reduce(
26
+ (prev, value) => ({
27
+ ...prev,
28
+ [value]: styleWithLayer({
29
+ vars: {
30
+ [backgroundVar]: theme.color[value][500],
31
+ [foregroundVar]: theme.color[value][50],
32
+ },
33
+ }),
34
+ }),
35
+ {} as Record<ScaleColor, string>,
36
+ );
37
+
38
+ export const chip = recipeWithLayer({
39
+ base: {
40
+ display: 'inline-flex',
41
+ alignItems: 'center',
42
+ lineHeight: '0',
43
+ gap: '0.125em',
44
+
45
+ height: '2em',
46
+ padding: '0 0.75em',
47
+ borderRadius: theme.borderRadius,
48
+
49
+ backgroundColor: `rgb(${backgroundVar})`,
50
+ color: `rgb(${foregroundVar})`,
51
+
52
+ userSelect: 'none',
53
+ },
54
+
55
+ variants: {
56
+ color: {
57
+ ...semanticColors,
58
+ ...scaleColors,
59
+ },
60
+
61
+ size: {
62
+ sm: {
63
+ fontSize: '0.75em',
64
+ },
65
+
66
+ md: {
67
+ fontSize: '0.875em',
68
+ },
69
+
70
+ lg: {
71
+ fontSize: '1em',
72
+ },
73
+ },
74
+ },
75
+ });
@@ -0,0 +1,6 @@
1
+ import { uiTest } from '../../tests/uiTest';
2
+ import { Chip } from './Chip';
3
+
4
+ describe('Chip 컴포넌트', () => {
5
+ uiTest(Chip, 'Chip');
6
+ });