@uniai-fe/uds-primitives 0.6.4 → 0.6.6

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.
@@ -21,6 +21,18 @@
21
21
  z-index: 30;
22
22
  }
23
23
 
24
+ .calendar-range-root,
25
+ .calendar-range-grid {
26
+ // 변경: range는 Figma 2 calendar 기준으로 두 month column을 같은 panel에 고정한다.
27
+ --calendar-range-column-width: 322px;
28
+ --calendar-range-column-gap: var(--spacing-gap-6);
29
+ --calendar-width: calc(
30
+ (var(--calendar-range-column-width) * 2) +
31
+ var(--calendar-range-column-gap) + (var(--calendar-inline-padding) * 2)
32
+ );
33
+ max-width: min(100vw - (var(--spacing-padding-5) * 2), 720px);
34
+ }
35
+
24
36
  .calendar-header {
25
37
  margin-bottom: var(--spacing-gap-2);
26
38
  }
@@ -34,6 +46,10 @@
34
46
  width: var(--calendar-body-width);
35
47
  }
36
48
 
49
+ .calendar-range-grid {
50
+ width: var(--calendar-body-width);
51
+ }
52
+
37
53
  .calendar-grid table {
38
54
  width: auto;
39
55
  }
@@ -48,6 +64,17 @@
48
64
  width: 100%;
49
65
  }
50
66
 
67
+ .calendar-range-grid .calendar-month-level {
68
+ display: grid;
69
+ grid-template-columns: repeat(2, var(--calendar-range-column-width));
70
+ column-gap: var(--calendar-range-column-gap);
71
+ align-items: start;
72
+ }
73
+
74
+ .calendar-range-grid .calendar-month-level > [data-month-level="true"] {
75
+ width: var(--calendar-range-column-width);
76
+ }
77
+
51
78
  .calendar-header-row {
52
79
  display: grid;
53
80
  grid-template-columns: 44px 1fr 44px;
@@ -55,7 +82,8 @@
55
82
  width: 100%;
56
83
  max-width: none;
57
84
  column-gap: var(--spacing-gap-5);
58
- padding: 0 var(--spacing-padding-9);
85
+ // 변경: page navigation arrow는 Figma처럼 calendar header row 양 끝에 붙인다.
86
+ padding: 0;
59
87
  margin-bottom: var(--spacing-gap-5);
60
88
  --dch-fz: var(--font-heading-small-size) !important;
61
89
  }
@@ -64,7 +92,7 @@
64
92
  width: 44px;
65
93
  height: 44px;
66
94
  border-radius: 999px;
67
- display: inline-flex;
95
+ display: flex;
68
96
  align-items: center;
69
97
  justify-content: center;
70
98
  color: var(--color-label-alternative);
@@ -74,15 +102,26 @@
74
102
  }
75
103
 
76
104
  .calendar-header-control[data-direction="previous"] {
105
+ grid-column: 1;
106
+ grid-row: 1;
77
107
  justify-self: start;
78
108
  }
79
109
 
80
110
  .calendar-header-control[data-direction="next"] {
111
+ grid-column: 3;
112
+ grid-row: 1;
81
113
  justify-self: end;
82
114
  }
83
115
 
84
116
  .calendar-header-level {
117
+ // 변경: range 두 번째 month처럼 이전 버튼이 없는 header에서도 level을 중앙 column에 고정한다.
118
+ grid-column: 2;
119
+ grid-row: 1;
85
120
  justify-self: center;
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ gap: var(--spacing-gap-4);
86
125
  font-size: var(--font-heading-small-size);
87
126
  font-weight: var(--font-heading-small-weight);
88
127
  text-align: center;
@@ -93,6 +132,18 @@
93
132
  color: var(--color-label-strong);
94
133
  }
95
134
 
135
+ .calendar-header-level::after {
136
+ // 변경: Mantine header level button에 Figma의 연/월 선택 up-down 표시를 CSS layer에서 보강한다.
137
+ content: "";
138
+ flex: 0 0 auto;
139
+ width: 20px;
140
+ height: 20px;
141
+ background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.66675 8.33333L10.0001 5L13.3334 8.33333' stroke='%2394989E' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M6.66675 11.6667L10.0001 15L13.3334 11.6667' stroke='%2394989E' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
142
+ background-repeat: no-repeat;
143
+ background-position: center;
144
+ background-size: 20px 20px;
145
+ }
146
+
96
147
  .calendar-header-control:where(:not([data-disabled="true"])):hover {
97
148
  background-color: var(--color-tertiary-default);
98
149
  color: var(--color-label-standard);
@@ -215,18 +266,105 @@
215
266
  padding: 0;
216
267
  border: none;
217
268
  border-radius: var(--theme-radius-large-1);
269
+ box-sizing: border-box;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
218
273
  font-size: var(--font-body-medium-size);
219
274
  color: var(--color-label-standard);
220
275
  }
