@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.
package/dist/styles.css CHANGED
@@ -1636,6 +1636,17 @@
1636
1636
  z-index: 30;
1637
1637
  }
1638
1638
 
1639
+ .calendar-range-root,
1640
+ .calendar-range-grid {
1641
+ --calendar-range-column-width: 322px;
1642
+ --calendar-range-column-gap: var(--spacing-gap-6);
1643
+ --calendar-width: calc(
1644
+ (var(--calendar-range-column-width) * 2) +
1645
+ var(--calendar-range-column-gap) + (var(--calendar-inline-padding) * 2)
1646
+ );
1647
+ max-width: min(100vw - var(--spacing-padding-5) * 2, 720px);
1648
+ }
1649
+
1639
1650
  .calendar-header {
1640
1651
  margin-bottom: var(--spacing-gap-2);
1641
1652
  }
@@ -1649,6 +1660,10 @@
1649
1660
  width: var(--calendar-body-width);
1650
1661
  }
1651
1662
 
1663
+ .calendar-range-grid {
1664
+ width: var(--calendar-body-width);
1665
+ }
1666
+
1652
1667
  .calendar-grid table {
1653
1668
  width: auto;
1654
1669
  }
@@ -1663,6 +1678,17 @@
1663
1678
  width: 100%;
1664
1679
  }
1665
1680
 
1681
+ .calendar-range-grid .calendar-month-level {
1682
+ display: grid;
1683
+ grid-template-columns: repeat(2, var(--calendar-range-column-width));
1684
+ column-gap: var(--calendar-range-column-gap);
1685
+ align-items: start;
1686
+ }
1687
+
1688
+ .calendar-range-grid .calendar-month-level > [data-month-level=true] {
1689
+ width: var(--calendar-range-column-width);
1690
+ }
1691
+
1666
1692
  .calendar-header-row {
1667
1693
  display: grid;
1668
1694
  grid-template-columns: 44px 1fr 44px;
@@ -1670,7 +1696,7 @@
1670
1696
  width: 100%;
1671
1697
  max-width: none;
1672
1698
  column-gap: var(--spacing-gap-5);
1673
- padding: 0 var(--spacing-padding-9);
1699
+ padding: 0;
1674
1700
  margin-bottom: var(--spacing-gap-5);
1675
1701
  --dch-fz: var(--font-heading-small-size) !important;
1676
1702
  }
@@ -1679,7 +1705,7 @@
1679
1705
  width: 44px;
1680
1706
  height: 44px;
1681
1707
  border-radius: 999px;
1682
- display: inline-flex;
1708
+ display: flex;
1683
1709
  align-items: center;
1684
1710
  justify-content: center;
1685
1711
  color: var(--color-label-alternative);
@@ -1687,15 +1713,25 @@
1687
1713
  }
1688
1714
 
1689
1715
  .calendar-header-control[data-direction=previous] {
1716
+ grid-column: 1;
1717
+ grid-row: 1;
1690
1718
  justify-self: start;
1691
1719
  }
1692
1720
 
1693
1721
  .calendar-header-control[data-direction=next] {
1722
+ grid-column: 3;
1723
+ grid-row: 1;
1694
1724
  justify-self: end;
1695
1725
  }
1696
1726
 
1697
1727
  .calendar-header-level {
1728
+ grid-column: 2;
1729
+ grid-row: 1;
1698
1730
  justify-self: center;
1731
+ display: flex;
1732
+ align-items: center;
1733
+ justify-content: center;
1734
+ gap: var(--spacing-gap-4);
1699
1735
  font-size: var(--font-heading-small-size);
1700
1736
  font-weight: var(--font-heading-small-weight);
1701
1737
  text-align: center;
@@ -1706,6 +1742,17 @@
1706
1742
  color: var(--color-label-strong);
1707
1743
  }
1708
1744
 
