@artemy-tech/datepicker 0.1.0 → 0.3.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/dist/index.cjs CHANGED
@@ -20,10 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ Button: () => Button,
23
24
  Calendar: () => Calendar,
24
25
  DatePicker: () => DatePicker,
25
- DateRangePicker: () => DateRangePicker,
26
- VERSION: () => VERSION
26
+ DateRangePicker: () => DateRangePicker
27
27
  });
28
28
  module.exports = __toCommonJS(src_exports);
29
29
 
@@ -41,7 +41,7 @@ function Calendar({ className, ...props }) {
41
41
  }
42
42
 
43
43
  // src/components/DatePicker/DatePicker.tsx
44
- var import_react2 = require("react");
44
+ var import_react3 = require("react");
45
45
  var import_date_fns = require("date-fns");
46
46
  var import_locale = require("date-fns/locale");
47
47
 
@@ -58,14 +58,89 @@ function useClickOutside(ref, handler) {
58
58
  }, [ref, handler]);
59
59
  }
60
60
 
61
- // src/components/DatePicker/DatePicker.tsx
61
+ // src/components/TimePanel/TimePanel.tsx
62
+ var import_react2 = require("react");
62
63
  var import_jsx_runtime2 = require("react/jsx-runtime");
64
+ var HOURS = Array.from({ length: 24 }, (_, i) => i);
65
+ var MINUTES = Array.from({ length: 60 }, (_, i) => i);
66
+ var SECONDS = Array.from({ length: 60 }, (_, i) => i);
67
+ function pad2(n) {
68
+ return String(n).padStart(2, "0");
69
+ }
70
+ function Column({ values, selected, onSelect }) {
71
+ const selectedRef = (0, import_react2.useRef)(null);
72
+ (0, import_react2.useEffect)(() => {
73
+ var _a;
74
+ (_a = selectedRef.current) == null ? void 0 : _a.scrollIntoView({ block: "center", behavior: "instant" });
75
+ }, [selected]);
76
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "time-panel__column", children: values.map((v) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
77
+ "button",
78
+ {
79
+ ref: v === selected ? selectedRef : void 0,
80
+ className: "time-panel__item",
81
+ "data-selected": v === selected || void 0,
82
+ onClick: () => onSelect(v),
83
+ type: "button",
84
+ tabIndex: -1,
85
+ children: pad2(v)
86
+ },
87
+ v
88
+ )) });
89
+ }
90
+ function TimePanel({ value, showSeconds, onChange }) {
91
+ var _a, _b, _c;
92
+ const h = (_a = value == null ? void 0 : value.getHours()) != null ? _a : 0;
93
+ const m = (_b = value == null ? void 0 : value.getMinutes()) != null ? _b : 0;
94
+ const s = (_c = value == null ? void 0 : value.getSeconds()) != null ? _c : 0;
95
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "time-panel", children: [
96
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Column, { values: HOURS, selected: h, onSelect: (v) => onChange(v, m, s) }),
97
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Column, { values: MINUTES, selected: m, onSelect: (v) => onChange(h, v, s) }),
98
+ showSeconds && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Column, { values: SECONDS, selected: s, onSelect: (v) => onChange(h, m, v) })
99
+ ] });
100
+ }
101
+
102
+ // src/components/icons/CalendarIcon.tsx
103
+ var import_jsx_runtime3 = require("react/jsx-runtime");
104
+ function CalendarIcon() {
105
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
106
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "1", y: "2.5", width: "14", height: "12", rx: "1.5", stroke: "currentColor", strokeWidth: "1.5" }),
107
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M1 6.5H15", stroke: "currentColor", strokeWidth: "1.5" }),
108
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M5 1V4M11 1V4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
109
+ ] });
110
+ }
111
+
112
+ // src/components/icons/Spinner.tsx
113
+ var import_jsx_runtime4 = require("react/jsx-runtime");
114
+ function Spinner() {
115
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "datepicker-spinner", "aria-hidden": "true" });
116
+ }
117
+
118
+ // src/components/DatePicker/DatePicker.tsx
119
+ var import_jsx_runtime5 = require("react/jsx-runtime");
63
120
  var DATE_FORMAT = "dd.MM.yyyy";
