sh-ui-cli 0.40.0 → 0.42.0
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/bin/sh-ui.mjs +11 -2
- package/data/changelog/versions.json +29 -0
- package/data/registry/react/components/calendar/index.tsx +802 -0
- package/data/registry/react/components/calendar/styles.css +227 -0
- package/data/registry/react/components/date-picker/index.tsx +23 -275
- package/data/registry/react/components/date-picker/styles.css +1 -177
- package/data/registry/react/registry.json +21 -1
- package/package.json +1 -1
- package/src/add.mjs +79 -16
- package/src/create/generator.js +28 -0
- package/src/mcp.mjs +4 -0
- package/templates/flutter-standalone/gitignore +47 -0
- package/templates/monorepo/gitignore +44 -0
- package/templates/nextjs-standalone/gitignore +42 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* ── Calendar root ── */
|
|
2
|
+
|
|
3
|
+
.sh-ui-calendar {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
gap: var(--space-4);
|
|
6
|
+
user-select: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sh-ui-calendar--multi {
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.sh-ui-calendar__month {
|
|
14
|
+
width: 17.5rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ── Header (compound) ── */
|
|
18
|
+
|
|
19
|
+
.sh-ui-calendar__header {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
gap: var(--space-1);
|
|
24
|
+
margin-bottom: var(--space-2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sh-ui-calendar__title {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: var(--space-1);
|
|
31
|
+
flex: 1 1 auto;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ── Nav buttons ── */
|
|
36
|
+
|
|
37
|
+
.sh-ui-calendar__nav {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
width: 1.75rem;
|
|
42
|
+
height: 1.75rem;
|
|
43
|
+
padding: 0;
|
|
44
|
+
border: none;
|
|
45
|
+
border-radius: calc(var(--radius) - 2px);
|
|
46
|
+
background: transparent;
|
|
47
|
+
color: var(--foreground-muted);
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
transition: background-color var(--duration-fast), color var(--duration-fast);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sh-ui-calendar__nav:hover:not(:disabled) {
|
|
54
|
+
background: var(--background-muted);
|
|
55
|
+
color: var(--foreground);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.sh-ui-calendar__nav:focus-visible {
|
|
59
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
60
|
+
outline-offset: 2px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.sh-ui-calendar__nav--placeholder {
|
|
64
|
+
visibility: hidden;
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ── Select (year / month dropdown) ── */
|
|
69
|
+
/* sh-ui Select 의 trigger 를 캘린더 헤더용으로 컴팩트하게 오버라이드 */
|
|
70
|
+
|
|
71
|
+
.sh-ui-calendar__select-trigger.sh-ui-select__trigger {
|
|
72
|
+
min-width: 0;
|
|
73
|
+
height: 1.75rem;
|
|
74
|
+
gap: var(--space-1);
|
|
75
|
+
padding: 0 var(--space-2);
|
|
76
|
+
background: transparent;
|
|
77
|
+
border-color: transparent;
|
|
78
|
+
font-weight: var(--weight-semibold);
|
|
79
|
+
font-size: var(--text-sm);
|
|
80
|
+
color: var(--foreground);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.sh-ui-calendar__select-trigger.sh-ui-select__trigger:hover:not(:disabled) {
|
|
84
|
+
background: var(--background-muted);
|
|
85
|
+
border-color: transparent;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sh-ui-calendar__select-trigger.sh-ui-select__trigger[data-popup-open] {
|
|
89
|
+
background: var(--background-muted);
|
|
90
|
+
border-color: transparent;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* popover 안의 캘린더에서도 dropdown 이 위로 올라오도록 z-index 보강.
|
|
94
|
+
* (Select 의 기본 z-dropdown=200 < z-popover=500 이므로 :has 로 캘린더 select 만 선택해 z-popover 로 끌어올림.) */
|
|
95
|
+
.sh-ui-select__positioner:has(.sh-ui-calendar__select-popup) {
|
|
96
|
+
z-index: var(--z-popover);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ── Weekdays ── */
|
|
100
|
+
|
|
101
|
+
.sh-ui-calendar__weekdays {
|
|
102
|
+
display: grid;
|
|
103
|
+
grid-template-columns: repeat(7, 1fr);
|
|
104
|
+
margin-bottom: var(--space-1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sh-ui-calendar__weekday {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
height: 2rem;
|
|
112
|
+
font-size: var(--text-xs);
|
|
113
|
+
font-weight: var(--weight-medium);
|
|
114
|
+
color: var(--foreground-muted);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ── Grid ── */
|
|
118
|
+
|
|
119
|
+
.sh-ui-calendar__grid {
|
|
120
|
+
display: grid;
|
|
121
|
+
grid-template-columns: repeat(7, 1fr);
|
|
122
|
+
outline: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.sh-ui-calendar__grid:focus-visible {
|
|
126
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
127
|
+
outline-offset: 2px;
|
|
128
|
+
border-radius: calc(var(--radius) - 2px);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── Day cell ── */
|
|
132
|
+
|
|
133
|
+
.sh-ui-calendar__day {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
width: 2.25rem;
|
|
138
|
+
height: 2.25rem;
|
|
139
|
+
margin: 0.0625rem auto;
|
|
140
|
+
padding: 0;
|
|
141
|
+
border: none;
|
|
142
|
+
border-radius: calc(var(--radius) - 2px);
|
|
143
|
+
background: transparent;
|
|
144
|
+
color: var(--foreground);
|
|
145
|
+
font-size: 0.8125rem;
|
|
146
|
+
font-family: inherit;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
transition: background-color var(--duration-fast), color var(--duration-fast);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.sh-ui-calendar__day:hover:not(:disabled) {
|
|
152
|
+
background: var(--background-muted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.sh-ui-calendar__day:focus-visible {
|
|
156
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
157
|
+
outline-offset: 2px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.sh-ui-calendar__day--outside {
|
|
161
|
+
color: var(--foreground-subtle, var(--foreground-muted));
|
|
162
|
+
opacity: 0.4;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.sh-ui-calendar__day--hidden {
|
|
166
|
+
visibility: hidden;
|
|
167
|
+
pointer-events: none;
|
|
168
|
+
cursor: default;
|
|
169
|
+
background: transparent;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.sh-ui-calendar__day--today {
|
|
173
|
+
font-weight: var(--weight-bold);
|
|
174
|
+
text-decoration: underline;
|
|
175
|
+
text-underline-offset: 0.125rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.sh-ui-calendar__day--selected {
|
|
179
|
+
background: var(--primary);
|
|
180
|
+
color: var(--primary-foreground);
|
|
181
|
+
font-weight: var(--weight-semibold);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.sh-ui-calendar__day--selected:hover:not(:disabled) {
|
|
185
|
+
background: var(--primary-hover);
|
|
186
|
+
color: var(--primary-foreground);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.sh-ui-calendar__day:disabled {
|
|
190
|
+
opacity: 0.3;
|
|
191
|
+
cursor: not-allowed;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ── Range ── */
|
|
195
|
+
|
|
196
|
+
.sh-ui-calendar__day--in-range {
|
|
197
|
+
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
|
198
|
+
border-radius: 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.sh-ui-calendar__day--in-range:hover:not(:disabled) {
|
|
202
|
+
background: color-mix(in srgb, var(--primary) 22%, transparent);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.sh-ui-calendar__day--range-start {
|
|
206
|
+
background: var(--primary);
|
|
207
|
+
color: var(--primary-foreground);
|
|
208
|
+
font-weight: var(--weight-semibold);
|
|
209
|
+
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.sh-ui-calendar__day--range-end {
|
|
213
|
+
background: var(--primary);
|
|
214
|
+
color: var(--primary-foreground);
|
|
215
|
+
font-weight: var(--weight-semibold);
|
|
216
|
+
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.sh-ui-calendar__day--range-start.sh-ui-calendar__day--range-end {
|
|
220
|
+
border-radius: calc(var(--radius) - 2px);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.sh-ui-calendar__day--range-start:hover:not(:disabled),
|
|
224
|
+
.sh-ui-calendar__day--range-end:hover:not(:disabled) {
|
|
225
|
+
background: var(--primary-hover);
|
|
226
|
+
color: var(--primary-foreground);
|
|
227
|
+
}
|
|
@@ -2,84 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
5
|
+
import { Calendar, type DateRange } from "../calendar";
|
|
5
6
|
import "./styles.css";
|
|
6
7
|
|
|
8
|
+
export type { DateRange };
|
|
9
|
+
|
|
7
10
|
/* ───────── Helpers ───────── */
|
|
8
11
|
|
|
9
12
|
function cx(...args: (string | undefined | false)[]) {
|
|
10
13
|
return args.filter(Boolean).join(" ");
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
const WEEK_LABELS = ["일", "월", "화", "수", "목", "금", "토"] as const;
|
|
14
|
-
|
|
15
|
-
const isSameDay = (a: Date, b: Date) =>
|
|
16
|
-
a.getFullYear() === b.getFullYear() &&
|
|
17
|
-
a.getMonth() === b.getMonth() &&
|
|
18
|
-
a.getDate() === b.getDate();
|
|
19
|
-
|
|
20
|
-
const toDateOnly = (d: Date) =>
|
|
21
|
-
new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
22
|
-
|
|
23
16
|
const formatDefault = (d: Date) =>
|
|
24
17
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
25
18
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (month > 11) return [year + 1, 0];
|
|
29
|
-
return [year, month];
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
function getDaysGrid(year: number, month: number) {
|
|
33
|
-
const first = new Date(year, month, 1);
|
|
34
|
-
const startDay = first.getDay();
|
|
35
|
-
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
36
|
-
const prevDays = new Date(year, month, 0).getDate();
|
|
37
|
-
|
|
38
|
-
const cells: { date: Date; current: boolean }[] = [];
|
|
39
|
-
|
|
40
|
-
for (let i = startDay - 1; i >= 0; i--) {
|
|
41
|
-
cells.push({ date: new Date(year, month - 1, prevDays - i), current: false });
|
|
42
|
-
}
|
|
43
|
-
for (let d = 1; d <= daysInMonth; d++) {
|
|
44
|
-
cells.push({ date: new Date(year, month, d), current: true });
|
|
45
|
-
}
|
|
46
|
-
const remaining = 7 - (cells.length % 7);
|
|
47
|
-
if (remaining < 7) {
|
|
48
|
-
for (let d = 1; d <= remaining; d++) {
|
|
49
|
-
cells.push({ date: new Date(year, month + 1, d), current: false });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return cells;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* ───────── Types ───────── */
|
|
57
|
-
|
|
58
|
-
export interface DateRange {
|
|
59
|
-
/** 시작일 (포함). */
|
|
60
|
-
from: Date;
|
|
61
|
-
/** 종료일 (포함). */
|
|
62
|
-
to: Date;
|
|
63
|
-
}
|
|
19
|
+
const startOfMonth = (d: Date) =>
|
|
20
|
+
new Date(d.getFullYear(), d.getMonth(), 1);
|
|
64
21
|
|
|
65
22
|
/* ───────── Icons ───────── */
|
|
66
23
|
|
|
67
|
-
function ChevronLeftIcon() {
|
|
68
|
-
return (
|
|
69
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
70
|
-
<path d="M10 3 5.5 8 10 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
71
|
-
</svg>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function ChevronRightIcon() {
|
|
76
|
-
return (
|
|
77
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
78
|
-
<path d="M6 3 10.5 8 6 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
79
|
-
</svg>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
24
|
function CalendarIcon() {
|
|
84
25
|
return (
|
|
85
26
|
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
@@ -118,178 +59,6 @@ function useDatePickerContext(component: string) {
|
|
|
118
59
|
return ctx;
|
|
119
60
|
}
|
|
120
61
|
|
|
121
|
-
/* ───────── Internal Calendar Grid (shared w/ range picker) ───────── */
|
|
122
|
-
|
|
123
|
-
interface CalendarProps {
|
|
124
|
-
selected?: Date;
|
|
125
|
-
onSelect: (date: Date) => void;
|
|
126
|
-
min?: Date;
|
|
127
|
-
max?: Date;
|
|
128
|
-
focusedDate: Date;
|
|
129
|
-
onFocusedDateChange: (date: Date) => void;
|
|
130
|
-
rangeFrom?: Date;
|
|
131
|
-
rangeTo?: Date;
|
|
132
|
-
hoverDate?: Date;
|
|
133
|
-
onHoverDate?: (date: Date | undefined) => void;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function Calendar({
|
|
137
|
-
selected,
|
|
138
|
-
onSelect,
|
|
139
|
-
min,
|
|
140
|
-
max,
|
|
141
|
-
focusedDate,
|
|
142
|
-
onFocusedDateChange,
|
|
143
|
-
rangeFrom,
|
|
144
|
-
rangeTo,
|
|
145
|
-
hoverDate,
|
|
146
|
-
onHoverDate,
|
|
147
|
-
}: CalendarProps) {
|
|
148
|
-
const year = focusedDate.getFullYear();
|
|
149
|
-
const month = focusedDate.getMonth();
|
|
150
|
-
const cells = getDaysGrid(year, month);
|
|
151
|
-
const today = new Date();
|
|
152
|
-
|
|
153
|
-
const navigate = (newYear: number, newMonth: number) => {
|
|
154
|
-
const [y, m] = clampMonth(newYear, newMonth);
|
|
155
|
-
onFocusedDateChange(new Date(y, m, 1));
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const isDisabled = (date: Date) => {
|
|
159
|
-
if (min && date < toDateOnly(min)) return true;
|
|
160
|
-
if (max && date > toDateOnly(max)) return true;
|
|
161
|
-
return false;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
165
|
-
let next: Date | null = null;
|
|
166
|
-
const cursor = selected ?? focusedDate;
|
|
167
|
-
|
|
168
|
-
switch (e.key) {
|
|
169
|
-
case "ArrowLeft":
|
|
170
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - 1);
|
|
171
|
-
break;
|
|
172
|
-
case "ArrowRight":
|
|
173
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1);
|
|
174
|
-
break;
|
|
175
|
-
case "ArrowUp":
|
|
176
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - 7);
|
|
177
|
-
break;
|
|
178
|
-
case "ArrowDown":
|
|
179
|
-
next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 7);
|
|
180
|
-
break;
|
|
181
|
-
case "Enter":
|
|
182
|
-
case " ":
|
|
183
|
-
e.preventDefault();
|
|
184
|
-
if (!isDisabled(cursor)) onSelect(cursor);
|
|
185
|
-
return;
|
|
186
|
-
default:
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
e.preventDefault();
|
|
191
|
-
if (next && !isDisabled(next)) {
|
|
192
|
-
if (next.getMonth() !== month || next.getFullYear() !== year) {
|
|
193
|
-
onFocusedDateChange(new Date(next.getFullYear(), next.getMonth(), 1));
|
|
194
|
-
}
|
|
195
|
-
onSelect(next);
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const getRangeState = (date: Date) => {
|
|
200
|
-
if (!rangeFrom) return { inRange: false, isStart: false, isEnd: false };
|
|
201
|
-
|
|
202
|
-
const end = rangeTo ?? hoverDate;
|
|
203
|
-
if (!end) return { inRange: false, isStart: isSameDay(date, rangeFrom), isEnd: false };
|
|
204
|
-
|
|
205
|
-
let [rStart, rEnd] = rangeFrom <= end ? [rangeFrom, end] : [end, rangeFrom];
|
|
206
|
-
rStart = toDateOnly(rStart);
|
|
207
|
-
rEnd = toDateOnly(rEnd);
|
|
208
|
-
const d = toDateOnly(date);
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
inRange: d >= rStart && d <= rEnd,
|
|
212
|
-
isStart: isSameDay(d, rStart),
|
|
213
|
-
isEnd: isSameDay(d, rEnd),
|
|
214
|
-
};
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const monthLabel = `${year}년 ${month + 1}월`;
|
|
218
|
-
|
|
219
|
-
return (
|
|
220
|
-
<div className="sh-ui-calendar" role="group" aria-label={monthLabel}>
|
|
221
|
-
<div className="sh-ui-calendar__header">
|
|
222
|
-
<button
|
|
223
|
-
type="button"
|
|
224
|
-
className="sh-ui-calendar__nav"
|
|
225
|
-
onClick={() => navigate(year, month - 1)}
|
|
226
|
-
aria-label="이전 달"
|
|
227
|
-
>
|
|
228
|
-
<ChevronLeftIcon />
|
|
229
|
-
</button>
|
|
230
|
-
<span className="sh-ui-calendar__title">{monthLabel}</span>
|
|
231
|
-
<button
|
|
232
|
-
type="button"
|
|
233
|
-
className="sh-ui-calendar__nav"
|
|
234
|
-
onClick={() => navigate(year, month + 1)}
|
|
235
|
-
aria-label="다음 달"
|
|
236
|
-
>
|
|
237
|
-
<ChevronRightIcon />
|
|
238
|
-
</button>
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
<div className="sh-ui-calendar__weekdays" role="row">
|
|
242
|
-
{WEEK_LABELS.map((label) => (
|
|
243
|
-
<span key={label} className="sh-ui-calendar__weekday" role="columnheader" aria-label={label}>
|
|
244
|
-
{label}
|
|
245
|
-
</span>
|
|
246
|
-
))}
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
|
-
<div
|
|
250
|
-
className="sh-ui-calendar__grid"
|
|
251
|
-
role="grid"
|
|
252
|
-
tabIndex={0}
|
|
253
|
-
onKeyDown={handleKeyDown}
|
|
254
|
-
aria-label={monthLabel}
|
|
255
|
-
>
|
|
256
|
-
{cells.map(({ date, current }, i) => {
|
|
257
|
-
const disabled = isDisabled(date);
|
|
258
|
-
const isSelected = selected && isSameDay(date, selected);
|
|
259
|
-
const isToday = isSameDay(date, today);
|
|
260
|
-
const { inRange, isStart, isEnd } = getRangeState(date);
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<button
|
|
264
|
-
key={i}
|
|
265
|
-
type="button"
|
|
266
|
-
className={cx(
|
|
267
|
-
"sh-ui-calendar__day",
|
|
268
|
-
!current && "sh-ui-calendar__day--outside",
|
|
269
|
-
isSelected && "sh-ui-calendar__day--selected",
|
|
270
|
-
isToday && "sh-ui-calendar__day--today",
|
|
271
|
-
inRange && "sh-ui-calendar__day--in-range",
|
|
272
|
-
isStart && "sh-ui-calendar__day--range-start",
|
|
273
|
-
isEnd && "sh-ui-calendar__day--range-end",
|
|
274
|
-
)}
|
|
275
|
-
disabled={disabled}
|
|
276
|
-
tabIndex={-1}
|
|
277
|
-
onClick={() => { if (!disabled) onSelect(date); }}
|
|
278
|
-
onMouseEnter={() => onHoverDate?.(date)}
|
|
279
|
-
onMouseLeave={() => onHoverDate?.(undefined)}
|
|
280
|
-
aria-label={formatDefault(date)}
|
|
281
|
-
aria-selected={isSelected || inRange || undefined}
|
|
282
|
-
data-today={isToday || undefined}
|
|
283
|
-
>
|
|
284
|
-
{date.getDate()}
|
|
285
|
-
</button>
|
|
286
|
-
);
|
|
287
|
-
})}
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
62
|
/* ───────── DatePicker Root ───────── */
|
|
294
63
|
|
|
295
64
|
export interface DatePickerProps {
|
|
@@ -372,7 +141,7 @@ export function DatePicker({
|
|
|
372
141
|
|
|
373
142
|
React.useEffect(() => {
|
|
374
143
|
if (open && selected) {
|
|
375
|
-
setFocusedDate(
|
|
144
|
+
setFocusedDate(startOfMonth(selected));
|
|
376
145
|
}
|
|
377
146
|
}, [open, selected]);
|
|
378
147
|
|
|
@@ -574,19 +343,20 @@ export const DatePickerContent = React.forwardRef<HTMLDivElement, DatePickerCont
|
|
|
574
343
|
export function DatePickerCalendar() {
|
|
575
344
|
const ctx = useDatePickerContext("DatePickerCalendar");
|
|
576
345
|
|
|
577
|
-
const handleSelect = (date: Date) => {
|
|
346
|
+
const handleSelect = (date: Date | undefined) => {
|
|
578
347
|
ctx.setSelected(date);
|
|
579
|
-
if (ctx.closeOnSelect) ctx.setOpen(false);
|
|
348
|
+
if (date && ctx.closeOnSelect) ctx.setOpen(false);
|
|
580
349
|
};
|
|
581
350
|
|
|
582
351
|
return (
|
|
583
352
|
<Calendar
|
|
584
|
-
|
|
585
|
-
|
|
353
|
+
mode="single"
|
|
354
|
+
value={ctx.selected}
|
|
355
|
+
onValueChange={handleSelect}
|
|
356
|
+
month={ctx.focusedDate}
|
|
357
|
+
onMonthChange={ctx.setFocusedDate}
|
|
586
358
|
min={ctx.min}
|
|
587
359
|
max={ctx.max}
|
|
588
|
-
focusedDate={ctx.focusedDate}
|
|
589
|
-
onFocusedDateChange={ctx.setFocusedDate}
|
|
590
360
|
/>
|
|
591
361
|
);
|
|
592
362
|
}
|
|
@@ -684,36 +454,20 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
684
454
|
const selected = isControlled ? value : internal;
|
|
685
455
|
|
|
686
456
|
const [open, setOpen] = React.useState(false);
|
|
687
|
-
const [
|
|
688
|
-
const [hoverDate, setHoverDate] = React.useState<Date | undefined>(undefined);
|
|
689
|
-
const [focusedDate, setFocusedDate] = React.useState(
|
|
457
|
+
const [calendarMonth, setCalendarMonth] = React.useState<Date>(
|
|
690
458
|
() => selected?.from ?? new Date(),
|
|
691
459
|
);
|
|
692
460
|
|
|
693
461
|
React.useEffect(() => {
|
|
694
|
-
if (open) {
|
|
695
|
-
|
|
696
|
-
setHoverDate(undefined);
|
|
697
|
-
if (selected?.from) {
|
|
698
|
-
setFocusedDate(new Date(selected.from.getFullYear(), selected.from.getMonth(), 1));
|
|
699
|
-
}
|
|
462
|
+
if (open && selected?.from) {
|
|
463
|
+
setCalendarMonth(startOfMonth(selected.from));
|
|
700
464
|
}
|
|
701
465
|
}, [open, selected?.from]);
|
|
702
466
|
|
|
703
|
-
const
|
|
704
|
-
if (!picking) {
|
|
705
|
-
setPicking(date);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const [from, to] = picking <= date ? [picking, date] : [date, picking];
|
|
710
|
-
const range: DateRange = { from, to };
|
|
711
|
-
|
|
467
|
+
const handleRangeChange = (range: DateRange | undefined) => {
|
|
712
468
|
if (!isControlled) setInternal(range);
|
|
713
469
|
onValueChange?.(range);
|
|
714
|
-
|
|
715
|
-
setHoverDate(undefined);
|
|
716
|
-
setOpen(false);
|
|
470
|
+
if (range) setOpen(false);
|
|
717
471
|
};
|
|
718
472
|
|
|
719
473
|
const displayText = selected
|
|
@@ -749,20 +503,14 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
749
503
|
align="start"
|
|
750
504
|
>
|
|
751
505
|
<BasePopover.Popup className="sh-ui-date-picker__popup">
|
|
752
|
-
{picking && (
|
|
753
|
-
<p className="sh-ui-date-picker__hint">종료일을 선택하세요</p>
|
|
754
|
-
)}
|
|
755
506
|
<Calendar
|
|
756
|
-
|
|
757
|
-
|
|
507
|
+
mode="range"
|
|
508
|
+
value={selected}
|
|
509
|
+
onValueChange={handleRangeChange}
|
|
510
|
+
month={calendarMonth}
|
|
511
|
+
onMonthChange={setCalendarMonth}
|
|
758
512
|
min={min}
|
|
759
513
|
max={max}
|
|
760
|
-
focusedDate={focusedDate}
|
|
761
|
-
onFocusedDateChange={setFocusedDate}
|
|
762
|
-
rangeFrom={picking ?? selected?.from}
|
|
763
|
-
rangeTo={picking ? undefined : selected?.to}
|
|
764
|
-
hoverDate={picking ? hoverDate : undefined}
|
|
765
|
-
onHoverDate={picking ? setHoverDate : undefined}
|
|
766
514
|
/>
|
|
767
515
|
</BasePopover.Popup>
|
|
768
516
|
</BasePopover.Positioner>
|