@uniai-fe/uds-primitives 0.2.2 → 0.2.4

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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/styles.css +299 -81
  3. package/package.json +16 -9
  4. package/src/components/{input/img/calendar → calendar/img}/calendar.svg +5 -0
  5. package/src/components/calendar/index.tsx +5 -3
  6. package/src/components/calendar/markup/Core.tsx +67 -0
  7. package/src/components/calendar/markup/Icon.tsx +20 -0
  8. package/src/components/calendar/markup/Root.tsx +126 -0
  9. package/src/components/calendar/markup/index.tsx +24 -2
  10. package/src/components/calendar/markup/layout/Body.tsx +12 -0
  11. package/src/components/calendar/markup/layout/Container.tsx +43 -0
  12. package/src/components/calendar/markup/layout/Footer.tsx +12 -0
  13. package/src/components/calendar/markup/layout/Header.tsx +12 -0
  14. package/src/components/calendar/styles/index.scss +2 -0
  15. package/src/components/calendar/styles/layout.scss +21 -0
  16. package/src/components/calendar/styles/mantine-calendar.scss +240 -0
  17. package/src/components/calendar/types/calendar.ts +208 -0
  18. package/src/components/calendar/types/index.ts +1 -4
  19. package/src/components/calendar/utils/index.ts +1 -4
  20. package/src/components/calendar/utils/value-mapper.ts +24 -0
  21. package/src/components/input/index.scss +1 -1
  22. package/src/components/input/markup/date/Template.tsx +181 -0
  23. package/src/components/input/markup/date/Trigger.tsx +79 -0
  24. package/src/components/input/markup/date/button/ApplyButton.tsx +38 -0
  25. package/src/components/input/markup/date/button/ClearButton.tsx +36 -0
  26. package/src/components/input/markup/date/button/TodayButton.tsx +36 -0
  27. package/src/components/input/markup/date/footer/Container.tsx +24 -0
  28. package/src/components/input/markup/date/footer/Template.tsx +36 -0
  29. package/src/components/input/markup/date/footer/UtilContainer.tsx +23 -0
  30. package/src/components/input/markup/date/footer/index.ts +3 -0
  31. package/src/components/input/markup/date/index.tsx +27 -0
  32. package/src/components/input/markup/index.tsx +2 -4
  33. package/src/components/input/styles/date.scss +45 -0
  34. package/src/components/input/types/date.ts +286 -0
  35. package/src/components/input/types/index.ts +1 -1
  36. package/src/components/input/utils/address.ts +2 -2
  37. package/src/components/input/utils/date.ts +61 -0
  38. package/src/components/input/utils/index.tsx +1 -0
  39. package/src/components/pop-over/index.scss +1 -0
  40. package/src/components/pop-over/index.tsx +4 -0
  41. package/src/components/pop-over/markup/Content.tsx +77 -0
  42. package/src/components/pop-over/markup/Root.tsx +28 -0
  43. package/src/components/pop-over/markup/Trigger.tsx +26 -0
  44. package/src/components/pop-over/markup/index.tsx +17 -0
  45. package/src/components/pop-over/styles/base.scss +5 -0
  46. package/src/components/pop-over/styles/content.scss +24 -0
  47. package/src/components/pop-over/styles/index.scss +2 -0
  48. package/src/components/pop-over/types/index.ts +1 -0
  49. package/src/components/pop-over/types/pop-over.ts +86 -0
  50. package/src/index.scss +1 -0
  51. package/src/index.tsx +3 -1
  52. package/src/init/mantine.css +5 -0
  53. package/src/init/mantine.ts +2 -0
  54. package/src/components/input/markup/calendar/Base.tsx +0 -329
  55. package/src/components/input/markup/calendar/index.tsx +0 -8
  56. package/src/components/input/styles/calendar.scss +0 -110
  57. package/src/components/input/types/calendar.ts +0 -208
  58. /package/src/components/{input/img/calendar → calendar/img}/chevron-down.svg +0 -0
  59. /package/src/components/{input/img/calendar → calendar/img}/chevron-left.svg +0 -0
  60. /package/src/components/{input/img/calendar → calendar/img}/chevron-right.svg +0 -0
  61. /package/src/components/{input/img/calendar → calendar/img}/chevron-up.svg +0 -0
