sh-ui-cli 0.52.1 → 0.52.3
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/data/changelog/versions.json +27 -0
- package/data/registry/react/components/_smoke/vanilla-extract.test.ts +33 -0
- package/data/registry/react/components/input/styles.css.ts +6 -6
- package/data/registry/react/registry.json +35 -852
- package/package.json +1 -1
- package/src/api.d.ts +3 -4
- package/src/constants.js +9 -5
- package/src/mcp.mjs +0 -1
- package/data/registry/react/components/accordion/index.vanilla-extract.tsx +0 -97
- package/data/registry/react/components/accordion/styles.css.ts +0 -131
- package/data/registry/react/components/avatar/index.vanilla-extract.tsx +0 -73
- package/data/registry/react/components/avatar/styles.css.ts +0 -68
- package/data/registry/react/components/badge/index.vanilla-extract.tsx +0 -40
- package/data/registry/react/components/badge/styles.css.ts +0 -71
- package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +0 -152
- package/data/registry/react/components/breadcrumb/styles.css.ts +0 -95
- package/data/registry/react/components/calendar/index.vanilla-extract.tsx +0 -806
- package/data/registry/react/components/calendar/styles.css.ts +0 -250
- package/data/registry/react/components/carousel/index.vanilla-extract.tsx +0 -430
- package/data/registry/react/components/carousel/styles.css.ts +0 -169
- package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +0 -96
- package/data/registry/react/components/checkbox/styles.css.ts +0 -74
- package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +0 -230
- package/data/registry/react/components/code-editor/styles.css.ts +0 -97
- package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +0 -191
- package/data/registry/react/components/code-panel/styles.css.ts +0 -151
- package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +0 -467
- package/data/registry/react/components/color-picker/styles.css.ts +0 -169
- package/data/registry/react/components/combobox/index.vanilla-extract.tsx +0 -165
- package/data/registry/react/components/combobox/styles.css.ts +0 -174
- package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +0 -251
- package/data/registry/react/components/context-menu/styles.css.ts +0 -167
- package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +0 -520
- package/data/registry/react/components/date-picker/styles.css.ts +0 -111
- package/data/registry/react/components/dialog/index.vanilla-extract.tsx +0 -95
- package/data/registry/react/components/dialog/styles.css.ts +0 -140
- package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +0 -255
- package/data/registry/react/components/dropdown-menu/styles.css.ts +0 -175
- package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +0 -487
- package/data/registry/react/components/file-upload/styles.css.ts +0 -193
- package/data/registry/react/components/form/index.vanilla-extract.tsx +0 -61
- package/data/registry/react/components/form/styles.css.ts +0 -56
- package/data/registry/react/components/header/index.vanilla-extract.tsx +0 -805
- package/data/registry/react/components/header/styles.css.ts +0 -413
- package/data/registry/react/components/label/index.vanilla-extract.tsx +0 -52
- package/data/registry/react/components/label/styles.css.ts +0 -141
- package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +0 -119
- package/data/registry/react/components/markdown-editor/styles.css.ts +0 -231
- package/data/registry/react/components/menubar/index.vanilla-extract.tsx +0 -32
- package/data/registry/react/components/menubar/styles.css.ts +0 -53
- package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +0 -148
- package/data/registry/react/components/numeric-input/styles.css.ts +0 -65
- package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +0 -174
- package/data/registry/react/components/page-toc/styles.css.ts +0 -97
- package/data/registry/react/components/pagination/index.vanilla-extract.tsx +0 -269
- package/data/registry/react/components/pagination/styles.css.ts +0 -113
- package/data/registry/react/components/popover/index.vanilla-extract.tsx +0 -113
- package/data/registry/react/components/popover/styles.css.ts +0 -78
- package/data/registry/react/components/progress/index.vanilla-extract.tsx +0 -54
- package/data/registry/react/components/progress/styles.css.ts +0 -53
- package/data/registry/react/components/radio/index.vanilla-extract.tsx +0 -65
- package/data/registry/react/components/radio/styles.css.ts +0 -79
- package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +0 -348
- package/data/registry/react/components/rich-text-editor/styles.css.ts +0 -243
- package/data/registry/react/components/select/index.vanilla-extract.tsx +0 -234
- package/data/registry/react/components/select/styles.css.ts +0 -225
- package/data/registry/react/components/separator/index.vanilla-extract.tsx +0 -46
- package/data/registry/react/components/separator/styles.css.ts +0 -24
- package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +0 -1067
- package/data/registry/react/components/sidebar/styles.css.ts +0 -578
- package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +0 -22
- package/data/registry/react/components/skeleton/styles.css.ts +0 -30
- package/data/registry/react/components/slider/index.vanilla-extract.tsx +0 -298
- package/data/registry/react/components/slider/styles.css.ts +0 -75
- package/data/registry/react/components/spinner/index.vanilla-extract.tsx +0 -38
- package/data/registry/react/components/spinner/styles.css.ts +0 -60
- package/data/registry/react/components/switch/index.vanilla-extract.tsx +0 -39
- package/data/registry/react/components/switch/styles.css.ts +0 -87
- package/data/registry/react/components/tabs/index.vanilla-extract.tsx +0 -91
- package/data/registry/react/components/tabs/styles.css.ts +0 -145
- package/data/registry/react/components/textarea/index.vanilla-extract.tsx +0 -23
- package/data/registry/react/components/textarea/styles.css.ts +0 -55
- package/data/registry/react/components/toast/index.vanilla-extract.tsx +0 -258
- package/data/registry/react/components/toast/styles.css.ts +0 -307
- package/data/registry/react/components/toggle/index.vanilla-extract.tsx +0 -131
- package/data/registry/react/components/toggle/styles.css.ts +0 -109
- package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +0 -83
- package/data/registry/react/components/tooltip/styles.css.ts +0 -59
|
@@ -1,806 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { cn } from "@SH_UI_UTILS@";
|
|
5
|
-
import {
|
|
6
|
-
Select,
|
|
7
|
-
SelectContent,
|
|
8
|
-
SelectItem,
|
|
9
|
-
SelectTrigger,
|
|
10
|
-
} from "../select";
|
|
11
|
-
import { byKey, calendar, calendarMulti, calendar__month, calendar__header, calendar__title, calendar__nav, calendarNavPlaceholder, calendarSelectTrigger, select__positioner, calendar__weekdays, calendar__weekday, calendar__grid, calendar__cell, calendarCellInRange, calendarCellRangeStart, calendarCellRangeEnd, calendar__day, calendarDayOutside, calendarDayToday, calendarDaySelected, calendarSelectValue, calendarSelectPopup, calendarGridWrap, calendarCellHidden } from "./styles.css";
|
|
12
|
-
|
|
13
|
-
/* ───────── Helpers ───────── */
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const DEFAULT_WEEKDAYS_KO = ["일", "월", "화", "수", "목", "금", "토"] as const;
|
|
17
|
-
|
|
18
|
-
const isSameDay = (a: Date, b: Date) =>
|
|
19
|
-
a.getFullYear() === b.getFullYear() &&
|
|
20
|
-
a.getMonth() === b.getMonth() &&
|
|
21
|
-
a.getDate() === b.getDate();
|
|
22
|
-
|
|
23
|
-
const toDateOnly = (d: Date) =>
|
|
24
|
-
new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
25
|
-
|
|
26
|
-
const startOfMonth = (d: Date) =>
|
|
27
|
-
new Date(d.getFullYear(), d.getMonth(), 1);
|
|
28
|
-
|
|
29
|
-
const addMonths = (d: Date, n: number) =>
|
|
30
|
-
new Date(d.getFullYear(), d.getMonth() + n, 1);
|
|
31
|
-
|
|
32
|
-
const formatIsoDate = (d: Date) =>
|
|
33
|
-
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
34
|
-
|
|
35
|
-
const defaultMonthLabel = (year: number, month: number) =>
|
|
36
|
-
`${year}년 ${month + 1}월`;
|
|
37
|
-
|
|
38
|
-
function getDaysGrid(year: number, month: number, weekStartsOn: 0 | 1) {
|
|
39
|
-
const first = new Date(year, month, 1);
|
|
40
|
-
const startDay = (first.getDay() - weekStartsOn + 7) % 7;
|
|
41
|
-
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
42
|
-
const prevDays = new Date(year, month, 0).getDate();
|
|
43
|
-
|
|
44
|
-
const cells: { date: Date; current: boolean }[] = [];
|
|
45
|
-
|
|
46
|
-
for (let i = startDay - 1; i >= 0; i--) {
|
|
47
|
-
cells.push({ date: new Date(year, month - 1, prevDays - i), current: false });
|
|
48
|
-
}
|
|
49
|
-
for (let d = 1; d <= daysInMonth; d++) {
|
|
50
|
-
cells.push({ date: new Date(year, month, d), current: true });
|
|
51
|
-
}
|
|
52
|
-
const remaining = 7 - (cells.length % 7);
|
|
53
|
-
if (remaining < 7) {
|
|
54
|
-
for (let d = 1; d <= remaining; d++) {
|
|
55
|
-
cells.push({ date: new Date(year, month + 1, d), current: false });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return cells;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function rotateWeekdays(labels: readonly string[], weekStartsOn: 0 | 1): string[] {
|
|
63
|
-
if (weekStartsOn === 0) return [...labels];
|
|
64
|
-
return [...labels.slice(weekStartsOn), ...labels.slice(0, weekStartsOn)];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* ───────── Types ───────── */
|
|
68
|
-
|
|
69
|
-
export interface DateRange {
|
|
70
|
-
/** 시작일 (포함). */
|
|
71
|
-
from: Date;
|
|
72
|
-
/** 종료일 (포함). */
|
|
73
|
-
to: Date;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type CalendarMode = "single" | "multiple" | "range";
|
|
77
|
-
|
|
78
|
-
interface CalendarCommonProps {
|
|
79
|
-
/** 표시 중인 월 (controlled). */
|
|
80
|
-
month?: Date;
|
|
81
|
-
/** 표시 월의 초기값 (uncontrolled). */
|
|
82
|
-
defaultMonth?: Date;
|
|
83
|
-
/** 표시 월 변경 콜백. */
|
|
84
|
-
onMonthChange?: (month: Date) => void;
|
|
85
|
-
/** 동시에 표시할 월 개수. compound(children 사용) 모드에서는 1로 강제된다. @default 1 */
|
|
86
|
-
numberOfMonths?: number;
|
|
87
|
-
/** 선택 가능 최소 날짜 (포함). */
|
|
88
|
-
min?: Date;
|
|
89
|
-
/** 선택 가능 최대 날짜 (포함). */
|
|
90
|
-
max?: Date;
|
|
91
|
-
/** 날짜별 비활성 콜백. */
|
|
92
|
-
disabled?: (date: Date) => boolean;
|
|
93
|
-
/** 외부 날짜(이전/다음 달) 표시 여부. @default true */
|
|
94
|
-
showOutsideDays?: boolean;
|
|
95
|
-
/** 주의 시작 요일. 0=일, 1=월. @default 0 */
|
|
96
|
-
weekStartsOn?: 0 | 1;
|
|
97
|
-
/** 요일 라벨 (Sunday-first 7개). */
|
|
98
|
-
weekdayLabels?: readonly string[];
|
|
99
|
-
/** 월 헤더 그룹의 aria-label 포맷. @default "{year}년 {month+1}월" */
|
|
100
|
-
formatMonthLabel?: (year: number, month: number) => string;
|
|
101
|
-
/** 연도 dropdown 시작 연도. */
|
|
102
|
-
fromYear?: number;
|
|
103
|
-
/** 연도 dropdown 끝 연도. */
|
|
104
|
-
toYear?: number;
|
|
105
|
-
/** 캘린더 컨테이너 클래스. */
|
|
106
|
-
className?: string;
|
|
107
|
-
/** 그리드 aria-label. 미지정 시 월 라벨 사용. */
|
|
108
|
-
"aria-label"?: string;
|
|
109
|
-
/**
|
|
110
|
-
* Compound 모드. 미지정 시 기본 레이아웃이 자동 렌더된다.
|
|
111
|
-
* 직접 조립하려면 `CalendarHeader`/`CalendarGrid` 등을 children 으로 넘긴다.
|
|
112
|
-
*/
|
|
113
|
-
children?: React.ReactNode;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export type CalendarSingleProps = CalendarCommonProps & {
|
|
117
|
-
mode?: "single";
|
|
118
|
-
value?: Date;
|
|
119
|
-
defaultValue?: Date;
|
|
120
|
-
onValueChange?: (date: Date | undefined) => void;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export type CalendarMultipleProps = CalendarCommonProps & {
|
|
124
|
-
mode: "multiple";
|
|
125
|
-
value?: Date[];
|
|
126
|
-
defaultValue?: Date[];
|
|
127
|
-
onValueChange?: (dates: Date[]) => void;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
export type CalendarRangeProps = CalendarCommonProps & {
|
|
131
|
-
mode: "range";
|
|
132
|
-
value?: DateRange;
|
|
133
|
-
defaultValue?: DateRange;
|
|
134
|
-
onValueChange?: (range: DateRange | undefined) => void;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export type CalendarProps =
|
|
138
|
-
| CalendarSingleProps
|
|
139
|
-
| CalendarMultipleProps
|
|
140
|
-
| CalendarRangeProps;
|
|
141
|
-
|
|
142
|
-
/* ───────── Icons ───────── */
|
|
143
|
-
|
|
144
|
-
function ChevronLeftIcon() {
|
|
145
|
-
return (
|
|
146
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
147
|
-
<path d="M10 3 5.5 8 10 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
148
|
-
</svg>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function ChevronRightIcon() {
|
|
153
|
-
return (
|
|
154
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
155
|
-
<path d="M6 3 10.5 8 6 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
156
|
-
</svg>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function ChevronDoubleLeftIcon() {
|
|
161
|
-
return (
|
|
162
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
163
|
-
<path d="M8 3 3.5 8 8 13M13 3 8.5 8 13 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
164
|
-
</svg>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function ChevronDoubleRightIcon() {
|
|
169
|
-
return (
|
|
170
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
171
|
-
<path d="M3 3 7.5 8 3 13M8 3 12.5 8 8 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
172
|
-
</svg>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/* ───────── Context ───────── */
|
|
177
|
-
|
|
178
|
-
interface CalendarContextValue {
|
|
179
|
-
/** 이 컨텍스트가 담당하는 월 (multi-month 의 경우 idx 별로 다름). */
|
|
180
|
-
visibleMonth: Date;
|
|
181
|
-
/** 현재 month index (multi-month). compound 모드에서는 항상 0. */
|
|
182
|
-
monthIndex: number;
|
|
183
|
-
monthsLength: number;
|
|
184
|
-
|
|
185
|
-
yearOptions: number[];
|
|
186
|
-
|
|
187
|
-
setYearForVisible: (y: number) => void;
|
|
188
|
-
setMonthForVisible: (m: number) => void;
|
|
189
|
-
prevMonth: () => void;
|
|
190
|
-
nextMonth: () => void;
|
|
191
|
-
prevYear: () => void;
|
|
192
|
-
nextYear: () => void;
|
|
193
|
-
|
|
194
|
-
weekStartsOn: 0 | 1;
|
|
195
|
-
weekdayLabels: string[];
|
|
196
|
-
showOutsideDays: boolean;
|
|
197
|
-
formatMonthLabel: (year: number, month: number) => string;
|
|
198
|
-
ariaLabel?: string;
|
|
199
|
-
|
|
200
|
-
isSelected: (date: Date) => boolean;
|
|
201
|
-
isInRange: (date: Date) => { inRange: boolean; isStart: boolean; isEnd: boolean };
|
|
202
|
-
isDisabled: (date: Date) => boolean;
|
|
203
|
-
handleSelect: (date: Date) => void;
|
|
204
|
-
setHoverDate: (date: Date | undefined) => void;
|
|
205
|
-
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
206
|
-
|
|
207
|
-
/** multi-month 일 때만 의미 있는 위치 표지. compound(single) 일 때는 항상 true. */
|
|
208
|
-
isFirstMonth: boolean;
|
|
209
|
-
isLastMonth: boolean;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const CalendarContext = React.createContext<CalendarContextValue | null>(null);
|
|
213
|
-
|
|
214
|
-
function useCalendarContext(component: string) {
|
|
215
|
-
const ctx = React.useContext(CalendarContext);
|
|
216
|
-
if (!ctx) {
|
|
217
|
-
throw new Error(`${component}는 <Calendar> 내부에서 사용해야 합니다.`);
|
|
218
|
-
}
|
|
219
|
-
return ctx;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/* ───────── Calendar (root) ───────── */
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 인라인 날짜 캘린더. single/multiple/range 모드, 다중 월 표시, 키보드 내비게이션을 지원한다.
|
|
226
|
-
* children 을 생략하면 기본 레이아웃이 자동 렌더되고, compound 파츠로 직접 조립도 가능하다.
|
|
227
|
-
*
|
|
228
|
-
* 팝오버에 띄우려면 `DatePicker` / `DateRangePicker`(date-picker) 를 사용한다.
|
|
229
|
-
*/
|
|
230
|
-
export function Calendar(props: CalendarProps) {
|
|
231
|
-
const {
|
|
232
|
-
mode = "single",
|
|
233
|
-
month: monthProp,
|
|
234
|
-
defaultMonth,
|
|
235
|
-
onMonthChange,
|
|
236
|
-
numberOfMonths: numberOfMonthsProp = 1,
|
|
237
|
-
min,
|
|
238
|
-
max,
|
|
239
|
-
disabled,
|
|
240
|
-
showOutsideDays = true,
|
|
241
|
-
weekStartsOn = 0,
|
|
242
|
-
weekdayLabels: weekdayLabelsProp,
|
|
243
|
-
formatMonthLabel = defaultMonthLabel,
|
|
244
|
-
fromYear,
|
|
245
|
-
toYear,
|
|
246
|
-
className,
|
|
247
|
-
"aria-label": ariaLabel,
|
|
248
|
-
children,
|
|
249
|
-
} = props as CalendarCommonProps & { mode?: CalendarMode };
|
|
250
|
-
|
|
251
|
-
// compound(children) 모드에서는 단일 월로 강제 (multi-month 헤더 조립은 본 버전 스코프 외).
|
|
252
|
-
const numberOfMonths = children ? 1 : Math.max(1, numberOfMonthsProp);
|
|
253
|
-
|
|
254
|
-
/* selected value (controlled / uncontrolled) */
|
|
255
|
-
const isControlled = "value" in props && props.value !== undefined;
|
|
256
|
-
const [internalSingle, setInternalSingle] = React.useState<Date | undefined>(
|
|
257
|
-
mode === "single" ? (props as CalendarSingleProps).defaultValue : undefined,
|
|
258
|
-
);
|
|
259
|
-
const [internalMultiple, setInternalMultiple] = React.useState<Date[]>(
|
|
260
|
-
mode === "multiple" ? (props as CalendarMultipleProps).defaultValue ?? [] : [],
|
|
261
|
-
);
|
|
262
|
-
const [internalRange, setInternalRange] = React.useState<DateRange | undefined>(
|
|
263
|
-
mode === "range" ? (props as CalendarRangeProps).defaultValue : undefined,
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
const singleValue = isControlled
|
|
267
|
-
? (props as CalendarSingleProps).value
|
|
268
|
-
: internalSingle;
|
|
269
|
-
const multipleValue = isControlled
|
|
270
|
-
? (props as CalendarMultipleProps).value ?? []
|
|
271
|
-
: internalMultiple;
|
|
272
|
-
const rangeValue = isControlled
|
|
273
|
-
? (props as CalendarRangeProps).value
|
|
274
|
-
: internalRange;
|
|
275
|
-
|
|
276
|
-
/* range picking state */
|
|
277
|
-
const [picking, setPicking] = React.useState<Date | undefined>(undefined);
|
|
278
|
-
const [hoverDate, setHoverDate] = React.useState<Date | undefined>(undefined);
|
|
279
|
-
|
|
280
|
-
/* displayed month (controlled / uncontrolled) */
|
|
281
|
-
const monthControlled = monthProp !== undefined;
|
|
282
|
-
const [internalMonth, setInternalMonth] = React.useState<Date>(() => {
|
|
283
|
-
if (defaultMonth) return startOfMonth(defaultMonth);
|
|
284
|
-
if (mode === "single" && singleValue) return startOfMonth(singleValue);
|
|
285
|
-
if (mode === "multiple" && multipleValue.length > 0) return startOfMonth(multipleValue[0]);
|
|
286
|
-
if (mode === "range" && rangeValue?.from) return startOfMonth(rangeValue.from);
|
|
287
|
-
return startOfMonth(new Date());
|
|
288
|
-
});
|
|
289
|
-
const currentMonth = monthControlled ? startOfMonth(monthProp!) : internalMonth;
|
|
290
|
-
|
|
291
|
-
const setMonth = React.useCallback(
|
|
292
|
-
(next: Date) => {
|
|
293
|
-
const normalized = startOfMonth(next);
|
|
294
|
-
if (!monthControlled) setInternalMonth(normalized);
|
|
295
|
-
onMonthChange?.(normalized);
|
|
296
|
-
},
|
|
297
|
-
[monthControlled, onMonthChange],
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
/* focused cursor for keyboard nav */
|
|
301
|
-
const [focusedDate, setFocusedDate] = React.useState<Date>(() => {
|
|
302
|
-
if (mode === "single" && singleValue) return singleValue;
|
|
303
|
-
if (mode === "range" && rangeValue?.from) return rangeValue.from;
|
|
304
|
-
return new Date();
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
/* weekday labels */
|
|
308
|
-
const weekdayLabels = React.useMemo(() => {
|
|
309
|
-
const base = weekdayLabelsProp ?? DEFAULT_WEEKDAYS_KO;
|
|
310
|
-
return rotateWeekdays(base, weekStartsOn);
|
|
311
|
-
}, [weekdayLabelsProp, weekStartsOn]);
|
|
312
|
-
|
|
313
|
-
/* year options */
|
|
314
|
-
const nowYear = new Date().getFullYear();
|
|
315
|
-
const resolvedFromYear = fromYear ?? min?.getFullYear() ?? nowYear - 10;
|
|
316
|
-
const resolvedToYear = toYear ?? max?.getFullYear() ?? nowYear + 10;
|
|
317
|
-
const yearOptions = React.useMemo(() => {
|
|
318
|
-
const out: number[] = [];
|
|
319
|
-
const start = Math.min(resolvedFromYear, resolvedToYear);
|
|
320
|
-
const end = Math.max(resolvedFromYear, resolvedToYear);
|
|
321
|
-
for (let y = start; y <= end; y++) out.push(y);
|
|
322
|
-
return out;
|
|
323
|
-
}, [resolvedFromYear, resolvedToYear]);
|
|
324
|
-
|
|
325
|
-
/* selection helpers */
|
|
326
|
-
const isDateDisabled = React.useCallback(
|
|
327
|
-
(date: Date) => {
|
|
328
|
-
const d = toDateOnly(date);
|
|
329
|
-
if (min && d < toDateOnly(min)) return true;
|
|
330
|
-
if (max && d > toDateOnly(max)) return true;
|
|
331
|
-
if (disabled?.(date)) return true;
|
|
332
|
-
return false;
|
|
333
|
-
},
|
|
334
|
-
[min, max, disabled],
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const isDateSelected = React.useCallback(
|
|
338
|
-
(date: Date) => {
|
|
339
|
-
if (mode === "single") return !!singleValue && isSameDay(date, singleValue);
|
|
340
|
-
if (mode === "multiple") return multipleValue.some((d) => isSameDay(d, date));
|
|
341
|
-
if (mode === "range") {
|
|
342
|
-
if (picking) return isSameDay(date, picking);
|
|
343
|
-
if (rangeValue?.from && isSameDay(date, rangeValue.from)) return true;
|
|
344
|
-
if (rangeValue?.to && isSameDay(date, rangeValue.to)) return true;
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
return false;
|
|
348
|
-
},
|
|
349
|
-
[mode, singleValue, multipleValue, rangeValue, picking],
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const getRangeState = React.useCallback(
|
|
353
|
-
(date: Date) => {
|
|
354
|
-
if (mode !== "range") return { inRange: false, isStart: false, isEnd: false };
|
|
355
|
-
|
|
356
|
-
const from = picking ?? rangeValue?.from;
|
|
357
|
-
if (!from) return { inRange: false, isStart: false, isEnd: false };
|
|
358
|
-
|
|
359
|
-
const to = picking ? hoverDate : rangeValue?.to;
|
|
360
|
-
if (!to) {
|
|
361
|
-
return { inRange: false, isStart: isSameDay(date, from), isEnd: false };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const [rStart, rEnd] = from <= to ? [from, to] : [to, from];
|
|
365
|
-
const s = toDateOnly(rStart);
|
|
366
|
-
const e = toDateOnly(rEnd);
|
|
367
|
-
const d = toDateOnly(date);
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
inRange: d >= s && d <= e,
|
|
371
|
-
isStart: isSameDay(d, s),
|
|
372
|
-
isEnd: isSameDay(d, e),
|
|
373
|
-
};
|
|
374
|
-
},
|
|
375
|
-
[mode, picking, hoverDate, rangeValue],
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
const handleSelect = React.useCallback(
|
|
379
|
-
(date: Date) => {
|
|
380
|
-
if (mode === "single") {
|
|
381
|
-
if (!isControlled) setInternalSingle(date);
|
|
382
|
-
(props as CalendarSingleProps).onValueChange?.(date);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (mode === "multiple") {
|
|
387
|
-
const exists = multipleValue.some((d) => isSameDay(d, date));
|
|
388
|
-
const next = exists
|
|
389
|
-
? multipleValue.filter((d) => !isSameDay(d, date))
|
|
390
|
-
: [...multipleValue, date];
|
|
391
|
-
if (!isControlled) setInternalMultiple(next);
|
|
392
|
-
(props as CalendarMultipleProps).onValueChange?.(next);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (mode === "range") {
|
|
397
|
-
if (!picking) {
|
|
398
|
-
setPicking(date);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const [from, to] = picking <= date ? [picking, date] : [date, picking];
|
|
402
|
-
const range: DateRange = { from, to };
|
|
403
|
-
setPicking(undefined);
|
|
404
|
-
setHoverDate(undefined);
|
|
405
|
-
if (!isControlled) setInternalRange(range);
|
|
406
|
-
(props as CalendarRangeProps).onValueChange?.(range);
|
|
407
|
-
}
|
|
408
|
-
},
|
|
409
|
-
[mode, isControlled, multipleValue, picking, props],
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
/* keyboard nav */
|
|
413
|
-
const handleKeyDown = React.useCallback(
|
|
414
|
-
(e: React.KeyboardEvent) => {
|
|
415
|
-
let next: Date | null = null;
|
|
416
|
-
const cursor = focusedDate;
|
|
417
|
-
|
|
418
|
-
switch (e.key) {
|
|
419
|
-
case "ArrowLeft":
|
|
420
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - 1);
|
|
421
|
-
break;
|
|
422
|
-
case "ArrowRight":
|
|
423
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1);
|
|
424
|
-
break;
|
|
425
|
-
case "ArrowUp":
|
|
426
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - 7);
|
|
427
|
-
break;
|
|
428
|
-
case "ArrowDown":
|
|
429
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 7);
|
|
430
|
-
break;
|
|
431
|
-
case "PageUp":
|
|
432
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth() - 1, cursor.getDate());
|
|
433
|
-
break;
|
|
434
|
-
case "PageDown":
|
|
435
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth() + 1, cursor.getDate());
|
|
436
|
-
break;
|
|
437
|
-
case "Home":
|
|
438
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), 1);
|
|
439
|
-
break;
|
|
440
|
-
case "End":
|
|
441
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0);
|
|
442
|
-
break;
|
|
443
|
-
case "Enter":
|
|
444
|
-
case " ":
|
|
445
|
-
e.preventDefault();
|
|
446
|
-
if (!isDateDisabled(cursor)) handleSelect(cursor);
|
|
447
|
-
return;
|
|
448
|
-
default:
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
e.preventDefault();
|
|
453
|
-
if (!next || isDateDisabled(next)) return;
|
|
454
|
-
|
|
455
|
-
setFocusedDate(next);
|
|
456
|
-
|
|
457
|
-
const visibleEnd = addMonths(currentMonth, numberOfMonths - 1);
|
|
458
|
-
if (next < currentMonth) {
|
|
459
|
-
setMonth(addMonths(currentMonth, -1));
|
|
460
|
-
} else if (next > new Date(visibleEnd.getFullYear(), visibleEnd.getMonth() + 1, 0)) {
|
|
461
|
-
setMonth(addMonths(currentMonth, 1));
|
|
462
|
-
}
|
|
463
|
-
},
|
|
464
|
-
[focusedDate, isDateDisabled, handleSelect, currentMonth, numberOfMonths, setMonth],
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
/* months to render */
|
|
468
|
-
const months = Array.from({ length: numberOfMonths }, (_, i) =>
|
|
469
|
-
addMonths(currentMonth, i),
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
/* per-month context factory */
|
|
473
|
-
const buildMonthContext = (visibleMonth: Date, idx: number): CalendarContextValue => ({
|
|
474
|
-
visibleMonth,
|
|
475
|
-
monthIndex: idx,
|
|
476
|
-
monthsLength: numberOfMonths,
|
|
477
|
-
yearOptions,
|
|
478
|
-
setYearForVisible: (y) => {
|
|
479
|
-
const yearDiff = y - visibleMonth.getFullYear();
|
|
480
|
-
setMonth(new Date(currentMonth.getFullYear() + yearDiff, currentMonth.getMonth(), 1));
|
|
481
|
-
},
|
|
482
|
-
setMonthForVisible: (m) => {
|
|
483
|
-
setMonth(addMonths(currentMonth, m - visibleMonth.getMonth()));
|
|
484
|
-
},
|
|
485
|
-
prevMonth: () => setMonth(addMonths(currentMonth, -1)),
|
|
486
|
-
nextMonth: () => setMonth(addMonths(currentMonth, 1)),
|
|
487
|
-
prevYear: () => setMonth(addMonths(currentMonth, -12)),
|
|
488
|
-
nextYear: () => setMonth(addMonths(currentMonth, 12)),
|
|
489
|
-
weekStartsOn,
|
|
490
|
-
weekdayLabels,
|
|
491
|
-
showOutsideDays,
|
|
492
|
-
formatMonthLabel,
|
|
493
|
-
ariaLabel,
|
|
494
|
-
isSelected: isDateSelected,
|
|
495
|
-
isInRange: getRangeState,
|
|
496
|
-
isDisabled: isDateDisabled,
|
|
497
|
-
handleSelect,
|
|
498
|
-
setHoverDate: mode === "range" ? setHoverDate : () => {},
|
|
499
|
-
onKeyDown: handleKeyDown,
|
|
500
|
-
isFirstMonth: idx === 0,
|
|
501
|
-
isLastMonth: idx === numberOfMonths - 1,
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
return (
|
|
505
|
-
<div
|
|
506
|
-
className={cn(calendar, numberOfMonths > 1 && calendarMulti, className)}
|
|
507
|
-
aria-label={ariaLabel}
|
|
508
|
-
>
|
|
509
|
-
{children
|
|
510
|
-
? (
|
|
511
|
-
<CalendarContext.Provider value={buildMonthContext(months[0], 0)}>
|
|
512
|
-
{children}
|
|
513
|
-
</CalendarContext.Provider>
|
|
514
|
-
)
|
|
515
|
-
: months.map((m, idx) => (
|
|
516
|
-
<CalendarContext.Provider
|
|
517
|
-
key={`${m.getFullYear()}-${m.getMonth()}`}
|
|
518
|
-
value={buildMonthContext(m, idx)}
|
|
519
|
-
>
|
|
520
|
-
<div className={calendar__month}>
|
|
521
|
-
<CalendarHeader>
|
|
522
|
-
{idx === 0 ? <CalendarPrevYearButton /> : <CalendarNavPlaceholder />}
|
|
523
|
-
{idx === 0 ? <CalendarPrevMonthButton /> : <CalendarNavPlaceholder />}
|
|
524
|
-
<div className={calendar__title}>
|
|
525
|
-
<CalendarYearSelect />
|
|
526
|
-
<CalendarMonthSelect />
|
|
527
|
-
</div>
|
|
528
|
-
{idx === months.length - 1 ? <CalendarNextMonthButton /> : <CalendarNavPlaceholder />}
|
|
529
|
-
{idx === months.length - 1 ? <CalendarNextYearButton /> : <CalendarNavPlaceholder />}
|
|
530
|
-
</CalendarHeader>
|
|
531
|
-
<CalendarGrid />
|
|
532
|
-
</div>
|
|
533
|
-
</CalendarContext.Provider>
|
|
534
|
-
))}
|
|
535
|
-
</div>
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/* ───────── Compound: Header ───────── */
|
|
540
|
-
|
|
541
|
-
export interface CalendarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
542
|
-
|
|
543
|
-
/** 헤더 컨테이너. 화살표/dropdown 등을 children 으로 자유롭게 배치. */
|
|
544
|
-
export const CalendarHeader = React.forwardRef<HTMLDivElement, CalendarHeaderProps>(
|
|
545
|
-
function CalendarHeader({ className, ...props }, ref) {
|
|
546
|
-
return <div ref={ref} className={cn(calendar__header, className)} {...props} />;
|
|
547
|
-
},
|
|
548
|
-
);
|
|
549
|
-
|
|
550
|
-
function CalendarNavPlaceholder() {
|
|
551
|
-
return <span className={cn(calendar__nav, calendarNavPlaceholder)} aria-hidden />;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/* ───────── Compound: Nav buttons ───────── */
|
|
555
|
-
|
|
556
|
-
export interface CalendarNavButtonProps
|
|
557
|
-
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
558
|
-
/** 버튼 본문. 미지정 시 기본 화살표 아이콘. */
|
|
559
|
-
children?: React.ReactNode;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function makeNavButton(
|
|
563
|
-
displayName: string,
|
|
564
|
-
defaultIcon: React.ReactNode,
|
|
565
|
-
defaultLabel: string,
|
|
566
|
-
resolveHandler: (ctx: CalendarContextValue) => () => void,
|
|
567
|
-
) {
|
|
568
|
-
const Component = React.forwardRef<HTMLButtonElement, CalendarNavButtonProps>(
|
|
569
|
-
function NavButton({ className, children, "aria-label": ariaLabel, onClick, ...props }, ref) {
|
|
570
|
-
const ctx = useCalendarContext(displayName);
|
|
571
|
-
return (
|
|
572
|
-
<button
|
|
573
|
-
ref={ref}
|
|
574
|
-
type="button"
|
|
575
|
-
className={cn(calendar__nav, className)}
|
|
576
|
-
aria-label={ariaLabel ?? defaultLabel}
|
|
577
|
-
onClick={(e) => {
|
|
578
|
-
resolveHandler(ctx)();
|
|
579
|
-
onClick?.(e);
|
|
580
|
-
}}
|
|
581
|
-
{...props}
|
|
582
|
-
>
|
|
583
|
-
{children ?? defaultIcon}
|
|
584
|
-
</button>
|
|
585
|
-
);
|
|
586
|
-
},
|
|
587
|
-
);
|
|
588
|
-
Component.displayName = displayName;
|
|
589
|
-
return Component;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/** 1년 이전. */
|
|
593
|
-
export const CalendarPrevYearButton = makeNavButton(
|
|
594
|
-
"CalendarPrevYearButton",
|
|
595
|
-
<ChevronDoubleLeftIcon />,
|
|
596
|
-
"이전 해",
|
|
597
|
-
(ctx) => ctx.prevYear,
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
/** 1년 다음. */
|
|
601
|
-
export const CalendarNextYearButton = makeNavButton(
|
|
602
|
-
"CalendarNextYearButton",
|
|
603
|
-
<ChevronDoubleRightIcon />,
|
|
604
|
-
"다음 해",
|
|
605
|
-
(ctx) => ctx.nextYear,
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
/** 1개월 이전. */
|
|
609
|
-
export const CalendarPrevMonthButton = makeNavButton(
|
|
610
|
-
"CalendarPrevMonthButton",
|
|
611
|
-
<ChevronLeftIcon />,
|
|
612
|
-
"이전 달",
|
|
613
|
-
(ctx) => ctx.prevMonth,
|
|
614
|
-
);
|
|
615
|
-
|
|
616
|
-
/** 1개월 다음. */
|
|
617
|
-
export const CalendarNextMonthButton = makeNavButton(
|
|
618
|
-
"CalendarNextMonthButton",
|
|
619
|
-
<ChevronRightIcon />,
|
|
620
|
-
"다음 달",
|
|
621
|
-
(ctx) => ctx.nextMonth,
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
/* ───────── Compound: Year / Month select ───────── */
|
|
625
|
-
|
|
626
|
-
export interface CalendarYearSelectProps {
|
|
627
|
-
/** select trigger 의 className. */
|
|
628
|
-
className?: string;
|
|
629
|
-
/** label 표시 포맷. @default "{year}년" */
|
|
630
|
-
formatYear?: (year: number) => string;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/** 연도 dropdown. sh-ui Select 로 구현. */
|
|
634
|
-
export function CalendarYearSelect({
|
|
635
|
-
className,
|
|
636
|
-
formatYear = (y) => `${y}년`,
|
|
637
|
-
}: CalendarYearSelectProps) {
|
|
638
|
-
const ctx = useCalendarContext("CalendarYearSelect");
|
|
639
|
-
const year = ctx.visibleMonth.getFullYear();
|
|
640
|
-
const items = ctx.yearOptions.includes(year)
|
|
641
|
-
? ctx.yearOptions
|
|
642
|
-
: [...ctx.yearOptions, year].sort((a, b) => a - b);
|
|
643
|
-
|
|
644
|
-
return (
|
|
645
|
-
<Select
|
|
646
|
-
value={String(year)}
|
|
647
|
-
onValueChange={(v) => ctx.setYearForVisible(Number(v))}
|
|
648
|
-
>
|
|
649
|
-
<SelectTrigger
|
|
650
|
-
className={cn(calendarSelectTrigger, className)}
|
|
651
|
-
aria-label="연도"
|
|
652
|
-
>
|
|
653
|
-
<span className={calendarSelectValue}>{formatYear(year)}</span>
|
|
654
|
-
</SelectTrigger>
|
|
655
|
-
<SelectContent className={calendarSelectPopup}>
|
|
656
|
-
{items.map((y) => (
|
|
657
|
-
<SelectItem key={y} value={String(y)}>
|
|
658
|
-
{formatYear(y)}
|
|
659
|
-
</SelectItem>
|
|
660
|
-
))}
|
|
661
|
-
</SelectContent>
|
|
662
|
-
</Select>
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
export interface CalendarMonthSelectProps {
|
|
667
|
-
className?: string;
|
|
668
|
-
/** label 표시 포맷. @default "{month+1}월" */
|
|
669
|
-
formatMonth?: (month: number) => string;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/** 월 dropdown. */
|
|
673
|
-
export function CalendarMonthSelect({
|
|
674
|
-
className,
|
|
675
|
-
formatMonth = (m) => `${m + 1}월`,
|
|
676
|
-
}: CalendarMonthSelectProps) {
|
|
677
|
-
const ctx = useCalendarContext("CalendarMonthSelect");
|
|
678
|
-
const month = ctx.visibleMonth.getMonth();
|
|
679
|
-
return (
|
|
680
|
-
<Select
|
|
681
|
-
value={String(month)}
|
|
682
|
-
onValueChange={(v) => ctx.setMonthForVisible(Number(v))}
|
|
683
|
-
>
|
|
684
|
-
<SelectTrigger
|
|
685
|
-
className={cn(calendarSelectTrigger, className)}
|
|
686
|
-
aria-label="월"
|
|
687
|
-
>
|
|
688
|
-
<span className={calendarSelectValue}>{formatMonth(month)}</span>
|
|
689
|
-
</SelectTrigger>
|
|
690
|
-
<SelectContent className={calendarSelectPopup}>
|
|
691
|
-
{Array.from({ length: 12 }, (_, m) => (
|
|
692
|
-
<SelectItem key={m} value={String(m)}>
|
|
693
|
-
{formatMonth(m)}
|
|
694
|
-
</SelectItem>
|
|
695
|
-
))}
|
|
696
|
-
</SelectContent>
|
|
697
|
-
</Select>
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/* ───────── Compound: Grid ───────── */
|
|
702
|
-
|
|
703
|
-
export interface CalendarGridProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
704
|
-
|
|
705
|
-
/** 요일 헤더 + 일자 버튼 그리드. */
|
|
706
|
-
export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
|
|
707
|
-
function CalendarGrid({ className, ...rest }, ref) {
|
|
708
|
-
const ctx = useCalendarContext("CalendarGrid");
|
|
709
|
-
const year = ctx.visibleMonth.getFullYear();
|
|
710
|
-
const month = ctx.visibleMonth.getMonth();
|
|
711
|
-
const cells = getDaysGrid(year, month, ctx.weekStartsOn);
|
|
712
|
-
const today = new Date();
|
|
713
|
-
const monthLabel = ctx.formatMonthLabel(year, month);
|
|
714
|
-
const ariaLabel = ctx.ariaLabel ?? monthLabel;
|
|
715
|
-
|
|
716
|
-
return (
|
|
717
|
-
<div ref={ref} className={cn(calendarGridWrap, className)} {...rest}>
|
|
718
|
-
<div className={calendar__weekdays} role="row">
|
|
719
|
-
{ctx.weekdayLabels.map((label) => (
|
|
720
|
-
<span
|
|
721
|
-
key={label}
|
|
722
|
-
className={calendar__weekday}
|
|
723
|
-
role="columnheader"
|
|
724
|
-
aria-label={label}
|
|
725
|
-
>
|
|
726
|
-
{label}
|
|
727
|
-
</span>
|
|
728
|
-
))}
|
|
729
|
-
</div>
|
|
730
|
-
|
|
731
|
-
<div
|
|
732
|
-
className={calendar__grid}
|
|
733
|
-
role="grid"
|
|
734
|
-
tabIndex={0}
|
|
735
|
-
onKeyDown={ctx.onKeyDown}
|
|
736
|
-
aria-label={ariaLabel}
|
|
737
|
-
>
|
|
738
|
-
{cells.map(({ date, current }, i) => {
|
|
739
|
-
const dDisabled = ctx.isDisabled(date);
|
|
740
|
-
const selected = ctx.isSelected(date);
|
|
741
|
-
const isToday = isSameDay(date, today);
|
|
742
|
-
const { inRange, isStart, isEnd } = ctx.isInRange(date);
|
|
743
|
-
const hidden = !current && !ctx.showOutsideDays;
|
|
744
|
-
|
|
745
|
-
if (hidden) {
|
|
746
|
-
return <span key={i} className={cn(calendar__cell, calendarCellHidden)} aria-hidden />;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return (
|
|
750
|
-
<div
|
|
751
|
-
key={i}
|
|
752
|
-
className={cn(
|
|
753
|
-
calendar__cell,
|
|
754
|
-
inRange && calendarCellInRange,
|
|
755
|
-
isStart && calendarCellRangeStart,
|
|
756
|
-
isEnd && calendarCellRangeEnd,
|
|
757
|
-
)}
|
|
758
|
-
>
|
|
759
|
-
<button
|
|
760
|
-
type="button"
|
|
761
|
-
className={cn(
|
|
762
|
-
calendar__day,
|
|
763
|
-
!current && calendarDayOutside,
|
|
764
|
-
selected && calendarDaySelected,
|
|
765
|
-
isToday && calendarDayToday,
|
|
766
|
-
)}
|
|
767
|
-
disabled={dDisabled}
|
|
768
|
-
tabIndex={-1}
|
|
769
|
-
onClick={() => { if (!dDisabled) ctx.handleSelect(date); }}
|
|
770
|
-
onMouseEnter={() => ctx.setHoverDate(date)}
|
|
771
|
-
onMouseLeave={() => ctx.setHoverDate(undefined)}
|
|
772
|
-
aria-label={formatIsoDate(date)}
|
|
773
|
-
aria-selected={selected || inRange || undefined}
|
|
774
|
-
data-today={isToday || undefined}
|
|
775
|
-
>
|
|
776
|
-
{date.getDate()}
|
|
777
|
-
</button>
|
|
778
|
-
</div>
|
|
779
|
-
);
|
|
780
|
-
})}
|
|
781
|
-
</div>
|
|
782
|
-
</div>
|
|
783
|
-
);
|
|
784
|
-
},
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
/* ───────── Hook: useCalendar ───────── */
|
|
788
|
-
|
|
789
|
-
/** Calendar 내부에서 visible month/탐색 핸들러를 직접 다룰 때 사용. */
|
|
790
|
-
export function useCalendar() {
|
|
791
|
-
const ctx = useCalendarContext("useCalendar");
|
|
792
|
-
return {
|
|
793
|
-
visibleMonth: ctx.visibleMonth,
|
|
794
|
-
monthIndex: ctx.monthIndex,
|
|
795
|
-
monthsLength: ctx.monthsLength,
|
|
796
|
-
setYear: ctx.setYearForVisible,
|
|
797
|
-
setMonth: ctx.setMonthForVisible,
|
|
798
|
-
prevMonth: ctx.prevMonth,
|
|
799
|
-
nextMonth: ctx.nextMonth,
|
|
800
|
-
prevYear: ctx.prevYear,
|
|
801
|
-
nextYear: ctx.nextYear,
|
|
802
|
-
yearOptions: ctx.yearOptions,
|
|
803
|
-
isFirstMonth: ctx.isFirstMonth,
|
|
804
|
-
isLastMonth: ctx.isLastMonth,
|
|
805
|
-
};
|
|
806
|
-
}
|