@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 @@
1
+ export type * from "./pop-over";
@@ -0,0 +1,86 @@
1
+ import type {
2
+ PopoverContentProps as RadixPopOverContentProps,
3
+ PopoverProps as RadixPopOverProps,
4
+ PopoverTriggerProps as RadixPopOverTriggerProps,
5
+ } from "@radix-ui/react-popover";
6
+ import type { ReactNode } from "react";
7
+
8
+ /**
9
+ * PopOver Content width 옵션.
10
+ * @property {"match"} width trigger width와 동일
11
+ * @property {"fit-content"} width 콘텐츠 기준 폭
12
+ * @property {"max-content"} width 최대 콘텐츠 폭
13
+ * @property {number} width px 단위 폭
14
+ * @property {string} width CSS width 문자열
15
+ */
16
+ export type PopOverContentWidth =
17
+ | "match"
18
+ | "fit-content"
19
+ | "max-content"
20
+ | number
21
+ | string;
22
+
23
+ /**
24
+ * PopOver Root props.
25
+ * @property {ReactNode} children PopOver 하위 노드
26
+ * @property {boolean} [open] 제어형 open 상태
27
+ * @property {boolean} [defaultOpen] 비제어 초기 open 상태
28
+ * @property {(open: boolean) => void} [onOpenChange] open 상태 변경 핸들러
29
+ * @property {boolean} [modal] Radix modal 모드
30
+ * @see Radix PopoverProps
31
+ */
32
+ export interface PopOverRootProps extends RadixPopOverProps {
33
+ /**
34
+ * PopOver 하위 노드
35
+ */
36
+ children: ReactNode;
37
+ }
38
+
39
+ /**
40
+ * PopOver Trigger props.
41
+ * @property {ReactNode} [children] Trigger 콘텐츠
42
+ * @property {boolean} [asChild] child 노드를 trigger로 사용할지 여부
43
+ * @see Radix PopoverTriggerProps
44
+ */
45
+ export interface PopOverTriggerProps extends RadixPopOverTriggerProps {
46
+ /**
47
+ * Trigger 콘텐츠
48
+ */
49
+ children?: ReactNode;
50
+ }
51
+
52
+ /**
53
+ * PopOver Content props.
54
+ * @property {ReactNode} [children] Content 내부 콘텐츠
55
+ * @property {string} [className] Content className
56
+ * @property {boolean} [withPortal=true] Portal 사용 여부
57
+ * @property {HTMLElement | null} [portalContainer] Portal 컨테이너
58
+ * @property {number} [sideOffset=4] trigger와 content 사이 간격
59
+ * @property {PopOverContentWidth} [width="fit-content"] content width 옵션
60
+ * @see Radix PopoverContentProps
61
+ */
62
+ export interface PopOverContentProps extends Omit<
63
+ RadixPopOverContentProps,
64
+ "children"
65
+ > {
66
+ /**
67
+ * Content 내부 콘텐츠
68
+ */
69
+ children?: ReactNode;
70
+ /**
71
+ * Content className
72
+ */
73
+ className?: string;
74
+ /**
75
+ * Portal 사용 여부
76
+ */
77
+ withPortal?: boolean;
78
+ /**
79
+ * Portal 컨테이너
80
+ */
81
+ portalContainer?: HTMLElement | null;
82
+ /**
83
+ * content width 옵션
84
+ */
85
+ width?: PopOverContentWidth;
86
+ }
package/src/index.scss CHANGED
@@ -6,6 +6,7 @@
6
6
  @use "./components/chip";
7
7
  @use "./components/drawer";
8
8
  @use "./components/dropdown";
9
+ @use "./components/pop-over";
9
10
  @use "./components/label";
10
11
  @use "./components/input";
11
12
  @use "./components/select";
package/src/index.tsx CHANGED
@@ -1,5 +1,6 @@
1
- // dayjs 초기화를 가장 먼저 수행해 날짜 계열 컴포넌트가 동일 컨텍스트를 사용한다.
1
+ // Mantine 날짜 생태계와 dayjs 설정을 번만 초기화한다.
2
2
  import "./init/dayjs";
