@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,5 @@
1
+ export * from './useDialog';
2
+ export * from './usePointerSlider';
3
+ export * from './useRipple';
4
+ export * from './useToast';
5
+ export * from './useToast/ToastProvider';
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode, useCallback, useMemo, useRef } from 'react';
4
+
5
+ import { useOverlay } from '@kimdw-rtk/utils';
6
+
7
+ import { Alert, Confirm } from '#components';
8
+
9
+ export const useDialog = () => {
10
+ const { push } = useOverlay();
11
+ const dialogRef = useRef<{ close: () => void }>(null);
12
+
13
+ const alert = useCallback(
14
+ (message: ReactNode) => {
15
+ return new Promise((resolve) => {
16
+ push(<Alert>{message}</Alert>, { onClose: () => resolve(true) });
17
+ });
18
+ },
19
+ [push],
20
+ );
21
+
22
+ const confirm = useCallback(
23
+ (message: ReactNode): Promise<boolean> => {
24
+ return new Promise((resolve) => {
25
+ const handleConfirm = () => {
26
+ resolve(true);
27
+ dialogRef.current?.close();
28
+ };
29
+
30
+ const handleCancle = () => {
31
+ resolve(false);
32
+ dialogRef.current?.close();
33
+ };
34
+
35
+ push(
36
+ <Confirm
37
+ ref={dialogRef}
38
+ onConfirm={handleConfirm}
39
+ onCancle={handleCancle}
40
+ >
41
+ {message}
42
+ </Confirm>,
43
+ { onClose: () => resolve(false) },
44
+ );
45
+ });
46
+ },
47
+ [push],
48
+ );
49
+
50
+ return useMemo(() => ({ alert, confirm }), [alert, confirm]);
51
+ };
@@ -0,0 +1,80 @@
1
+ import { act, render, screen, waitFor } from '@testing-library/react';
2
+
3
+ import { UIProvider } from '#contexts';
4
+ import { useDialog } from '#hooks';
5
+
6
+ describe('useDialog 테스트', () => {
7
+ const mockFn = jest.fn();
8
+
9
+ beforeEach(() => {
10
+ const TestComponent = () => {
11
+ const { alert, confirm } = useDialog();
12
+
13
+ const handleConfirmClick = async () => {
14
+ mockFn(await confirm('confirm'));
15
+ };
16
+
17
+ return (
18
+ <>
19
+ <button onClick={() => alert('alert')}>alert click</button>
20
+ <button onClick={handleConfirmClick}>confirm click</button>
21
+ </>
22
+ );
23
+ };
24
+
25
+ render(
26
+ <UIProvider overlayUnmountOn={'exit'}>
27
+ <TestComponent />
28
+ </UIProvider>,
29
+ );
30
+ });
31
+
32
+ it('확인 버튼을 누르면 alert을 닫을 수 있다.', async () => {
33
+ const alertButton = screen.getByRole('button', { name: 'alert click' });
34
+
35
+ expect(screen.queryByText('alert')).not.toBeInTheDocument();
36
+
37
+ act(() => {
38
+ alertButton.click();
39
+ });
40
+
41
+ expect(screen.getByText('alert')).toBeInTheDocument();
42
+
43
+ const okButton = screen.getByRole('button', { name: '확인' });
44
+ act(() => {
45
+ okButton.click();
46
+ });
47
+
48
+ expect(screen.queryByText('alert')).not.toBeInTheDocument();
49
+ });
50
+
51
+ it.skip('confirm의 확인 버튼을 누르면 true, 취소 버튼을 누르면 false를 반환한다.', async () => {
52
+ const confirmButton = screen.getByRole('button', { name: 'confirm click' });
53
+
54
+ act(() => {
55
+ confirmButton.click();
56
+ });
57
+
58
+ const okButton = screen.getByRole('button', { name: '확인' });
59
+ act(() => {
60
+ okButton.click();
61
+ });
62
+
63
+ await waitFor(() => {
64
+ expect(mockFn.mock.calls[0][0]).toBe(true);
65
+ });
66
+
67
+ act(() => {
68
+ confirmButton.click();
69
+ });
70
+
71
+ const cancelButton = screen.getByRole('button', { name: '취소' });
72
+ act(() => {
73
+ cancelButton.click();
74
+ });
75
+
76
+ await waitFor(() => {
77
+ expect(mockFn.mock.calls[1][0]).toBe(false);
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ /**
6
+ * 마우스를 사용하는 환경에서도 드래그로 가로 스크롤을 할 수 있게 만드는 hook
7
+ */
8
+ export const useMouseScroll = <T extends React.RefObject<HTMLElement | null>>(
9
+ ref: T,
10
+ ): void => {
11
+ useEffect(() => {
12
+ const element = ref.current;
13
+ let isDown = false,
14
+ startX = 0,
15
+ startLeft = 0;
16
+
17
+ if (element === null) {
18
+ return;
19
+ }
20
+
21
+ const handleMouseDown = (e: MouseEvent) => {
22
+ if (!element.contains(e.target as Node)) {
23
+ return;
24
+ }
25
+
26
+ isDown = true;
27
+
28
+ startLeft = element.scrollLeft;
29
+ startX = e.x;
30
+ };
31
+
32
+ const handleMouseMove = (e: MouseEvent) => {
33
+ if (!isDown) {
34
+ return;
35
+ }
36
+
37
+ element.scrollLeft = startLeft + startX - e.x;
38
+ };
39
+
40
+ const handleMouseUp = () => {
41
+ isDown = false;
42
+ };
43
+
44
+ const handleWheel = (e: WheelEvent) => {
45
+ element.scrollTo({
46
+ left: element.scrollLeft + e.deltaY,
47
+ behavior: 'smooth',
48
+ });
49
+ };
50
+
51
+ window.addEventListener('mousedown', handleMouseDown);
52
+ window.addEventListener('mousemove', handleMouseMove);
53
+ window.addEventListener('mouseup', handleMouseUp);
54
+ element.addEventListener('wheel', handleWheel);
55
+
56
+ return () => {
57
+ window.removeEventListener('mousedown', handleMouseDown);
58
+ window.removeEventListener('mousemove', handleMouseMove);
59
+ window.removeEventListener('mouseup', handleMouseUp);
60
+ element.removeEventListener('wheel', handleWheel);
61
+ };
62
+ }, [ref]);
63
+ };
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export const usePointerSlider = <T extends React.RefObject<HTMLElement | null>>(
6
+ ref: T,
7
+ {
8
+ min,
9
+ max,
10
+ defaultValue,
11
+ }: {
12
+ min: number;
13
+ max: number;
14
+ defaultValue: number;
15
+ },
16
+ ): number => {
17
+ const [value, setValue] = useState<number>(defaultValue);
18
+
19
+ useEffect(() => {
20
+ const element = ref.current;
21
+ let isDown = false,
22
+ parentWidth = 0,
23
+ parentX = 0;
24
+
25
+ if (element === null) {
26
+ return;
27
+ }
28
+
29
+ element.style.left = `${((value - min) / (max - min)) * 100}%`;
30
+
31
+ const handlePointerDown = (e: PointerEvent) => {
32
+ element.setPointerCapture(e.pointerId);
33
+ isDown = true;
34
+ const boundingRect = element.parentElement?.getBoundingClientRect();
35
+
36
+ if (boundingRect === undefined) {
37
+ return;
38
+ }
39
+
40
+ parentWidth = boundingRect.width;
41
+ parentX = boundingRect.x;
42
+ };
43
+
44
+ const handlePointerMove = (e: PointerEvent) => {
45
+ if (!isDown) {
46
+ return;
47
+ }
48
+
49
+ const currentValue = Math.min(
50
+ max,
51
+ Math.max(
52
+ min,
53
+ Math.round((e.clientX - parentX) / (parentWidth / (max - min))) + min,
54
+ ),
55
+ );
56
+
57
+ element.style.left = `${((currentValue - min) / (max - min)) * 100}%`;
58
+
59
+ setValue(currentValue);
60
+ };
61
+
62
+ const handlePointerUp = (e: PointerEvent) => {
63
+ element.releasePointerCapture(e.pointerId);
64
+ isDown = false;
65
+ };
66
+
67
+ element.addEventListener('pointerdown', handlePointerDown);
68
+ element.addEventListener('pointermove', handlePointerMove);
69
+ element.addEventListener('pointerup', handlePointerUp);
70
+
71
+ return () => {
72
+ element.removeEventListener('pointerdown', handlePointerDown);
73
+ element.removeEventListener('pointermove', handlePointerMove);
74
+ element.removeEventListener('pointerup', handlePointerUp);
75
+ };
76
+ }, [ref, max, min]);
77
+
78
+ return value;
79
+ };
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef } from 'react';
4
+
5
+ import * as s from './ripple.css';
6
+
7
+ export const useRipple = <T extends HTMLElement>(ref?: React.Ref<T>) => {
8
+ const rippleRef = useRef<HTMLDivElement>(null);
9
+
10
+ useEffect(() => {
11
+ let element: T | null = null;
12
+
13
+ if (!ref) {
14
+ return;
15
+ }
16
+
17
+ if (typeof ref === 'function') {
18
+ ref(element);
19
+ } else {
20
+ element = ref.current;
21
+ }
22
+
23
+ const ripple = rippleRef.current;
24
+
25
+ let timer: ReturnType<typeof setTimeout>;
26
+ let isAnimationPending = false;
27
+
28
+ let isFadeIn = false,
29
+ isMouseDown = false,
30
+ isTransitionEnd = true;
31
+
32
+ if (!ripple || !element) {
33
+ return;
34
+ }
35
+
36
+ const clearTimer = () => {
37
+ clearTimeout(timer);
38
+ isAnimationPending = false;
39
+ };
40
+
41
+ const runAnimation = (x: number, y: number) => {
42
+ const width =
43
+ element.clientWidth / 2 + Math.abs(element.clientWidth / 2 - x),
44
+ height =
45
+ element.clientHeight / 2 + Math.abs(element.clientHeight / 2 - y);
46
+ const size = Math.round(Math.sqrt(width ** 2 + height ** 2) * 2);
47
+
48
+ ripple.style.width = `${size}px`;
49
+ ripple.style.height = `${size}px`;
50
+ ripple.style.left = `${x}px`;
51
+ ripple.style.top = `${y}px`;
52
+
53
+ ripple.style.boxShadow = `${s.colorVar} 0 0 ${size / 10}px ${size / 10}px`;
54
+ ripple.className = s.ripple({ animation: false });
55
+ //eslint-disable-next-line
56
+ ripple.offsetTop;
57
+ ripple.className = s.ripple({ animation: true });
58
+ ripple.style.opacity = '1';
59
+
60
+ isAnimationPending = false;
61
+ isFadeIn = true;
62
+ isTransitionEnd = false;
63
+ };
64
+
65
+ const handleClick = (e: MouseEvent) => {
66
+ if (!isAnimationPending) {
67
+ return;
68
+ }
69
+
70
+ if (element instanceof HTMLButtonElement && element.disabled) {
71
+ return;
72
+ }
73
+
74
+ clearTimer();
75
+ runAnimation(e.offsetX, e.offsetY);
76
+ };
77
+
78
+ const handlePointerDown = (e: PointerEvent) => {
79
+ if (e.button !== 0 || !ripple || !isTransitionEnd) {
80
+ return;
81
+ }
82
+
83
+ if (element instanceof HTMLButtonElement && element.disabled) {
84
+ return;
85
+ }
86
+
87
+ isMouseDown = true;
88
+
89
+ if (e.pointerType === 'mouse') {
90
+ runAnimation(e.offsetX, e.offsetY);
91
+ return;
92
+ }
93
+
94
+ clearTimer();
95
+ isAnimationPending = true;
96
+ timer = setTimeout(() => runAnimation(e.offsetX, e.offsetY), 100);
97
+ };
98
+
99
+ const handlePointerUp = () => {
100
+ if (!isMouseDown) {
101
+ return;
102
+ }
103
+
104
+ clearTimer();
105
+
106
+ if (!isFadeIn) {
107
+ ripple.style.opacity = '0';
108
+ isFadeIn = false;
109
+ }
110
+
111
+ isMouseDown = false;
112
+ };
113
+
114
+ const handleTransitionEnd = (e: TransitionEvent) => {
115
+ if (e.propertyName === 'opacity' && isFadeIn) {
116
+ if (!isMouseDown) {
117
+ ripple.style.opacity = '0';
118
+ }
119
+
120
+ isFadeIn = false;
121
+ return;
122
+ }
123
+
124
+ if (e.propertyName === 'transform' && !isFadeIn) {
125
+ isTransitionEnd = true;
126
+ }
127
+ };
128
+
129
+ element.addEventListener('click', handleClick);
130
+ element.addEventListener('pointerdown', handlePointerDown);
131
+ element.addEventListener('pointerup', handlePointerUp);
132
+ element.addEventListener('pointerleave', handlePointerUp);
133
+ ripple.addEventListener('transitionend', handleTransitionEnd);
134
+
135
+ return () => {
136
+ element.removeEventListener('click', handleClick);
137
+ element.removeEventListener('pointerdown', handlePointerDown);
138
+ element.removeEventListener('pointerup', handlePointerUp);
139
+ element.removeEventListener('pointerleave', handlePointerUp);
140
+ ripple.removeEventListener('transitionend', handleTransitionEnd);
141
+ };
142
+ }, [ref, rippleRef]);
143
+
144
+ return useMemo(
145
+ () => ({
146
+ ripple: (
147
+ <div ref={rippleRef} className={s.ripple({ animation: false })} />
148
+ ),
149
+ }),
150
+ [rippleRef],
151
+ );
152
+ };
@@ -0,0 +1,40 @@
1
+ import { createVar } from '@vanilla-extract/css';
2
+ import { recipe } from '@vanilla-extract/recipes';
3
+
4
+ import { theme } from '#themes';
5
+
6
+ export const colorVar = createVar();
7
+
8
+ export const ripple = recipe({
9
+ base: {
10
+ position: 'absolute',
11
+ left: '0',
12
+ top: '0',
13
+
14
+ borderRadius: '50%',
15
+
16
+ backgroundColor: colorVar,
17
+
18
+ opacity: '0',
19
+ transformOrigin: 'center center',
20
+
21
+ pointerEvents: 'none',
22
+
23
+ vars: {
24
+ [colorVar]: `color-mix(in srgb, rgba(${theme.color.accent}, 0.2) 20%, rgba(${theme.color.foreground}, 0.2) 80%)`,
25
+ },
26
+ },
27
+ variants: {
28
+ animation: {
29
+ true: {
30
+ transition:
31
+ 'opacity 0.25s linear, transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
32
+ transform: 'translate(-50%, -50%) scale(1, 1)',
33
+ },
34
+
35
+ false: {
36
+ transform: 'translate(-50%, -50%) scale(0.1, 0.1)',
37
+ },
38
+ },
39
+ },
40
+ });
@@ -0,0 +1,12 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ export const container = style({
4
+ display: 'flex',
5
+ alignItems: 'flex-end',
6
+ flexDirection: 'column-reverse',
7
+
8
+ position: 'fixed',
9
+ right: '1rem',
10
+ bottom: '1rem',
11
+ left: '1rem',
12
+ });
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import * as s from './ToastContainer.css';
4
+
5
+ interface ToastContainerProps {
6
+ children: ReactNode;
7
+ }
8
+
9
+ export const ToastContainer = ({ children }: ToastContainerProps) => {
10
+ return <div className={s.container}>{children}</div>;
11
+ };
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ComponentProps,
10
+ type ReactNode,
11
+ } from 'react';
12
+
13
+ import { CSSTransition, TransitionGroup } from '@kimdw-rtk/utils';
14
+
15
+ import { Toast } from '#components';
16
+
17
+ import { ToastContainer } from './ToastContainer';
18
+
19
+ export interface ToastData {
20
+ id: number;
21
+ color?: ComponentProps<typeof Toast>['color'];
22
+ message: string;
23
+ autoClose?: boolean;
24
+ duration?: number;
25
+ onClick?: (id: number) => void;
26
+ }
27
+
28
+ interface ToastContextType {
29
+ push: (data: Omit<ToastData, 'id'>) => number;
30
+ remove: (id: number) => void;
31
+ }
32
+
33
+ export const ToastContext = createContext<ToastContextType | undefined>(
34
+ undefined,
35
+ );
36
+
37
+ interface ToastProviderProps {
38
+ children: ReactNode;
39
+ defaultDuration?: number;
40
+ }
41
+
42
+ const DEFAULT_TOAST_DURATION = 5000;
43
+
44
+ export const ToastProvider = ({
45
+ children,
46
+ defaultDuration = DEFAULT_TOAST_DURATION,
47
+ }: ToastProviderProps) => {
48
+ const [toasts, setToasts] = useState<ToastData[]>([]);
49
+ const idRef = useRef<number>(0);
50
+
51
+ const remove = useCallback((id: number) => {
52
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
53
+ }, []);
54
+
55
+ const push = useCallback<ToastContextType['push']>(
56
+ ({
57
+ color,
58
+ message,
59
+ autoClose = true,
60
+ duration = defaultDuration,
61
+ onClick,
62
+ }) => {
63
+ const id = ++idRef.current;
64
+ setToasts((prev) => [
65
+ ...prev,
66
+ { id, color, message, autoClose, duration, onClick },
67
+ ]);
68
+
69
+ if (!autoClose) {
70
+ return id;
71
+ }
72
+
73
+ // autoClose가 true면, 일정 시간이 지난 후 toast 제거
74
+ setTimeout(() => {
75
+ remove(id);
76
+ }, duration);
77
+
78
+ return id;
79
+ },
80
+ [defaultDuration, remove],
81
+ );
82
+
83
+ const handleToastClick = (toast: ToastData) => {
84
+ // onClick 이벤트가 정의되어 있지 않으면 기본적으로 toast를 닫도록 한다.
85
+ if (toast.onClick === undefined) {
86
+ remove(toast.id);
87
+ return;
88
+ }
89
+
90
+ toast.onClick(toast.id);
91
+ };
92
+
93
+ return (
94
+ <ToastContext.Provider
95
+ value={useMemo(() => ({ push, remove }), [push, remove])}
96
+ >
97
+ {children}
98
+ <ToastContainer>
99
+ <TransitionGroup>
100
+ {toasts.map((toast) => (
101
+ <CSSTransition
102
+ as="div"
103
+ key={toast.id}
104
+ initial={{
105
+ opacity: 0,
106
+ transform: 'translateY(1rem)',
107
+ height: '0',
108
+ }}
109
+ animate={{
110
+ opacity: 1,
111
+ transform: 'translateY(0)',
112
+ height: '3.5rem',
113
+ }}
114
+ exit={{ opacity: 0, height: '0' }}
115
+ duration={500}
116
+ style={{ display: 'flex', flexDirection: 'column-reverse' }}
117
+ >
118
+ <Toast
119
+ color={toast.color}
120
+ duration={toast.autoClose ? toast.duration : 0}
121
+ onClick={() => handleToastClick(toast)}
122
+ >
123
+ {toast.message}
124
+ </Toast>
125
+ </CSSTransition>
126
+ ))}
127
+ </TransitionGroup>
128
+ </ToastContainer>
129
+ </ToastContext.Provider>
130
+ );
131
+ };
@@ -0,0 +1,15 @@
1
+ import { useContext, useMemo } from 'react';
2
+
3
+ import { ToastContext } from './ToastProvider';
4
+
5
+ export const useToast = () => {
6
+ const toastContext = useContext(ToastContext);
7
+
8
+ if (!toastContext) {
9
+ throw new Error('useToast must be used within a ToastProvider');
10
+ }
11
+
12
+ const { push, remove } = toastContext;
13
+
14
+ return useMemo(() => ({ push, remove }), [push, remove]);
15
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './components';
2
+ export * from './contexts';
3
+ export * from './hooks';
4
+ export * from './styles';
5
+ export * from './styles/globalStyle.css';
6
+ export * from './themes';
7
+ export * from './types';
8
+ export * from './utils';
@@ -0,0 +1,36 @@
1
+ import { assignVars, globalStyle } from '@vanilla-extract/css';
2
+
3
+ import { darkThemeVars, lightThemeVars, theme } from '#themes';
4
+
5
+ globalStyle('*', {
6
+ boxSizing: 'border-box',
7
+
8
+ overscrollBehavior: 'none',
9
+ WebkitTapHighlightColor: 'transparent',
10
+ });
11
+
12
+ globalStyle('.light', {
13
+ vars: assignVars(theme, lightThemeVars),
14
+ });
15
+
16
+ globalStyle('.dark', {
17
+ vars: assignVars(theme, darkThemeVars),
18
+ });
19
+
20
+ globalStyle('body', {
21
+ margin: '0',
22
+
23
+ backgroundColor: `rgb(${theme.color.background})`,
24
+
25
+ lineHeight: '1',
26
+ color: `rgb(${theme.color.foreground})`,
27
+ });
28
+
29
+ globalStyle('a', {
30
+ textDecoration: 'none',
31
+ color: 'inherit',
32
+ });
33
+
34
+ globalStyle('svg', {
35
+ lineHeight: '0',
36
+ });
@@ -0,0 +1,4 @@
1
+ export * from './layers.css';
2
+ export * from './sprinkles.css';
3
+ export * from './globalStyle.css';
4
+ export * from './sx';
@@ -0,0 +1,4 @@
1
+ import { globalLayer } from '@vanilla-extract/css';
2
+
3
+ export const componentsLayer = globalLayer('components');
4
+ export const sprinklesLayer = globalLayer('utilities');