64
- function applyMask(digits) {
65
- const d = digits.slice(0, 8);
121
+ function resolveTimeFormat(showTime) {
122
+ if (!showTime) return null;
123
+ if (showTime === true) return "HH:mm:ss";
124
+ return showTime.format;
125
+ }
126
+ function buildDateFormat(timeFormat) {
127
+ return timeFormat ? `${DATE_FORMAT} ${timeFormat}` : DATE_FORMAT;
128
+ }
129
+ function buildMaxDigits(timeFormat) {
130
+ if (!timeFormat) return 8;
131
+ return timeFormat === "HH:mm" ? 12 : 14;
132
+ }
133
+ function buildPlaceholder(timeFormat) {
134
+ if (!timeFormat) return "\u0434\u0434.\u043C\u043C.\u0433\u0433\u0433\u0433";
135
+ return timeFormat === "HH:mm" ? "\u0434\u0434.\u043C\u043C.\u0433\u0433\u0433\u0433 \u0447\u0447:\u043C\u043C" : "\u0434\u0434.\u043C\u043C.\u0433\u0433\u0433\u0433 \u0447\u0447:\u043C\u043C:\u0441\u0441";
136
+ }
137
+ function applyMask(digits, maxDigits) {
138
+ const d = digits.slice(0, maxDigits);
66
139
  let result = "";
67
140
  for (let i = 0; i < d.length; i++) {
68
141
  if (i === 2 || i === 4) result += ".";
142
+ else if (i === 8) result += " ";
143
+ else if (i === 10 || i === 12) result += ":";
69
144
  result += d[i];
70
145
  }
71
146
  return result;
@@ -81,45 +156,86 @@ function getCursorPos(masked, digitCount) {
81
156
  }
82
157
  return masked.length;
83
158
  }
84
- function parseDate(masked) {
85
- if (masked.replace(/\D/g, "").length !== 8) return void 0;
86
- const date = (0, import_date_fns.parse)(masked, DATE_FORMAT, /* @__PURE__ */ new Date());
87
- return (0, import_date_fns.isValid)(date) && (0, import_date_fns.format)(date, DATE_FORMAT) === masked ? date : void 0;
159
+ function parseDateTime(masked, dateFormat, maxDigits) {
160
+ if (masked.replace(/\D/g, "").length !== maxDigits) return void 0;
161
+ const date = (0, import_date_fns.parse)(masked, dateFormat, /* @__PURE__ */ new Date());
162
+ return (0, import_date_fns.isValid)(date) && (0, import_date_fns.format)(date, dateFormat) === masked ? date : void 0;
88
163
  }
89
164
  function DatePicker({
90
165
  value,
91
166
  defaultValue,
92
167
  onChange,
93
168
  label,
94
- placeholder = "\u0434\u0434.\u043C\u043C.\u0433\u0433\u0433\u0433",
169
+ placeholder,
95
170
  fromDate,
96
171
  toDate,
97
172
  disabled = false,
98
173
  failed = false,
174
+ loading = false,
175
+ size = "m",
176
+ noCalendar = false,
177
+ showTime,
178
+ icon,
179
+ iconPosition = "end",
99
180
  className
100
181
  }) {
182
+ const timeFormat = resolveTimeFormat(showTime);
183
+ const dateFormat = buildDateFormat(timeFormat);
184
+ const maxDigits = buildMaxDigits(timeFormat);
185
+ const defaultPlaceholder = placeholder != null ? placeholder : buildPlaceholder(timeFormat);
186
+ const showSeconds = timeFormat === "HH:mm:ss";
187
+ const resolvedIcon = loading ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, {}) : icon === false ? null : icon != null ? icon : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CalendarIcon, {});
101
188
  const isControlled = value !== void 0;
102
- const [internalDate, setInternalDate] = (0, import_react2.useState)(defaultValue);
103
- const [open, setOpen] = (0, import_react2.useState)(false);
104
- const [focused, setFocused] = (0, import_react2.useState)(false);
105
- const [inputValue, setInputValue] = (0, import_react2.useState)(
106
- () => defaultValue && (0, import_date_fns.isValid)(defaultValue) ? (0, import_date_fns.format)(defaultValue, DATE_FORMAT) : ""
189
+ const [internalDate, setInternalDate] = (0, import_react3.useState)(defaultValue);
190
+ const [open, setOpen] = (0, import_react3.useState)(false);
191
+ const [focused, setFocused] = (0, import_react3.useState)(false);
192
+ const [inputValue, setInputValue] = (0, import_react3.useState)(
193
+ () => defaultValue && (0, import_date_fns.isValid)(defaultValue) ? (0, import_date_fns.format)(defaultValue, dateFormat) : ""
107
194
  );
108
- const [inputInvalid, setInputInvalid] = (0, import_react2.useState)(false);
109
- const inputRef = (0, import_react2.useRef)(null);
110
- const containerRef = (0, import_react2.useRef)(null);
195
+ const [inputInvalid, setInputInvalid] = (0, import_react3.useState)(false);
196
+ const inputRef = (0, import_react3.useRef)(null);
197
+ const containerRef = (0, import_react3.useRef)(null);
198
+ const lastValidRef = (0, import_react3.useRef)(inputValue);
199
+ const lastEmittedRef = (0, import_react3.useRef)(value !== void 0 ? value : defaultValue);
200
+ const wasControlledRef = (0, import_react3.useRef)(value !== void 0);
111
201
  const selected = isControlled ? value : internalDate;
112
202
  const filled = inputValue.length > 0;
113
- const close = (0, import_react2.useCallback)(() => setOpen(false), []);
203
+ const close = (0, import_react3.useCallback)(() => setOpen(false), []);
114
204
  useClickOutside(containerRef, close);
205
+ (0, import_react3.useEffect)(() => {
206
+ var _a, _b, _c;
207
+ if (value !== void 0) wasControlledRef.current = true;
208
+ const lastTime = (_b = (_a = lastEmittedRef.current) == null ? void 0 : _a.getTime()) != null ? _b : null;
209
+ const valueTime = (_c = value == null ? void 0 : value.getTime()) != null ? _c : null;
210
+ if (valueTime === lastTime) return;
211
+ if (!wasControlledRef.current && value === void 0) return;
212
+ const formatted = value && (0, import_date_fns.isValid)(value) ? (0, import_date_fns.format)(value, dateFormat) : "";
213
+ setInputValue(formatted);
214
+ lastValidRef.current = formatted;
215
+ setInputInvalid(false);
216
+ if (!isControlled) setInternalDate(value);
217
+ lastEmittedRef.current = value;
218
+ }, [value]);
219
+ function applyValid(masked, date) {
220
+ lastEmittedRef.current = date;
221
+ lastValidRef.current = masked;
222
+ setInputValue(masked);
223
+ setInputInvalid(false);
224
+ if (!isControlled) setInternalDate(date);
225
+ onChange == null ? void 0 : onChange(date);
226
+ }
115
227
  function commit(masked) {
116
228
  const digits = masked.replace(/\D/g, "");
117
229
  if (digits.length === 0) {
230
+ lastEmittedRef.current = void 0;
231
+ lastValidRef.current = "";
118
232
  setInputInvalid(false);
119
233
  if (!isControlled) setInternalDate(void 0);
120
234
  onChange == null ? void 0 : onChange(void 0);
121
- } else if (digits.length === 8) {
122
- const date = parseDate(masked);
235
+ } else if (digits.length === maxDigits) {
236
+ const date = parseDateTime(masked, dateFormat, maxDigits);
237
+ lastEmittedRef.current = date;
238
+ if (date) lastValidRef.current = masked;
123
239
  setInputInvalid(!date);
124
240
  if (!isControlled) setInternalDate(date);
125
241
  onChange == null ? void 0 : onChange(date);
@@ -127,13 +243,21 @@ function DatePicker({
127
243
  setInputInvalid(false);
128
244
  }
129
245
  }
246
+ function handleBlur() {
247
+ setFocused(false);
248
+ const digits = inputValue.replace(/\D/g, "");
249
+ if (digits.length > 0 && digits.length < maxDigits || inputInvalid) {
250
+ setInputValue(lastValidRef.current);
251
+ setInputInvalid(false);
252
+ }
253
+ }
130
254
  function handleChange(e) {
131
255
  var _a;
132
256
  const input = e.target;
133
257
  const cursorPos = (_a = input.selectionStart) != null ? _a : 0;
134
258
  const raw = input.value;
135
- const digits = raw.replace(/\D/g, "").slice(0, 8);
136
- const masked = applyMask(digits);
259
+ const digits = raw.replace(/\D/g, "").slice(0, maxDigits);
260
+ const masked = applyMask(digits, maxDigits);
137
261
  const digitsBeforeCursor = raw.slice(0, cursorPos).replace(/\D/g, "").length;
138
262
  const newCursorPos = getCursorPos(masked, digitsBeforeCursor);
139
263
  setInputValue(masked);
@@ -151,10 +275,10 @@ function DatePicker({
151
275
  e.preventDefault();
152
276
  return;
153
277
  }
154
- if (e.key === "Backspace" && pos > 0 && input.value[pos - 1] === ".") {
278
+ if (e.key === "Backspace" && pos > 0 && /[.: ]/.test(input.value[pos - 1])) {
155
279
  e.preventDefault();
156
280
  const val = input.value;
157
- const masked = applyMask((val.slice(0, pos - 2) + val.slice(pos)).replace(/\D/g, ""));
281
+ const masked = applyMask((val.slice(0, pos - 2) + val.slice(pos)).replace(/\D/g, ""), maxDigits);
158
282
  setInputValue(masked);
159
283
  commit(masked);
160
284
  requestAnimationFrame(() => input.setSelectionRange(pos - 2, pos - 2));
@@ -162,7 +286,7 @@ function DatePicker({
162
286
  }
163
287
  function handlePaste(e) {
164
288
  e.preventDefault();
165
- const masked = applyMask(e.clipboardData.getData("text").replace(/\D/g, ""));
289
+ const masked = applyMask(e.clipboardData.getData("text").replace(/\D/g, ""), maxDigits);
166
290
  setInputValue(masked);
167
291
  commit(masked);
168
292
  requestAnimationFrame(() => {
@@ -170,74 +294,149 @@ function DatePicker({
170
294
  return (_a = inputRef.current) == null ? void 0 : _a.setSelectionRange(masked.length, masked.length);
171
295
  });
172
296
  }
173
- function handleSelect(date) {
174
- if (!isControlled) setInternalDate(date);
175
- setInputValue(date && (0, import_date_fns.isValid)(date) ? (0, import_date_fns.format)(date, DATE_FORMAT) : "");
176
- setInputInvalid(false);
177
- onChange == null ? void 0 : onChange(date);
178
- setOpen(false);
297
+ function handleCalendarSelect(date) {
298
+ if (!date || !(0, import_date_fns.isValid)(date)) {
299
+ applyValid("", void 0);
300
+ if (!timeFormat) setOpen(false);
301
+ return;
302
+ }
303
+ let dateToCommit = date;
304
+ if (timeFormat) {
305
+ const base = selected && (0, import_date_fns.isValid)(selected) ? selected : /* @__PURE__ */ new Date(0);
306
+ dateToCommit = new Date(
307
+ date.getFullYear(),
308
+ date.getMonth(),
309
+ date.getDate(),
310
+ base.getHours(),
311
+ base.getMinutes(),
312
+ base.getSeconds()
313
+ );
314
+ }
315
+ applyValid((0, import_date_fns.format)(dateToCommit, dateFormat), dateToCommit);
316
+ if (!timeFormat) setOpen(false);
179
317
  }
180
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
318
+ function handleTimeChange(h, m, s) {
319
+ const base = selected && (0, import_date_fns.isValid)(selected) ? selected : /* @__PURE__ */ new Date();
320
+ const newDate = new Date(base.getFullYear(), base.getMonth(), base.getDate(), h, m, s);
321
+ applyValid((0, import_date_fns.format)(newDate, dateFormat), newDate);
322
+ }
323
+ const interactive = !disabled && !loading;
324
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
181
325
  "div",
182
326
  {
183
327
  ref: containerRef,
184
- className: ["datepicker", className].filter(Boolean).join(" "),
328
+ className: ["datepicker", `datepicker--${size}`, className].filter(Boolean).join(" "),
185
329
  "data-focused": focused || open || void 0,
186
330
  "data-filled": filled || void 0,
187
331
  "data-failed": failed || inputInvalid || void 0,
188
- "data-disabled": disabled || void 0,
332
+ "data-disabled": !interactive || void 0,
189
333
  children: [
190
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "datepicker__field", onClick: () => {
191
- var _a;
192
- return !disabled && ((_a = inputRef.current) == null ? void 0 : _a.focus());
193
- }, children: [
194
- label && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "datepicker__label", children: label }),
195
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
196
- "input",
197
- {
198
- ref: inputRef,
199
- type: "text",
200
- inputMode: "numeric",
201
- className: "datepicker__input",
202
- value: inputValue,
203
- placeholder: label && !focused ? void 0 : placeholder,
204
- disabled,
205
- onChange: handleChange,
206
- onKeyDown: handleKeyDown,
207
- onPaste: handlePaste,
208
- onFocus: () => {
209
- setFocused(true);
210
- if (!disabled) setOpen(true);
211
- },
212
- onBlur: () => setFocused(false),
213
- "aria-label": label != null ? label : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0430\u0442\u0443",
214
- "aria-expanded": open,
215
- "aria-haspopup": "dialog",
216
- "aria-invalid": inputInvalid || void 0
217
- }
218
- )
219
- ] }),
220
- open && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "datepicker__popover", role: "dialog", "aria-label": "\u041A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u044C", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
221
- Calendar,
334
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
335
+ "div",
222
336
  {
223
- mode: "single",
224
- selected,
225
- onSelect: handleSelect,
226
- startMonth: fromDate,
227
- endMonth: toDate,
228
- locale: import_locale.ru
337
+ className: "datepicker__field",
338
+ "data-icon-start": resolvedIcon && iconPosition === "start" ? true : void 0,
339
+ "data-icon-end": resolvedIcon && iconPosition === "end" ? true : void 0,
340
+ onClick: () => {
341
+ var _a;
342
+ return interactive && ((_a = inputRef.current) == null ? void 0 : _a.focus());
343
+ },
344
+ children: [
345
+ resolvedIcon && iconPosition === "start" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "datepicker__icon datepicker__icon--start", children: resolvedIcon }),
346
+ label && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "datepicker__label", children: label }),
347
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
348
+ "input",
349
+ {
350
+ ref: inputRef,
351
+ type: "text",
352
+ inputMode: "numeric",
353
+ className: "datepicker__input",
354
+ value: inputValue,
355
+ placeholder: label && !focused ? void 0 : defaultPlaceholder,
356
+ disabled: !interactive,
357
+ onChange: handleChange,
358
+ onKeyDown: handleKeyDown,
359
+ onPaste: handlePaste,
360
+ onFocus: () => {
361
+ setFocused(true);
362
+ if (interactive && !noCalendar) setOpen(true);
363
+ },
364
+ onBlur: handleBlur,
365
+ "aria-label": label != null ? label : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0430\u0442\u0443",
366
+ "aria-expanded": !noCalendar ? open : void 0,
367
+ "aria-haspopup": !noCalendar ? "dialog" : void 0,
368
+ "aria-invalid": inputInvalid || void 0
369
+ }
370
+ ),
371
+ resolvedIcon && iconPosition === "end" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "datepicker__icon datepicker__icon--end", children: resolvedIcon })
372
+ ]
229
373
  }
230
- ) })
374
+ ),
375
+ !noCalendar && open && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
376
+ "div",
377
+ {
378
+ className: [
379
+ "datepicker__popover",
380
+ `datepicker__popover--${size}`,
381
+ timeFormat && "datepicker__popover--with-time"
382
+ ].filter(Boolean).join(" "),
383
+ role: "dialog",
384
+ "aria-label": "\u041A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u044C",
385
+ children: timeFormat ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
386
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "datepicker__popover-body", children: [
387
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "datepicker__popover-calendar", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
388
+ Calendar,
389
+ {
390
+ mode: "single",
391
+ selected,
392
+ onSelect: handleCalendarSelect,
393
+ startMonth: fromDate,
394
+ endMonth: toDate,
395
+ locale: import_locale.ru
396
+ }
397
+ ) }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "datepicker__time-separator" }),
399
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "datepicker__popover-time", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
400
+ TimePanel,
401
+ {
402
+ value: selected,
403
+ showSeconds,
404
+ onChange: handleTimeChange
405
+ }
406
+ ) })
407
+ ] }),
408
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "datepicker__popover-footer", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
409
+ "button",
410
+ {
411
+ className: "datepicker__ok-btn",
412
+ type: "button",
413
+ onClick: () => setOpen(false),
414
+ children: "OK"
415
+ }
416
+ ) })
417
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
418
+ Calendar,
419
+ {
420
+ mode: "single",
421
+ selected,
422
+ onSelect: handleCalendarSelect,
423
+ startMonth: fromDate,
424
+ endMonth: toDate,
425
+ locale: import_locale.ru
426
+ }
427
+ )
428
+ }
429
+ )
231
430
  ]
