@uniai-fe/uds-primitives 0.0.1

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 (217) hide show
  1. package/README.md +63 -0
  2. package/package.json +85 -0
  3. package/src/components/alternate/hooks/index.ts +4 -0
  4. package/src/components/alternate/img/.gitkeep +0 -0
  5. package/src/components/alternate/index.scss +1 -0
  6. package/src/components/alternate/index.tsx +4 -0
  7. package/src/components/alternate/markup/index.tsx +4 -0
  8. package/src/components/alternate/styles/index.scss +3 -0
  9. package/src/components/alternate/types/index.ts +4 -0
  10. package/src/components/alternate/utils/index.ts +4 -0
  11. package/src/components/badge/hooks/index.ts +4 -0
  12. package/src/components/badge/img/.gitkeep +0 -0
  13. package/src/components/badge/index.scss +1 -0
  14. package/src/components/badge/index.tsx +6 -0
  15. package/src/components/badge/markup/Badge.tsx +51 -0
  16. package/src/components/badge/markup/index.tsx +1 -0
  17. package/src/components/badge/styles/index.scss +189 -0
  18. package/src/components/badge/types/index.ts +55 -0
  19. package/src/components/badge/utils/index.ts +21 -0
  20. package/src/components/button/hooks/index.ts +4 -0
  21. package/src/components/button/img/.gitkeep +0 -0
  22. package/src/components/button/index.scss +1 -0
  23. package/src/components/button/index.tsx +6 -0
  24. package/src/components/button/markup/Button.tsx +175 -0
  25. package/src/components/button/markup/index.tsx +1 -0
  26. package/src/components/button/styles/index.scss +847 -0
  27. package/src/components/button/types/index.ts +79 -0
  28. package/src/components/button/utils/index.ts +58 -0
  29. package/src/components/calendar/hooks/index.ts +4 -0
  30. package/src/components/calendar/img/.gitkeep +0 -0
  31. package/src/components/calendar/index.scss +1 -0
  32. package/src/components/calendar/index.tsx +4 -0
  33. package/src/components/calendar/markup/index.tsx +4 -0
  34. package/src/components/calendar/styles/index.scss +3 -0
  35. package/src/components/calendar/types/index.ts +4 -0
  36. package/src/components/calendar/utils/index.ts +4 -0
  37. package/src/components/checkbox/hooks/index.ts +4 -0
  38. package/src/components/checkbox/img/.gitkeep +0 -0
  39. package/src/components/checkbox/img/check-large.svg +3 -0
  40. package/src/components/checkbox/img/check-medium.svg +3 -0
  41. package/src/components/checkbox/img/check.svg +3 -0
  42. package/src/components/checkbox/index.scss +1 -0
  43. package/src/components/checkbox/index.tsx +4 -0
  44. package/src/components/checkbox/markup/Checkbox.tsx +127 -0
  45. package/src/components/checkbox/markup/index.ts +1 -0
  46. package/src/components/checkbox/styles/index.scss +164 -0
  47. package/src/components/checkbox/types/checkbox.ts +21 -0
  48. package/src/components/checkbox/types/index.ts +1 -0
  49. package/src/components/chip/hooks/index.ts +4 -0
  50. package/src/components/chip/img/.gitkeep +0 -0
  51. package/src/components/chip/img/remove.svg +3 -0
  52. package/src/components/chip/index.scss +1 -0
  53. package/src/components/chip/index.tsx +6 -0
  54. package/src/components/chip/markup/Chip.tsx +103 -0
  55. package/src/components/chip/markup/index.tsx +1 -0
  56. package/src/components/chip/styles/index.scss +140 -0
  57. package/src/components/chip/types/index.ts +52 -0
  58. package/src/components/chip/utils/index.ts +36 -0
  59. package/src/components/dialog/hooks/index.ts +4 -0
  60. package/src/components/dialog/img/.gitkeep +0 -0
  61. package/src/components/dialog/index.scss +1 -0
  62. package/src/components/dialog/index.tsx +3 -0
  63. package/src/components/dialog/markup/confirm-dialog.tsx +316 -0
  64. package/src/components/dialog/markup/index.tsx +4 -0
  65. package/src/components/dialog/markup/notice-dialog.tsx +191 -0
  66. package/src/components/dialog/styles/base.scss +153 -0
  67. package/src/components/dialog/styles/confirm.scss +58 -0
  68. package/src/components/dialog/styles/index.scss +3 -0
  69. package/src/components/dialog/styles/notice.scss +65 -0
  70. package/src/components/dialog/types/index.ts +70 -0
  71. package/src/components/dialog/utils/index.ts +4 -0
  72. package/src/components/drawer/hooks/index.ts +113 -0
  73. package/src/components/drawer/img/.gitkeep +0 -0
  74. package/src/components/drawer/img/close.svg +3 -0
  75. package/src/components/drawer/index.scss +1 -0
  76. package/src/components/drawer/index.tsx +3 -0
  77. package/src/components/drawer/markup/drawer.tsx +421 -0
  78. package/src/components/drawer/markup/index.tsx +3 -0
  79. package/src/components/drawer/styles/index.scss +232 -0
  80. package/src/components/drawer/types/index.ts +51 -0
  81. package/src/components/drawer/utils/context.ts +15 -0
  82. package/src/components/drawer/utils/index.tsx +77 -0
  83. package/src/components/dropdown/hooks/index.ts +4 -0
  84. package/src/components/dropdown/img/.gitkeep +0 -0
  85. package/src/components/dropdown/index.scss +1 -0
  86. package/src/components/dropdown/index.tsx +4 -0
  87. package/src/components/dropdown/markup/index.tsx +4 -0
  88. package/src/components/dropdown/styles/index.scss +3 -0
  89. package/src/components/dropdown/types/index.ts +4 -0
  90. package/src/components/dropdown/utils/index.ts +4 -0
  91. package/src/components/input/hooks/index.ts +4 -0
  92. package/src/components/input/img/.gitkeep +0 -0
  93. package/src/components/input/img/check-correct.svg +3 -0
  94. package/src/components/input/img/check-default.svg +3 -0
  95. package/src/components/input/img/check-incorrect.svg +3 -0
  96. package/src/components/input/img/error.svg +5 -0
  97. package/src/components/input/img/hide-off.svg +4 -0
  98. package/src/components/input/img/hide-on.svg +6 -0
  99. package/src/components/input/img/reset.svg +3 -0
  100. package/src/components/input/img/search.svg +4 -0
  101. package/src/components/input/img/success.svg +3 -0
  102. package/src/components/input/index.scss +1 -0
  103. package/src/components/input/index.tsx +6 -0
  104. package/src/components/input/markup/index.tsx +1 -0
  105. package/src/components/input/markup/text/Base.tsx +311 -0
  106. package/src/components/input/markup/text/Identification.tsx +145 -0
  107. package/src/components/input/markup/text/Password.tsx +71 -0
  108. package/src/components/input/markup/text/Phone.tsx +115 -0
  109. package/src/components/input/markup/text/Search.tsx +35 -0
  110. package/src/components/input/markup/text/index.ts +10 -0
  111. package/src/components/input/styles/index.scss +375 -0
  112. package/src/components/input/types/index.ts +56 -0
  113. package/src/components/input/utils/index.ts +54 -0
  114. package/src/components/label/hooks/index.ts +4 -0
  115. package/src/components/label/img/.gitkeep +0 -0
  116. package/src/components/label/index.scss +1 -0
  117. package/src/components/label/index.tsx +4 -0
  118. package/src/components/label/markup/index.tsx +4 -0
  119. package/src/components/label/styles/index.scss +3 -0
  120. package/src/components/label/types/index.ts +4 -0
  121. package/src/components/label/utils/index.ts +4 -0
  122. package/src/components/navigation/hooks/index.ts +4 -0
  123. package/src/components/navigation/img/.gitkeep +0 -0
  124. package/src/components/navigation/index.scss +1 -0
  125. package/src/components/navigation/index.tsx +8 -0
  126. package/src/components/navigation/markup/index.tsx +2 -0
  127. package/src/components/navigation/markup/mobile/BottomNavigation.tsx +127 -0
  128. package/src/components/navigation/markup/mobile/index.ts +1 -0
  129. package/src/components/navigation/markup/web/index.ts +4 -0
  130. package/src/components/navigation/styles/index.scss +133 -0
  131. package/src/components/navigation/types/index.ts +38 -0
  132. package/src/components/navigation/utils/index.ts +23 -0
  133. package/src/components/pagination/hooks/index.ts +4 -0
  134. package/src/components/pagination/img/.gitkeep +0 -0
  135. package/src/components/pagination/index.scss +1 -0
  136. package/src/components/pagination/index.tsx +6 -0
  137. package/src/components/pagination/markup/Carousel.tsx +76 -0
  138. package/src/components/pagination/markup/Count.tsx +54 -0
  139. package/src/components/pagination/markup/Pagination.tsx +83 -0
  140. package/src/components/pagination/markup/index.tsx +3 -0
  141. package/src/components/pagination/styles/index.scss +155 -0
  142. package/src/components/pagination/types/index.ts +68 -0
  143. package/src/components/pagination/utils/index.ts +58 -0
  144. package/src/components/radio/hooks/index.ts +4 -0
  145. package/src/components/radio/img/.gitkeep +0 -0
  146. package/src/components/radio/index.scss +1 -0
  147. package/src/components/radio/index.tsx +7 -0
  148. package/src/components/radio/markup/Radio.tsx +121 -0
  149. package/src/components/radio/markup/RadioCard.tsx +68 -0
  150. package/src/components/radio/markup/RadioCardGroup.tsx +75 -0
  151. package/src/components/radio/markup/index.tsx +3 -0
  152. package/src/components/radio/styles/index.scss +252 -0
  153. package/src/components/radio/types/index.ts +1 -0
  154. package/src/components/radio/types/radio.ts +63 -0
  155. package/src/components/radio/utils/index.ts +4 -0
  156. package/src/components/scrollbar/hooks/index.ts +4 -0
  157. package/src/components/scrollbar/img/.gitkeep +0 -0
  158. package/src/components/scrollbar/index.scss +1 -0
  159. package/src/components/scrollbar/index.tsx +4 -0
  160. package/src/components/scrollbar/markup/index.tsx +4 -0
  161. package/src/components/scrollbar/styles/index.scss +3 -0
  162. package/src/components/scrollbar/types/index.ts +4 -0
  163. package/src/components/scrollbar/utils/index.ts +4 -0
  164. package/src/components/segmented-control/index.scss +1 -0
  165. package/src/components/segmented-control/index.tsx +7 -0
  166. package/src/components/segmented-control/markup/SegmentedControl.tsx +117 -0
  167. package/src/components/segmented-control/markup/index.ts +1 -0
  168. package/src/components/segmented-control/styles/index.scss +113 -0
  169. package/src/components/segmented-control/types/index.ts +22 -0
  170. package/src/components/select/hooks/index.ts +4 -0
  171. package/src/components/select/img/.gitkeep +0 -0
  172. package/src/components/select/index.scss +1 -0
  173. package/src/components/select/index.tsx +4 -0
  174. package/src/components/select/markup/index.tsx +4 -0
  175. package/src/components/select/styles/index.scss +3 -0
  176. package/src/components/select/types/index.ts +4 -0
  177. package/src/components/select/utils/index.ts +4 -0
  178. package/src/components/spinner/hooks/index.ts +4 -0
  179. package/src/components/spinner/img/.gitkeep +0 -0
  180. package/src/components/spinner/index.scss +1 -0
  181. package/src/components/spinner/index.tsx +4 -0
  182. package/src/components/spinner/markup/index.tsx +4 -0
  183. package/src/components/spinner/styles/index.scss +3 -0
  184. package/src/components/spinner/types/index.ts +4 -0
  185. package/src/components/spinner/utils/index.ts +4 -0
  186. package/src/components/tab/hooks/index.ts +4 -0
  187. package/src/components/tab/img/.gitkeep +0 -0
  188. package/src/components/tab/index.scss +1 -0
  189. package/src/components/tab/index.tsx +6 -0
  190. package/src/components/tab/markup/TabContent.tsx +29 -0
  191. package/src/components/tab/markup/TabList.tsx +60 -0
  192. package/src/components/tab/markup/TabRoot.tsx +74 -0
  193. package/src/components/tab/markup/TabTrigger.tsx +47 -0
  194. package/src/components/tab/markup/index.tsx +4 -0
  195. package/src/components/tab/styles/index.scss +182 -0
  196. package/src/components/tab/types/index.ts +46 -0
  197. package/src/components/tab/utils/index.ts +5 -0
  198. package/src/components/tab/utils/tab-context.ts +20 -0
  199. package/src/components/table/hooks/index.ts +4 -0
  200. package/src/components/table/img/.gitkeep +0 -0
  201. package/src/components/table/index.scss +1 -0
  202. package/src/components/table/index.tsx +4 -0
  203. package/src/components/table/markup/index.tsx +4 -0
  204. package/src/components/table/styles/index.scss +3 -0
  205. package/src/components/table/types/index.ts +4 -0
  206. package/src/components/table/utils/index.ts +4 -0
  207. package/src/hooks/index.ts +4 -0
  208. package/src/img/.gitkeep +0 -0
  209. package/src/index.scss +3 -0
  210. package/src/index.tsx +26 -0
  211. package/src/init/dayjs.ts +14 -0
  212. package/src/theme/ThemeProvider.tsx +25 -0
  213. package/src/theme/config.ts +29 -0
  214. package/src/theme/index.ts +3 -0
  215. package/src/theme/overrides.scss +215 -0
  216. package/src/types/index.ts +4 -0
  217. package/src/utils/index.ts +4 -0
