podo-ui 1.1.15 → 1.2.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/cdn/podo-datepicker.css +14 -1
- package/cdn/podo-datepicker.js +82 -11
- package/cdn/podo-datepicker.min.css +2 -2
- package/cdn/podo-datepicker.min.js +2 -2
- package/cdn/podo-ui.css +1 -1
- package/cdn/podo-ui.min.css +1 -1
- package/dist/react/molecule/datepicker.d.ts +27 -2
- package/dist/react/molecule/datepicker.d.ts.map +1 -1
- package/dist/react/molecule/datepicker.js +62 -6
- package/dist/react/molecule/datepicker.module.scss +14 -0
- package/dist/svelte/molecule/DatePicker.svelte +109 -7
- package/dist/svelte/molecule/DatePicker.svelte.d.ts +13 -3
- package/package.json +1 -1
- package/public/ai/components/datepicker.json +27 -3
- package/public/ai.json +1 -1
- package/vanilla/datepicker.css +13 -0
- package/vanilla/datepicker.js +81 -10
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type DatePickerMode = 'instant' | 'period';
|
|
2
|
-
export type DatePickerType = 'date' | 'time' | 'datetime';
|
|
2
|
+
export type DatePickerType = 'date' | 'time' | 'datetime' | 'hour';
|
|
3
|
+
/** 시(hour) 표시 포맷: 24시간제 | 12시간제(오전/오후) */
|
|
4
|
+
export type HourFormat = '24' | '12';
|
|
5
|
+
/** 시(hour) 선택 간격 */
|
|
6
|
+
export type HourStep = 1 | 2 | 3 | 4 | 6 | 12;
|
|
3
7
|
/** 시간 값 (시, 분) */
|
|
4
8
|
export interface TimeValue {
|
|
5
9
|
hour: number;
|
|
@@ -73,12 +77,33 @@ export interface DatePickerProps {
|
|
|
73
77
|
minDate?: Date | DateTimeLimit;
|
|
74
78
|
/** 선택 가능한 최대 날짜 (Date 또는 { date, time }) */
|
|
75
79
|
maxDate?: Date | DateTimeLimit;
|
|
76
|
-
/** 분 단위 선택 간격 (1, 5, 10, 15, 30) 기본값: 1
|
|
80
|
+
/** 분 단위 선택 간격 (1, 5, 10, 15, 30) 기본값: 1
|
|
81
|
+
* type='hour'일 때는 무시됨 (분 컬럼이 렌더링되지 않음)
|
|
82
|
+
*/
|
|
77
83
|
minuteStep?: MinuteStep;
|
|
84
|
+
/**
|
|
85
|
+
* 시(hour) 표시 포맷 (type='hour'일 때만 유효)
|
|
86
|
+
* '24': 0~23시 (기본), '12': 오전/오후 1~12시
|
|
87
|
+
*/
|
|
88
|
+
hourFormat?: HourFormat;
|
|
89
|
+
/**
|
|
90
|
+
* 선택 불가능한 시간 배열 (0~23, type='hour'일 때만 유효)
|
|
91
|
+
* hourFormat과 무관하게 항상 24시 기준 값
|
|
92
|
+
* 예: [0,1,2,3,4,5,22,23] → 새벽/심야 비활성
|
|
93
|
+
*/
|
|
94
|
+
disabledHours?: number[];
|
|
95
|
+
/**
|
|
96
|
+
* 시(hour) 선택 간격 (type='hour'일 때만 유효)
|
|
97
|
+
* 예: hourStep=2 → 0,2,4,...,22 만 선택 가능
|
|
98
|
+
* 기본값: 1
|
|
99
|
+
*/
|
|
100
|
+
hourStep?: HourStep;
|
|
78
101
|
/**
|
|
79
102
|
* 날짜/시간 표시 포맷
|
|
80
103
|
* y: 년, m: 월, d: 일, h: 시, i: 분
|
|
81
104
|
* 예시: "y-m-d", "y.m.d h:i", "y년 m월 d일 h시 i분"
|
|
105
|
+
* type='hour'에서는 이 prop이 무시되고, hourFormat에 따른
|
|
106
|
+
* 기본 라벨("h시" 또는 "오전/오후 h시")로 표시됨
|
|
82
107
|
*/
|
|
83
108
|
format?: string;
|
|
84
109
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"datepicker.d.ts","sourceRoot":"","sources":["../../../react/molecule/datepicker.tsx"],"names":[],"mappings":"AAOA,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAClD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"datepicker.d.ts","sourceRoot":"","sources":["../../../react/molecule/datepicker.tsx"],"names":[],"mappings":"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"}
|
|
@@ -475,7 +475,7 @@ const PeriodCalendar = ({ value, endValue, onSelect, viewDate, endViewDate, onVi
|
|
|
475
475
|
return (_jsxs("div", { className: styles.periodCalendars, children: [_jsx("div", { className: styles.periodCalendarLeft, children: _jsx(Calendar, { value: value, endValue: endValue, mode: "period", onSelect: onSelect, viewDate: viewDate, onViewDateChange: onViewDateChange, showPrevNav: true, showNextNav: true, maxViewDate: isMobile ? undefined : endViewDate, disable: disable, enable: enable, minDate: minDate, maxDate: maxDate, yearRange: yearRange }) }), _jsx("div", { className: styles.periodCalendarRight, children: _jsx(Calendar, { value: value, endValue: endValue, mode: "period", onSelect: onSelect, viewDate: endViewDate, onViewDateChange: onEndViewDateChange, showPrevNav: true, showNextNav: true, minViewDate: viewDate, disable: disable, enable: enable, minDate: minDate, maxDate: maxDate, yearRange: yearRange }) })] }));
|
|
476
476
|
};
|
|
477
477
|
// Main DatePicker Component
|
|
478
|
-
const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placeholder, disabled = false, showActions, align = 'left', className, disable, enable, minDate, maxDate, minuteStep = 1, format, initialCalendar, yearRange, portal = false, quickSelect = false, hideNavArrow = false, direction = 'down', onReset, }) => {
|
|
478
|
+
const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placeholder, disabled = false, showActions, align = 'left', className, disable, enable, minDate, maxDate, minuteStep = 1, hourFormat = '24', disabledHours, hourStep = 1, format, initialCalendar, yearRange, portal = false, quickSelect = false, hideNavArrow = false, direction = 'down', onReset, }) => {
|
|
479
479
|
const [selectingPart, setSelectingPart] = useState(null);
|
|
480
480
|
const [tempValue, setTempValue] = useState(value || {});
|
|
481
481
|
const [navigationStep, setNavigationStep] = useState(() => {
|
|
@@ -526,7 +526,8 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
526
526
|
const [dropdownMaxWidth, setDropdownMaxWidth] = useState(null);
|
|
527
527
|
// 실제 드롭다운 열림 방향 ('up' | 'down'), direction='auto'일 때만 동적으로 변경
|
|
528
528
|
const [resolvedDirection, setResolvedDirection] = useState(direction === 'up' ? 'up' : 'down');
|
|
529
|
-
|
|
529
|
+
// hour-only는 native select 단일 입력이라 적용/초기화 액션이 필요 없음 → 즉시 commit
|
|
530
|
+
const shouldShowActions = showActions ?? (mode === 'period' && type !== 'hour');
|
|
530
531
|
// 날짜 선택 시에만 드롭다운 표시 (시/분은 native select 사용)
|
|
531
532
|
const isOpen = selectingPart === 'date' || selectingPart === 'endDate';
|
|
532
533
|
// 드롭다운 max-width 계산 (드롭다운 위치 기준으로 남은 뷰포트 너비)
|
|
@@ -671,6 +672,14 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
671
672
|
}
|
|
672
673
|
}, [value?.date, value?.endDate, quickSelect, mode]);
|
|
673
674
|
const formatPeriodText = () => {
|
|
675
|
+
// hour-only: date 없이 time 만으로 표시
|
|
676
|
+
if (type === 'hour') {
|
|
677
|
+
const startText = tempValue.time ? formatHourLabel(tempValue.time.hour) : '';
|
|
678
|
+
const endText = tempValue.endTime ? formatHourLabel(tempValue.endTime.hour) : '';
|
|
679
|
+
if (startText && endText)
|
|
680
|
+
return `${startText} ~ ${endText}`;
|
|
681
|
+
return startText;
|
|
682
|
+
}
|
|
674
683
|
if (!tempValue.date)
|
|
675
684
|
return '';
|
|
676
685
|
// format prop이 있으면 사용
|
|
@@ -957,16 +966,40 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
957
966
|
: `${date.getFullYear()} - ${String(date.getMonth() + 1).padStart(2, '0')} - ${String(date.getDate()).padStart(2, '0')}`;
|
|
958
967
|
return (_jsx("button", { type: "button", className: `${styles.inputPart} ${isActive ? styles.active : ''}`, onClick: onClick, children: displayText }));
|
|
959
968
|
};
|
|
969
|
+
// hour-only 모드: 24시간 기준 hour 값을 표시 라벨(한국어/12h) 로 변환
|
|
970
|
+
const formatHourLabel = (h) => {
|
|
971
|
+
if (hourFormat === '12') {
|
|
972
|
+
// 0시 → 오전 12시, 1~11시 → 오전 N시, 12시 → 오후 12시, 13~23시 → 오후 (N-12)시
|
|
973
|
+
const period = h < 12 ? '오전' : '오후';
|
|
974
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
975
|
+
return `${period} ${h12}시`;
|
|
976
|
+
}
|
|
977
|
+
return `${h}시`;
|
|
978
|
+
};
|
|
979
|
+
// hour-only 옵션 리스트 생성 (hourStep 적용)
|
|
980
|
+
const buildHourOptions = () => {
|
|
981
|
+
const step = hourStep ?? 1;
|
|
982
|
+
const list = [];
|
|
983
|
+
for (let h = 0; h < 24; h += step)
|
|
984
|
+
list.push(h);
|
|
985
|
+
return list;
|
|
986
|
+
};
|
|
960
987
|
// Helper to render hour select
|
|
961
988
|
const renderHourSelect = (time, part, isPlaceholder) => {
|
|
989
|
+
const isHourOnly = type === 'hour';
|
|
962
990
|
const hour = time?.hour ?? 0;
|
|
963
|
-
const hours =
|
|
991
|
+
const hours = isHourOnly
|
|
992
|
+
? buildHourOptions()
|
|
993
|
+
: Array.from({ length: 24 }, (_, i) => i);
|
|
964
994
|
const isEnd = part === 'endHour';
|
|
965
995
|
const currentDate = isEnd ? tempValue.endDate : tempValue.date;
|
|
966
996
|
// minDate/maxDate 시간 제한 계산
|
|
967
997
|
const minLimit = minDate ? extractDateTimeLimit(minDate) : null;
|
|
968
998
|
const maxLimit = maxDate ? extractDateTimeLimit(maxDate) : null;
|
|
969
999
|
const isHourDisabled = (h) => {
|
|
1000
|
+
// hour-only: disabledHours 우선 적용
|
|
1001
|
+
if (isHourOnly && disabledHours && disabledHours.includes(h))
|
|
1002
|
+
return true;
|
|
970
1003
|
if (!currentDate)
|
|
971
1004
|
return false;
|
|
972
1005
|
// minDate와 같은 날짜인 경우
|
|
@@ -983,6 +1016,18 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
983
1016
|
};
|
|
984
1017
|
const handleChange = (e) => {
|
|
985
1018
|
const selectedHour = parseInt(e.target.value, 10);
|
|
1019
|
+
// hour-only: 분은 항상 0으로 정규화, minuteStep/min,maxDate time 보정 모두 무시
|
|
1020
|
+
if (isHourOnly) {
|
|
1021
|
+
const newTime = { hour: selectedHour, minute: 0 };
|
|
1022
|
+
const newValue = isEnd
|
|
1023
|
+
? { ...tempValue, endTime: newTime }
|
|
1024
|
+
: { ...tempValue, time: newTime };
|
|
1025
|
+
setTempValue(newValue);
|
|
1026
|
+
if (!shouldShowActions) {
|
|
1027
|
+
onChange?.(newValue);
|
|
1028
|
+
}
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
986
1031
|
const currentTime = isEnd ? tempValue.endTime : tempValue.time;
|
|
987
1032
|
let newMinute = currentTime?.minute ?? 0;
|
|
988
1033
|
// 시간 변경 시 분이 범위를 벗어나면 자동 보정
|
|
@@ -1015,7 +1060,11 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
1015
1060
|
onChange?.(newValue);
|
|
1016
1061
|
}
|
|
1017
1062
|
};
|
|
1018
|
-
|
|
1063
|
+
// hour-only: 현재 값이 옵션 리스트에 없으면 가장 가까운 옵션으로 표시
|
|
1064
|
+
const displayHour = isHourOnly && !hours.includes(hour)
|
|
1065
|
+
? (hours.reduce((prev, curr) => Math.abs(curr - hour) < Math.abs(prev - hour) ? curr : prev, hours[0] ?? 0))
|
|
1066
|
+
: hour;
|
|
1067
|
+
return (_jsx("select", { className: `${styles.timeSelect} ${isHourOnly ? styles.hourSelect : ''} ${isPlaceholder ? styles.placeholder : ''}`, value: displayHour, onChange: handleChange, disabled: disabled, "aria-label": isHourOnly ? '시간 선택' : undefined, children: hours.map((h) => (_jsx("option", { value: h, disabled: isHourDisabled(h), children: isHourOnly ? formatHourLabel(h) : String(h).padStart(2, '0') }, h))) }));
|
|
1019
1068
|
};
|
|
1020
1069
|
// Helper to render minute select
|
|
1021
1070
|
const renderMinuteSelect = (time, part, isPlaceholder) => {
|
|
@@ -1092,6 +1141,13 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
1092
1141
|
}
|
|
1093
1142
|
return (_jsx("div", { className: styles.inputContent, children: _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.time, 'hour', !hasStartTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.time, 'minute', !hasStartTime)] }) }));
|
|
1094
1143
|
}
|
|
1144
|
+
if (type === 'hour') {
|
|
1145
|
+
// hour-only 모드: 분 컬럼/구분자 제거, 시 컬럼만 렌더
|
|
1146
|
+
if (mode === 'period') {
|
|
1147
|
+
return (_jsxs("div", { className: styles.inputContent, children: [_jsx("div", { className: `${styles.timeSection} ${styles.hourSection}`, children: renderHourSelect(displayValue?.time, 'hour', !hasStartTime) }), _jsx("span", { className: styles.separator, children: "~" }), _jsx("div", { className: `${styles.timeSection} ${styles.hourSection}`, children: renderHourSelect(displayValue?.endTime, 'endHour', !hasEndTime) })] }));
|
|
1148
|
+
}
|
|
1149
|
+
return (_jsx("div", { className: styles.inputContent, children: _jsx("div", { className: `${styles.timeSection} ${styles.hourSection}`, children: renderHourSelect(displayValue?.time, 'hour', !hasStartTime) }) }));
|
|
1150
|
+
}
|
|
1095
1151
|
// datetime
|
|
1096
1152
|
if (mode === 'period') {
|
|
1097
1153
|
return (_jsxs("div", { className: styles.inputContent, children: [renderDateButton(displayValue?.date, selectingPart === 'date', () => handlePartClick('date'), !hasStartValue), _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.time, 'hour', !hasStartTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.time, 'minute', !hasStartTime)] }), _jsx("span", { className: styles.separator, children: "~" }), renderDateButton(displayValue?.endDate, selectingPart === 'endDate', () => handlePartClick('endDate'), !hasEndValue), _jsxs("div", { className: styles.timeSection, children: [renderHourSelect(displayValue?.endTime, 'endHour', !hasEndTime), _jsx("span", { className: styles.timeSeparator, children: ":" }), renderMinuteSelect(displayValue?.endTime, 'endMinute', !hasEndTime)] })] }));
|
|
@@ -1120,8 +1176,8 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
1120
1176
|
}
|
|
1121
1177
|
return null;
|
|
1122
1178
|
};
|
|
1123
|
-
// 아이콘 결정 (time 타입은 icon-time, 나머지는 icon-calendar)
|
|
1124
|
-
const inputIcon = type === 'time' ? 'icon-time' : 'icon-calendar';
|
|
1179
|
+
// 아이콘 결정 (time/hour 타입은 icon-time, 나머지는 icon-calendar)
|
|
1180
|
+
const inputIcon = type === 'time' || type === 'hour' ? 'icon-time' : 'icon-calendar';
|
|
1125
1181
|
// 드롭다운 콘텐츠 렌더링
|
|
1126
1182
|
const renderDropdown = () => {
|
|
1127
1183
|
const dropdownContent = (_jsxs(_Fragment, { children: [renderDropdownContent(), shouldShowActions && (_jsxs("div", { className: styles.bottomActions, children: [_jsx("span", { className: styles.periodText, children: mode === 'period' && tempValue.date ? formatPeriodText() : '' }), _jsxs("div", { className: styles.actionButtons, children: [_jsxs("button", { type: "button", className: `${styles.actionButton} ${styles.reset}`, onClick: handleReset, children: [_jsx("i", { className: "icon-refresh" }), "\uCD08\uAE30\uD654"] }), _jsx("button", { type: "button", className: `${styles.actionButton} ${styles.apply}`, onClick: handleApply, children: "\uC801\uC6A9" })] })] }))] }));
|
|
@@ -130,6 +130,12 @@
|
|
|
130
130
|
gap: 0;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// hour-only 모드: 분 컬럼이 제거되므로 단일 select가 충분한 너비 확보
|
|
134
|
+
.hourSection {
|
|
135
|
+
flex: 1;
|
|
136
|
+
min-width: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
133
139
|
.timeSelect {
|
|
134
140
|
@include p4;
|
|
135
141
|
appearance: none;
|
|
@@ -166,6 +172,14 @@
|
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
// hour-only select: 한국어 라벨("3시"/"오전 3시")을 위한 너비 확보
|
|
176
|
+
.hourSelect {
|
|
177
|
+
// 12h 라벨 "오전 12시" 폭 + 여백 정도
|
|
178
|
+
min-width: 72px;
|
|
179
|
+
text-align: center;
|
|
180
|
+
text-align-last: center;
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
.timeSeparator {
|
|
170
184
|
@include p4;
|
|
171
185
|
color: color('text-body');
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
// Types
|
|
6
6
|
export type DatePickerMode = 'instant' | 'period';
|
|
7
|
-
export type DatePickerType = 'date' | 'time' | 'datetime';
|
|
7
|
+
export type DatePickerType = 'date' | 'time' | 'datetime' | 'hour';
|
|
8
|
+
|
|
9
|
+
/** 시(hour) 표시 포맷: 24시간제 | 12시간제(오전/오후) */
|
|
10
|
+
export type HourFormat = '24' | '12';
|
|
11
|
+
|
|
12
|
+
/** 시(hour) 선택 간격 */
|
|
13
|
+
export type HourStep = 1 | 2 | 3 | 4 | 6 | 12;
|
|
8
14
|
|
|
9
15
|
export interface TimeValue {
|
|
10
16
|
hour: number;
|
|
@@ -194,9 +200,15 @@
|
|
|
194
200
|
minDate?: Date | DateTimeLimit;
|
|
195
201
|
/** Maximum selectable date */
|
|
196
202
|
maxDate?: Date | DateTimeLimit;
|
|
197
|
-
/** Minute selection step */
|
|
203
|
+
/** Minute selection step. Ignored when type='hour'. */
|
|
198
204
|
minuteStep?: MinuteStep;
|
|
199
|
-
/**
|
|
205
|
+
/** Hour display format (type='hour' only). '24': 0~23시, '12': 오전/오후 표기. Default '24'. */
|
|
206
|
+
hourFormat?: HourFormat;
|
|
207
|
+
/** Hours (0-23) that cannot be selected (type='hour' only). Always 24h reference. */
|
|
208
|
+
disabledHours?: number[];
|
|
209
|
+
/** Step interval for hour options (type='hour' only). Default 1. */
|
|
210
|
+
hourStep?: HourStep;
|
|
211
|
+
/** Date/time format pattern. Ignored when type='hour'. */
|
|
200
212
|
format?: string;
|
|
201
213
|
/** Initial calendar display month for period mode */
|
|
202
214
|
initialCalendar?: InitialCalendar;
|
|
@@ -227,6 +239,9 @@
|
|
|
227
239
|
minDate,
|
|
228
240
|
maxDate,
|
|
229
241
|
minuteStep = 1,
|
|
242
|
+
hourFormat = '24',
|
|
243
|
+
disabledHours,
|
|
244
|
+
hourStep = 1,
|
|
230
245
|
format,
|
|
231
246
|
initialCalendar,
|
|
232
247
|
yearRange,
|
|
@@ -255,14 +270,33 @@
|
|
|
255
270
|
const today = new Date();
|
|
256
271
|
|
|
257
272
|
// Computed
|
|
258
|
-
|
|
273
|
+
// hour-only는 native select 단일 입력이라 적용/초기화 액션이 필요 없음 → 즉시 commit
|
|
274
|
+
const shouldShowActions = $derived(showActions ?? (mode === 'period' && type !== 'hour'));
|
|
259
275
|
const isOpen = $derived(selectingPart === 'date' || selectingPart === 'endDate');
|
|
260
276
|
const displayValue = $derived(shouldShowActions ? tempValue : value);
|
|
261
277
|
const hasStartValue = $derived(!!displayValue?.date);
|
|
262
278
|
const hasEndValue = $derived(!!displayValue?.endDate);
|
|
263
|
-
const inputIcon = $derived(type === 'time' ? 'icon-time' : 'icon-calendar');
|
|
279
|
+
const inputIcon = $derived(type === 'time' || type === 'hour' ? 'icon-time' : 'icon-calendar');
|
|
264
280
|
const showNavigation = $derived(quickSelect && mode === 'period' && !hideNavArrow);
|
|
265
281
|
|
|
282
|
+
// hour-only 라벨 (24h="3시", 12h="오전 3시")
|
|
283
|
+
const formatHourLabel = (h: number): string => {
|
|
284
|
+
if (hourFormat === '12') {
|
|
285
|
+
const period = h < 12 ? '오전' : '오후';
|
|
286
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
287
|
+
return `${period} ${h12}시`;
|
|
288
|
+
}
|
|
289
|
+
return `${h}시`;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// hour-only 옵션 리스트 (hourStep 적용)
|
|
293
|
+
const buildHourOptions = (): number[] => {
|
|
294
|
+
const step = hourStep ?? 1;
|
|
295
|
+
const list: number[] = [];
|
|
296
|
+
for (let h = 0; h < 24; h += step) list.push(h);
|
|
297
|
+
return list;
|
|
298
|
+
};
|
|
299
|
+
|
|
266
300
|
// 드롭다운 방향 결정 (direction='auto'일 때 공간 측정)
|
|
267
301
|
let resolvedDirection = $state<'up' | 'down'>(direction === 'up' ? 'up' : 'down');
|
|
268
302
|
$effect(() => {
|
|
@@ -533,6 +567,14 @@
|
|
|
533
567
|
|
|
534
568
|
// Format display
|
|
535
569
|
const formatPeriodText = () => {
|
|
570
|
+
// hour-only: date 없이 time 만으로 표시
|
|
571
|
+
if (type === 'hour') {
|
|
572
|
+
const startText = tempValue.time ? formatHourLabel(tempValue.time.hour) : '';
|
|
573
|
+
const endText = tempValue.endTime ? formatHourLabel(tempValue.endTime.hour) : '';
|
|
574
|
+
if (startText && endText) return `${startText} ~ ${endText}`;
|
|
575
|
+
return startText;
|
|
576
|
+
}
|
|
577
|
+
|
|
536
578
|
if (!tempValue.date) return '';
|
|
537
579
|
|
|
538
580
|
if (format) {
|
|
@@ -847,7 +889,10 @@
|
|
|
847
889
|
const target = e.target as HTMLSelectElement;
|
|
848
890
|
const hour = parseInt(target.value);
|
|
849
891
|
const currentTime = isEnd ? tempValue.endTime : tempValue.time;
|
|
850
|
-
|
|
892
|
+
// hour-only: 분을 0으로 정규화
|
|
893
|
+
const newTime: TimeValue = type === 'hour'
|
|
894
|
+
? { hour, minute: 0 }
|
|
895
|
+
: { hour, minute: currentTime?.minute ?? 0 };
|
|
851
896
|
|
|
852
897
|
const newValue = isEnd
|
|
853
898
|
? { ...tempValue, endTime: newTime }
|
|
@@ -876,9 +921,22 @@
|
|
|
876
921
|
};
|
|
877
922
|
|
|
878
923
|
// Time select options
|
|
879
|
-
|
|
924
|
+
// type='hour'면 hourStep 기반 옵션, 아니면 0~23 전체
|
|
925
|
+
const hours = $derived<number[]>(type === 'hour' ? buildHourOptions() : Array.from({ length: 24 }, (_, i) => i));
|
|
880
926
|
const minutes = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep);
|
|
881
927
|
|
|
928
|
+
// hour-only: 옵션에 비활성 여부 (disabledHours + min/maxDate time)
|
|
929
|
+
const isHourOptionDisabled = (h: number, isEnd = false): boolean => {
|
|
930
|
+
if (type === 'hour' && disabledHours && disabledHours.includes(h)) return true;
|
|
931
|
+
const currentDate = isEnd ? tempValue.endDate : tempValue.date;
|
|
932
|
+
if (!currentDate) return false;
|
|
933
|
+
const minLimit = minDate ? (minDate instanceof Date ? { date: minDate } : minDate) : null;
|
|
934
|
+
const maxLimit = maxDate ? (maxDate instanceof Date ? { date: maxDate } : maxDate) : null;
|
|
935
|
+
if (minLimit?.time && isSameDay(currentDate, minLimit.date) && h < minLimit.time.hour) return true;
|
|
936
|
+
if (maxLimit?.time && isSameDay(currentDate, maxLimit.date) && h > maxLimit.time.hour) return true;
|
|
937
|
+
return false;
|
|
938
|
+
};
|
|
939
|
+
|
|
882
940
|
// Calendar cell class computation
|
|
883
941
|
const getCellClass = (dayInfo: { day: number; date: Date; isOther: boolean; isDisabled: boolean }) => {
|
|
884
942
|
const { date, isOther, isDisabled } = dayInfo;
|
|
@@ -997,6 +1055,50 @@
|
|
|
997
1055
|
{formatDateDisplay(displayValue?.date)}
|
|
998
1056
|
</button>
|
|
999
1057
|
{/if}
|
|
1058
|
+
{:else if type === 'hour'}
|
|
1059
|
+
{#if mode === 'period'}
|
|
1060
|
+
<div class="{styles.timeSection} {styles.hourSection}">
|
|
1061
|
+
<select
|
|
1062
|
+
class="{styles.timeSelect} {styles.hourSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
1063
|
+
value={displayValue?.time?.hour ?? 0}
|
|
1064
|
+
onchange={(e) => handleHourChange(e, false)}
|
|
1065
|
+
{disabled}
|
|
1066
|
+
aria-label="시간 선택"
|
|
1067
|
+
>
|
|
1068
|
+
{#each hours as h}
|
|
1069
|
+
<option value={h} disabled={isHourOptionDisabled(h, false)}>{formatHourLabel(h)}</option>
|
|
1070
|
+
{/each}
|
|
1071
|
+
</select>
|
|
1072
|
+
</div>
|
|
1073
|
+
<span class={styles.separator}>~</span>
|
|
1074
|
+
<div class="{styles.timeSection} {styles.hourSection}">
|
|
1075
|
+
<select
|
|
1076
|
+
class="{styles.timeSelect} {styles.hourSelect} {!displayValue?.endTime ? styles.placeholder : ''}"
|
|
1077
|
+
value={displayValue?.endTime?.hour ?? 0}
|
|
1078
|
+
onchange={(e) => handleHourChange(e, true)}
|
|
1079
|
+
{disabled}
|
|
1080
|
+
aria-label="종료 시간 선택"
|
|
1081
|
+
>
|
|
1082
|
+
{#each hours as h}
|
|
1083
|
+
<option value={h} disabled={isHourOptionDisabled(h, true)}>{formatHourLabel(h)}</option>
|
|
1084
|
+
{/each}
|
|
1085
|
+
</select>
|
|
1086
|
+
</div>
|
|
1087
|
+
{:else}
|
|
1088
|
+
<div class="{styles.timeSection} {styles.hourSection}">
|
|
1089
|
+
<select
|
|
1090
|
+
class="{styles.timeSelect} {styles.hourSelect} {!displayValue?.time ? styles.placeholder : ''}"
|
|
1091
|
+
value={displayValue?.time?.hour ?? 0}
|
|
1092
|
+
onchange={(e) => handleHourChange(e, false)}
|
|
1093
|
+
{disabled}
|
|
1094
|
+
aria-label="시간 선택"
|
|
1095
|
+
>
|
|
1096
|
+
{#each hours as h}
|
|
1097
|
+
<option value={h} disabled={isHourOptionDisabled(h, false)}>{formatHourLabel(h)}</option>
|
|
1098
|
+
{/each}
|
|
1099
|
+
</select>
|
|
1100
|
+
</div>
|
|
1101
|
+
{/if}
|
|
1000
1102
|
{:else if type === 'time'}
|
|
1001
1103
|
{#if mode === 'period'}
|
|
1002
1104
|
<div class={styles.timeSection}>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type DatePickerMode = 'instant' | 'period';
|
|
2
|
-
export type DatePickerType = 'date' | 'time' | 'datetime';
|
|
2
|
+
export type DatePickerType = 'date' | 'time' | 'datetime' | 'hour';
|
|
3
|
+
/** 시(hour) 표시 포맷: 24시간제 | 12시간제(오전/오후) */
|
|
4
|
+
export type HourFormat = '24' | '12';
|
|
5
|
+
/** 시(hour) 선택 간격 */
|
|
6
|
+
export type HourStep = 1 | 2 | 3 | 4 | 6 | 12;
|
|
3
7
|
export interface TimeValue {
|
|
4
8
|
hour: number;
|
|
5
9
|
minute: number;
|
|
@@ -58,9 +62,15 @@ interface Props {
|
|
|
58
62
|
minDate?: Date | DateTimeLimit;
|
|
59
63
|
/** Maximum selectable date */
|
|
60
64
|
maxDate?: Date | DateTimeLimit;
|
|
61
|
-
/** Minute selection step */
|
|
65
|
+
/** Minute selection step. Ignored when type='hour'. */
|
|
62
66
|
minuteStep?: MinuteStep;
|
|
63
|
-
/**
|
|
67
|
+
/** Hour display format (type='hour' only). '24': 0~23시, '12': 오전/오후 표기. Default '24'. */
|
|
68
|
+
hourFormat?: HourFormat;
|
|
69
|
+
/** Hours (0-23) that cannot be selected (type='hour' only). Always 24h reference. */
|
|
70
|
+
disabledHours?: number[];
|
|
71
|
+
/** Step interval for hour options (type='hour' only). Default 1. */
|
|
72
|
+
hourStep?: HourStep;
|
|
73
|
+
/** Date/time format pattern. Ignored when type='hour'. */
|
|
64
74
|
format?: string;
|
|
65
75
|
/** Initial calendar display month for period mode */
|
|
66
76
|
initialCalendar?: InitialCalendar;
|
package/package.json
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
"documentation": "https://podoui.com/components/datepicker",
|
|
6
6
|
"import": {
|
|
7
7
|
"react": "import { DatePicker } from 'podo-ui'",
|
|
8
|
+
"svelte": "import { DatePicker } from 'podo-ui/svelte'",
|
|
8
9
|
"vanilla": "import 'podo-ui/vanilla/datepicker'"
|
|
9
10
|
},
|
|
10
11
|
"props": [
|
|
11
12
|
{ "name": "mode", "type": "'instant' | 'period'", "default": "'instant'", "description": "Single date or date range" },
|
|
12
|
-
{ "name": "type", "type": "'date' | 'time' | 'datetime'", "default": "'date'" },
|
|
13
|
+
{ "name": "type", "type": "'date' | 'time' | 'datetime' | 'hour'", "default": "'date'", "description": "Value type. 'hour' renders only an hour selector (no minutes)." },
|
|
13
14
|
{ "name": "value", "type": "DatePickerValue", "required": false },
|
|
14
15
|
{ "name": "onChange", "type": "(value: DatePickerValue) => void", "required": false },
|
|
15
16
|
{ "name": "placeholder", "type": "string", "required": false },
|
|
@@ -20,8 +21,11 @@
|
|
|
20
21
|
{ "name": "enable", "type": "DateCondition[]", "required": false, "description": "Only enable specific dates" },
|
|
21
22
|
{ "name": "minDate", "type": "Date | DateTimeLimit", "required": false },
|
|
22
23
|
{ "name": "maxDate", "type": "Date | DateTimeLimit", "required": false },
|
|
23
|
-
{ "name": "minuteStep", "type": "1 | 5 | 10 | 15 | 20 | 30", "default": "1" },
|
|
24
|
-
{ "name": "
|
|
24
|
+
{ "name": "minuteStep", "type": "1 | 5 | 10 | 15 | 20 | 30", "default": "1", "description": "Ignored when type='hour' (no minute column rendered)." },
|
|
25
|
+
{ "name": "hourFormat", "type": "'24' | '12'", "default": "'24'", "required": false, "description": "Hour display format (type='hour' only). '24' shows 0시~23시, '12' shows 오전/오후 1~12시." },
|
|
26
|
+
{ "name": "disabledHours", "type": "number[]", "required": false, "description": "Hours (0-23) that cannot be selected (type='hour' only). Always uses 24h reference values regardless of hourFormat." },
|
|
27
|
+
{ "name": "hourStep", "type": "1 | 2 | 3 | 4 | 6 | 12", "default": "1", "required": false, "description": "Step interval for hour options (type='hour' only). e.g., hourStep=2 → 0,2,4,...,22." },
|
|
28
|
+
{ "name": "format", "type": "string", "required": false, "description": "Custom format: y-m-d h:i. Ignored when type='hour' (use hourFormat instead)." },
|
|
25
29
|
{ "name": "initialCalendar", "type": "{ start?: CalendarInitial, end?: CalendarInitial }", "required": false, "description": "Initial calendar display month for left/right calendars. When specified, takes priority over value.date/value.endDate so the calendar opens at the explicitly requested month. CalendarInitial: 'now' | 'prevMonth' | 'nextMonth' | Date" },
|
|
26
30
|
{ "name": "yearRange", "type": "YearRange", "required": false, "description": "Year selection range (default: ±100 years from current year, takes priority over minDate/maxDate)" },
|
|
27
31
|
{ "name": "quickSelect", "type": "boolean", "default": "false", "required": false, "description": "Show quick select preset panel with common date ranges (period mode only). Presets: today, yesterday, this week, last week, last 7/30 days, this/last month. Disabled presets based on minDate/maxDate. When quickSelect=true and mode='period', left/right arrow navigation buttons appear around the input. Arrows shift the selected range by the preset unit (e.g., 7 days for weekly) or by the manually selected range size. Arrows are disabled when no range is selected or when shifting would exceed minDate/maxDate." },
|
|
@@ -67,6 +71,26 @@
|
|
|
67
71
|
{
|
|
68
72
|
"title": "Quick Select with arrow navigation",
|
|
69
73
|
"code": "<DatePicker mode=\"period\" quickSelect />"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"title": "Hour-only (24h)",
|
|
77
|
+
"code": "<DatePicker type=\"hour\" value={{ time: { hour: 9, minute: 0 } }} onChange={(v) => setHour(v.time?.hour ?? null)} />"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"title": "Hour-only (12h)",
|
|
81
|
+
"code": "<DatePicker type=\"hour\" hourFormat=\"12\" value={value} onChange={setValue} />"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"title": "Hour-only with disabled hours (no midnight/early morning)",
|
|
85
|
+
"code": "<DatePicker type=\"hour\" disabledHours={[0, 1, 2, 3, 4, 5, 22, 23]} value={value} onChange={setValue} />"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"title": "Hour-only with step (every 2 hours)",
|
|
89
|
+
"code": "<DatePicker type=\"hour\" hourStep={2} value={value} onChange={setValue} />"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"title": "Hour range (period)",
|
|
93
|
+
"code": "<DatePicker type=\"hour\" mode=\"period\" value={value} onChange={setValue} />"
|
|
70
94
|
}
|
|
71
95
|
],
|
|
72
96
|
"related": ["field", "input"]
|
package/public/ai.json
CHANGED
package/vanilla/datepicker.css
CHANGED
|
@@ -135,6 +135,19 @@
|
|
|
135
135
|
gap: 0;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/* hour-only 모드: 분 컬럼이 제거되므로 단일 select가 충분한 너비 확보 */
|
|
139
|
+
.podo-datepicker__hour-section {
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* hour-only select: "오전 12시"~"오후 11시" 라벨 폭 확보 */
|
|
145
|
+
.podo-datepicker__hour-select {
|
|
146
|
+
min-width: 72px;
|
|
147
|
+
text-align: center;
|
|
148
|
+
text-align-last: center;
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
.podo-datepicker__time-select {
|
|
139
152
|
appearance: none;
|
|
140
153
|
-webkit-appearance: none;
|