221
276
 
277
+ .calendar-day-label {
278
+ width: 44px;
279
+ height: 44px;
280
+ box-sizing: border-box;
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: center;
284
+ position: relative;
285
+ color: inherit;
286
+ }
287
+
222
288
  .calendar-day[data-outside="true"] {
223
289
  color: var(--color-label-alternative);
224
290
  }
225
291
 
226
292
  .calendar-day[data-selected="true"],
227
293
  .calendar-day[data-focused="true"] {
294
+ color: var(--color-common-100);
295
+ }
296
+
297
+ .calendar-day:where([data-in-range]) {
298
+ // 변경: button은 period surface만 담당해 selected blue layer와 range bridge를 분리한다.
299
+ background-color: var(--color-surface-static-blue);
300
+ color: var(--color-label-standard);
301
+ border-radius: 0;
302
+ box-shadow:
303
+ -1px 0 0 var(--color-surface-static-blue),
304
+ 1px 0 0 var(--color-surface-static-blue);
305
+ }
306
+
307
+ .calendar-day:where([data-first-in-range]) {
308
+ border-start-start-radius: var(--theme-radius-medium-3);
309
+ border-end-start-radius: var(--theme-radius-medium-3);
310
+ box-shadow: 1px 0 0 var(--color-surface-static-blue);
311
+ }
312
+
313
+ .calendar-day:where([data-last-in-range]) {
314
+ border-start-end-radius: var(--theme-radius-medium-3);
315
+ border-end-end-radius: var(--theme-radius-medium-3);
316
+ box-shadow: -1px 0 0 var(--color-surface-static-blue);
317
+ }
318
+
319
+ .calendar-day:where([data-first-in-range][data-last-in-range]) {
320
+ box-shadow: none;
321
+ }
322
+
323
+ .calendar-day:where([data-selected="true"], [data-focused="true"]) {
324
+ background-color: transparent;
325
+ color: var(--color-common-100);
326
+ }
327
+
328
+ .calendar-day:where([data-in-range][data-selected="true"]) {
329
+ background-color: var(--color-surface-static-blue);
330
+ }
331
+
332
+ .calendar-day:where([data-selected="true"], [data-focused="true"])
333
+ .calendar-day-label {
228
334
  background-color: var(--color-primary-default);
229
335
  color: var(--color-common-100);
336
+ border-radius: var(--theme-radius-medium-3);
337
+ }
338
+
339
+ .calendar-day:where(
340
+ [data-today][data-highlight-today]:not(
341
+ [data-selected="true"],
342
+ [data-in-range],
343
+ [data-disabled="true"]
344
+ )
345
+ ) {
346
+ // 변경: today는 Figma node처럼 selected와 별개인 border/dot 레이어로 표현한다.
347
+ border: 1px solid var(--color-primary-default);
348
+ color: var(--color-label-standard);
349
+ }
350
+
351
+ .calendar-day:where(
352
+ [data-today][data-highlight-today]:not(
353
+ [data-selected="true"],
354
+ [data-in-range],
355
+ [data-disabled="true"]
356
+ )
357
+ )
358
+ .calendar-day-label::after {
359
+ content: "";
360
+ width: 4px;
361
+ height: 4px;
362
+ border-radius: 999px;
363
+ background-color: var(--color-primary-default);
364
+ position: absolute;
365
+ bottom: 7px;
366
+ left: 50%;
367
+ transform: translateX(-50%);
230
368
  }
231
369
 
232
370
  .calendar-day:where(:disabled, [data-disabled="true"]) {
@@ -234,10 +372,43 @@
234
372
  color: var(--color-label-disabled);
235
373
  }
236
374
 