1745
+ .calendar-header-level::after {
1746
+ content: "";
1747
+ flex: 0 0 auto;
1748
+ width: 20px;
1749
+ height: 20px;
1750
+ 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");
1751
+ background-repeat: no-repeat;
1752
+ background-position: center;
1753
+ background-size: 20px 20px;
1754
+ }
1755
+
1709
1756
  .calendar-header-control:where(:not([data-disabled=true])):hover {
1710
1757
  background-color: var(--color-tertiary-default);
1711
1758
  color: var(--color-label-standard);
@@ -1824,28 +1871,128 @@
1824
1871
  padding: 0;
1825
1872
  border: none;
1826
1873
  border-radius: var(--theme-radius-large-1);
1874
+ box-sizing: border-box;
1875
+ display: flex;
1876
+ align-items: center;
1877
+ justify-content: center;
1827
1878
  font-size: var(--font-body-medium-size);
1828
1879
  color: var(--color-label-standard);
1829
1880
  }
1830
1881
 
1882
+ .calendar-day-label {
1883
+ width: 44px;
1884
+ height: 44px;
1885
+ box-sizing: border-box;
1886
+ display: flex;
1887
+ align-items: center;
1888
+ justify-content: center;
1889
+ position: relative;
1890
+ color: inherit;
1891
+ }
1892
+
1831
1893
  .calendar-day[data-outside=true] {
1832
1894
  color: var(--color-label-alternative);
1833
1895
  }
1834
1896
 
1835
1897
  .calendar-day[data-selected=true],
1836
1898
  .calendar-day[data-focused=true] {
1899
+ color: var(--color-common-100);
1900
+ }
1901
+
1902
+ .calendar-day:where([data-in-range]) {
1903
+ background-color: var(--color-surface-static-blue);
1904
+ color: var(--color-label-standard);
1905
+ border-radius: 0;
1906
+ box-shadow: -1px 0 0 var(--color-surface-static-blue), 1px 0 0 var(--color-surface-static-blue);
1907
+ }
1908
+
1909
+ .calendar-day:where([data-first-in-range]) {
1910
+ border-start-start-radius: var(--theme-radius-medium-3);
1911
+ border-end-start-radius: var(--theme-radius-medium-3);
1912
+ box-shadow: 1px 0 0 var(--color-surface-static-blue);
1913
+ }
1914
+
1915
+ .calendar-day:where([data-last-in-range]) {
1916
+ border-start-end-radius: var(--theme-radius-medium-3);
1917
+ border-end-end-radius: var(--theme-radius-medium-3);
1918
+ box-shadow: -1px 0 0 var(--color-surface-static-blue);
1919
+ }
1920
+
1921
+ .calendar-day:where([data-first-in-range][data-last-in-range]) {
1922
+ box-shadow: none;
1923
+ }
1924
+
1925
+ .calendar-day:where([data-selected=true], [data-focused=true]) {
1926
+ background-color: transparent;
1927
+ color: var(--color-common-100);
1928
+ }
1929
+
1930
+ .calendar-day:where([data-in-range][data-selected=true]) {
1931
+ background-color: var(--color-surface-static-blue);
1932
+ }
1933
+
1934
+ .calendar-day:where([data-selected=true], [data-focused=true]) .calendar-day-label {
1837
1935
  background-color: var(--color-primary-default);
1838
1936
  color: var(--color-common-100);
1937
+ border-radius: var(--theme-radius-medium-3);
1938
+ }
1939
+
1940
+ .calendar-day:where([data-today][data-highlight-today]:not([data-selected=true],
1941
+ [data-in-range],
1942
+ [data-disabled=true])) {
1943
+ border: 1px solid var(--color-primary-default);
1944
+ color: var(--color-label-standard);
1945
+ }
1946
+
1947
+ .calendar-day:where([data-today][data-highlight-today]:not([data-selected=true],
1948
+ [data-in-range],
1949
+ [data-disabled=true])) .calendar-day-label::after {
1950
+ content: "";
1951
+ width: 4px;
1952
+ height: 4px;
1953
+ border-radius: 999px;
1954
+ background-color: var(--color-primary-default);
1955
+ position: absolute;
1956
+ bottom: 7px;
1957
+ left: 50%;
1958
+ transform: translateX(-50%);
1839
1959
  }
1840
1960
 
1841
1961
  .calendar-day:where(:disabled, [data-disabled=true]) {
1842
1962
  color: var(--color-label-disabled);
1843
1963
  }
1844
1964
 