232
431
  }
233
432
  );
234
433
  }
235
434
 
236
435
  // src/components/DateRangePicker/DateRangePicker.tsx
237
- var import_react3 = require("react");
436
+ var import_react4 = require("react");
238
437
  var import_date_fns2 = require("date-fns");
239
438
  var import_locale2 = require("date-fns/locale");
240
- var import_jsx_runtime3 = require("react/jsx-runtime");
439
+ var import_jsx_runtime6 = require("react/jsx-runtime");
241
440
  var DATE_FORMAT2 = "dd.MM.yyyy";
242
441
  function applyDateMask(digits) {
243
442
  const d = digits.slice(0, 8);
@@ -266,7 +465,7 @@ function getRangeCursorPos(masked, digitCount) {
266
465
  }
267
466
  return masked.length;
268
467
  }
269
- function parseDate2(masked) {
468
+ function parseDate(masked) {
270
469
  if (masked.replace(/\D/g, "").length !== 8) return void 0;
271
470
  const date = (0, import_date_fns2.parse)(masked, DATE_FORMAT2, /* @__PURE__ */ new Date());
272
471
  return (0, import_date_fns2.isValid)(date) && (0, import_date_fns2.format)(date, DATE_FORMAT2) === masked ? date : void 0;
@@ -277,6 +476,11 @@ function formatRange(from, to) {
277
476
  if (!to) return fromStr;
278
477
  return `${fromStr} \u2014 ${(0, import_date_fns2.format)(to, DATE_FORMAT2)}`;
279
478
  }
479
+ function resolveShowSeconds(showTime) {
480
+ if (!showTime) return false;
481
+ if (showTime === true) return true;
482
+ return showTime.format === "HH:mm:ss";
483
+ }
280
484
  function DateRangePicker({
281
485
  value,
282
486
  defaultValue,
@@ -286,46 +490,98 @@ function DateRangePicker({
286
490
  toDate: toConstraint,
287
491
  disabled = false,
288
492
  failed = false,
493
+ loading = false,
494
+ size = "m",
495
+ calendarLayout = "vertical",
496
+ showTime,
497
+ icon,
498
+ iconPosition = "end",
289
499
  className
290
500
  }) {
501
+ const resolvedIcon = loading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, {}) : icon === false ? null : icon != null ? icon : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CalendarIcon, {});
291
502
  const isControlled = value !== void 0;
292
- const [internalFrom, setInternalFrom] = (0, import_react3.useState)(defaultValue == null ? void 0 : defaultValue.from);
293
- const [internalTo, setInternalTo] = (0, import_react3.useState)(defaultValue == null ? void 0 : defaultValue.to);
294
- const [inputValue, setInputValue] = (0, import_react3.useState)(
503
+ const showSeconds = resolveShowSeconds(showTime);
504
+ const [internalFrom, setInternalFrom] = (0, import_react4.useState)(defaultValue == null ? void 0 : defaultValue.from);
505
+ const [internalTo, setInternalTo] = (0, import_react4.useState)(defaultValue == null ? void 0 : defaultValue.to);
506
+ const [inputValue, setInputValue] = (0, import_react4.useState)(
295
507
  () => formatRange(defaultValue == null ? void 0 : defaultValue.from, defaultValue == null ? void 0 : defaultValue.to)
296
508
  );
297
- const [inputInvalid, setInputInvalid] = (0, import_react3.useState)(false);
298
- const [open, setOpen] = (0, import_react3.useState)(false);
299
- const [focused, setFocused] = (0, import_react3.useState)(false);
300
- const [anchorDate, setAnchorDate] = (0, import_react3.useState)(void 0);
301
- const [hoveredDate, setHoveredDate] = (0, import_react3.useState)(void 0);
302
- const inputRef = (0, import_react3.useRef)(null);
303
- const containerRef = (0, import_react3.useRef)(null);
509
+ const [inputInvalid, setInputInvalid] = (0, import_react4.useState)(false);
510
+ const [open, setOpen] = (0, import_react4.useState)(false);
511
+ const [focused, setFocused] = (0, import_react4.useState)(false);
512
+ const [anchorDate, setAnchorDate] = (0, import_react4.useState)(void 0);
513
+ const [hoveredDate, setHoveredDate] = (0, import_react4.useState)(void 0);
514
+ const inputRef = (0, import_react4.useRef)(null);
515
+ const containerRef = (0, import_react4.useRef)(null);
516
+ const lastEmittedFromRef = (0, import_react4.useRef)(value !== void 0 ? value == null ? void 0 : value.from : defaultValue == null ? void 0 : defaultValue.from);
517
+ const lastEmittedToRef = (0, import_react4.useRef)(value !== void 0 ? value == null ? void 0 : value.to : defaultValue == null ? void 0 : defaultValue.to);
518
+ const wasControlledRef = (0, import_react4.useRef)(value !== void 0);
304
519
  const confirmedFrom = isControlled ? value == null ? void 0 : value.from : internalFrom;
305
520
  const confirmedTo = isControlled ? value == null ? void 0 : value.to : internalTo;
306
521
  const filled = inputValue.length > 0;
307
- const close = (0, import_react3.useCallback)(() => {
522
+ const close = (0, import_react4.useCallback)(() => {
308
523
  setOpen(false);
309
524
  setAnchorDate(void 0);
310
525
  setHoveredDate(void 0);
311
526
  }, []);
312
527
  useClickOutside(containerRef, close);
528
+ (0, import_react4.useEffect)(() => {
529
+ var _a, _b, _c, _d, _e, _f;
530
+ if (value !== void 0) wasControlledRef.current = true;
531
+ const newFrom = value == null ? void 0 : value.from;
532
+ const newTo = value == null ? void 0 : value.to;
533
+ const fromTime = (_a = newFrom == null ? void 0 : newFrom.getTime()) != null ? _a : null;
534
+ const toTime = (_b = newTo == null ? void 0 : newTo.getTime()) != null ? _b : null;
535
+ const lastFromTime = (_d = (_c = lastEmittedFromRef.current) == null ? void 0 : _c.getTime()) != null ? _d : null;
536
+ const lastToTime = (_f = (_e = lastEmittedToRef.current) == null ? void 0 : _e.getTime()) != null ? _f : null;
537
+ if (fromTime === lastFromTime && toTime === lastToTime) return;
538
+ if (!wasControlledRef.current && value === void 0) return;
539
+ setInputValue(formatRange(newFrom, newTo));
540
+ setInputInvalid(false);
541
+ if (!isControlled) {
542
+ setInternalFrom(newFrom);
543
+ setInternalTo(newTo);
544
+ }
545
+ setAnchorDate(void 0);
546
+ setHoveredDate(void 0);
547
+ lastEmittedFromRef.current = newFrom;
548
+ lastEmittedToRef.current = newTo;
549
+ }, [value]);
313
550
  const calendarSelected = anchorDate ? hoveredDate ? anchorDate <= hoveredDate ? { from: anchorDate, to: hoveredDate } : { from: hoveredDate, to: anchorDate } : { from: anchorDate, to: void 0 } : { from: confirmedFrom, to: confirmedTo };
314
551
  function handleDayClick(day) {
552
+ var _a, _b, _c, _d, _e, _f;
315
553
  if (!anchorDate) {
316
- setAnchorDate(day);
554
+ const from = showTime ? new Date(
555
+ day.getFullYear(),
556
+ day.getMonth(),
557
+ day.getDate(),
558
+ (_a = confirmedFrom == null ? void 0 : confirmedFrom.getHours()) != null ? _a : 0,
559
+ (_b = confirmedFrom == null ? void 0 : confirmedFrom.getMinutes()) != null ? _b : 0,
560
+ (_c = confirmedFrom == null ? void 0 : confirmedFrom.getSeconds()) != null ? _c : 0
561
+ ) : day;
562
+ setAnchorDate(from);
317
563
  if (!isControlled) {
318
- setInternalFrom(day);
564
+ setInternalFrom(from);
319
565
  setInternalTo(void 0);
320
566
  }
321
567
  setInputValue((0, import_date_fns2.format)(day, DATE_FORMAT2));
322
568
  setInputInvalid(false);
323
- onChange == null ? void 0 : onChange(day ? { from: day, to: void 0 } : void 0);
569
+ lastEmittedFromRef.current = from;
570
+ lastEmittedToRef.current = void 0;
571
+ onChange == null ? void 0 : onChange({ from, to: void 0 });
324
572
  } else {
325
- let from = anchorDate, to = day;
573
+ let from = anchorDate, to = showTime ? new Date(
574
+ day.getFullYear(),
575
+ day.getMonth(),
576
+ day.getDate(),
577
+ (_d = confirmedTo == null ? void 0 : confirmedTo.getHours()) != null ? _d : 0,
578
+ (_e = confirmedTo == null ? void 0 : confirmedTo.getMinutes()) != null ? _e : 0,
579
+ (_f = confirmedTo == null ? void 0 : confirmedTo.getSeconds()) != null ? _f : 0
580
+ ) : day;
326
581
  if (day < anchorDate) {
327
- from = day;
328
- to = anchorDate;
582
+ const tmp = from;
583
+ from = to;
584
+ to = tmp;
329
585
  }
330
586
  if (!isControlled) {
331
587
  setInternalFrom(from);
@@ -333,13 +589,30 @@ function DateRangePicker({
333
589
  }
334
590
  setInputValue(formatRange(from, to));
335
591
  setInputInvalid(false);
592
+ lastEmittedFromRef.current = from;
593
+ lastEmittedToRef.current = to;
336
594
  onChange == null ? void 0 : onChange({ from, to });
337
- close();
595
+ if (!showTime) close();
596
+ else setAnchorDate(void 0);
338
597
  }
339
598
  }
340
599
  function handleDayMouseEnter(day) {
341
600
  if (anchorDate) setHoveredDate(day);
342
601
  }
602
+ function handleFromTimeChange(h, m, s) {
603
+ const base = confirmedFrom != null ? confirmedFrom : /* @__PURE__ */ new Date();
604
+ const newDate = new Date(base.getFullYear(), base.getMonth(), base.getDate(), h, m, s);
605
+ if (!isControlled) setInternalFrom(newDate);
606
+ lastEmittedFromRef.current = newDate;
607
+ onChange == null ? void 0 : onChange({ from: newDate, to: confirmedTo });
608
+ }
609
+ function handleToTimeChange(h, m, s) {
610
+ const base = confirmedTo != null ? confirmedTo : /* @__PURE__ */ new Date();
611
+ const newDate = new Date(base.getFullYear(), base.getMonth(), base.getDate(), h, m, s);
612
+ if (!isControlled) setInternalTo(newDate);
613
+ lastEmittedToRef.current = newDate;
614
+ onChange == null ? void 0 : onChange({ from: confirmedFrom, to: newDate });
615
+ }
343
616
  function handleChange(e) {
344
617
  var _a;
345
618
  const input = e.target;
@@ -353,8 +626,8 @@ function DateRangePicker({
353
626
  setHoveredDate(void 0);
354
627
  const fromDigits = digits.slice(0, 8);
355
628
  const toDigits = digits.slice(8);
356
- const parsedFrom = fromDigits.length === 8 ? parseDate2(applyDateMask(fromDigits)) : void 0;
357
- const parsedTo = toDigits.length === 8 ? parseDate2(applyDateMask(toDigits)) : void 0;
629
+ const parsedFrom = fromDigits.length === 8 ? parseDate(applyDateMask(fromDigits)) : void 0;
630
+ const parsedTo = toDigits.length === 8 ? parseDate(applyDateMask(toDigits)) : void 0;
358
631
  const fromComplete = fromDigits.length === 8;
359
632
  const toComplete = toDigits.length === 8;
360
633
  setInputInvalid(fromComplete && !parsedFrom || toComplete && !parsedTo);
@@ -362,6 +635,8 @@ function DateRangePicker({
362
635
  setInternalFrom(parsedFrom);
363
636
  setInternalTo(parsedTo);
364
637
  }
638
+ lastEmittedFromRef.current = parsedFrom;
639
+ lastEmittedToRef.current = parsedTo;
365
640
  onChange == null ? void 0 : onChange(parsedFrom || parsedTo ? { from: parsedFrom, to: parsedTo } : void 0);
366
641
  requestAnimationFrame(
367
642
  () => {
@@ -399,13 +674,15 @@ function DateRangePicker({
399
674
  setInputValue(masked);
400
675
  setAnchorDate(void 0);
401
676
  setHoveredDate(void 0);
402
- const parsedFrom = digits.length >= 8 ? parseDate2(applyDateMask(digits.slice(0, 8))) : void 0;
403
- const parsedTo = digits.length >= 16 ? parseDate2(applyDateMask(digits.slice(8, 16))) : void 0;
677
+ const parsedFrom = digits.length >= 8 ? parseDate(applyDateMask(digits.slice(0, 8))) : void 0;
678
+ const parsedTo = digits.length >= 16 ? parseDate(applyDateMask(digits.slice(8, 16))) : void 0;
404
679
  setInputInvalid(digits.length >= 8 && !parsedFrom || digits.length >= 16 && !parsedTo);
405
680
  if (!isControlled) {
406
681
  setInternalFrom(parsedFrom);
407
682
  setInternalTo(parsedTo);
408
683
  }
684
+ lastEmittedFromRef.current = parsedFrom;
685
+ lastEmittedToRef.current = parsedTo;
409
686
  onChange == null ? void 0 : onChange(parsedFrom || parsedTo ? { from: parsedFrom, to: parsedTo } : void 0);
410
687
  requestAnimationFrame(() => {
411
688
  var _a;
@@ -413,74 +690,149 @@ function DateRangePicker({
413
690
  });
414
691
  }
415
692
  const placeholder = label && !focused && !filled ? void 0 : "\u0434\u0434.\u043C\u043C.\u0433\u0433\u0433\u0433 \u2014 \u0434\u0434.\u043C\u043C.\u0433\u0433\u0433\u0433";
416
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
693
+ const interactive = !disabled && !loading;
694
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
417
695
  "div",
418
696
  {
419
697
  ref: containerRef,
420
- className: ["datepicker", "daterangepicker", className].filter(Boolean).join(" "),
698
+ className: ["datepicker", "daterangepicker", `datepicker--${size}`, className].filter(Boolean).join(" "),
421
699
  "data-focused": focused || open || void 0,
422
700
  "data-filled": filled || void 0,
423
701
  "data-failed": failed || inputInvalid || void 0,
424
- "data-disabled": disabled || void 0,
702
+ "data-disabled": !interactive || void 0,
425
703
  children: [
426
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "datepicker__field", onClick: () => {
427
- var _a;
428
- return !disabled && ((_a = inputRef.current) == null ? void 0 : _a.focus());
429
- }, children: [
430
- label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "datepicker__label", children: label }),
431
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
432
- "input",
433
- {
434
- ref: inputRef,
435
- type: "text",
436
- inputMode: "numeric",
437
- className: "datepicker__input",
438
- value: inputValue,
439
- placeholder,
440
- disabled,
441
- onChange: handleChange,
442
- onKeyDown: handleKeyDown,
443
- onPaste: handlePaste,
444
- onFocus: () => {
445
- setFocused(true);
446
- if (!disabled) setOpen(true);
447
- },
448
- onBlur: () => setFocused(false),
449
- "aria-label": label != null ? label : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0435\u0440\u0438\u043E\u0434",
450
- "aria-expanded": open,
451
- "aria-haspopup": "dialog",
452
- "aria-invalid": inputInvalid || void 0
453
- }
454
- )
455
- ] }),
456
- open && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "datepicker__popover", role: "dialog", "aria-label": "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0435\u0440\u0438\u043E\u0434", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
457
- Calendar,
704
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
705
+ "div",
458
706
  {
459
- mode: "range",
460
- selected: calendarSelected,
461
- onSelect: () => {
707
+ className: "datepicker__field",
708
+ "data-icon-start": resolvedIcon && iconPosition === "start" ? true : void 0,
709
+ "data-icon-end": resolvedIcon && iconPosition === "end" ? true : void 0,
710
+ onClick: () => {
711
+ var _a;
712
+ return interactive && ((_a = inputRef.current) == null ? void 0 : _a.focus());
462
713
  },
463
- onDayClick: handleDayClick,
464
- onDayMouseEnter: handleDayMouseEnter,
465
- onDayMouseLeave: () => setHoveredDate(void 0),
466
- startMonth: fromConstraint,
467
- endMonth: toConstraint,
468
- numberOfMonths: 2,
469
- locale: import_locale2.ru
714
+ children: [
715
+ resolvedIcon && iconPosition === "start" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "datepicker__icon datepicker__icon--start", children: resolvedIcon }),
716
+ label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "datepicker__label", children: label }),
717
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
718
+ "input",
719
+ {
720
+ ref: inputRef,
721
+ type: "text",
722
+ inputMode: "numeric",
723
+ className: "datepicker__input",
724
+ value: inputValue,
725
+ placeholder,
726
+ disabled: !interactive,
727
+ onChange: handleChange,
728
+ onKeyDown: handleKeyDown,
729
+ onPaste: handlePaste,
730
+ onFocus: () => {
731
+ setFocused(true);
732
+ if (interactive) setOpen(true);
733
+ },
734
+ onBlur: () => setFocused(false),
735
+ "aria-label": label != null ? label : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0435\u0440\u0438\u043E\u0434",
736
+ "aria-expanded": open,
737
+ "aria-haspopup": "dialog",
738
+ "aria-invalid": inputInvalid || void 0
739
+ }
740
+ ),
741
+ resolvedIcon && iconPosition === "end" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "datepicker__icon datepicker__icon--end", children: resolvedIcon })
742
+ ]
470
743
  }
471
- ) })
744
+ ),
745
+ open && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
746
+ "div",
747
+ {
748
+ className: [
749
+ "datepicker__popover",
750
+ `datepicker__popover--${size}`,
751
+ calendarLayout === "horizontal" && "datepicker__popover--horizontal",
752
+ showTime && "datepicker__popover--with-time"
753
+ ].filter(Boolean).join(" "),
754
+ role: "dialog",
755
+ "aria-label": "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0435\u0440\u0438\u043E\u0434",
756
+ children: showTime ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
757
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "datepicker__popover-body", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "datepicker__popover-calendar", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
758
+ Calendar,
759
+ {
760
+ mode: "range",
761
+ selected: calendarSelected,
762
+ onSelect: () => {
763
+ },
764
+ onDayClick: handleDayClick,
765
+ onDayMouseEnter: handleDayMouseEnter,
766
+ onDayMouseLeave: () => setHoveredDate(void 0),
767
+ startMonth: fromConstraint,
768
+ endMonth: toConstraint,
769
+ numberOfMonths: 2,
770
+ locale: import_locale2.ru
771
+ }
772
+ ) }) }),
773
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "datepicker__time-row", children: [
774
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "datepicker__time-col", children: [
775
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "datepicker__time-label", children: "\u041D\u0430\u0447\u0430\u043B\u043E" }),
776
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TimePanel, { value: confirmedFrom, showSeconds, onChange: handleFromTimeChange })
777
+ ] }),
778
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "datepicker__time-separator" }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "datepicker__time-col", children: [
780
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "datepicker__time-label", children: "\u041A\u043E\u043D\u0435\u0446" }),
781
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(TimePanel, { value: confirmedTo, showSeconds, onChange: handleToTimeChange })
782
+ ] })
783
+ ] }),
784
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "datepicker__popover-footer", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { className: "datepicker__ok-btn", type: "button", onClick: close, children: "OK" }) })
785
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
786
+ Calendar,
787
+ {
788
+ mode: "range",
789
+ selected: calendarSelected,
790
+ onSelect: () => {
791
+ },
792
+ onDayClick: handleDayClick,
793
+ onDayMouseEnter: handleDayMouseEnter,
794
+ onDayMouseLeave: () => setHoveredDate(void 0),
795
+ startMonth: fromConstraint,
796
+ endMonth: toConstraint,
797
+ numberOfMonths: 2,
798
+ locale: import_locale2.ru
799
+ }
800
+ )
801
+ }
802
+ )
472
803
  ]
473
804
  }
474
805
  );
475
806
  }
476
807
 
477
- // src/index.ts
478
- var VERSION = "0.0.1";
808
+ // src/components/Button/Button.tsx
809
+ var import_jsx_runtime7 = require("react/jsx-runtime");
810
+ function Button({
811
+ variant = "primary",
812
+ size = "m",
813
+ loading = false,
814
+ disabled,
815
+ className,
816
+ children,
817
+ ...rest
818
+ }) {
819
+ const classes = [
820
+ "dp-btn",
821
+ `dp-btn--${variant}`,
822
+ `dp-btn--${size}`,
823
+ loading && "dp-btn--loading",
824
+ className
825
+ ].filter(Boolean).join(" ");
826
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("button", { ...rest, className: classes, disabled: disabled || loading, children: [
827
+ children,
828
+ loading && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Spinner, {})
829
+ ] });
830
+ }
479
831
  // Annotate the CommonJS export names for ESM import in node:
480
832
  0 && (module.exports = {
833
+ Button,
481
834
  Calendar,
482
835
  DatePicker,
483
- DateRangePicker,
484
- VERSION
836
+ DateRangePicker
485
837
  });
486
838
  //# sourceMappingURL=index.cjs.map