@@ -0,0 +1,421 @@
1
+ import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
2
+ import clsx from "clsx";
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import type { CSSProperties } from "react";
13
+ import CloseIcon from "../img/close.svg";
14
+ import type {
15
+ DrawerContextValue,
16
+ DrawerCloseProps,
17
+ DrawerContentProps,
18
+ DrawerDescriptionProps,
19
+ DrawerOverlayProps,
20
+ DrawerPortalProps,
21
+ DrawerPhase,
22
+ DrawerRootProps,
23
+ DrawerSectionProps,
24
+ DrawerTitleProps,
25
+ DrawerTriggerProps,
26
+ } from "../types";
27
+ import { useDrawerDrag } from "../hooks";
28
+ import {
29
+ composeEventHandlers,
30
+ DrawerContext,
31
+ mergeRefs,
32
+ renderButtonLike,
33
+ useDrawerContext,
34
+ } from "../utils";
35
+
36
+ const ANIMATION_DURATION = 320;
37
+
38
+ const DrawerRoot = ({
39
+ children,
40
+ open: openProp,
41
+ defaultOpen,
42
+ onOpenChange,
43
+ }: DrawerRootProps) => {
44
+ const isControlled = openProp !== undefined;
45
+ const [uncontrolledOpen, setUncontrolledOpen] = useState<boolean>(
46
+ defaultOpen ?? false,
47
+ );
48
+ const open = isControlled ? (openProp as boolean) : uncontrolledOpen;
49
+ const [phase, setPhase] = useState<DrawerPhase>(open ? "entered" : "exited");
50
+ const closeTimerRef = useRef<number | null>(null);
51
+ const enterRafRef = useRef<number | null>(null);
52
+
53
+ const setOpen = useCallback(
54
+ (nextOpen: boolean) => {
55
+ if (!isControlled) {
56
+ setUncontrolledOpen(nextOpen);
57
+ }
58
+ onOpenChange?.(nextOpen);
59
+ },
60
+ [isControlled, onOpenChange],
61
+ );
62
+
63
+ useEffect(() => {
64
+ if (open) {
65
+ if (closeTimerRef.current) {
66
+ window.clearTimeout(closeTimerRef.current);
67
+ closeTimerRef.current = null;
68
+ }
69
+ setPhase("entering");
70
+ enterRafRef.current = window.requestAnimationFrame(() => {
71
+ setPhase("entered");
72
+ enterRafRef.current = null;
73
+ });
74
+ return () => {
75
+ if (enterRafRef.current) {
76
+ window.cancelAnimationFrame(enterRafRef.current);
77
+ enterRafRef.current = null;
78
+ }
79
+ };
80
+ }
81
+ if (enterRafRef.current) {
82
+ window.cancelAnimationFrame(enterRafRef.current);
83
+ enterRafRef.current = null;
84
+ }
85
+ setPhase("exiting");
86
+ closeTimerRef.current = window.setTimeout(() => {
87
+ setPhase("exited");
88
+ closeTimerRef.current = null;
89
+ }, ANIMATION_DURATION);
90
+ return () => {
91
+ if (closeTimerRef.current) {
92
+ window.clearTimeout(closeTimerRef.current);
93
+ closeTimerRef.current = null;
94
+ }
95
+ };
96
+ }, [open]);
97
+
98
+ useEffect(() => {
99
+ if (phase === "exited") {
100
+ return undefined;
101
+ }
102
+ const originalOverflow = document.body.style.overflow;
103
+ document.body.style.overflow = "hidden";
104
+ return () => {
105
+ document.body.style.overflow = originalOverflow;
106
+ };
107
+ }, [phase]);
108
+
109
+ const contextValue = useMemo<DrawerContextValue>(
110
+ () => ({
111
+ open,
112
+ setOpen,
113
+ phase,
114
+ }),
115
+ [open, setOpen, phase],
116
+ );
117
+
118
+ return (
119
+ <DrawerContext.Provider value={contextValue}>
120
+ {children}
121
+ </DrawerContext.Provider>
122
+ );
123
+ };
124
+
125
+ const DrawerTrigger = forwardRef<HTMLButtonElement, DrawerTriggerProps>(
126
+ ({ asChild, children, onClick, ...props }, forwardedRef) => {
127
+ const { setOpen } = useDrawerContext();
128
+ const handleClick = composeEventHandlers(onClick, () => setOpen(true));
129
+ return renderButtonLike(
130
+ asChild,
131
+ children,
132
+ { ...props, onClick: handleClick },
133
+ forwardedRef,
134
+ );
135
+ },
136
+ );
137
+
138
+ DrawerTrigger.displayName = "DrawerTrigger";
139
+
140
+ const DrawerPortal = ({ children }: DrawerPortalProps) => {
141
+ useDrawerContext();
142
+ const [mounted, setMounted] = useState(false);
143
+
144
+ useEffect(() => {
145
+ setMounted(true);
146
+ return () => setMounted(false);
147
+ }, []);
148
+
149
+ if (!mounted) {
150
+ return null;
151
+ }
152
+
153
+ return createPortal(children, document.body);
154
+ };
155
+
156
+ /**
157
+ * DrawerOverlay; 전체 dim
158
+ * @component
159
+ * @param {DrawerOverlayProps} props
160
+ */
161
+ const DrawerOverlay = forwardRef<HTMLDivElement, DrawerOverlayProps>(
162
+ ({ className, onClick, ...props }, forwardedRef) => {
163
+ const { phase, setOpen } = useDrawerContext();
164
+ const handleClick = composeEventHandlers(onClick, () => setOpen(false));
165
+ return (
166
+ <div
167
+ {...props}
168
+ ref={forwardedRef}
169
+ data-phase={phase}
170
+ className={clsx("drawer-overlay", className)}
171
+ onClick={handleClick}
172
+ />
173
+ );
174
+ },
175
+ );
176
+
177
+ DrawerOverlay.displayName = "DrawerOverlay";
178
+
179
+ /**
180
+ * DrawerTitle; Drawer 제목
181
+ * @component
182
+ * @param {DrawerTitleProps} props
183
+ */
184
+ const DrawerTitle = forwardRef<HTMLHeadingElement, DrawerTitleProps>(
185
+ ({ visuallyHidden = false, className, children, ...props }, forwardedRef) => {
186
+ const titleNode = (
187
+ <h2
188
+ {...props}
189
+ ref={forwardedRef}
190
+ className={clsx("drawer-title", className)}
191
+ >
192
+ {children}
193
+ </h2>
194
+ );
195
+
196
+ if (visuallyHidden) {
197
+ return <VisuallyHidden asChild>{titleNode}</VisuallyHidden>;
198
+ }
199
+
200
+ return titleNode;
201
+ },
202
+ );
203
+
204
+ DrawerTitle.displayName = "DrawerTitle";
205
+
206
+ /**
207
+ * DrawerDescription; Drawer 본문 안내
208
+ * @component
209
+ * @param {DrawerDescriptionProps} props
210
+ */
211
+ const DrawerDescription = forwardRef<
212
+ HTMLParagraphElement,
213
+ DrawerDescriptionProps
214
+ >(({ visuallyHidden = false, className, children, ...props }, forwardedRef) => {
215
+ const descriptionNode = (
216
+ <p
217
+ {...props}
218
+ ref={forwardedRef}
219
+ className={clsx("drawer-description", className)}
220
+ >
221
+ {children}
222
+ </p>
223
+ );
224
+
225
+ if (visuallyHidden) {
226
+ return <VisuallyHidden asChild>{descriptionNode}</VisuallyHidden>;
227
+ }
228
+
229
+ return descriptionNode;
230
+ });
231
+
232
+ DrawerDescription.displayName = "DrawerDescription";
233
+
234
+ /**
235
+ * DrawerSection; header/body/footer 레이아웃
236
+ * @component
237
+ * @param {DrawerSectionProps} props
238
+ */
239
+ const DrawerSection = forwardRef<HTMLDivElement, DrawerSectionProps>(
240
+ ({ className, section = "body", ...props }, forwardedRef) => (
241
+ <div
242
+ {...props}
243
+ ref={forwardedRef}
244
+ className={clsx("drawer-section", `drawer-${section}`, className)}
245
+ data-section={section}
246
+ />
247
+ ),
248
+ );
249
+
250
+ DrawerSection.displayName = "DrawerSection";
251
+
252
+ const DrawerHeader = forwardRef<HTMLDivElement, DrawerSectionProps>(
253
+ ({ className, ...props }, forwardedRef) => (
254
+ <DrawerSection
255
+ {...props}
256
+ ref={forwardedRef}
257
+ className={className}
258
+ section="header"
259
+ />
260
+ ),
261
+ );
262
+
263
+ DrawerHeader.displayName = "DrawerHeader";
264
+
265
+ const DrawerBody = forwardRef<HTMLDivElement, DrawerSectionProps>(
266
+ ({ className, ...props }, forwardedRef) => (
267
+ <DrawerSection
268
+ {...props}
269
+ ref={forwardedRef}
270
+ className={className}
271
+ section="body"
272
+ />
273
+ ),
274
+ );
275
+
276
+ DrawerBody.displayName = "DrawerBody";
277
+
278
+ const DrawerFooter = forwardRef<HTMLDivElement, DrawerSectionProps>(
279
+ ({ className, ...props }, forwardedRef) => (
280
+ <DrawerSection
281
+ {...props}
282
+ ref={forwardedRef}
283
+ className={className}
284
+ section="footer"
285
+ />
286
+ ),
287
+ );
288
+
289
+ DrawerFooter.displayName = "DrawerFooter";
290
+
291
+ /**
292
+ * DrawerContent; handle/closeButton 제어
293
+ * @component
294
+ * @param {DrawerContentProps} props
295
+ */
296
+ const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
297
+ (
298
+ {
299
+ className,
300
+ children,
301
+ closeButton = false,
302
+ closeButtonLabel = "닫기",
303
+ style,
304
+ ...restProps
305
+ },
306
+ forwardedRef,
307
+ ) => {
308
+ const { open, setOpen, phase } = useDrawerContext();
309
+ const isVisible = phase !== "exited";
310
+ const contentRef = useRef<HTMLDivElement>(null);
311
+ const mergedRef = mergeRefs(forwardedRef, contentRef);
312
+ const handleClose = useCallback(() => setOpen(false), [setOpen]);
313
+ const { dragOffset, isDragging, handleProps } = useDrawerDrag({
314
+ onClose: handleClose,
315
+ });
316
+ const contentStyle = useMemo(() => {
317
+ type DrawerContentStyle = CSSProperties & {
318
+ "--drawer-drag-offset"?: string;
319
+ };
320
+ const customStyle: DrawerContentStyle = {
321
+ ...style,
322
+ "--drawer-drag-offset": `${dragOffset}px`,
323
+ };
324
+ return customStyle;
325
+ }, [dragOffset, style]);
326
+
327
+ useEffect(() => {
328
+ if (!open) {
329
+ return undefined;
330
+ }
331
+ const previouslyFocused = document.activeElement as HTMLElement | null;
332
+ const timer = window.setTimeout(() => {
333
+ contentRef.current?.focus();
334
+ }, 10);
335
+ return () => {
336
+ window.clearTimeout(timer);
337
+ previouslyFocused?.focus?.();
338
+ };
339
+ }, [open]);
340
+
341
+ useEffect(() => {
342
+ if (!open) {
343
+ return undefined;
344
+ }
345
+ const handleKeyDown = (event: KeyboardEvent) => {
346
+ if (event.key === "Escape") {
347
+ event.preventDefault();
348
+ handleClose();
349
+ }
350
+ };
351
+ window.addEventListener("keydown", handleKeyDown);
352
+ return () => window.removeEventListener("keydown", handleKeyDown);
353
+ }, [open, handleClose]);
354
+
355
+ return (
356
+ <div
357
+ {...restProps}
358
+ role="dialog"
359
+ aria-modal={isVisible ? true : undefined}
360
+ aria-hidden={isVisible ? undefined : true}
361
+ tabIndex={-1}
362
+ ref={mergedRef}
363
+ className={clsx("drawer-content", className)}
364
+ data-phase={phase}
365
+ data-dragging={isDragging ? "true" : undefined}
366
+ style={contentStyle}
367
+ >
368
+ <button
369
+ type="button"
370
+ className="drawer-handle"
371
+ aria-label="드래그로 닫기"
372
+ {...handleProps}
373
+ >
374
+ <span className="drawer-handle-bar" aria-hidden />
375
+ </button>
376
+ {closeButton ? (
377
+ <button
378
+ type="button"
379
+ className="drawer-close-button"
380
+ aria-label={closeButtonLabel}
381
+ onClick={handleClose}
382
+ >
383
+ <CloseIcon aria-hidden focusable="false" />
384
+ </button>
385
+ ) : null}
386
+ <div className="drawer-inner">{children}</div>
387
+ </div>
388
+ );
389
+ },
390
+ );
391
+
392
+ DrawerContent.displayName = "DrawerContent";
393
+
394
+ const DrawerClose = forwardRef<HTMLButtonElement, DrawerCloseProps>(
395
+ ({ asChild, children, onClick, ...props }, forwardedRef) => {
396
+ const { setOpen } = useDrawerContext();
397
+ const handleClick = composeEventHandlers(onClick, () => setOpen(false));
398
+ return renderButtonLike(
399
+ asChild,
400
+ children,
401
+ { ...props, onClick: handleClick },
402
+ forwardedRef,
403
+ );
404
+ },
405
+ );
406
+
407
+ DrawerClose.displayName = "DrawerClose";
408
+
409
+ export {
410
+ DrawerRoot,
411
+ DrawerTrigger,
412
+ DrawerPortal,
413
+ DrawerOverlay,
414
+ DrawerContent,
415
+ DrawerTitle,
416
+ DrawerDescription,
417
+ DrawerHeader,
418
+ DrawerBody,
419
+ DrawerFooter,
420
+ DrawerClose,
421
+ };
@@ -0,0 +1,3 @@
1
+ "use client";
2
+
3
+ export * from "./drawer";
@@ -0,0 +1,232 @@
1
+ @use "@uniai-fe/uds-foundation/css";
2
+
3
+ :where(.radix-themes, .theme-root, :root) {
4
+ // Figma 기준 bottom sheet 여백/톤을 전역 변수로 정리한다.
5
+ --drawer-overlay-bg: rgba(0, 0, 0, 0.44);
6
+ --drawer-surface-bg: var(--color-bg-surface-static-white);
7
+ --drawer-radius-large: var(--theme-radius-large-2);
8
+ --drawer-radius-medium: var(--theme-radius-large-1);
9
+ --drawer-body-color: var(--color-label-standard);
10
+ --drawer-title-color: var(--color-label-strong);
11
+ --drawer-gap: calc(var(--spacing-gap-9, 28px) + 2px);
12
+ --drawer-padding-x: calc(var(--spacing-padding-10, 32px) - 2px);
13
+ --drawer-padding-top: var(--spacing-padding-7, 20px);
14
+ --drawer-padding-bottom: calc(
15
+ var(--spacing-padding-11, 40px) + var(--spacing-padding-7, 20px)
16
+ );
17
+ --drawer-footer-gap: var(--spacing-gap-6, 16px);
18
+ --drawer-section-gap: var(--spacing-gap-6, 16px);
19
+ --drawer-handle-width: calc(
20
+ var(--spacing-padding-11, 40px) + var(--spacing-padding-7, 20px)
21
+ );
22
+ --drawer-handle-height: var(--spacing-padding-2, 4px);
23
+ --drawer-handle-bg: var(--color-border-standard-cool-gray);
24
+ --drawer-close-size: 36px;
25
+ --drawer-drag-offset: 0px;
26
+ }
27
+
28
+ .drawer-overlay {
29
+ position: fixed;
30
+ inset: 0;
31
+ background-color: var(--drawer-overlay-bg);
32
+ opacity: 0;
33
+ pointer-events: none;
34
+ transition: opacity 0.25s ease;
35
+ will-change: opacity;
36
+
37
+ &[data-phase="entering"],
38
+ &[data-phase="entered"] {
39
+ opacity: 1;
40
+ pointer-events: auto;
41
+ }
42
+
43
+ &[data-phase="exiting"] {
44
+ opacity: 0;
45
+ pointer-events: auto;
46
+ }
47
+ }
48
+
49
+ @media (prefers-reduced-motion: reduce) {
50
+ .drawer-overlay {
51
+ transition: none;
52
+ }
53
+ }
54
+
55
+ // Overlay dim만 사용하는 Figma 규격에 맞춰 box-shadow 대신 opacity 전환으로 깊이를 표현한다.
56
+ .drawer-content {
57
+ position: fixed;
58
+ display: flex;
59
+ flex-direction: column;
60
+ background-color: var(--drawer-surface-bg);
61
+ outline: none;
62
+ border: none;
63
+ box-shadow: none;
64
+ color: var(--drawer-body-color);
65
+ overscroll-behavior: contain;
66
+ left: 50%;
67
+ bottom: 0;
68
+ transform: translate3d(-50%, 100%, 0);
69
+ border-top-left-radius: var(--drawer-radius-large);
70
+ border-top-right-radius: var(--drawer-radius-large);
71
+ max-width: 640px;
72
+ padding-bottom: env(safe-area-inset-bottom);
73
+ max-width: min(640px, 100%);
74
+ width: 100%;
75
+ max-height: calc(100vh - var(--spacing-padding-8));
76
+ transition:
77
+ transform 0.32s cubic-bezier(0.16, 1, 0.3, 1),
78
+ opacity 0.2s ease;
79
+ opacity: 0;
80
+ pointer-events: none;
81
+ will-change: transform;
82
+ }
83
+
84
+ .drawer-content[data-phase="entering"],
85
+ .drawer-content[data-phase="entered"] {
86
+ transform: translate3d(-50%, var(--drawer-drag-offset, 0px), 0);
87
+ opacity: 1;
88
+ pointer-events: auto;
89
+ }
90
+
91
+ .drawer-content[data-phase="exiting"] {
92
+ transform: translate3d(-50%, calc(100% + var(--drawer-drag-offset, 0px)), 0);
93
+ opacity: 1;
94
+ pointer-events: none;
95
+ }
96
+
97
+ .drawer-content[data-phase="exited"] {
98
+ transform: translate3d(-50%, 100%, 0);
99
+ opacity: 0;
100
+ pointer-events: none;
101
+ }
102
+
103
+ .drawer-content[data-dragging="true"] {
104
+ transition: none;
105
+ }
106
+
107
+ @media (prefers-reduced-motion: reduce) {
108
+ .drawer-content {
109
+ transition: none;
110
+ }
111
+ }
112
+
113
+ .drawer-handle {
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ border: none;
118
+ background: transparent;
119
+ padding: var(--drawer-padding-top) 0 var(--spacing-padding-6);
120
+ cursor: grab;
121
+ }
122
+
123
+ .drawer-handle:active {
124
+ cursor: grabbing;
125
+ }
126
+
127
+ .drawer-handle-bar {
128
+ width: var(--drawer-handle-width);
129
+ height: var(--drawer-handle-height);
130
+ border-radius: var(--drawer-handle-height);
131
+ background-color: var(--drawer-handle-bg);
132
+ display: block;
133
+ }
134
+
135
+ // close_line 아이콘 스타일: 투명 배경 + 24px 아이콘만 노출.
136
+ .drawer-close-button {
137
+ position: absolute;
138
+ top: var(--drawer-padding-top);
139
+ right: var(--drawer-padding-x);
140
+ width: 24px;
141
+ height: 24px;
142
+ border-radius: 0;
143
+ border: none;
144
+ background: transparent;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ cursor: pointer;
149
+ padding: 0;
150
+ color: var(--color-label-neutral, #7f8a94);
151
+ }
152
+
153
+ .drawer-close-button svg {
154
+ width: 24px;
155
+ height: 24px;
156
+ }
157
+
158
+ .drawer-inner {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: var(--drawer-gap);
162
+ padding-inline: var(--drawer-padding-x);
163
+ padding-bottom: var(--drawer-padding-bottom);
164
+ padding-top: var(--drawer-padding-top);
165
+ flex: 1 1 auto;
166
+ min-height: 0;
167
+ }
168
+
169
+ .drawer-section {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: var(--drawer-section-gap);
173
+ color: var(--drawer-body-color);
174
+ width: 100%;
175
+ }
176
+
177
+ .drawer-header {
178
+ text-align: left;
179
+ padding-top: 0;
180
+ }
181
+
182
+ .drawer-body {
183
+ flex: 1 1 auto;
184
+ overflow-y: auto;
185
+ gap: var(--spacing-gap-5);
186
+ font-size: var(--font-body-small-size);
187
+ line-height: var(--font-body-small-line-height);
188
+ color: var(--drawer-body-color);
189
+ width: 100%;
190
+ word-break: keep-all;
191
+ }
192
+
193
+ .drawer-body > * {
194
+ width: 100%;
195
+ }
196
+
197
+ // 긴 약관 본문이 div로 감싸지더라도 문단 스타일이 유지되도록 지정한다.
198
+ .drawer-body p {
199
+ margin: 0;
200
+ word-break: inherit;
201
+ }
202
+
203
+ .drawer-footer {
204
+ padding: var(--spacing-padding-8) 0 0;
205
+ border-top: none;
206
+ flex-shrink: 0;
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: var(--drawer-footer-gap);
210
+ width: 100%;
211
+ }
212
+
213
+ .drawer-title {
214
+ margin: 0;
215
+ font-size: var(--font-heading-medium-size);
216
+ line-height: var(--font-heading-medium-line-height);
217
+ font-weight: var(--font-heading-medium-weight);
218
+ color: var(--drawer-title-color);
219
+ letter-spacing: var(--font-heading-medium-letter-spacing, normal);
220
+ }
221
+
222
+ .drawer-description {
223
+ margin: 0;
224
+ color: var(--drawer-body-color);
225
+ font-size: var(--font-body-xsmall-size);
226
+ line-height: var(--font-body-xsmall-line-height);
227
+ letter-spacing: var(--font-body-xsmall-letter-spacing, normal);
228
+ }
229
+
230
+ .drawer-dismiss {
231
+ display: none;
232
+ }
@@ -0,0 +1,51 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+
3
+ export type DrawerPhase = "exited" | "entering" | "entered" | "exiting";
4
+
5
+ export interface DrawerContextValue {
6
+ open: boolean;
7
+ setOpen: (next: boolean) => void;
8
+ phase: DrawerPhase;
9
+ }
10
+
11
+ export interface DrawerRootProps {
12
+ children: ReactNode;
13
+ open?: boolean;
14
+ defaultOpen?: boolean;
15
+ onOpenChange?: (open: boolean) => void;
16
+ }
17
+
18
+ export interface DrawerTriggerProps extends ComponentPropsWithoutRef<"button"> {
19
+ asChild?: boolean;
20
+ children?: ReactNode;
21
+ }
22
+
23
+ export interface DrawerCloseProps extends ComponentPropsWithoutRef<"button"> {
24
+ asChild?: boolean;
25
+ children?: ReactNode;
26
+ }
27
+
28
+ export interface DrawerPortalProps {
29
+ children: ReactNode;
30
+ }
31
+
32
+ export type DrawerOverlayProps = ComponentPropsWithoutRef<"div">;
33
+
34
+ export type DrawerTitleProps = ComponentPropsWithoutRef<"h2"> & {
35
+ visuallyHidden?: boolean;
36
+ };
37
+
38
+ export type DrawerDescriptionProps = ComponentPropsWithoutRef<"p"> & {
39
+ visuallyHidden?: boolean;
40
+ };
41
+
42
+ export type DrawerSectionVariant = "header" | "body" | "footer";
43
+
44
+ export interface DrawerSectionProps extends ComponentPropsWithoutRef<"div"> {
45
+ section?: DrawerSectionVariant;
46
+ }
47
+
48
+ export interface DrawerContentProps extends ComponentPropsWithoutRef<"div"> {
49
+ closeButton?: boolean;
50
+ closeButtonLabel?: string;
51
+ }
@@ -0,0 +1,15 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { DrawerContextValue } from "../types";
3
+
4
+ // Drawer context를 별도 모듈로 분리해 Root 외부에서도 재사용 가능하게 한다.
5
+ const DrawerContext = createContext<DrawerContextValue | null>(null);
6
+
7
+ const useDrawerContext = () => {
8
+ const context = useContext(DrawerContext);
9
+ if (!context) {
10
+ throw new Error("Drawer components must be used within DrawerRoot");
11
+ }
12
+ return context;
13
+ };
14
+
15
+ export { DrawerContext, useDrawerContext };