237
- .calendar-day:where(:not([data-disabled="true"])):hover {
375
+ .calendar-day:where([hidden]) {
376
+ // 변경: range의 outside date hidden row도 6주 높이 계산에 참여시켜 month 전환 점프를 막는다.
377
+ display: flex !important;
378
+ visibility: hidden;
379
+ pointer-events: none;
380
+ }
381
+
382
+ .calendar-range-grid .calendar-day:where([data-outside="true"]) {
383
+ // 변경: range outside date는 hidden attribute 없이 공간만 유지한다.
384
+ visibility: hidden;
385
+ pointer-events: none;
386
+ }
387
+
388
+ .calendar-day:where(
389
+ :not(
390
+ [data-disabled="true"],
391
+ [data-selected="true"],
392
+ [data-focused="true"],
393
+ [data-in-range]
394
+ )
395
+ ):hover {
238
396
  background-color: var(--color-secondary-default);
239
397
  color: var(--color-label-standard);
240
- border: none;
398
+ }
399
+
400
+ .calendar-day:where([data-selected="true"]):hover {
401
+ background-color: transparent;
402
+ color: var(--color-common-100);
403
+ }
404
+
405
+ .calendar-day:where([data-in-range][data-selected="true"]):hover {
406
+ background-color: var(--color-surface-static-blue);
407
+ }
408
+
409
+ .calendar-day:where([data-selected="true"]):hover .calendar-day-label {
410
+ background-color: var(--color-primary-default);
411
+ color: var(--color-common-100);
241
412
  }
242
413
 
243
414
  .calendar-footer {
@@ -22,6 +22,23 @@ export type CalendarColumns = 1 | 2;
22
22
  */
23
23
  export type CalendarValue = string | null;
24
24
 
25
+ /**
26
+ * Calendar range value 직렬화 타입.
27
+ * @property {CalendarValue} start 시작 날짜 YYYY-MM-DD 문자열 또는 null
28
+ * @property {CalendarValue} end 종료 날짜 YYYY-MM-DD 문자열 또는 null
29
+ */
30
+ export type CalendarRangeValue = [CalendarValue, CalendarValue];
31
+
32
+ /**
33
+ * Calendar range DatePicker 내부 값 타입.
34
+ * @property {Date | string | null} start DatePicker 시작 날짜 값
35
+ * @property {Date | string | null} end DatePicker 종료 날짜 값
36
+ */
37
+ export type CalendarRangePickerValue = [
38
+ Date | string | null,
39
+ Date | string | null,
40
+ ];
41
+
25
42
  /**
26
43
  * Calendar 값 변경 핸들러.
27
44
  * @param {CalendarValue} value 선택된 직렬화 값
@@ -32,6 +49,16 @@ export type CalendarValue = string | null;
32
49
  */
33
50
  export type CalendarOnChange = (value: CalendarValue) => void;
34
51
 
52
+ /**
53
+ * Calendar range 값 변경 핸들러.
54
+ * @param {CalendarRangeValue} value 선택된 range 직렬화 tuple
55
+ * @example
56
+ * const onChange = (value: CalendarRangeValue) => {
57
+ * console.log(value);
58
+ * };
59
+ */
60
+ export type CalendarRangeOnChange = (value: CalendarRangeValue) => void;
61
+
35
62
  /**
36
63
  * Mantine DatePicker 공개 옵션.
37
64
  * @property {Partial<Record<DatePickerStylesNames, string>>} [classNames] Mantine 내부 스타일 클래스 매핑
@@ -54,6 +81,28 @@ export type CalendarDatePickerProps = Partial<
54
81
  classNames?: Partial<Record<DatePickerStylesNames, string>>;
55
82
  };
56
83
 
84
+ /**
85
+ * Mantine range DatePicker 공개 옵션.
86
+ * @property {Partial<Record<DatePickerStylesNames, string>>} [classNames] Mantine range 내부 스타일 클래스 매핑
87
+ */
88
+ export type CalendarRangeDatePickerProps = Partial<
89
+ Omit<
90
+ MantineDatePickerProps<"range">,
91
+ | "value"
92
+ | "defaultValue"
93
+ | "onChange"
94
+ | "type"
95
+ | "numberOfColumns"
96
+ | "classNames"
97
+ | "styles"
98
+ >
99
+ > & {
100
+ /**
101
+ * Mantine range 내부 스타일 classNames override.
102
+ */
103
+ classNames?: Partial<Record<DatePickerStylesNames, string>>;
104
+ };
105
+
57
106
  /**
58
107
  * Calendar Grid props.
59
108
  * @property {CalendarValue} value 현재 선택 값
@@ -75,6 +124,27 @@ export interface CalendarGridProps {
75
124
  datePickerProps?: CalendarDatePickerProps;
76
125
  }
77
126
 
127
+ /**
128
+ * Calendar Range Grid props.
129
+ * @property {CalendarRangeValue} value 현재 range 선택 값
130
+ * @property {CalendarRangeOnChange} onChange range 값 변경 핸들러
131
+ * @property {CalendarRangeDatePickerProps} [datePickerProps] Mantine range DatePicker 옵션
132
+ */
133
+ export interface CalendarRangeGridProps {
134
+ /**
135
+ * 현재 range 선택 값
136
+ */
137
+ value: CalendarRangeValue;
138
+ /**
139
+ * range 값 변경 핸들러
140
+ */
141
+ onChange: CalendarRangeOnChange;
142
+ /**
143
+ * Mantine range DatePicker 옵션
144
+ */
145
+ datePickerProps?: CalendarRangeDatePickerProps;
146
+ }
147
+
78
148
  /**
79
149
  * Calendar Layout Container props.
80
150
  * @property {ReactNode} [header] 상단 콘텐츠
@@ -196,6 +266,86 @@ export interface CalendarRootProps extends Omit<
196
266
  portalContainer?: HTMLElement | null;
197
267
  }
198
268
 
269
+ /**
270
+ * Calendar Range Root props.
271
+ * @extends CalendarContainerProps
272
+ * @property {string} [className] Trigger element className override
273
+ * @property {CalendarRangeValue} value 현재 range 선택 값
274
+ * @property {CalendarRangeOnChange} onChange range 값 변경 핸들러
275
+ * @property {CalendarRangeDatePickerProps} [datePickerProps] Mantine range DatePicker 옵션
276
+ * @property {ReactNode} children Trigger 슬롯(children)
277
+ * @property {boolean} [open] 제어형 open 상태
278
+ * @property {boolean} [defaultOpen] 비제어 초기 open 상태
279
+ * @property {(open: boolean) => void} [onOpenChange] open 상태 변경 핸들러
280
+ * @property {"top" | "right" | "bottom" | "left"} [side="bottom"] content 배치 방향
281
+ * @property {"start" | "center" | "end"} [align="start"] content 정렬 기준
282
+ * @property {number} [sideOffset=4] trigger와 content 간격
283
+ * @property {number} [alignOffset] 정렬 보정값
284
+ * @property {boolean} [withPortal=true] Portal 사용 여부
285
+ * @property {HTMLElement | null} [portalContainer] Portal 컨테이너
286
+ */
287
+ export interface CalendarRangeRootProps extends Omit<
288
+ CalendarContainerProps,
289
+ "body" | "columns"
290
+ > {
291
+ /**
292
+ * Trigger element className override
293
+ */
294
+ className?: string;
295
+ /**
296
+ * 현재 range 선택 값
297
+ */
298
+ value: CalendarRangeValue;
299
+ /**
300
+ * range 값 변경 핸들러
301
+ */
302
+ onChange: CalendarRangeOnChange;
303
+ /**
304
+ * Mantine range DatePicker 옵션
305
+ */
306
+ datePickerProps?: CalendarRangeDatePickerProps;
307
+ /**
308
+ * Trigger 슬롯(children)
309
+ */
310
+ children: ReactNode;
311
+ /**
312
+ * 제어형 open 상태
313
+ */
314
+ open?: boolean;
315
+ /**
316
+ * 비제어 초기 open 상태
317
+ */
318
+ defaultOpen?: boolean;
319
+ /**
320
+ * open 상태 변경 핸들러
321
+ */
322
+ onOpenChange?: (open: boolean) => void;
323
+ /**
324
+ * content 배치 방향
325
+ */
326
+ side?: "top" | "right" | "bottom" | "left";
327
+ /**
328
+ * content 정렬 기준
329
+ */
330
+ align?: "start" | "center" | "end";
331
+ /**
332
+ * trigger와 content 간격
333
+ */
334
+ sideOffset?: number;
335
+ /**
336
+ * 정렬 보정값
337
+ */
338
+ alignOffset?: number;
339
+ /**
340
+ * Portal 사용 여부
341
+ */
342
+ withPortal?: boolean;
343
+ /**
344
+ * Portal 컨테이너
345
+ */
346
+ portalContainer?: HTMLElement | null;
347
+ }
348
+
199
349
  /**
200
350
  * Calendar 섹션 공용 props.
201
351
  * @property {ReactNode} [children] 섹션 내부 콘텐츠
@@ -1,5 +1,9 @@
1
1
  import { dayjs } from "../../../init/dayjs";
2
- import type { CalendarValue } from "../types";
2
+ import type {
3
+ CalendarRangePickerValue,
4
+ CalendarRangeValue,
5
+ CalendarValue,
6
+ } from "../types";
3
7
 
4
8
  const DATE_FORMAT = "YYYY-MM-DD";
5
9
 
@@ -15,10 +19,39 @@ export const mapValueToPicker = (value: CalendarValue) =>
15
19
 
16
20
  /**
17
21
  * DatePicker value를 Calendar 직렬화 값으로 변환한다.
18
- * @param {Date | null} value DatePicker value
22
+ * @param {Date | string | null} value DatePicker value
19
23
  * @returns {CalendarValue} YYYY-MM-DD 직렬화 값
20
24
  * @example
21
25
  * parseValueFromPicker(new Date("2026-02-13"));
22
26
  */
23
- export const parseValueFromPicker = (value: Date | null): CalendarValue =>
24
- value ? dayjs(value).format(DATE_FORMAT) : null;
27
+ export const parseValueFromPicker = (
28
+ value: Date | string | null,
29
+ ): CalendarValue => (value ? dayjs(value).format(DATE_FORMAT) : null);
30
+
31
+ /**
32
+ * Calendar range value를 DatePicker range value로 변환한다.
33
+ * @param {CalendarRangeValue} value YYYY-MM-DD range tuple
34
+ * @returns {[Date | null, Date | null]} DatePicker range value
35
+ * @example
36
+ * mapRangeValueToPicker(["2026-02-13", "2026-02-20"]);
37
+ */
38
+ export const mapRangeValueToPicker = (
39
+ value: CalendarRangeValue,
40
+ ): [Date | null, Date | null] => [
41
+ mapValueToPicker(value[0]),
42
+ mapValueToPicker(value[1]),
43
+ ];
44
+
45
+ /**
46
+ * DatePicker range value를 Calendar range 직렬화 값으로 변환한다.
47
+ * @param {CalendarRangePickerValue} value DatePicker range value
48
+ * @returns {CalendarRangeValue} YYYY-MM-DD range 직렬화 tuple
49
+ * @example
50
+ * parseRangeValueFromPicker([new Date("2026-02-13"), new Date("2026-02-20")]);
51
+ */
52
+ export const parseRangeValueFromPicker = (
53
+ value: CalendarRangePickerValue,
54
+ ): CalendarRangeValue => [
55
+ parseValueFromPicker(value[0]),
56
+ parseValueFromPicker(value[1]),
57
+ ];
@@ -3,7 +3,6 @@
3
3
  import type { ChangeEvent, MouseEvent as ReactMouseEvent } from "react";
4
4
  import { forwardRef, useCallback, useMemo, useState } from "react";
5
5
  import { useUncontrolled } from "@mantine/hooks";
6
- import clsx from "clsx";
7
6
  import { Calendar } from "../../../calendar";
8
7
  import type {
9
8
  InputCalendarProps,
@@ -46,7 +45,6 @@ const INPUT_DATE_TABLE_FORMAT = "YY-MM-DD";
46
45
  * @param {unknown} [props.timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
47
46
  * @param {boolean} [props.calendarOpened] calendar 열림 제어 상태
48
47
  * @param {(event: MouseEvent<Element>) => void} [props.onClick] trigger 클릭 핸들러
49
- * @param {string} [props.className] root className
50
48
  * @param {string} [props.id] trigger id
51
49
  * @param {ReactNode} [props.trigger] 커스텀 trigger 슬롯
52
50
  * @param {(props: InputCalendarTriggerRenderProps) => ReactNode} [props.renderTrigger] 커스텀 trigger 렌더 함수
@@ -68,7 +66,6 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
68
66
  placeholder = "YYYY-MM-DD",
69
67
  priority = "primary",
70
68
  state = "default",
71
- className,
72
69
  header,
73
70
  footer,
74
71
  calendarOpened,
@@ -182,7 +179,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
182
179
  <>
183
180
  <Calendar.Root
184
181
  ref={ref}
185
- className={clsx("input-date-field", className)}
182
+ className="input-date-field"
186
183
  mode={mode}
187
184
  columns={columns}
188
185
  disabled={disabled}
@@ -4,15 +4,26 @@ import InputDateTrigger from "./Trigger";
4
4
  import InputDateApplyButton from "./button/ApplyButton";
5
5
  import InputDateClearButton from "./button/ClearButton";
6
6
  import InputDateTodayButton from "./button/TodayButton";
7
+ import InputDateRangeTemplate from "./range/Template";
7
8
  import {
8
9
  InputDateFooterTemplate,
9
10
  InputDateFooterTemplateContainer,
10
11
  InputDateFooterUtilContainer,
11
12
  } from "./footer";
12
13
 
14
+ export const InputDateDefault = {
15
+ Template: InputDateTemplate,
16
+ };
17
+
18
+ export const InputDateRange = {
19
+ Template: InputDateRangeTemplate,
20
+ };
21
+
13
22
  // Input.Date 네임스페이스: template/foundation 구성요소 집합
14
23
  export const InputDate = {
15
24
  Template: InputDateTemplate,
25
+ Default: InputDateDefault,
26
+ Range: InputDateRange,
16
27
  Trigger: InputDateTrigger,
17
28
  Button: {
18
29
  Apply: InputDateApplyButton,