@fangxinyan/lumina 0.6.14 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Calendar-RUTPKREW.css → Calendar-I6NHCXIB.css} +65 -0
- package/dist/Calendar.cjs +2 -2
- package/dist/Calendar.d.cts +2 -0
- package/dist/Calendar.d.ts +2 -0
- package/dist/Calendar.js +1 -1
- package/dist/DatePicker-VDVSIARF.css +60 -0
- package/dist/DatePicker.cjs +16 -0
- package/dist/DatePicker.d.cts +49 -0
- package/dist/DatePicker.d.ts +49 -0
- package/dist/DatePicker.js +7 -0
- package/dist/DateTimePicker-DRX7XA74.css +224 -0
- package/dist/DateTimePicker.cjs +16 -0
- package/dist/DateTimePicker.d.cts +61 -0
- package/dist/DateTimePicker.d.ts +61 -0
- package/dist/DateTimePicker.js +7 -0
- package/dist/TimePicker-ZBJOXH3U.css +151 -0
- package/dist/TimePicker.cjs +15 -0
- package/dist/TimePicker.d.cts +63 -0
- package/dist/TimePicker.d.ts +63 -0
- package/dist/TimePicker.js +6 -0
- package/dist/chunk-32FWB5NO.cjs +505 -0
- package/dist/chunk-4KDABJSY.cjs +241 -0
- package/dist/chunk-DU4VSUBF.js +393 -0
- package/dist/chunk-FKP5D5IS.js +271 -0
- package/dist/chunk-I7TOBKJO.js +219 -0
- package/dist/chunk-KHRYPNUV.cjs +293 -0
- package/dist/chunk-SFEWMO45.cjs +415 -0
- package/dist/chunk-YPE6CDXQ.js +483 -0
- package/dist/index.cjs +66 -51
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +15 -12
- package/dist/styles.css +439 -0
- package/docs/llms/calendar.md +2 -1
- package/docs/llms/datepicker.md +69 -0
- package/docs/llms/datetimepicker.md +83 -0
- package/docs/llms/timepicker.md +81 -0
- package/docs/llms.md +5 -2
- package/package.json +7 -3
- package/dist/chunk-XG5D777V.js +0 -90
- package/dist/chunk-ZBTGYTQE.cjs +0 -112
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkQMX67CVT_cjs = require('./chunk-QMX67CVT.cjs');
|
|
4
|
+
require('./tokens-RYJ3DFTU.css');
|
|
5
|
+
require('./shared-NJHRNLRH.css');
|
|
6
|
+
require('./Calendar-I6NHCXIB.css');
|
|
7
|
+
var React = require('react');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
function _interopNamespace(e) {
|
|
11
|
+
if (e && e.__esModule) return e;
|
|
12
|
+
var n = Object.create(null);
|
|
13
|
+
if (e) {
|
|
14
|
+
Object.keys(e).forEach(function (k) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return e[k]; }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
29
|
+
|
|
30
|
+
var WEEKDAYS = ["\u65E5", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D"];
|
|
31
|
+
var MONTHS = Array.from({ length: 12 }, (_, index) => `${index + 1} \u6708`);
|
|
32
|
+
var YEARS_PER_PAGE = 12;
|
|
33
|
+
var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
34
|
+
var isValidDate = (date) => date instanceof Date && !Number.isNaN(date.getTime());
|
|
35
|
+
var Calendar = React__namespace.forwardRef(({
|
|
36
|
+
value,
|
|
37
|
+
defaultValue,
|
|
38
|
+
onChange,
|
|
39
|
+
viewDate,
|
|
40
|
+
min,
|
|
41
|
+
max,
|
|
42
|
+
disabledDate,
|
|
43
|
+
className = "",
|
|
44
|
+
...rest
|
|
45
|
+
}, ref) => {
|
|
46
|
+
const [inner, setInner] = React__namespace.useState(defaultValue ?? /* @__PURE__ */ new Date());
|
|
47
|
+
const isControlled = value !== void 0;
|
|
48
|
+
const sel = isControlled ? value : inner;
|
|
49
|
+
const [view, setView] = React__namespace.useState(new Date(sel.getFullYear(), sel.getMonth(), 1));
|
|
50
|
+
const [mode, setMode] = React__namespace.useState("date");
|
|
51
|
+
const viewDateTime = isValidDate(viewDate) ? viewDate.getTime() : null;
|
|
52
|
+
const viewDateYear = isValidDate(viewDate) ? viewDate.getFullYear() : null;
|
|
53
|
+
const viewDateMonth = isValidDate(viewDate) ? viewDate.getMonth() : null;
|
|
54
|
+
React__namespace.useEffect(() => {
|
|
55
|
+
if (viewDateTime == null || viewDateYear == null || viewDateMonth == null) return;
|
|
56
|
+
setView(new Date(viewDateYear, viewDateMonth, 1));
|
|
57
|
+
setMode("date");
|
|
58
|
+
}, [viewDateMonth, viewDateTime, viewDateYear]);
|
|
59
|
+
const first = new Date(view.getFullYear(), view.getMonth(), 1);
|
|
60
|
+
const startDay = first.getDay();
|
|
61
|
+
const daysInMonth = new Date(view.getFullYear(), view.getMonth() + 1, 0).getDate();
|
|
62
|
+
const cells = [];
|
|
63
|
+
for (let i = 0; i < startDay; i++) cells.push(null);
|
|
64
|
+
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
|
|
65
|
+
const isDisabledDay = React__namespace.useCallback(
|
|
66
|
+
(date) => min && date < min || max && date > max || (disabledDate?.(date) ?? false),
|
|
67
|
+
[disabledDate, max, min]
|
|
68
|
+
);
|
|
69
|
+
const isMonthDisabled = React__namespace.useCallback(
|
|
70
|
+
(year, month) => {
|
|
71
|
+
const count = new Date(year, month + 1, 0).getDate();
|
|
72
|
+
for (let day = 1; day <= count; day++) {
|
|
73
|
+
if (!isDisabledDay(new Date(year, month, day))) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
[isDisabledDay]
|
|
78
|
+
);
|
|
79
|
+
const isYearDisabled = React__namespace.useCallback(
|
|
80
|
+
(year) => Array.from({ length: 12 }, (_, month) => month).every(
|
|
81
|
+
(month) => isMonthDisabled(year, month)
|
|
82
|
+
),
|
|
83
|
+
[isMonthDisabled]
|
|
84
|
+
);
|
|
85
|
+
const pick = (d) => {
|
|
86
|
+
const next = new Date(view.getFullYear(), view.getMonth(), d);
|
|
87
|
+
if (isDisabledDay(next)) return;
|
|
88
|
+
if (!isControlled) setInner(next);
|
|
89
|
+
onChange?.(next);
|
|
90
|
+
};
|
|
91
|
+
const shiftView = (step) => {
|
|
92
|
+
if (mode === "year") {
|
|
93
|
+
setView(new Date(view.getFullYear() + step * YEARS_PER_PAGE, view.getMonth(), 1));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (mode === "month") {
|
|
97
|
+
setView(new Date(view.getFullYear() + step, view.getMonth(), 1));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setView(new Date(view.getFullYear(), view.getMonth() + step, 1));
|
|
101
|
+
};
|
|
102
|
+
const pickMonth = (month) => {
|
|
103
|
+
if (isMonthDisabled(view.getFullYear(), month)) return;
|
|
104
|
+
setView(new Date(view.getFullYear(), month, 1));
|
|
105
|
+
setMode("date");
|
|
106
|
+
};
|
|
107
|
+
const pickYear = (year) => {
|
|
108
|
+
if (isYearDisabled(year)) return;
|
|
109
|
+
setView(new Date(year, view.getMonth(), 1));
|
|
110
|
+
setMode("month");
|
|
111
|
+
};
|
|
112
|
+
const yearPageStart = Math.floor(view.getFullYear() / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
113
|
+
const years = Array.from({ length: YEARS_PER_PAGE }, (_, index) => yearPageStart + index);
|
|
114
|
+
const renderTitle = () => {
|
|
115
|
+
if (mode === "year") {
|
|
116
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "calendar-title static", children: [
|
|
117
|
+
yearPageStart,
|
|
118
|
+
" - ",
|
|
119
|
+
yearPageStart + YEARS_PER_PAGE - 1
|
|
120
|
+
] });
|
|
121
|
+
}
|
|
122
|
+
if (mode === "month") {
|
|
123
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
124
|
+
"button",
|
|
125
|
+
{
|
|
126
|
+
type: "button",
|
|
127
|
+
className: "calendar-title",
|
|
128
|
+
onClick: () => setMode("year"),
|
|
129
|
+
"aria-label": "\u9009\u62E9\u5E74\u4EFD",
|
|
130
|
+
children: [
|
|
131
|
+
view.getFullYear(),
|
|
132
|
+
" \u5E74"
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "calendar-title-group", children: [
|
|
138
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
139
|
+
"button",
|
|
140
|
+
{
|
|
141
|
+
type: "button",
|
|
142
|
+
className: "calendar-title-part",
|
|
143
|
+
onClick: () => setMode("year"),
|
|
144
|
+
"aria-label": "\u9009\u62E9\u5E74\u4EFD",
|
|
145
|
+
children: [
|
|
146
|
+
view.getFullYear(),
|
|
147
|
+
" \u5E74"
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
152
|
+
"button",
|
|
153
|
+
{
|
|
154
|
+
type: "button",
|
|
155
|
+
className: "calendar-title-part",
|
|
156
|
+
onClick: () => setMode("month"),
|
|
157
|
+
"aria-label": "\u9009\u62E9\u6708\u4EFD",
|
|
158
|
+
children: [
|
|
159
|
+
view.getMonth() + 1,
|
|
160
|
+
" \u6708"
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
] });
|
|
165
|
+
};
|
|
166
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: `calendar ${className}`, ...rest, children: [
|
|
167
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "calendar-head", children: [
|
|
168
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
className: "btn icon sm ghost",
|
|
173
|
+
onClick: () => shiftView(-1),
|
|
174
|
+
"aria-label": mode === "year" ? "Previous years" : mode === "month" ? "Previous year" : "Previous month",
|
|
175
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(chunkQMX67CVT_cjs.Icon, { name: "chevLeft", size: 14 })
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
renderTitle(),
|
|
179
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
180
|
+
"button",
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
className: "btn icon sm ghost",
|
|
184
|
+
onClick: () => shiftView(1),
|
|
185
|
+
"aria-label": mode === "year" ? "Next years" : mode === "month" ? "Next year" : "Next month",
|
|
186
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(chunkQMX67CVT_cjs.Icon, { name: "chevRight", size: 14 })
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
] }),
|
|
190
|
+
mode === "date" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "calendar-grid", children: [
|
|
191
|
+
WEEKDAYS.map((w) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "calendar-dow", children: w }, w)),
|
|
192
|
+
cells.map((d, i) => {
|
|
193
|
+
if (d === null) return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "calendar-cell out" }, i);
|
|
194
|
+
const date = new Date(view.getFullYear(), view.getMonth(), d);
|
|
195
|
+
const disabled = isDisabledDay(date);
|
|
196
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
197
|
+
"button",
|
|
198
|
+
{
|
|
199
|
+
type: "button",
|
|
200
|
+
disabled: !!disabled,
|
|
201
|
+
className: `calendar-cell ${isSameDay(date, sel) ? "selected" : ""} ${isSameDay(date, /* @__PURE__ */ new Date()) ? "today" : ""} ${disabled ? "disabled" : ""}`,
|
|
202
|
+
onClick: () => pick(d),
|
|
203
|
+
children: d
|
|
204
|
+
},
|
|
205
|
+
i
|
|
206
|
+
);
|
|
207
|
+
})
|
|
208
|
+
] }),
|
|
209
|
+
mode === "month" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "calendar-select-grid month", children: MONTHS.map((month, index) => {
|
|
210
|
+
const disabled = isMonthDisabled(view.getFullYear(), index);
|
|
211
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
212
|
+
"button",
|
|
213
|
+
{
|
|
214
|
+
type: "button",
|
|
215
|
+
disabled,
|
|
216
|
+
className: `calendar-select-option ${index === view.getMonth() ? "selected" : ""} ${disabled ? "disabled" : ""}`,
|
|
217
|
+
onClick: () => pickMonth(index),
|
|
218
|
+
children: month
|
|
219
|
+
},
|
|
220
|
+
month
|
|
221
|
+
);
|
|
222
|
+
}) }),
|
|
223
|
+
mode === "year" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "calendar-select-grid year", children: years.map((year) => {
|
|
224
|
+
const disabled = isYearDisabled(year);
|
|
225
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
226
|
+
"button",
|
|
227
|
+
{
|
|
228
|
+
type: "button",
|
|
229
|
+
disabled,
|
|
230
|
+
className: `calendar-select-option ${year === view.getFullYear() ? "selected" : ""} ${disabled ? "disabled" : ""}`,
|
|
231
|
+
onClick: () => pickYear(year),
|
|
232
|
+
children: year
|
|
233
|
+
},
|
|
234
|
+
year
|
|
235
|
+
);
|
|
236
|
+
}) })
|
|
237
|
+
] });
|
|
238
|
+
});
|
|
239
|
+
Calendar.displayName = "Calendar";
|
|
240
|
+
|
|
241
|
+
exports.Calendar = Calendar;
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { useFloating } from './chunk-DUFRASGO.js';
|
|
2
|
+
import { Input } from './chunk-IOAOG5L7.js';
|
|
3
|
+
import { Button } from './chunk-IPSNC5VZ.js';
|
|
4
|
+
import './tokens-RYJ3DFTU.css';
|
|
5
|
+
import './shared-NJHRNLRH.css';
|
|
6
|
+
import './TimePicker-ZBJOXH3U.css';
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
9
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
var clampStep = (value) => {
|
|
12
|
+
if (!Number.isFinite(value) || !value || value < 1) return 1;
|
|
13
|
+
return Math.floor(value);
|
|
14
|
+
};
|
|
15
|
+
var pad = (value) => String(value).padStart(2, "0");
|
|
16
|
+
var parseTime = (value) => {
|
|
17
|
+
if (!value) return null;
|
|
18
|
+
const match = value.trim().match(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
const hour = Number(match[1]);
|
|
21
|
+
const minute = Number(match[2]);
|
|
22
|
+
const second = match[3] == null ? 0 : Number(match[3]);
|
|
23
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { hour, minute, second };
|
|
27
|
+
};
|
|
28
|
+
var formatTime = (time, includeSecond) => {
|
|
29
|
+
const base = `${pad(time.hour)}:${pad(time.minute)}`;
|
|
30
|
+
return includeSecond ? `${base}:${pad(time.second)}` : base;
|
|
31
|
+
};
|
|
32
|
+
var normalizeTime = (value, includeSecond) => {
|
|
33
|
+
const parsed = parseTime(value);
|
|
34
|
+
return parsed ? formatTime(parsed, includeSecond) : null;
|
|
35
|
+
};
|
|
36
|
+
var toSeconds = (time) => time.hour * 3600 + time.minute * 60 + time.second;
|
|
37
|
+
var getNow = () => {
|
|
38
|
+
const now = /* @__PURE__ */ new Date();
|
|
39
|
+
return {
|
|
40
|
+
hour: now.getHours(),
|
|
41
|
+
minute: now.getMinutes(),
|
|
42
|
+
second: now.getSeconds()
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
var buildOptions = (max, step, selected) => {
|
|
46
|
+
const safeStep = clampStep(step);
|
|
47
|
+
const values = [];
|
|
48
|
+
for (let value = 0; value <= max; value += safeStep) {
|
|
49
|
+
values.push(value);
|
|
50
|
+
}
|
|
51
|
+
if (selected != null && !values.includes(selected)) {
|
|
52
|
+
values.push(selected);
|
|
53
|
+
values.sort((a, b) => a - b);
|
|
54
|
+
}
|
|
55
|
+
return values;
|
|
56
|
+
};
|
|
57
|
+
var compose = (base, key, value) => ({
|
|
58
|
+
hour: key === "hour" ? value : base.hour,
|
|
59
|
+
minute: key === "minute" ? value : base.minute,
|
|
60
|
+
second: key === "second" ? value : base.second
|
|
61
|
+
});
|
|
62
|
+
var byDistanceFrom = (preferred) => (a, b) => Math.abs(a - preferred) - Math.abs(b - preferred) || a - b;
|
|
63
|
+
var TimePicker = React.forwardRef(
|
|
64
|
+
({
|
|
65
|
+
value,
|
|
66
|
+
defaultValue = null,
|
|
67
|
+
onChange,
|
|
68
|
+
format = "HH:mm",
|
|
69
|
+
showSecond,
|
|
70
|
+
placeholder = "\u8BF7\u9009\u62E9\u65F6\u95F4",
|
|
71
|
+
disabled,
|
|
72
|
+
readOnly,
|
|
73
|
+
invalid,
|
|
74
|
+
size = "md",
|
|
75
|
+
allowClear,
|
|
76
|
+
hourStep = 1,
|
|
77
|
+
minuteStep = 1,
|
|
78
|
+
secondStep = 1,
|
|
79
|
+
min,
|
|
80
|
+
max,
|
|
81
|
+
disabledTime,
|
|
82
|
+
placement = "bottom",
|
|
83
|
+
open: openProp,
|
|
84
|
+
defaultOpen = false,
|
|
85
|
+
onOpenChange,
|
|
86
|
+
popupClassName = "",
|
|
87
|
+
dropdownClassName = "",
|
|
88
|
+
className = "",
|
|
89
|
+
onKeyDown,
|
|
90
|
+
...rest
|
|
91
|
+
}, ref) => {
|
|
92
|
+
const includeSecond = showSecond ?? format === "HH:mm:ss";
|
|
93
|
+
const controlled = value !== void 0;
|
|
94
|
+
const initial = normalizeTime(defaultValue, includeSecond);
|
|
95
|
+
const [inner, setInner] = React.useState(initial);
|
|
96
|
+
const current = controlled ? normalizeTime(value, includeSecond) : inner;
|
|
97
|
+
const scrollSignature = current ?? "";
|
|
98
|
+
const currentParts = parseTime(current);
|
|
99
|
+
const [draft, setDraft] = React.useState(current ?? "");
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
setDraft(current ?? "");
|
|
102
|
+
}, [current]);
|
|
103
|
+
const openControlled = openProp !== void 0;
|
|
104
|
+
const [innerOpen, setInnerOpen] = React.useState(defaultOpen);
|
|
105
|
+
const open = openControlled ? !!openProp : innerOpen;
|
|
106
|
+
const wasOpenRef = React.useRef(false);
|
|
107
|
+
const scrollSignatureRef = React.useRef(null);
|
|
108
|
+
const scrollFramesRef = React.useRef(/* @__PURE__ */ new Map());
|
|
109
|
+
const setOpen = React.useCallback(
|
|
110
|
+
(next) => {
|
|
111
|
+
if (disabled || readOnly) return;
|
|
112
|
+
if (!openControlled) setInnerOpen(next);
|
|
113
|
+
onOpenChange?.(next);
|
|
114
|
+
},
|
|
115
|
+
[disabled, onOpenChange, openControlled, readOnly]
|
|
116
|
+
);
|
|
117
|
+
const rootRef = React.useRef(null);
|
|
118
|
+
const panelRef = React.useRef(null);
|
|
119
|
+
const { triggerRef, floatingStyle } = useFloating({
|
|
120
|
+
open,
|
|
121
|
+
placement,
|
|
122
|
+
panelWidth: includeSecond ? 312 : 236,
|
|
123
|
+
panelHeight: 330
|
|
124
|
+
});
|
|
125
|
+
const setRootRef = React.useCallback(
|
|
126
|
+
(node) => {
|
|
127
|
+
rootRef.current = node;
|
|
128
|
+
triggerRef.current = node;
|
|
129
|
+
if (typeof ref === "function") ref(node);
|
|
130
|
+
else if (ref) ref.current = node;
|
|
131
|
+
},
|
|
132
|
+
[ref, triggerRef]
|
|
133
|
+
);
|
|
134
|
+
const minParts = React.useMemo(() => parseTime(min), [min]);
|
|
135
|
+
const maxParts = React.useMemo(() => parseTime(max), [max]);
|
|
136
|
+
const minSeconds = minParts ? toSeconds(minParts) : null;
|
|
137
|
+
const maxSeconds = maxParts ? toSeconds(maxParts) : null;
|
|
138
|
+
const isDisabledTime = React.useCallback(
|
|
139
|
+
(time) => {
|
|
140
|
+
const seconds = toSeconds(time);
|
|
141
|
+
if (minSeconds != null && seconds < minSeconds) return true;
|
|
142
|
+
if (maxSeconds != null && seconds > maxSeconds) return true;
|
|
143
|
+
return disabledTime?.(time) ?? false;
|
|
144
|
+
},
|
|
145
|
+
[disabledTime, maxSeconds, minSeconds]
|
|
146
|
+
);
|
|
147
|
+
const commit = React.useCallback(
|
|
148
|
+
(next) => {
|
|
149
|
+
if (!next) {
|
|
150
|
+
if (!controlled) setInner(null);
|
|
151
|
+
setDraft("");
|
|
152
|
+
onChange?.(null, null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (isDisabledTime(next)) return;
|
|
156
|
+
const nextValue = formatTime(next, includeSecond);
|
|
157
|
+
if (!controlled) setInner(nextValue);
|
|
158
|
+
setDraft(nextValue);
|
|
159
|
+
onChange?.(nextValue, next);
|
|
160
|
+
},
|
|
161
|
+
[controlled, includeSecond, isDisabledTime, onChange]
|
|
162
|
+
);
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
if (!open) return;
|
|
165
|
+
const onDown = (event) => {
|
|
166
|
+
const target = event.target;
|
|
167
|
+
if (rootRef.current?.contains(target)) return;
|
|
168
|
+
if (panelRef.current?.contains(target)) return;
|
|
169
|
+
setOpen(false);
|
|
170
|
+
};
|
|
171
|
+
const onDocKey = (event) => {
|
|
172
|
+
if (event.key === "Escape") setOpen(false);
|
|
173
|
+
};
|
|
174
|
+
document.addEventListener("mousedown", onDown);
|
|
175
|
+
document.addEventListener("keydown", onDocKey);
|
|
176
|
+
return () => {
|
|
177
|
+
document.removeEventListener("mousedown", onDown);
|
|
178
|
+
document.removeEventListener("keydown", onDocKey);
|
|
179
|
+
};
|
|
180
|
+
}, [open, setOpen]);
|
|
181
|
+
const scrollListTo = React.useCallback(
|
|
182
|
+
(list, top, animate) => {
|
|
183
|
+
const frame = scrollFramesRef.current.get(list);
|
|
184
|
+
if (frame != null) cancelAnimationFrame(frame);
|
|
185
|
+
if (!animate) {
|
|
186
|
+
list.scrollTop = top;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const start = list.scrollTop;
|
|
190
|
+
const distance = top - start;
|
|
191
|
+
if (Math.abs(distance) < 1) {
|
|
192
|
+
list.scrollTop = top;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const startedAt = performance.now();
|
|
196
|
+
const duration = 180;
|
|
197
|
+
const tick = (now2) => {
|
|
198
|
+
const progress = Math.min((now2 - startedAt) / duration, 1);
|
|
199
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
200
|
+
list.scrollTop = start + distance * eased;
|
|
201
|
+
if (progress < 1) {
|
|
202
|
+
scrollFramesRef.current.set(list, requestAnimationFrame(tick));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
scrollFramesRef.current.delete(list);
|
|
206
|
+
list.scrollTop = top;
|
|
207
|
+
};
|
|
208
|
+
scrollFramesRef.current.set(list, requestAnimationFrame(tick));
|
|
209
|
+
},
|
|
210
|
+
[]
|
|
211
|
+
);
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
if (!open) return;
|
|
214
|
+
const shouldAnimate = wasOpenRef.current && scrollSignatureRef.current != null && scrollSignatureRef.current !== scrollSignature;
|
|
215
|
+
requestAnimationFrame(() => {
|
|
216
|
+
panelRef.current?.querySelectorAll(".time-picker-list").forEach((list) => {
|
|
217
|
+
const selected = list.querySelector(".time-picker-option.selected");
|
|
218
|
+
if (!selected) return;
|
|
219
|
+
const top = selected.offsetTop - list.offsetTop - list.clientHeight / 2 + selected.offsetHeight / 2;
|
|
220
|
+
scrollListTo(list, top, shouldAnimate);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
wasOpenRef.current = open;
|
|
224
|
+
scrollSignatureRef.current = scrollSignature;
|
|
225
|
+
}, [open, scrollListTo, scrollSignature]);
|
|
226
|
+
React.useEffect(() => {
|
|
227
|
+
if (open) return;
|
|
228
|
+
wasOpenRef.current = false;
|
|
229
|
+
scrollSignatureRef.current = null;
|
|
230
|
+
}, [open]);
|
|
231
|
+
React.useEffect(
|
|
232
|
+
() => () => {
|
|
233
|
+
scrollFramesRef.current.forEach((frame) => cancelAnimationFrame(frame));
|
|
234
|
+
scrollFramesRef.current.clear();
|
|
235
|
+
},
|
|
236
|
+
[]
|
|
237
|
+
);
|
|
238
|
+
const panelBase = currentParts ?? getNow();
|
|
239
|
+
const hourOptions = buildOptions(23, hourStep, currentParts?.hour);
|
|
240
|
+
const minuteOptions = buildOptions(59, minuteStep, currentParts?.minute);
|
|
241
|
+
const secondOptions = buildOptions(59, secondStep, currentParts?.second);
|
|
242
|
+
const findAvailableTime = (key, value2) => {
|
|
243
|
+
const preferred = compose(panelBase, key, value2);
|
|
244
|
+
if (!isDisabledTime(preferred)) return preferred;
|
|
245
|
+
const hours = key === "hour" ? [value2] : [...hourOptions].sort(byDistanceFrom(panelBase.hour));
|
|
246
|
+
const minutes = key === "minute" ? [value2] : [...minuteOptions].sort(byDistanceFrom(panelBase.minute));
|
|
247
|
+
const seconds = key === "second" ? [value2] : includeSecond ? [...secondOptions].sort(byDistanceFrom(panelBase.second)) : [0];
|
|
248
|
+
for (const hour of hours) {
|
|
249
|
+
for (const minute of minutes) {
|
|
250
|
+
for (const second of seconds) {
|
|
251
|
+
const candidate = { hour, minute, second };
|
|
252
|
+
if (!isDisabledTime(candidate)) return candidate;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
258
|
+
const pick = (key, nextValue) => {
|
|
259
|
+
const next = findAvailableTime(key, nextValue);
|
|
260
|
+
if (next) commit(next);
|
|
261
|
+
};
|
|
262
|
+
const handleInputChange = (next) => {
|
|
263
|
+
setDraft(next);
|
|
264
|
+
if (next.trim() === "") commit(null);
|
|
265
|
+
};
|
|
266
|
+
const handleInputBlur = () => {
|
|
267
|
+
if (!draft.trim()) {
|
|
268
|
+
setDraft(current ?? "");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const parsed = parseTime(draft);
|
|
272
|
+
if (!parsed || isDisabledTime(parsed)) {
|
|
273
|
+
setDraft(current ?? "");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
commit(parsed);
|
|
277
|
+
};
|
|
278
|
+
const handleRootKeyDown = (event) => {
|
|
279
|
+
onKeyDown?.(event);
|
|
280
|
+
if (event.defaultPrevented) return;
|
|
281
|
+
if ((event.key === "ArrowDown" || event.key === "Enter") && !open) {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
setOpen(true);
|
|
284
|
+
}
|
|
285
|
+
if (event.key === "Escape" && open) {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
setOpen(false);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const renderColumn = (label, key, options) => /* @__PURE__ */ jsxs("div", { className: "time-picker-column", children: [
|
|
291
|
+
/* @__PURE__ */ jsx("div", { className: "time-picker-column-label", children: label }),
|
|
292
|
+
/* @__PURE__ */ jsx("div", { className: "time-picker-list", role: "listbox", "aria-label": label, children: options.map((option) => {
|
|
293
|
+
const selected = currentParts?.[key] === option;
|
|
294
|
+
const optionDisabled = findAvailableTime(key, option) == null;
|
|
295
|
+
return /* @__PURE__ */ jsx(
|
|
296
|
+
"button",
|
|
297
|
+
{
|
|
298
|
+
type: "button",
|
|
299
|
+
role: "option",
|
|
300
|
+
"aria-selected": selected,
|
|
301
|
+
disabled: optionDisabled,
|
|
302
|
+
className: `time-picker-option ${selected ? "selected" : ""}`,
|
|
303
|
+
onClick: () => pick(key, option),
|
|
304
|
+
children: pad(option)
|
|
305
|
+
},
|
|
306
|
+
option
|
|
307
|
+
);
|
|
308
|
+
}) })
|
|
309
|
+
] });
|
|
310
|
+
const mergedPanelClassName = [popupClassName, dropdownClassName].filter(Boolean).join(" ");
|
|
311
|
+
const now = getNow();
|
|
312
|
+
const nowDisabled = isDisabledTime(now);
|
|
313
|
+
const rootClassName = [
|
|
314
|
+
"time-picker",
|
|
315
|
+
size,
|
|
316
|
+
open && "open",
|
|
317
|
+
disabled && "disabled",
|
|
318
|
+
readOnly && "readonly",
|
|
319
|
+
invalid && "invalid",
|
|
320
|
+
className
|
|
321
|
+
].filter(Boolean).join(" ");
|
|
322
|
+
return /* @__PURE__ */ jsxs("div", { ref: setRootRef, className: rootClassName, onKeyDown: handleRootKeyDown, ...rest, children: [
|
|
323
|
+
/* @__PURE__ */ jsx(
|
|
324
|
+
Input,
|
|
325
|
+
{
|
|
326
|
+
value: draft,
|
|
327
|
+
size,
|
|
328
|
+
placeholder,
|
|
329
|
+
disabled,
|
|
330
|
+
readOnly,
|
|
331
|
+
invalid,
|
|
332
|
+
allowClear,
|
|
333
|
+
leadingIcon: "clock",
|
|
334
|
+
onFocus: () => setOpen(true),
|
|
335
|
+
onClick: () => setOpen(true),
|
|
336
|
+
onValueChange: handleInputChange,
|
|
337
|
+
onBlur: handleInputBlur,
|
|
338
|
+
inputMode: "numeric",
|
|
339
|
+
"aria-expanded": open,
|
|
340
|
+
"aria-haspopup": "dialog"
|
|
341
|
+
}
|
|
342
|
+
),
|
|
343
|
+
open && !disabled && !readOnly && typeof document !== "undefined" && createPortal(
|
|
344
|
+
/* @__PURE__ */ jsxs(
|
|
345
|
+
"div",
|
|
346
|
+
{
|
|
347
|
+
ref: panelRef,
|
|
348
|
+
className: `time-picker-panel ${includeSecond ? "with-second" : "without-second"} ${mergedPanelClassName}`,
|
|
349
|
+
role: "dialog",
|
|
350
|
+
style: floatingStyle,
|
|
351
|
+
children: [
|
|
352
|
+
/* @__PURE__ */ jsxs("div", { className: "time-picker-columns", children: [
|
|
353
|
+
renderColumn("\u65F6", "hour", hourOptions),
|
|
354
|
+
renderColumn("\u5206", "minute", minuteOptions),
|
|
355
|
+
includeSecond && renderColumn("\u79D2", "second", secondOptions)
|
|
356
|
+
] }),
|
|
357
|
+
/* @__PURE__ */ jsxs("div", { className: "time-picker-footer", children: [
|
|
358
|
+
/* @__PURE__ */ jsx(
|
|
359
|
+
Button,
|
|
360
|
+
{
|
|
361
|
+
size: "sm",
|
|
362
|
+
variant: "ghost",
|
|
363
|
+
disabled: nowDisabled,
|
|
364
|
+
onClick: () => commit(now),
|
|
365
|
+
children: "\u73B0\u5728"
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
/* @__PURE__ */ jsx("span", { className: "time-picker-footer-spacer" }),
|
|
369
|
+
allowClear && /* @__PURE__ */ jsx(
|
|
370
|
+
Button,
|
|
371
|
+
{
|
|
372
|
+
size: "sm",
|
|
373
|
+
variant: "ghost",
|
|
374
|
+
onClick: () => {
|
|
375
|
+
commit(null);
|
|
376
|
+
setOpen(false);
|
|
377
|
+
},
|
|
378
|
+
children: "\u6E05\u7A7A"
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "primary", onClick: () => setOpen(false), children: "\u786E\u5B9A" })
|
|
382
|
+
] })
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
),
|
|
386
|
+
document.body
|
|
387
|
+
)
|
|
388
|
+
] });
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
TimePicker.displayName = "TimePicker";
|
|
392
|
+
|
|
393
|
+
export { TimePicker };
|