@@ -0,0 +1,23 @@
1
+ import clsx from "clsx";
2
+ import type { InputCalendarFooterUtilContainerProps } from "../../../types";
3
+
4
+ /**
5
+ * FooterTemplate utility button container.
6
+ * @component
7
+ * @param {InputCalendarFooterUtilContainerProps} props
8
+ * @param {React.ReactNode} [props.children] util 버튼(children)
9
+ * @param {string} [props.className] container className
10
+ * @example
11
+ * <InputDateFooterUtilContainer>
12
+ * <Input.Date.Button.Clear />
13
+ * <Input.Date.Button.Today />
14
+ * </InputDateFooterUtilContainer>
15
+ */
16
+ export default function InputDateFooterUtilContainer({
17
+ children,
18
+ className,
19
+ }: InputCalendarFooterUtilContainerProps) {
20
+ return (
21
+ <div className={clsx("input-date-footer-util", className)}>{children}</div>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ export { default as InputDateFooterTemplate } from "./Template";
2
+ export { default as InputDateFooterTemplateContainer } from "./Container";
3
+ export { default as InputDateFooterUtilContainer } from "./UtilContainer";
@@ -0,0 +1,27 @@
1
+ import "../../styles/date.scss";
2
+ import InputDateTemplate from "./Template";
3
+ import InputDateTrigger from "./Trigger";
4
+ import InputDateApplyButton from "./button/ApplyButton";
5
+ import InputDateClearButton from "./button/ClearButton";
6
+ import InputDateTodayButton from "./button/TodayButton";
7
+ import {
8
+ InputDateFooterTemplate,
9
+ InputDateFooterTemplateContainer,
10
+ InputDateFooterUtilContainer,
11
+ } from "./footer";
12
+
13
+ // Input.Date 네임스페이스: template/foundation 구성요소 집합
14
+ export const InputDate = {
15
+ Template: InputDateTemplate,
16
+ Trigger: InputDateTrigger,
17
+ Button: {
18
+ Apply: InputDateApplyButton,
19
+ Clear: InputDateClearButton,
20
+ Today: InputDateTodayButton,
21
+ },
22
+ Footer: {
23
+ Template: InputDateFooterTemplate,
24
+ Container: InputDateFooterTemplateContainer,
25
+ UtilContainer: InputDateFooterUtilContainer,
26
+ },
27
+ };
@@ -1,13 +1,11 @@
1
- "use client";
2
-
3
1
  import { InputFoundation } from "./foundation";
4
2
  import { InputText } from "./text";
5
- import { InputCalendar } from "./calendar";
3
+ import { InputDate } from "./date";
6
4
  import { InputAddress } from "./address";
7
5
 
8
6
  export const Input = {
9
7
  ...InputFoundation,
10
8
  Text: InputText,
11
- Calendar: InputCalendar,
9
+ Date: InputDate,
12
10
  Address: InputAddress,
13
11
  };
@@ -0,0 +1,45 @@
1
+ .input-date-field {
2
+ // Date 입력 영역은 trigger만 관리하고 캘린더 레이어는 calendar 모듈이 담당한다.
3
+ display: grid;
4
+ gap: var(--spacing-gap-5);
5
+ width: 100%;
6
+ }
7
+
8
+ .input-date-trigger-input {
9
+ width: 100%;
10
+ }
11
+
12
+ .input-date-trigger-icon {
13
+ margin: 0;
14
+ display: inline-flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ width: 28px;
18
+ height: 28px;
19
+ color: var(--color-label-alternative);
20
+ // 아이콘 영역 클릭은 input으로 위임한다.
21
+ pointer-events: none;
22
+ }
23
+
24
+ .input-date-footer-template {
25
+ // footer 템플릿은 input 스킨에서만 관리한다.
26
+ display: grid;
27
+ gap: var(--spacing-gap-5);
28
+ width: 100%;
29
+ }
30
+
31
+ .input-date-footer-util {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: space-between;
35
+ gap: var(--spacing-gap-3);
36
+ width: 100%;
37
+ }
38
+
39
+ .input-date-action-button {
40
+ width: auto;
41
+ }
42
+
43
+ .input-date-apply-button {
44
+ width: 100%;
45
+ }
@@ -0,0 +1,286 @@
1
+ import type { MouseEvent, ReactNode } from "react";
2
+ import type { UseFormRegisterReturn } from "react-hook-form";
3
+ import type {
4
+ CalendarColumns,
5
+ CalendarDatePickerProps,
6
+ CalendarMode,
7
+ CalendarOnChange,
8
+ CalendarValue,
9
+ } from "../../calendar";
10
+
11
+ /**
12
+ * Calendar trigger(Input) 영역에서 필요한 속성 묶음.
13
+ * FormField와의 조합을 고려해 label/helper 등을 상위 Template에 위임한다.
14
+ * @property {string} [id] trigger id (label htmlFor 연동)
15
+ * @property {string} [name] form name; register 미사용 시 수동 지정
16
+ * @property {UseFormRegisterReturn} [register] react-hook-form register 결과
17
+ * @property {(event: MouseEvent<Element>) => void} [onClick] trigger 클릭 핸들러
18
+ * @property {boolean} [disabled] disabled 여부
19
+ * @property {string} [placeholder] trigger 내부 placeholder
20
+ */
21
+ export interface InputCalendarTriggerProps {
22
+ /**
23
+ * trigger id (label htmlFor 연동)
24
+ */
25
+ id?: string;
26
+ /**
27
+ * form name; register 미사용 시 수동 지정
28
+ */
29
+ name?: string;
30
+ /**
31
+ * react-hook-form register 결과
32
+ */
33
+ register?: UseFormRegisterReturn;
34
+ /**
35
+ * trigger 클릭 핸들러
36
+ */
37
+ onClick?: (event: MouseEvent<Element>) => void;
38
+ /**
39
+ * disabled 여부
40
+ */
41
+ disabled?: boolean;
42
+ /**
43
+ * trigger 내부 placeholder
44
+ */
45
+ placeholder?: string;
46
+ }
47
+
48
+ /**
49
+ * Trigger view에서만 사용하는 부가 props.
50
+ * @property {string} [className] trigger root className
51
+ * @property {string} [displayValue] 표시 값 문자열
52
+ */
53
+ export interface InputCalendarTriggerViewProps extends InputCalendarTriggerProps {
54
+ /**
55
+ * trigger root className
56
+ */
57
+ className?: string;
58
+ /**
59
+ * 표시 값 문자열
60
+ */
61
+ displayValue?: string;
62
+ }
63
+
64
+ /**
65
+ * Calendar Root가 커스텀 trigger에 전달하는 렌더 props.
66
+ * @property {string} [id] trigger id
67
+ * @property {string} [name] input name
68
+ * @property {string} [displayValue] 트리거 표시 문자열
69
+ * @property {string} [placeholder] 트리거 placeholder
70
+ * @property {boolean} [disabled] disabled 여부
71
+ * @property {(event: MouseEvent<Element>) => void} [onClick] 트리거 클릭 핸들러
72
+ */
73
+ export interface InputCalendarTriggerRenderProps {
74
+ /**
75
+ * trigger id
76
+ */
77
+ id?: string;
78
+ /**
79
+ * input name
80
+ */
81
+ name?: string;
82
+ /**
83
+ * 트리거 표시 문자열
84
+ */
85
+ displayValue?: string;
86
+ /**
87
+ * 트리거 placeholder
88
+ */
89
+ placeholder?: string;
90
+ /**
91
+ * disabled 여부
92
+ */
93
+ disabled?: boolean;
94
+ /**
95
+ * 트리거 클릭 핸들러
96
+ */
97
+ onClick?: (event: MouseEvent<Element>) => void;
98
+ }
99
+
100
+ /**
101
+ * Input Calendar core props.
102
+ * @property {CalendarMode} [mode="date"] 날짜/시간 모드
103
+ * @property {CalendarColumns} [columns=1] 동시에 노출할 달력 열 수
104
+ * @property {CalendarValue} [value] 제어형 값
105
+ * @property {CalendarValue} [defaultValue] 비제어 초기값
106
+ * @property {CalendarOnChange} [onChange] 값 변경 핸들러(직렬화 문자열 기준)
107
+ * @property {CalendarOnChange} [onValueChange] onChange 별칭(추가 파이프라인용)
108
+ * @property {boolean} [readOnly] 읽기 전용 여부
109
+ * @property {boolean} [disabled] disabled 여부
110
+ * @property {CalendarDatePickerProps} [datePickerProps] Mantine DatePicker 직접 옵션
111
+ * @property {ReactNode} [header] 커스텀 header 콘텐츠
112
+ * @property {ReactNode} [footer] 커스텀 footer 콘텐츠
113
+ * @property {unknown} [timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
114
+ * @property {boolean} [calendarOpened] calendar 열림 제어 여부
115
+ * @property {string} [className] root className
116
+ * @property {ReactNode} [trigger] 커스텀 trigger 슬롯
117
+ * @property {(props: InputCalendarTriggerRenderProps) => ReactNode} [renderTrigger] 커스텀 trigger 렌더 함수
118
+ */
119
+ export interface InputCalendarProps extends InputCalendarTriggerProps {
120
+ /**
121
+ * 날짜/시간 모드
122
+ */
123
+ mode?: CalendarMode;
124
+ /**
125
+ * 동시에 노출할 달력 열 수
126
+ */
127
+ columns?: CalendarColumns;
128
+ /**
129
+ * 제어형 값
130
+ */
131
+ value?: CalendarValue;
132
+ /**
133
+ * 비제어 초기값
134
+ */
135
+ defaultValue?: CalendarValue;
136
+ /**
137
+ * 값 변경 핸들러
138
+ */
139
+ onChange?: CalendarOnChange;
140
+ /**
141
+ * onChange 별칭; 외부 파이프라인 시 사용
142
+ */
143
+ onValueChange?: CalendarOnChange;
144
+ /**
145
+ * 읽기 전용 여부
146
+ */
147
+ readOnly?: boolean;
148
+ /**
149
+ * disabled 여부
150
+ */
151
+ disabled?: boolean;
152
+ /**
153
+ * Mantine DatePicker 옵션
154
+ */
155
+ datePickerProps?: CalendarDatePickerProps;
156
+ /**
157
+ * 커스텀 header 콘텐츠
158
+ */
159
+ header?: ReactNode;
160
+ /**
161
+ * 커스텀 footer 콘텐츠
162
+ */
163
+ footer?: ReactNode;
164
+ /**
165
+ * TimePicker 확장용 예약 슬롯(현재 미구현).
166
+ * 추후 Calendar Body 조합 확장 시점에 구체 타입을 확정한다.
167
+ */
168
+ timePicker?: unknown;
169
+ /**
170
+ * calendar 열림 제어 여부
171
+ */
172
+ calendarOpened?: boolean;
173
+ /**
174
+ * root className
175
+ */
176
+ className?: string;
177
+ /**
178
+ * 커스텀 trigger 슬롯
179
+ */
180
+ trigger?: ReactNode;
181
+ /**
182
+ * 커스텀 trigger 렌더 함수
183
+ */
184
+ renderTrigger?: (props: InputCalendarTriggerRenderProps) => ReactNode;
185
+ }
186
+
187
+ /**
188
+ * footer 개별 버튼 공용 props.
189
+ * @property {ReactNode} [children] 버튼 라벨
190
+ * @property {() => void} [onClick] click 핸들러
191
+ * @property {boolean} [disabled] disabled 여부
192
+ * @property {string} [className] 버튼 className
193
+ */
194
+ export interface InputCalendarInlineButtonProps {
195
+ /**
196
+ * 버튼 라벨
197
+ */
198
+ children?: ReactNode;
199
+ /**
200
+ * click 핸들러
201
+ */
202
+ onClick?: () => void;
203
+ /**
204
+ * disabled 여부
205
+ */
206
+ disabled?: boolean;
207
+ /**
208
+ * 버튼 className
209
+ */
210
+ className?: string;
211
+ }
212
+
213
+ /**
214
+ * Apply 버튼 전용 props.
215
+ * @extends InputCalendarInlineButtonProps
216
+ * @property {ReactNode} [label] children 미지정 시 사용할 기본 라벨
217
+ */
218
+ export interface InputCalendarApplyButtonProps extends InputCalendarInlineButtonProps {
219
+ /**
220
+ * 버튼 라벨
221
+ */
222
+ label?: ReactNode;
223
+ }
224
+
225
+ /**
226
+ * Footer template의 util 버튼 컨테이너 props.
227
+ * @property {ReactNode} [children] util 버튼(children)
228
+ * @property {string} [className] container className
229
+ */
230
+ export interface InputCalendarFooterUtilContainerProps {
231
+ /**
232
+ * util 버튼(children)
233
+ */
234
+ children?: ReactNode;
235
+ /**
236
+ * container className
237
+ */
238
+ className?: string;
239
+ }
240
+
241
+ /**
242
+ * Footer template의 루트 컨테이너 props.
243
+ * @property {ReactNode} [children] footer 내부 children
244
+ * @property {string} [className] container className
245
+ */
246
+ export interface InputCalendarFooterTemplateContainerProps {
247
+ /**
248
+ * footer 내부 children
249
+ */
250
+ children?: ReactNode;
251
+ /**
252
+ * container className
253
+ */
254
+ className?: string;
255
+ }
256
+
257
+ /**
258
+ * 기본 FooterTemplate props.
259
+ * @property {string} [className] root className
260
+ * @property {() => void} [onClear] clear 버튼 클릭 핸들러
261
+ * @property {() => void} [onToday] today 버튼 클릭 핸들러
262
+ * @property {() => void} [onApply] apply 버튼 클릭 핸들러
263
+ * @property {boolean} [disabled] 전체 버튼 disabled 여부
264
+ */
265
+ export interface InputCalendarFooterTemplateProps {
266
+ /**
267
+ * root className
268
+ */
269
+ className?: string;
270
+ /**
271
+ * clear 버튼 클릭 핸들러
272
+ */
273
+ onClear?: () => void;
274
+ /**
275
+ * today 버튼 클릭 핸들러
276
+ */
277
+ onToday?: () => void;
278
+ /**
279
+ * apply 버튼 클릭 핸들러
280
+ */
281
+ onApply?: () => void;
282
+ /**
283
+ * 전체 버튼 disabled 여부
284
+ */
285
+ disabled?: boolean;
286
+ }
@@ -1,6 +1,6 @@
1
1
  export type * from "./foundation";
2
2
  export type * from "./text";
3
- export type * from "./calendar";
3
+ export type * from "./date";
4
4
  export type * from "./verification";
5
5
  export type * from "./hooks";
6
6
  export type * from "./address";
@@ -1,4 +1,4 @@
1
- import { string as toString } from "@uniai-fe/util-functions";
1
+ import { string } from "@uniai-fe/util-functions";
2
2
  import type { Address } from "react-daum-postcode";
3
3
 
4
4
  import type {
@@ -37,7 +37,7 @@ const isAddressStructureLike = (
37
37
  const toAddressParts = (items: unknown[] | undefined): string[] => {
38
38
  if (!Array.isArray(items)) return [];
39
39
  return items
40
- .map(item => toString(item, ""))
40
+ .map(item => string(item, ""))
41
41
  .filter((segment): segment is string => segment.length > 0);
42
42
  };
43
43
 
@@ -0,0 +1,61 @@
1
+ import { dayjs } from "../../../init/dayjs";
2
+ import type { CalendarColumns, CalendarValue } from "../../calendar";
3
+
4
+ const DATE_FORMAT = "YYYY-MM-DD";
5
+
6
+ /**
7
+ * Calendar에 빈 값을 선언한다.
8
+ * @returns {CalendarValue} null 직렬화 값
9
+ */
10
+ export const createEmptyValue = (): CalendarValue => null;
11
+
12
+ /**
13
+ * Date 객체를 YYYY-MM-DD 문자열로 포맷한다.
14
+ * @param {Date | string | null} date 포맷 대상
15
+ * @returns {CalendarValue} 포맷 결과
16
+ */
17
+ const formatDate = (date: Date | string | null) =>
18
+ date ? dayjs(date).format(DATE_FORMAT) : null;
19
+
20
+ /**
21
+ * Mantine DatePicker에 전달할 Date 객체를 만든다.
22
+ * @param {CalendarValue} value YYYY-MM-DD 문자열
23
+ * @returns {Date | null} DatePicker value
24
+ */
25
+ export const mapValueToPicker = (value: CalendarValue) =>
26
+ value ? dayjs(value).toDate() : null;
27
+
28
+ /**
29
+ * DatePicker 반환 값을 문자열로 직렬화한다.
30
+ * @param {Date | null} value DatePicker 값
31
+ * @returns {CalendarValue} 직렬화 문자열
32
+ */
33
+ export const parseValueFromPicker = (value: Date | null) =>
34
+ formatDate(value ?? null);
35
+
36
+ /**
37
+ * 오늘 날짜를 YYYY-MM-DD 문자열로 반환한다.
38
+ * @returns {CalendarValue} 오늘 값
39
+ */
40
+ export const getTodayValue = (): CalendarValue => dayjs().format(DATE_FORMAT);
41
+
42
+ /**
43
+ * hidden input 직렬화용 문자열.
44
+ * @param {CalendarValue} value 현재 값
45
+ * @returns {string} 직렬화된 문자열
46
+ */
47
+ export const serializeCalendarValue = (value: CalendarValue) => value ?? "";
48
+
49
+ /**
50
+ * trigger에 표시할 문자열을 계산한다.
51
+ * @param {CalendarValue} value 현재 값
52
+ * @returns {string} 표시 문자열
53
+ */
54
+ export const formatTriggerValue = (value: CalendarValue) => value ?? "";
55
+
56
+ /**
57
+ * columns 값을 Mantine numberOfColumns와 맞춘다.
58
+ * @param {CalendarColumns} columns 열 개수
59
+ * @returns {number} numberOfColumns 값
60
+ */
61
+ export const mapColumnsToNumber = (columns: CalendarColumns) => columns;
@@ -1,2 +1,3 @@
1
1
  export * from "./verification";
2
+ export * from "./date";
2
3
  export * from "./address";
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,4 @@
1
+ import "./index.scss";
2
+
3
+ export * from "./markup";
4
+ export type * from "./types";
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import * as PopOverPrimitive from "@radix-ui/react-popover";
4
+ import clsx from "clsx";
5
+ import { forwardRef } from "react";
6
+ import type { PopOverContentProps } from "../types";
7
+
8
+ /**
9
+ * PopOver Content.
10
+ * @component
11
+ * @param {PopOverContentProps} props Content props
12
+ * @param {React.ReactNode} [props.children] Content 내부 콘텐츠
13
+ * @param {string} [props.className] Content className
14
+ * @param {boolean} [props.withPortal=true] Portal 사용 여부
15
+ * @param {HTMLElement | null} [props.portalContainer] Portal 컨테이너
16
+ * @param {number} [props.sideOffset=4] Trigger와 Content 간격
17
+ * @param {PopOverContentWidth} [props.width="fit-content"] Content width 옵션
18
+ * @example
19
+ * <PopOver.Content sideOffset={6}>popover content</PopOver.Content>
20
+ */
21
+ const PopOverContent = forwardRef<HTMLDivElement, PopOverContentProps>(
22
+ (
23
+ {
24
+ children,
25
+ className,
26
+ withPortal = true,
27
+ portalContainer,
28
+ sideOffset = 4,
29
+ width = "fit-content",
30
+ style,
31
+ ...restProps
32
+ },
33
+ ref,
34
+ ) => {
35
+ const resolvedWidth =
36
+ width === "match"
37
+ ? "var(--radix-popover-trigger-width)"
38
+ : typeof width === "number"
39
+ ? `${width}px`
40
+ : width;
41
+ const resolvedWidthToken =
42
+ width === "match" || width === "fit-content" || width === "max-content"
43
+ ? width
44
+ : "custom";
45
+
46
+ // Content 노드는 portal 여부에 따라 동일 노드를 재사용한다.
47
+ const contentNode = (
48
+ <PopOverPrimitive.Content
49
+ ref={ref}
50
+ className={clsx("pop-over-content", className)}
51
+ sideOffset={sideOffset}
52
+ data-width={resolvedWidthToken}
53
+ style={{
54
+ ...style,
55
+ width: resolvedWidth,
56
+ }}
57
+ {...restProps}
58
+ >
59
+ {children}
60
+ </PopOverPrimitive.Content>
61
+ );
62
+
63
+ if (!withPortal) {
64
+ return contentNode;
65
+ }
66
+
67
+ return (
68
+ <PopOverPrimitive.Portal container={portalContainer ?? undefined}>
69
+ {contentNode}
70
+ </PopOverPrimitive.Portal>
71
+ );
72
+ },
73
+ );
74
+
75
+ PopOverContent.displayName = "PopOverContent";
76
+
77
+ export default PopOverContent;
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import * as PopOverPrimitive from "@radix-ui/react-popover";
4
+ import type { PopOverRootProps } from "../types";
5
+
6
+ /**
7
+ * PopOver Root.
8
+ * @component
9
+ * @param {PopOverRootProps} props Root props
10
+ * @param {React.ReactNode} props.children PopOver 하위 노드
11
+ * @param {boolean} [props.open] 제어형 open 상태
12
+ * @param {boolean} [props.defaultOpen] 비제어 초기 open 상태
13
+ * @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 핸들러
14
+ * @param {boolean} [props.modal] Radix modal 모드
15
+ * @example
16
+ * <PopOver.Root>
17
+ * <PopOver.Trigger asChild><button>open</button></PopOver.Trigger>
18
+ * <PopOver.Content>content</PopOver.Content>
19
+ * </PopOver.Root>
20
+ */
21
+ export default function PopOverRoot({
22
+ children,
23
+ ...restProps
24
+ }: PopOverRootProps) {
25
+ return (
26
+ <PopOverPrimitive.Root {...restProps}>{children}</PopOverPrimitive.Root>
27
+ );
28
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import * as PopOverPrimitive from "@radix-ui/react-popover";
4
+ import { forwardRef } from "react";
5
+ import type { PopOverTriggerProps } from "../types";
6
+
7
+ /**
8
+ * PopOver Trigger.
9
+ * @component
10
+ * @param {PopOverTriggerProps} props Trigger props
11
+ * @param {React.ReactNode} [props.children] Trigger 콘텐츠
12
+ * @param {boolean} [props.asChild] child 노드를 Trigger element로 사용할지 여부
13
+ * @example
14
+ * <PopOver.Trigger asChild><button>open</button></PopOver.Trigger>
15
+ */
16
+ const PopOverTrigger = forwardRef<HTMLButtonElement, PopOverTriggerProps>(
17
+ ({ children, ...restProps }, ref) => (
18
+ <PopOverPrimitive.Trigger ref={ref} {...restProps}>
19
+ {children}
20
+ </PopOverPrimitive.Trigger>
21
+ ),
22
+ );
23
+
24
+ PopOverTrigger.displayName = "PopOverTrigger";
25
+
26
+ export default PopOverTrigger;
@@ -0,0 +1,17 @@
1
+ import PopOverContent from "./Content";
2
+ import PopOverRoot from "./Root";
3
+ import PopOverTrigger from "./Trigger";
4
+
5
+ export { PopOverRoot, PopOverTrigger, PopOverContent };
6
+
7
+ /**
8
+ * PopOver namespace.
9
+ * - Root
10
+ * - Trigger
11
+ * - Content
12
+ */
13
+ export const PopOver = {
14
+ Root: PopOverRoot,
15
+ Trigger: PopOverTrigger,
16
+ Content: PopOverContent,
17
+ };
@@ -0,0 +1,5 @@
1
+ .pop-over-content {
2
+ position: relative;
3
+ z-index: 50;
4
+ outline: none;
5
+ }
@@ -0,0 +1,24 @@
1
+ .pop-over-content {
2
+ // PopOver 공통 surface 스킨(배경/라운드/그림자)은 Content에서 기본 제공한다.
3
+ width: fit-content;
4
+ max-width: min(100vw - 24px, max-content);
5
+ box-sizing: border-box;
6
+ background-color: var(--color-common-100);
7
+ border-radius: var(--theme-radius-large-2);
8
+ overflow: hidden;
9
+ box-shadow:
10
+ 0 4px 20px rgba(0, 0, 0, 0.16),
11
+ 0 0 2px rgba(0, 0, 0, 0.12);
12
+ }
13
+
14
+ .pop-over-content[data-width="match"] {
15
+ width: var(--radix-popover-trigger-width);
16
+ }
17
+
18
+ .pop-over-content[data-width="fit-content"] {
19
+ width: fit-content;
20
+ }
21
+
22
+ .pop-over-content[data-width="max-content"] {
23
+ width: max-content;
24
+ }
@@ -0,0 +1,2 @@
1
+ @use "./base.scss";
2
+ @use "./content.scss";