1845
- .calendar-day:where(:not([data-disabled=true])):hover {
1965
+ .calendar-day:where([hidden]) {
1966
+ display: flex !important;
1967
+ visibility: hidden;
1968
+ pointer-events: none;
1969
+ }
1970
+
1971
+ .calendar-range-grid .calendar-day:where([data-outside=true]) {
1972
+ visibility: hidden;
1973
+ pointer-events: none;
1974
+ }
1975
+
1976
+ .calendar-day:where(:not([data-disabled=true],
1977
+ [data-selected=true],
1978
+ [data-focused=true],
1979
+ [data-in-range])):hover {
1846
1980
  background-color: var(--color-secondary-default);
1847
1981
  color: var(--color-label-standard);
1848
- border: none;
1982
+ }
1983
+
1984
+ .calendar-day:where([data-selected=true]):hover {
1985
+ background-color: transparent;
1986
+ color: var(--color-common-100);
1987
+ }
1988
+
1989
+ .calendar-day:where([data-in-range][data-selected=true]):hover {
1990
+ background-color: var(--color-surface-static-blue);
1991
+ }
1992
+
1993
+ .calendar-day:where([data-selected=true]):hover .calendar-day-label {
1994
+ background-color: var(--color-primary-default);
1995
+ color: var(--color-common-100);
1849
1996
  }
1850
1997
 
1851
1998
  .calendar-footer {
@@ -3441,6 +3588,45 @@ figure.chip {
3441
3588
  width: 100%;
3442
3589
  }
3443
3590
 
3591
+ .input-date-range-footer-template {
3592
+ gap: var(--spacing-gap-6);
3593
+ }
3594
+
3595
+ .input-date-range-today-row {
3596
+ justify-content: flex-end;
3597
+ }
3598
+
3599
+ .input-date-range-footer-row {
3600
+ justify-content: space-between;
3601
+ }
3602
+
3603
+ .input-date-range-clear-button {
3604
+ min-height: 20px;
3605
+ padding-inline: var(--spacing-padding-5);
3606
+ border-color: transparent;
3607
+ background-color: transparent;
3608
+ color: var(--color-feedback-error);
3609
+ }
3610
+ .input-date-range-clear-button:hover:not(:disabled):not([aria-disabled=true]), .input-date-range-clear-button:active:not(:disabled):not([aria-disabled=true]) {
3611
+ border-color: transparent;
3612
+ background-color: transparent;
3613
+ color: var(--color-feedback-error);
3614
+ box-shadow: none;
3615
+ }
3616
+ .input-date-range-clear-button .button-label {
3617
+ font-size: var(--font-body-xxsmall-size);
3618
+ line-height: 1.4;
3619
+ font-weight: var(--font-body-medium-weight);
3620
+ letter-spacing: 0;
3621
+ }
3622
+
3623
+ .input-date-range-apply-button {
3624
+ width: fit-content;
3625
+ flex: 0 0 auto;
3626
+ --button-width: fit-content;
3627
+ --button-flex: 0 0 auto;
3628
+ }
3629
+
3444
3630
  .input-address-container {
3445
3631
  width: 100%;
3446
3632
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -15,7 +15,7 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "packageManager": "pnpm@10.33.0",
18
+ "packageManager": "pnpm@10.33.2",
19
19
  "engines": {
20
20
  "node": ">=24",
21
21
  "pnpm": ">=10"
@@ -103,7 +103,7 @@
103
103
  "@uniai-fe/util-functions": "workspace:*",
104
104
  "eslint": "^9.39.2",
105
105
  "prettier": "^3.8.3",
106
- "react-hook-form": "^7.72.1",
106
+ "react-hook-form": "^7.73.1",
107
107
  "sass": "^1.99.0",
108
108
  "typescript": "5.9.3"
109
109
  }
@@ -4,6 +4,7 @@
4
4
  * - `Calendar.Root`: trigger + popover + core 조합 템플릿이다.
5
5
  * - `Calendar.Container`: Header/Body/Footer 레이아웃 컨테이너다.
6
6
  * - `Calendar.Core`: Mantine DatePicker SOT 렌더러다.
7
+ * - `Calendar.Range`: Mantine DatePicker range SOT namespace다.
7
8
  * - `CalendarHooks`, `CalendarUtils`: 관련 hook/util namespace다.
8
9
  */
9
10
  import "./index.scss";
@@ -1,6 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { DatePicker } from "@mantine/dates";
3
+ import {
4
+ DatePicker,
5
+ DatesProvider,
6
+ type DateStringValue,
7
+ } from "@mantine/dates";
8
+ import { dayjs } from "../../../init/dayjs";
4
9
  import { CalendarIcon } from "./Icon";
5
10
  import type { CalendarDatePickerProps, CalendarGridProps } from "../types";
6
11
  import { mapValueToPicker, parseValueFromPicker } from "../utils";
@@ -20,7 +25,7 @@ export default function CalendarCore({
20
25
  onChange,
21
26
  datePickerProps,
22
27
  }: CalendarGridProps) {
23
- const { valueFormat, ...safeDatePickerProps } = (datePickerProps ??
28
+ const { valueFormat, renderDay, ...safeDatePickerProps } = (datePickerProps ??
24
29
  {}) as CalendarDatePickerProps & {
25
30
  /**
26
31
  * deprecated DatePicker 표시 포맷 옵션
@@ -33,12 +38,23 @@ export default function CalendarCore({
33
38
  // 기본 DatePicker 옵션/스타일 책임을 Calendar.Core에 고정한다.
34
39
  const resolvedDatePickerProps = {
35
40
  size: "sm" as const,
41
+ locale: "ko",
36
42
  firstDayOfWeek: 0 as const,
37
43
  weekendDays: [] as Array<0 | 1 | 2 | 3 | 4 | 5 | 6>,
38
44
  nextIcon: <CalendarIcon.Chevron.right />,
39
45
  previousIcon: <CalendarIcon.Chevron.left />,
46
+ // 변경: Figma today node 기준의 border/dot 상태를 기본 활성화한다.
47
+ highlightToday: true,
48
+ // 변경: Calendar header는 Figma 기준의 연도-월 순서 표기를 기본값으로 둔다.
49
+ monthLabelFormat: "YYYY년 M월",
40
50
  // 변경: 지원하지 않는 valueFormat은 제거한 뒤 DatePicker 옵션을 병합한다.
41
51
  ...safeDatePickerProps,
52
+ // 변경: selected/range/today 시각 레이어를 분리하기 위해 날짜 값을 내부 span에 고정한다.
53
+ renderDay: (date: DateStringValue) => (
54
+ <span className="calendar-day-label">
55
+ {renderDay ? renderDay(date) : dayjs(date).date()}
56
+ </span>
57
+ ),
42
58
  classNames: {
43
59
  levelsGroup: "calendar-month-level",
44
60
  month: "calendar-month-table",
@@ -65,14 +81,24 @@ export default function CalendarCore({
65
81
  };
66
82
 
67
83
  return (
68
- <DatePicker
69
- className="calendar-grid"
70
- // Core는 SOT 단일 달력으로 고정한다.
71
- numberOfColumns={1}
72
- type="default"
73
- value={mapValueToPicker(value) as never}
74
- onChange={handleChange as never}
75
- {...resolvedDatePickerProps}
76
- />
84
+ <DatesProvider
85
+ settings={{
86
+ // 변경: 월별 week row 수를 6주로 고정해 popover 높이 변동을 막는다.
87
+ consistentWeeks: true,
88
+ locale: resolvedDatePickerProps.locale ?? "ko",
89
+ firstDayOfWeek: resolvedDatePickerProps.firstDayOfWeek ?? 0,
90
+ weekendDays: resolvedDatePickerProps.weekendDays ?? [],
91
+ }}
92
+ >
93
+ <DatePicker
94
+ className="calendar-grid"
95
+ // Core는 SOT 단일 달력으로 고정한다.
96
+ numberOfColumns={1}
97
+ type="default"
98
+ value={mapValueToPicker(value) as never}
99
+ onChange={handleChange as never}
100
+ {...resolvedDatePickerProps}
101
+ />
102
+ </DatesProvider>
77
103
  );
78
104
  }
@@ -5,6 +5,7 @@ import CalendarFooter from "./layout/Footer";
5
5
  import CalendarCore from "./Core";
6
6
  import CalendarRoot from "./Root";
7
7
  import { CalendarIcon } from "./Icon";
8
+ import { CalendarRange } from "./range";
8
9
 
9
10
  /**
10
11
  * Calendar; date picker namespace
@@ -13,6 +14,7 @@ import { CalendarIcon } from "./Icon";
13
14
  * - `Calendar.Container`: Header/Body/Footer 레이아웃 컨테이너다.
14
15
  * - `Calendar.Header`, `Calendar.Body`, `Calendar.Footer`: depth 고정 레이아웃 섹션이다.
15
16
  * - `Calendar.Core`: Mantine DatePicker SOT 렌더러다.
17
+ * - `Calendar.Range`: Mantine DatePicker range SOT namespace다.
16
18
  * - `Calendar.Icon`: 입력/네비게이션 아이콘 namespace다.
17
19
  */
18
20
  export const Calendar = {
@@ -22,5 +24,8 @@ export const Calendar = {
22
24
  Body: CalendarBody,
23
25
  Footer: CalendarFooter,
24
26
  Core: CalendarCore,
27
+ Range: CalendarRange,
25
28
  Icon: CalendarIcon,
26
29
  };
30
+
31
+ export * from "./range";
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import {
4
+ DatePicker,
5
+ DatesProvider,
6
+ type DateStringValue,
7
+ } from "@mantine/dates";
8
+ import { dayjs } from "../../../../init/dayjs";
9
+ import { CalendarIcon } from "../Icon";
10
+ import type {
11
+ CalendarRangeDatePickerProps,
12
+ CalendarRangeGridProps,
13
+ CalendarRangePickerValue,
14
+ } from "../../types";
15
+ import { mapRangeValueToPicker, parseRangeValueFromPicker } from "../../utils";
16
+
17
+ /**
18
+ * Calendar Range Grid; Mantine DatePicker range 래퍼.
19
+ * @component
20
+ * @param {CalendarRangeGridProps} props range grid props
21
+ * @param {CalendarRangeValue} props.value 현재 range 선택 값
22
+ * @param {CalendarRangeOnChange} props.onChange range 값 변경 핸들러
23
+ * @param {CalendarRangeDatePickerProps} [props.datePickerProps] Mantine range DatePicker 옵션
24
+ * @example
25
+ * <Calendar.Range.Core value={value} onChange={setValue} />
26
+ */
27
+ export default function CalendarRangeCore({
28
+ value,
29
+ onChange,
30
+ datePickerProps,
31
+ }: CalendarRangeGridProps) {
32
+ const { valueFormat, renderDay, ...safeDatePickerProps } = (datePickerProps ??
33
+ {}) as CalendarRangeDatePickerProps & {
34
+ /**
35
+ * deprecated DatePicker 표시 포맷 옵션
36
+ */
37
+ valueFormat?: unknown;
38
+ };
39
+ // 변경: range에서도 valueFormat은 지원하지 않는 옵션이므로 Calendar 단에서 제거한다.
40
+ void valueFormat;
41
+
42
+ // Range Core는 Figma의 2 calendar 구조에 맞춰 두 달을 한 패널에서 렌더링한다.
43
+ const resolvedDatePickerProps = {
44
+ size: "sm" as const,
45
+ locale: "ko",
46
+ firstDayOfWeek: 0 as const,
47
+ weekendDays: [] as Array<0 | 1 | 2 | 3 | 4 | 5 | 6>,
48
+ nextIcon: <CalendarIcon.Chevron.right />,
49
+ previousIcon: <CalendarIcon.Chevron.left />,
50
+ // 변경: range는 2개월을 보여주되 page navigation은 1개월씩 이동한다.
51
+ columnsToScroll: 1,
52
+ // 변경: Figma today node 기준의 border/dot 상태를 range에서도 기본 활성화한다.
53
+ highlightToday: true,
54
+ // 변경: Range header도 단일 Core와 같은 연도-월 순서 표기를 기본값으로 둔다.
55
+ monthLabelFormat: "YYYY년 M월",
56
+ ...safeDatePickerProps,
57
+ // 변경: hidden attribute로 인한 empty week row collapse를 막고 outside date는 CSS에서 숨긴다.
58
+ hideOutsideDates: false,
59
+ // 변경: button은 period surface, span은 selected/today 날짜 레이어로 분리한다.
60
+ renderDay: (date: DateStringValue) => (
61
+ <span className="calendar-day-label">
62
+ {renderDay ? renderDay(date) : dayjs(date).date()}
63
+ </span>
64
+ ),
65
+ classNames: {
66
+ levelsGroup: "calendar-month-level",
67
+ month: "calendar-month-table",
68
+ monthCell: "calendar-month-cell",
69
+ calendarHeader: "calendar-header-row",
70
+ calendarHeaderControl: "calendar-header-control",
71
+ calendarHeaderLevel: "calendar-header-level",
72
+ day: "calendar-day",
73
+ weekday: "calendar-weekday",
74
+ weekdaysRow: "calendar-weekdays",
75
+ monthsList: "calendar-months-list",
76
+ monthsListCell: "calendar-months-list-cell",
77
+ monthsListControl: "calendar-months-list-control",
78
+ yearsList: "calendar-years-list",
79
+ yearsListCell: "calendar-years-list-cell",
80
+ yearsListControl: "calendar-years-list-control",
81
+ ...(safeDatePickerProps.classNames ?? {}),
82
+ },
83
+ };
84
+
85
+ const handleChange = (nextValue: CalendarRangePickerValue) => {
86
+ onChange(parseRangeValueFromPicker(nextValue));
87
+ };
88
+
89
+ return (
90
+ <DatesProvider
91
+ settings={{
92
+ // 변경: range 양쪽 month도 항상 6주 row로 렌더해 패널 높이를 고정한다.
93
+ consistentWeeks: true,
94
+ locale: resolvedDatePickerProps.locale ?? "ko",
95
+ firstDayOfWeek: resolvedDatePickerProps.firstDayOfWeek ?? 0,
96
+ weekendDays: resolvedDatePickerProps.weekendDays ?? [],
97
+ }}
98
+ >
99
+ <DatePicker
100
+ className="calendar-grid calendar-range-grid"
101
+ numberOfColumns={2}
102
+ type="range"
103
+ value={mapRangeValueToPicker(value)}
104
+ onChange={handleChange}
105
+ {...resolvedDatePickerProps}
106
+ />
107
+ </DatesProvider>
108
+ );
109
+ }
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { forwardRef } from "react";
5
+ import { PopOver } from "../../../pop-over";
6
+ import type { CalendarRangeRootProps } from "../../types";
7
+ import CalendarContainer from "../layout/Container";
8
+ import CalendarRangeCore from "./Core";
9
+
10
+ /**
11
+ * Calendar Range Root; Trigger/Popover + Range Core 조합 템플릿.
12
+ * @component
13
+ * @param {CalendarRangeRootProps} props range root props
14
+ * @param {string} [props.className] Trigger element className override
15
+ * @param {CalendarRangeValue} props.value 현재 range 선택 값
16
+ * @param {CalendarRangeOnChange} props.onChange range 값 변경 핸들러
17
+ * @param {CalendarRangeDatePickerProps} [props.datePickerProps] Mantine range DatePicker 옵션
18
+ * @param {React.ReactNode} [props.header] Calendar Header 슬롯
19
+ * @param {React.ReactNode} [props.footer] Calendar Footer 슬롯
20
+ * @param {CalendarMode} [props.mode="date"] calendar 모드
21
+ * @param {boolean} [props.disabled] 비활성화 상태
22
+ * @param {boolean} [props.readOnly] 읽기 전용 상태
23
+ * @param {React.ReactNode} props.children Trigger 슬롯(children)
24
+ * @param {boolean} [props.open] 제어형 open 상태
25
+ * @param {boolean} [props.defaultOpen] 비제어 초기 open 상태
26
+ * @param {(open: boolean) => void} [props.onOpenChange] open 상태 변경 핸들러
27
+ * @param {"top" | "right" | "bottom" | "left"} [props.side="bottom"] Content 배치 방향
28
+ * @param {"start" | "center" | "end"} [props.align="start"] Content 정렬 기준
29
+ * @param {number} [props.sideOffset=4] Trigger와 Content 간격
30
+ * @param {number} [props.alignOffset] Content 정렬 보정값
31
+ * @param {boolean} [props.withPortal=true] Portal 사용 여부
32
+ * @param {HTMLElement | null} [props.portalContainer] Portal 컨테이너
33
+ * @example
34
+ * <Calendar.Range.Root value={value} onChange={setValue} />
35
+ */
36
+ const CalendarRangeRoot = forwardRef<HTMLElement, CalendarRangeRootProps>(
37
+ (
38
+ {
39
+ className,
40
+ value,
41
+ onChange,
42
+ datePickerProps,
43
+ header,
44
+ footer,
45
+ mode = "date",
46
+ disabled,
47
+ readOnly,
48
+ children,
49
+ open,
50
+ defaultOpen,
51
+ onOpenChange,
52
+ side = "bottom",
53
+ align = "start",
54
+ sideOffset = 4,
55
+ alignOffset,
56
+ withPortal = true,
57
+ portalContainer,
58
+ },
59
+ ref,
60
+ ) => {
61
+ // disabled/readOnly 상태에서는 range PopOver 토글을 막는다.
62
+ const isInteractive = !disabled && !readOnly;
63
+
64
+ const calendarNode = (
65
+ <CalendarContainer
66
+ ref={ref}
67
+ header={header}
68
+ footer={footer}
69
+ mode={mode}
70
+ columns={2}
71
+ disabled={disabled}
72
+ readOnly={readOnly}
73
+ body={
74
+ <CalendarRangeCore
75
+ value={value}
76
+ onChange={onChange}
77
+ datePickerProps={datePickerProps}
78
+ />
79
+ }
80
+ />
81
+ );
82
+
83
+ return (
84
+ <PopOver.Root
85
+ open={isInteractive ? open : false}
86
+ defaultOpen={isInteractive ? defaultOpen : false}
87
+ onOpenChange={nextOpen => {
88
+ if (!isInteractive) {
89
+ onOpenChange?.(false);
90
+ return;
91
+ }
92
+ onOpenChange?.(nextOpen);
93
+ }}
94
+ modal={false}
95
+ >
96
+ {/* Trigger 래퍼 div 없이 children 노드에 PopOver trigger props를 직접 주입한다. */}
97
+ <PopOver.Trigger
98
+ asChild
99
+ disabled={!isInteractive}
100
+ className={clsx("calendar-trigger", className)}
101
+ >
102
+ {/* children은 asChild 계약상 단일 element여야 하며, 해당 요소가 props를 DOM으로 전달해야 한다. */}
103
+ {children}
104
+ </PopOver.Trigger>
105
+ <PopOver.Content
106
+ className="calendar-pop-over-content"
107
+ side={side}
108
+ align={align}
109
+ sideOffset={sideOffset}
110
+ alignOffset={alignOffset}
111
+ withPortal={withPortal}
112
+ portalContainer={portalContainer}
113
+ onInteractOutside={event => {
114
+ // 변경 설명: trigger 클릭은 outside dismiss에서 제외해 close 후 즉시 reopen 경합을 방지한다.
115
+ const nextTarget = event.target as HTMLElement | null;
116
+ if (nextTarget?.closest(".calendar-trigger")) {
117
+ event.preventDefault();
118
+ }
119
+ }}
120
+ >
121
+ {/* range calendar 스킨 class는 content 내부에 고정한다. */}
122
+ <div className="calendar-root calendar-range-root">
123
+ {calendarNode}
124
+ </div>
125
+ </PopOver.Content>
126
+ </PopOver.Root>
127
+ );
128
+ },
129
+ );
130
+
131
+ CalendarRangeRoot.displayName = "CalendarRangeRoot";
132
+
133
+ export default CalendarRangeRoot;
@@ -0,0 +1,15 @@
1
+ import CalendarRangeCore from "./Core";
2
+ import CalendarRangeRoot from "./Root";
3
+
4
+ export { CalendarRangeCore, CalendarRangeRoot };
5
+
6
+ /**
7
+ * Calendar Range; date range picker namespace
8
+ * @desc
9
+ * - `Calendar.Range.Root`: trigger + popover + range core 조합 템플릿이다.
10
+ * - `Calendar.Range.Core`: Mantine DatePicker range SOT 렌더러다.
11
+ */
12
+ export const CalendarRange = {
13
+ Root: CalendarRangeRoot,
14
+ Core: CalendarRangeCore,
15
+ };