3
+ import "./init/mantine";
3
4
  // ThemeProvider는 foundation provider에서만 export한다(one-source 규칙).
4
5
 
5
6
  // 루트 배럴: 카테고리별 export를 집계한다.
@@ -12,6 +13,7 @@ export * from "./components/select";
12
13
  export * from "./components/tab";
13
14
  export * from "./components/navigation";
14
15
  export * from "./components/dropdown";
16
+ export * from "./components/pop-over";
15
17
  export * from "./components/drawer";
16
18
  export * from "./components/scrollbar";
17
19
  export * from "./components/calendar";
@@ -0,0 +1,5 @@
1
+ @import url("@mantine/core/styles/default-css-variables.css");
2
+ @import url("@mantine/core/styles/global.css");
3
+ @import url("@mantine/dates/styles.css");
4
+
5
+ /* Mantine DatePicker/시간 컴포넌트에 대한 추가 override가 필요하면 이 파일에서 정의한다. */
@@ -0,0 +1,2 @@
1
+ // Mantine Date 계열 CSS를 한 번만 import해 전역 스타일 충돌을 방지한다.
2
+ import "./mantine.css";
@@ -1,329 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- DatePicker,
5
- type DatePickerStylesNames,
6
- type DatePickerType,
7
- } from "@mantine/dates";
8
- import { useUncontrolled } from "@mantine/hooks";
9
- import type { ChangeEvent, FocusEvent } from "react";
10
- import { forwardRef, useCallback, useMemo } from "react";
11
- import { Button } from "../../../button";
12
- import ChevronLeftIcon from "../../img/calendar/chevron-left.svg";
13
- import ChevronRightIcon from "../../img/calendar/chevron-right.svg";
14
- import type {
15
- InputCalendarActionButtonProps,
16
- InputCalendarProps,
17
- InputCalendarValue,
18
- } from "../../types";
19
- import { dayjs } from "../../../../init/dayjs";
20
-
21
- const DATE_FORMAT = "YYYY-MM-DD";
22
- type CalendarValue = InputCalendarValue<DatePickerType>;
23
-
24
- const DATE_PICKER_CLASS_NAMES = {
25
- calendar: "input-calendar-panel",
26
- datePickerRoot: "input-calendar-root",
27
- calendarHeader: "input-calendar-header",
28
- calendarHeaderControl: "input-calendar-header-control",
29
- calendarHeaderLevel: "input-calendar-header-level",
30
- day: "input-calendar-day",
31
- weekday: "input-calendar-weekday",
32
- weekdaysRow: "input-calendar-weekdays",
33
- monthRow: "input-calendar-month-row",
34
- monthCell: "input-calendar-month-cell",
35
- month: "input-calendar-month",
36
- monthThead: "input-calendar-month-thead",
37
- monthTbody: "input-calendar-month-tbody",
38
- } as Partial<Record<DatePickerStylesNames, string>>;
39
-
40
- const DEFAULT_ACTION_LABELS: Record<"clear" | "today" | "apply", string> = {
41
- clear: "삭제",
42
- today: "오늘",
43
- apply: "적용하기",
44
- };
45
-
46
- const createEmptyValue = (type: DatePickerType): CalendarValue => {
47
- if (type === "range") {
48
- return [null, null];
49
- }
50
- if (type === "multiple") {
51
- return [];
52
- }
53
- return null;
54
- };
55
-
56
- const serializeValue = (value: CalendarValue, type: DatePickerType) => {
57
- if (type === "range" || type === "multiple") {
58
- return JSON.stringify(value ?? createEmptyValue(type));
59
- }
60
- return (value as string) ?? "";
61
- };
62
-
63
- const formatDate = (date: Date | string | null) =>
64
- date ? dayjs(date).format(DATE_FORMAT) : null;
65
-
66
- const mapValueToPicker = (value: CalendarValue, type: DatePickerType) => {
67
- if (type === "range") {
68
- const [start, end] = (value ?? [null, null]) as InputCalendarValue<"range">;
69
- return [
70
- start ? dayjs(start).toDate() : null,
71
- end ? dayjs(end).toDate() : null,
72
- ];
73
- }
74
- if (type === "multiple") {
75
- return ((value ?? []) as InputCalendarValue<"multiple">).map(entry =>
76
- entry ? dayjs(entry).toDate() : null,
77
- );
78
- }
79
- if (typeof value === "string" && value) {
80
- return dayjs(value).toDate();
81
- }
82
- return null;
83
- };
84
-
85
- const parseValueFromPicker = (
86
- value: unknown,
87
- type: DatePickerType,
88
- ): CalendarValue => {
89
- if (type === "range") {
90
- const [start, end] = (value as [Date | null, Date | null]) ?? [null, null];
91
- return [formatDate(start), formatDate(end)] as CalendarValue;
92
- }
93
- if (type === "multiple") {
94
- return ((value as (Date | null)[]) || []).map(entry =>
95
- formatDate(entry),
96
- ) as CalendarValue;
97
- }
98
- return formatDate((value as Date | null) ?? null);
99
- };
100
-
101
- const getTodayValue = (type: DatePickerType): CalendarValue => {
102
- const today = dayjs().format(DATE_FORMAT);
103
- if (type === "range") {
104
- return [today, today];
105
- }
106
- if (type === "multiple") {
107
- return [today];
108
- }
109
- return today;
110
- };
111
-
112
- const resolveAction = (
113
- action: InputCalendarActionButtonProps | undefined,
114
- key: keyof typeof DEFAULT_ACTION_LABELS,
115
- ) => {
116
- const visible = action?.visible ?? true;
117
- if (!visible) return null;
118
- return {
119
- label: action?.label ?? DEFAULT_ACTION_LABELS[key],
120
- disabled: action?.disabled ?? false,
121
- };
122
- };
123
-
124
- const InputCalendarBase = forwardRef<
125
- HTMLDivElement,
126
- InputCalendarProps<DatePickerType>
127
- >(
128
- // DatePickerType 유니언을 명시해 range/multiple 비교 로직 타입 오류를 방지한다.
129
- (props, ref) => {
130
- const {
131
- mode = "date",
132
- type = "default",
133
- columns = 1,
134
- value,
135
- defaultValue,
136
- onChange,
137
- onValueChange,
138
- onClear,
139
- onToday,
140
- onApply,
141
- readOnly,
142
- disabled,
143
- datePickerProps,
144
- actions,
145
- name,
146
- register,
147
- } = props;
148
-
149
- const [calendarValue, setCalendarValue] = useUncontrolled<CalendarValue>({
150
- value,
151
- defaultValue,
152
- finalValue: createEmptyValue(type),
153
- onChange: changedValue => {
154
- onChange?.(changedValue as InputCalendarValue<typeof type>);
155
- onValueChange?.(changedValue as InputCalendarValue<typeof type>);
156
- },
157
- });
158
-
159
- const serializedValue = useMemo(
160
- () => serializeValue(calendarValue, type),
161
- [calendarValue, type],
162
- );
163
-
164
- const emitRegisterChange = useCallback(
165
- (nextValue: CalendarValue) => {
166
- if (!register?.onChange) {
167
- return;
168
- }
169
-
170
- const syntheticEvent = {
171
- target: {
172
- name: register.name,
173
- value: serializeValue(nextValue, type),
174
- },
175
- currentTarget: {
176
- name: register.name,
177
- value: serializeValue(nextValue, type),
178
- },
179
- } as unknown as ChangeEvent<HTMLInputElement>;
180
-
181
- register.onChange(syntheticEvent);
182
- },
183
- [register, type],
184
- );
185
-
186
- const updateValue = useCallback(
187
- (nextValue: CalendarValue) => {
188
- setCalendarValue(nextValue as InputCalendarValue<typeof type>);
189
- emitRegisterChange(nextValue);
190
- },
191
- [emitRegisterChange, setCalendarValue],
192
- );
193
-
194
- const handleDateChange = useCallback(
195
- (next: unknown) => {
196
- const normalized = parseValueFromPicker(next, type);
197
- updateValue(normalized);
198
- },
199
- [type, updateValue],
200
- );
201
-
202
- const handleClear = () => {
203
- updateValue(createEmptyValue(type));
204
- onClear?.();
205
- };
206
-
207
- const handleToday = () => {
208
- updateValue(getTodayValue(type));
209
- onToday?.();
210
- };
211
-
212
- const handleApply = () => {
213
- onApply?.(calendarValue as InputCalendarValue<typeof type>);
214
- };
215
-
216
- const resolvedClear = resolveAction(actions?.clear, "clear");
217
- const resolvedToday = resolveAction(actions?.today, "today");
218
- const resolvedApply = resolveAction(actions?.apply, "apply");
219
-
220
- const isInteractive = !(readOnly || disabled);
221
- const hiddenName = register?.name ?? name;
222
-
223
- const handleHiddenRef = useCallback(
224
- (node: HTMLInputElement | null) => {
225
- if (register?.ref) {
226
- register.ref(node);
227
- }
228
- },
229
- [register],
230
- );
231
-
232
- const handleHiddenBlur = (event: FocusEvent<HTMLInputElement>) => {
233
- register?.onBlur?.(event);
234
- };
235
-
236
- if (mode !== "date" && process.env.NODE_ENV !== "production") {
237
- console.warn("InputCalendarBase: 현재 date 모드만 지원합니다.");
238
- }
239
-
240
- const singleRangeProps =
241
- type === "range"
242
- ? {
243
- allowSingleDateInRange: true,
244
- }
245
- : {};
246
-
247
- return (
248
- <div
249
- ref={ref}
250
- className="input-calendar"
251
- data-mode={mode}
252
- data-type={type}
253
- data-columns={columns}
254
- data-disabled={disabled ? "true" : undefined}
255
- >
256
- {hiddenName ? (
257
- <input
258
- type="hidden"
259
- name={hiddenName}
260
- value={serializedValue}
261
- readOnly
262
- ref={handleHiddenRef}
263
- onBlur={handleHiddenBlur}
264
- />
265
- ) : null}
266
- <div className="input-calendar-container">
267
- <DatePicker
268
- classNames={DATE_PICKER_CLASS_NAMES}
269
- value={mapValueToPicker(calendarValue, type) as never}
270
- onChange={handleDateChange as never}
271
- type={type}
272
- numberOfColumns={columns}
273
- nextIcon={<ChevronRightIcon />}
274
- previousIcon={<ChevronLeftIcon />}
275
- // Mantine DatePicker에는 disabled prop이 없어 data-disabled로 상단에서 제어한다.
276
- {...(datePickerProps ?? {})}
277
- {...singleRangeProps}
278
- />
279
- <div className="input-calendar-footer">
280
- <div className="input-calendar-footer-actions">
281
- {resolvedClear ? (
282
- <Button.Default
283
- fill="outlined"
284
- size="small"
285
- priority="tertiary"
286
- disabled={!isInteractive || resolvedClear.disabled}
287
- onClick={handleClear}
288
- className="input-calendar-action-button"
289
- >
290
- {resolvedClear.label}
291
- </Button.Default>
292
- ) : (
293
- <span />
294
- )}
295
- {resolvedToday ? (
296
- <Button.Default
297
- fill="outlined"
298
- size="small"
299
- priority="secondary"
300
- disabled={!isInteractive || resolvedToday.disabled}
301
- onClick={handleToday}
302
- className="input-calendar-action-button"
303
- >
304
- {resolvedToday.label}
305
- </Button.Default>
306
- ) : null}
307
- </div>
308
- {resolvedApply ? (
309
- <Button.Default
310
- fill="solid"
311
- size="large"
312
- priority="primary"
313
- disabled={!isInteractive || resolvedApply.disabled}
314
- onClick={handleApply}
315
- className="input-calendar-apply"
316
- >
317
- {resolvedApply.label}
318
- </Button.Default>
319
- ) : null}
320
- </div>
321
- </div>
322
- </div>
323
- );
324
- },
325
- );
326
-
327
- InputCalendarBase.displayName = "InputCalendarBase";
328
-
329
- export { InputCalendarBase };
@@ -1,8 +0,0 @@
1
- "use client";
2
-
3
- import "../../styles/calendar.scss";
4
- import { InputCalendarBase } from "./Base";
5
-
6
- export const InputCalendar = {
7
- Base: InputCalendarBase,
8
- };
@@ -1,110 +0,0 @@
1
- .input-calendar {
2
- display: flex;
3
- flex-direction: column;
4
- gap: var(--spacing-gap-6, 20px);
5
- width: 100%;
6
-
7
- &[data-disabled="true"] {
8
- opacity: 0.6;
9
- }
10
- }
11
-
12
- .input-calendar-container {
13
- background-color: var(--color-common-100, #fff);
14
- border-radius: var(--theme-radius-large-3, 16px);
15
- box-shadow:
16
- 0 4px 20px rgba(0, 0, 0, 0.16),
17
- 0 0 2px rgba(0, 0, 0, 0.12);
18
- padding: var(--spacing-padding-10, 32px) var(--spacing-padding-6, 16px)
19
- var(--spacing-padding-6, 16px);
20
- display: flex;
21
- flex-direction: column;
22
- gap: var(--spacing-gap-5, 16px);
23
- }
24
-
25
- .input-calendar-root {
26
- width: 100%;
27
- }
28
-
29
- .input-calendar-panel {
30
- width: 100%;
31
- border-radius: var(--theme-radius-large-1, 12px);
32
- background-color: transparent;
33
- }
34
-
35
- .input-calendar-header {
36
- display: flex;
37
- align-items: center;
38
- justify-content: space-between;
39
- padding-inline: var(--spacing-padding-6, 16px);
40
- margin-bottom: var(--spacing-gap-4, 12px);
41
- }
42
-
43
- .input-calendar-header-control {
44
- width: 24px;
45
- height: 24px;
46
- display: inline-flex;
47
- align-items: center;
48
- justify-content: center;
49
- border-radius: 999px;
50
- background: transparent;
51
- color: var(--color-label-strong, #18191b);
52
- }
53
-
54
- .input-calendar-header-level {
55
- font-size: var(--font-heading-small-size, 19px);
56
- font-weight: var(--font-heading-small-weight, 600);
57
- }
58
-
59
- .input-calendar-weekdays {
60
- display: grid;
61
- grid-template-columns: repeat(7, minmax(0, 1fr));
62
- padding-bottom: var(--spacing-padding-2, 4px);
63
- }
64
-
65
- .input-calendar-weekday {
66
- text-transform: none;
67
- font-size: var(--font-heading-xxsmall-size, 15px);
68
- color: var(--color-label-alternative, #afb1b6);
69
- text-align: center;
70
- }
71
-
72
- .input-calendar-day {
73
- width: 44px;
74
- height: 44px;
75
- border-radius: var(--theme-radius-large-1, 12px);
76
- justify-self: center;
77
- font-size: var(--font-body-medium-size, 17px);
78
- color: var(--color-label-strong, #3d3f43);
79
- }
80
-
81
- .input-calendar-footer {
82
- display: flex;
83
- flex-direction: column;
84
- gap: var(--spacing-gap-4, 12px);
85
- width: 100%;
86
- }
87
-
88
- .input-calendar-footer-actions {
89
- display: flex;
90
- align-items: center;
91
- justify-content: space-between;
92
- gap: var(--spacing-gap-3, 8px);
93
- }
94
-
95
- .input-calendar-action-button {
96
- width: 100%;
97
- }
98
-
99
- .input-calendar-footer-actions > :first-child {
100
- flex: 1 1 auto;
101
- }
102
-
103
- .input-calendar-footer-actions > :last-child {
104
- flex: 1 1 auto;
105
- justify-content: flex-end;
106
- }
107
-
108
- .input-calendar-apply {
109
- width: 100%;
110
- }