podo-ui 1.1.14 → 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 +91 -12
- 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 +69 -7
- package/dist/react/molecule/datepicker.module.scss +14 -0
- package/dist/svelte/molecule/DatePicker.svelte +116 -8
- 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 +90 -11
|
@@ -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이 있으면 사용
|
|
@@ -806,8 +815,14 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
806
815
|
setNavOffset(0);
|
|
807
816
|
};
|
|
808
817
|
const handleReset = () => {
|
|
818
|
+
// 선택값만 비우고 팝오버는 열린 상태 유지
|
|
819
|
+
// selectingPart을 null로 두면 isOpen이 false가 되어 팝오버가 닫힘 → 'date'로 되돌려 시작일 입력 대기 상태 유지
|
|
809
820
|
setTempValue({});
|
|
810
|
-
setSelectingPart(
|
|
821
|
+
setSelectingPart('date');
|
|
822
|
+
setActivePresetKey(null);
|
|
823
|
+
setNavigationStep(null);
|
|
824
|
+
setNavigationAnchor(null);
|
|
825
|
+
setNavOffset(0);
|
|
811
826
|
onReset?.();
|
|
812
827
|
};
|
|
813
828
|
const handleApply = () => {
|
|
@@ -951,16 +966,40 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
951
966
|
: `${date.getFullYear()} - ${String(date.getMonth() + 1).padStart(2, '0')} - ${String(date.getDate()).padStart(2, '0')}`;
|
|
952
967
|
return (_jsx("button", { type: "button", className: `${styles.inputPart} ${isActive ? styles.active : ''}`, onClick: onClick, children: displayText }));
|
|
953
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
|
+
};
|
|
954
987
|
// Helper to render hour select
|
|
955
988
|
const renderHourSelect = (time, part, isPlaceholder) => {
|
|
989
|
+
const isHourOnly = type === 'hour';
|
|
956
990
|
const hour = time?.hour ?? 0;
|
|
957
|
-
const hours =
|
|
991
|
+
const hours = isHourOnly
|
|
992
|
+
? buildHourOptions()
|
|
993
|
+
: Array.from({ length: 24 }, (_, i) => i);
|
|
958
994
|
const isEnd = part === 'endHour';
|
|
959
995
|
const currentDate = isEnd ? tempValue.endDate : tempValue.date;
|
|
960
996
|
// minDate/maxDate 시간 제한 계산
|
|
961
997
|
const minLimit = minDate ? extractDateTimeLimit(minDate) : null;
|
|
962
998
|
const maxLimit = maxDate ? extractDateTimeLimit(maxDate) : null;
|
|
963
999
|
const isHourDisabled = (h) => {
|
|
1000
|
+
// hour-only: disabledHours 우선 적용
|
|
1001
|
+
if (isHourOnly && disabledHours && disabledHours.includes(h))
|
|
1002
|
+
return true;
|
|
964
1003
|
if (!currentDate)
|
|
965
1004
|
return false;
|
|
966
1005
|
// minDate와 같은 날짜인 경우
|
|
@@ -977,6 +1016,18 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
977
1016
|
};
|
|
978
1017
|
const handleChange = (e) => {
|
|
979
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
|
+
}
|
|
980
1031
|
const currentTime = isEnd ? tempValue.endTime : tempValue.time;
|
|
981
1032
|
let newMinute = currentTime?.minute ?? 0;
|
|
982
1033
|
// 시간 변경 시 분이 범위를 벗어나면 자동 보정
|
|
@@ -1009,7 +1060,11 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
1009
1060
|
onChange?.(newValue);
|
|
1010
1061
|
}
|
|
1011
1062
|
};
|
|
1012
|
-
|
|
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))) }));
|
|
1013
1068
|
};
|
|
1014
1069
|
// Helper to render minute select
|
|
1015
1070
|
const renderMinuteSelect = (time, part, isPlaceholder) => {
|
|
@@ -1086,6 +1141,13 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
1086
1141
|
}
|
|
1087
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)] }) }));
|
|
1088
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
|
+
}
|
|
1089
1151
|
// datetime
|
|
1090
1152
|
if (mode === 'period') {
|
|
1091
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)] })] }));
|
|
@@ -1114,8 +1176,8 @@ const DatePicker = ({ mode = 'instant', type = 'date', value, onChange, placehol
|
|
|
1114
1176
|
}
|
|
1115
1177
|
return null;
|
|
1116
1178
|
};
|
|
1117
|
-
// 아이콘 결정 (time 타입은 icon-time, 나머지는 icon-calendar)
|
|
1118
|
-
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';
|
|
1119
1181
|
// 드롭다운 콘텐츠 렌더링
|
|
1120
1182
|
const renderDropdown = () => {
|
|
1121
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) {
|
|
@@ -790,8 +832,14 @@
|
|
|
790
832
|
};
|
|
791
833
|
|
|
792
834
|
const handleReset = () => {
|
|
835
|
+
// 선택값만 비우고 팝오버는 열린 상태 유지
|
|
836
|
+
// selectingPart을 null로 두면 isOpen이 false가 되어 팝오버가 닫힘 → 'date'로 되돌려 시작일 입력 대기 상태 유지
|
|
793
837
|
tempValue = {};
|
|
794
|
-
selectingPart =
|
|
838
|
+
selectingPart = 'date';
|
|
839
|
+
activePresetKeyState = null;
|
|
840
|
+
navigationStep = null;
|
|
841
|
+
navigationAnchor = null;
|
|
842
|
+
navOffset = 0;
|
|
795
843
|
onreset?.();
|
|
796
844
|
};
|
|
797
845
|
|
|
@@ -841,7 +889,10 @@
|
|
|
841
889
|
const target = e.target as HTMLSelectElement;
|
|
842
890
|
const hour = parseInt(target.value);
|
|
843
891
|
const currentTime = isEnd ? tempValue.endTime : tempValue.time;
|
|
844
|
-
|
|
892
|
+
// hour-only: 분을 0으로 정규화
|
|
893
|
+
const newTime: TimeValue = type === 'hour'
|
|
894
|
+
? { hour, minute: 0 }
|
|
895
|
+
: { hour, minute: currentTime?.minute ?? 0 };
|
|
845
896
|
|
|
846
897
|
const newValue = isEnd
|
|
847
898
|
? { ...tempValue, endTime: newTime }
|
|
@@ -870,9 +921,22 @@
|
|
|
870
921
|
};
|
|
871
922
|
|
|
872
923
|
// Time select options
|
|
873
|
-
|
|
924
|
+
// type='hour'면 hourStep 기반 옵션, 아니면 0~23 전체
|
|
925
|
+
const hours = $derived<number[]>(type === 'hour' ? buildHourOptions() : Array.from({ length: 24 }, (_, i) => i));
|
|
874
926
|
const minutes = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep);
|
|
875
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
|
+
|
|
876
940
|
// Calendar cell class computation
|
|
877
941
|
const getCellClass = (dayInfo: { day: number; date: Date; isOther: boolean; isDisabled: boolean }) => {
|
|
878
942
|
const { date, isOther, isDisabled } = dayInfo;
|
|
@@ -991,6 +1055,50 @@
|
|
|
991
1055
|
{formatDateDisplay(displayValue?.date)}
|
|
992
1056
|
</button>
|
|
993
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}
|
|
994
1102
|
{:else if type === 'time'}
|
|
995
1103
|
{#if mode === 'period'}
|
|
996
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;
|