cynx-ui 1.2.48 → 1.3.1

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.
@@ -0,0 +1,1070 @@
1
+ import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Clock, ChevronDown, ChevronUp, Check, X, Globe, List, Sliders } from 'lucide-react';
4
+
5
+ const STYLES = `
6
+ @keyframes cynxTimePickerPopIn {
7
+ 0% {
8
+ opacity: 0;
9
+ transform: translateY(-8px) scale(0.96);
10
+ }
11
+ 100% {
12
+ opacity: 1;
13
+ transform: translateY(0) scale(1);
14
+ }
15
+ }
16
+
17
+ @keyframes cynxTimePickerPopOut {
18
+ 0% {
19
+ opacity: 1;
20
+ transform: translateY(0) scale(1);
21
+ }
22
+ 100% {
23
+ opacity: 0;
24
+ transform: translateY(-8px) scale(0.96);
25
+ }
26
+ }
27
+
28
+ .cynx-timepicker-drop {
29
+ animation: cynxTimePickerPopIn 0.18s cubic-bezier(0.16, 1, 0.3, 1) forwards;
30
+ will-change: opacity, transform;
31
+ }
32
+
33
+ .cynx-timepicker-drop.closing {
34
+ animation: cynxTimePickerPopOut 0.14s cubic-bezier(0.16, 1, 0.3, 1) forwards;
35
+ }
36
+
37
+ .cynx-timepicker-cell {
38
+ transition: background-color 0.12s ease, color 0.12s ease;
39
+ }
40
+
41
+ .cynx-timepicker-cell:hover {
42
+ background-color: var(--grey-1, var(--surface-hover));
43
+ }
44
+
45
+ .cynx-timepicker-stepper-btn {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ width: 100%;
50
+ height: 28px;
51
+ border-radius: var(--rads, 6px);
52
+ border: 1px solid var(--border-light, var(--border));
53
+ background: var(--grey-1, var(--surface-hover));
54
+ color: var(--fg, var(--t1));
55
+ cursor: pointer;
56
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
57
+ }
58
+
59
+ .cynx-timepicker-stepper-btn:hover {
60
+ background: var(--surface, var(--bg));
61
+ border-color: var(--accent, var(--blue));
62
+ color: var(--accent, var(--blue));
63
+ }
64
+
65
+ .cynx-timepicker-stepper-btn:active {
66
+ background: var(--accent-bg, rgba(37,99,235,0.08));
67
+ }
68
+ `;
69
+
70
+ const STYLE_ID = 'cynx-timepicker-styles';
71
+
72
+ function ensureStyles() {
73
+ if (typeof document === 'undefined') return;
74
+ if (document.getElementById(STYLE_ID)) return;
75
+ const tag = document.createElement('style');
76
+ tag.id = STYLE_ID;
77
+ tag.textContent = STYLES;
78
+ document.head.appendChild(tag);
79
+ }
80
+
81
+ const COMMON_TIMEZONES = [
82
+ { value: 'system', label: 'Системное время (Local)' },
83
+ { value: 'Asia/Tashkent', label: 'Ташкент (UTC+5)' },
84
+ { value: 'America/New_York', label: 'Нью-Йорк (UTC-5)' },
85
+ { value: 'Europe/London', label: 'Лондон (UTC+0)' },
86
+ { value: 'Asia/Tokyo', label: 'Токио (UTC+9)' },
87
+ { value: 'UTC', label: 'UTC' },
88
+ ];
89
+
90
+ function pad(n) {
91
+ return String(n).padStart(2, '0');
92
+ }
93
+
94
+ function to12h(h24) {
95
+ const h = parseInt(h24, 10);
96
+ if (isNaN(h)) return { h12: '12', ampm: 'AM' };
97
+ const ampm = h >= 12 ? 'PM' : 'AM';
98
+ let h12 = h % 12;
99
+ if (h12 === 0) h12 = 12;
100
+ return { h12: pad(h12), ampm };
101
+ }
102
+
103
+ function to24h(h12, ampm) {
104
+ let h = parseInt(h12, 10);
105
+ if (isNaN(h)) return '00';
106
+ if (ampm === 'PM' && h < 12) h += 12;
107
+ if (ampm === 'AM' && h === 12) h = 0;
108
+ return pad(h);
109
+ }
110
+
111
+ function parseTime(val, is12h, showSeconds) {
112
+ if (!val || typeof val !== 'string') return { h24: '00', h12: '12', m: '00', s: '00', ampm: 'AM' };
113
+
114
+ let str = val.trim();
115
+ let ampm = 'AM';
116
+ if (/pm/i.test(str)) { ampm = 'PM'; str = str.replace(/pm/i, '').trim(); }
117
+ else if (/am/i.test(str)) { ampm = 'AM'; str = str.replace(/am/i, '').trim(); }
118
+
119
+ let p0 = 0, p1 = 0, p2 = 0;
120
+
121
+ if (str.includes(':')) {
122
+ const parts = str.split(':').map(p => p.trim());
123
+ p0 = parseInt(parts[0], 10) || 0;
124
+ p1 = parseInt(parts[1], 10) || 0;
125
+ p2 = parseInt(parts[2], 10) || 0;
126
+ } else if (/^\d{3,6}$/.test(str)) {
127
+ p0 = parseInt(str.slice(0, 2), 10) || 0;
128
+ p1 = parseInt(str.slice(2, 4), 10) || 0;
129
+ p2 = parseInt(str.slice(4, 6), 10) || 0;
130
+ } else {
131
+ p0 = parseInt(str, 10) || 0;
132
+ }
133
+
134
+ let h24 = '00';
135
+ let h12 = '12';
136
+
137
+ if (is12h) {
138
+ if (p0 > 12) {
139
+ h24 = pad(Math.min(23, Math.max(0, p0)));
140
+ const conv = to12h(h24);
141
+ h12 = conv.h12;
142
+ ampm = conv.ampm;
143
+ } else {
144
+ let hNum = Math.min(12, Math.max(1, p0));
145
+ h12 = pad(hNum);
146
+ h24 = to24h(h12, ampm);
147
+ }
148
+ } else {
149
+ let hNum = Math.min(23, Math.max(0, p0));
150
+ h24 = pad(hNum);
151
+ const conv = to12h(h24);
152
+ h12 = conv.h12;
153
+ ampm = conv.ampm;
154
+ }
155
+
156
+ const m = pad(Math.min(59, Math.max(0, p1)));
157
+ const s = pad(Math.min(59, Math.max(0, p2)));
158
+
159
+ return { h24, h12, m, s, ampm };
160
+ }
161
+
162
+ export function TimePicker({
163
+ value = '',
164
+ onChange,
165
+ variant = 'list', // 'stepper' (+/- buttons) | 'list' (full 00..59 scrollable list)
166
+ showVariantToggle = false, // Default false: clean list mode by default!
167
+ format = '24h', // '24h' | '12h'
168
+ showSeconds = false,
169
+ showIcon = false, // Default false: clock icon hidden by default, set showIcon={true} to display
170
+ hideIcon = false, // Legacy prop alias for showIcon = false
171
+ showFormatToggle = false, // Minimal default: false
172
+ showPresets = false, // Minimal default: false
173
+ showTimezoneSelect = false, // Minimal default: false
174
+ showPlaceholder = false, // Minimal default: false
175
+ timezone = 'Asia/Tashkent',
176
+ label,
177
+ placeholder,
178
+ size = 'sm', // 'xs' | 'sm' | 'md'
179
+ disabled = false,
180
+ readOnly = false,
181
+ style = {},
182
+ className = '',
183
+ }) {
184
+ const displayClockIcon = showIcon && !hideIcon;
185
+ const [open, setOpen] = useState(false);
186
+ const [rendered, setRendered] = useState(false);
187
+ const [closing, setClosing] = useState(false);
188
+
189
+ const [activeVariant, setActiveVariant] = useState(variant);
190
+ const [mode, setMode] = useState(format);
191
+ const [tz, setTz] = useState(timezone);
192
+ const [showTzPicker, setShowTzPicker] = useState(false);
193
+
194
+ const btnRef = useRef(null);
195
+ const dropRef = useRef(null);
196
+ const isTypingRef = useRef(false);
197
+
198
+ const [pos, setPos] = useState({ top: 0, left: 0, width: 230 });
199
+
200
+ ensureStyles();
201
+
202
+ useEffect(() => { setActiveVariant(variant); }, [variant]);
203
+ useEffect(() => { setMode(format); }, [format]);
204
+ useEffect(() => { setTz(timezone); }, [timezone]);
205
+
206
+ const openPicker = useCallback(() => {
207
+ if (disabled || readOnly) return;
208
+ setRendered(true);
209
+ setClosing(false);
210
+ setOpen(true);
211
+ }, [disabled, readOnly]);
212
+
213
+ const closePicker = useCallback(() => {
214
+ if (!open) return;
215
+ setClosing(true);
216
+ setOpen(false);
217
+ setShowTzPicker(false);
218
+ setTimeout(() => {
219
+ setRendered(false);
220
+ setClosing(false);
221
+ }, 140);
222
+ }, [open]);
223
+
224
+ const togglePicker = useCallback(() => {
225
+ if (open) closePicker();
226
+ else openPicker();
227
+ }, [open, closePicker, openPicker]);
228
+
229
+ const formatOutput = useCallback((parsedObj, useMode = mode) => {
230
+ const { h24, h12, m, s, ampm } = parsedObj;
231
+ if (useMode === '12h') {
232
+ return showSeconds ? `${h12}:${m}:${s} ${ampm}` : `${h12}:${m} ${ampm}`;
233
+ }
234
+ return showSeconds ? `${h24}:${m}:${s}` : `${h24}:${m}`;
235
+ }, [mode, showSeconds]);
236
+
237
+ const parsed = parseTime(value, mode === '12h', showSeconds);
238
+ const [inputValue, setInputValue] = useState(() => value ? formatOutput(parsed) : '');
239
+
240
+ // Keep inputValue in sync with props ONLY when user is NOT actively typing!
241
+ useEffect(() => {
242
+ if (!isTypingRef.current) {
243
+ if (value) {
244
+ const p = parseTime(value, mode === '12h', showSeconds);
245
+ setInputValue(formatOutput(p));
246
+ } else {
247
+ setInputValue('');
248
+ }
249
+ }
250
+ }, [value, mode, showSeconds, formatOutput]);
251
+
252
+ const calcPos = useCallback(() => {
253
+ if (!btnRef.current) return;
254
+ const r = btnRef.current.getBoundingClientRect();
255
+ const dropW = mode === '12h' ? 290 : 230;
256
+ const dropH = dropRef.current ? dropRef.current.offsetHeight : 230;
257
+
258
+ let left = r.left;
259
+ if (left + dropW > window.innerWidth - 12) {
260
+ left = Math.max(12, r.right - dropW);
261
+ }
262
+ if (left < 12) left = 12;
263
+
264
+ let top = r.bottom + 4;
265
+ if (top + dropH > window.innerHeight - 12 && r.top - 4 - dropH > 12) {
266
+ top = r.top - 4 - dropH;
267
+ }
268
+
269
+ setPos({ top, left, width: Math.max(r.width, dropW) });
270
+ }, [mode]);
271
+
272
+ useLayoutEffect(() => {
273
+ if (!rendered) return;
274
+ calcPos();
275
+ }, [rendered, calcPos]);
276
+
277
+ useEffect(() => {
278
+ if (!rendered || closing) return;
279
+ function onDown(e) {
280
+ const inBtn = btnRef.current && btnRef.current.contains(e.target);
281
+ const inDrop = dropRef.current && dropRef.current.contains(e.target);
282
+ if (!inBtn && !inDrop) {
283
+ closePicker();
284
+ }
285
+ }
286
+ const timer = setTimeout(() => {
287
+ document.addEventListener('mousedown', onDown, true);
288
+ }, 0);
289
+ window.addEventListener('scroll', calcPos, true);
290
+ window.addEventListener('resize', calcPos);
291
+ return () => {
292
+ clearTimeout(timer);
293
+ document.removeEventListener('mousedown', onDown, true);
294
+ window.removeEventListener('scroll', calcPos, true);
295
+ window.removeEventListener('resize', calcPos);
296
+ };
297
+ }, [rendered, closing, calcPos, closePicker]);
298
+
299
+ const emitChange = (parsedObj, useMode = mode) => {
300
+ const formatted = formatOutput(parsedObj, useMode);
301
+ setInputValue(formatted);
302
+ if (onChange) {
303
+ onChange(formatted, {
304
+ hours: parseInt(parsedObj.h24, 10),
305
+ minutes: parseInt(parsedObj.m, 10),
306
+ seconds: parseInt(parsedObj.s, 10),
307
+ ampm: parsedObj.ampm,
308
+ format: useMode,
309
+ timezone: tz,
310
+ variant: activeVariant,
311
+ });
312
+ }
313
+ };
314
+
315
+ const handleKeyDown = (e) => {
316
+ if (
317
+ ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key) ||
318
+ e.ctrlKey || e.metaKey || e.altKey
319
+ ) {
320
+ return;
321
+ }
322
+ const allowed = mode === '12h' ? /^[0-9:\sAPMapm]$/ : /^[0-9:]$/;
323
+ if (!allowed.test(e.key)) {
324
+ e.preventDefault();
325
+ }
326
+ };
327
+
328
+ const handleInputChange = (e) => {
329
+ let raw = e.target.value;
330
+ isTypingRef.current = true;
331
+
332
+ // 1. Strict filter: allow ONLY digits and colon (and AM/PM in 12h)
333
+ if (mode === '12h') {
334
+ raw = raw.replace(/[^0-9:\sAPMapm]/g, '');
335
+ } else {
336
+ raw = raw.replace(/[^0-9:]/g, '');
337
+ }
338
+
339
+ // 2. Single-digit hour shortcut (e.g. typing "3" -> "03:")
340
+ if (mode === '24h' && /^[3-9]$/.test(raw)) {
341
+ raw = '0' + raw + ':';
342
+ } else if (mode === '12h' && /^[2-9]$/.test(raw) && !/pm|am/i.test(raw)) {
343
+ raw = '0' + raw + ':';
344
+ }
345
+
346
+ // 3. Auto-append 1st colon after 2 hour digits + cap hour max (23 in 24h, 12 in 12h)
347
+ if (/^\d{2}$/.test(raw) && !inputValue.endsWith(':')) {
348
+ const hNum = parseInt(raw, 10);
349
+ if (mode === '24h' && hNum > 23) raw = '23:';
350
+ else if (mode === '12h' && hNum > 12) raw = '12:';
351
+ else raw = raw + ':';
352
+ }
353
+
354
+ // 4. Minutes tens digit validation: first digit of minute (after 1st colon) CANNOT be > 5!
355
+ if (/^\d{2}:[6-9]/.test(raw)) {
356
+ raw = raw.replace(/^(\d{2}:)[6-9]/, '$15');
357
+ }
358
+
359
+ // 5. Auto-append 2nd colon after 2 minute digits if showSeconds is true (e.g. "16:16" -> "16:16:")
360
+ if (showSeconds && /^\d{2}:\d{2}$/.test(raw) && !inputValue.endsWith(':')) {
361
+ const mNum = parseInt(raw.slice(3, 5), 10);
362
+ if (mNum > 59) raw = raw.slice(0, 3) + '59:';
363
+ else raw = raw + ':';
364
+ }
365
+
366
+ // 6. Seconds tens digit validation (if showSeconds): first digit of second CANNOT be > 5!
367
+ if (showSeconds && /^\d{2}:\d{2}:[6-9]/.test(raw)) {
368
+ raw = raw.replace(/^(\d{2}:\d{2}:)[6-9]/, '$15');
369
+ }
370
+
371
+ // 7. Prevent 3rd minute digit when showSeconds is FALSE! (e.g. "16:166" -> "16:16")
372
+ if (!showSeconds && /^\d{2}:\d{3,}/.test(raw)) {
373
+ raw = raw.slice(0, 5);
374
+ }
375
+
376
+ // 8. Prevent 3rd second digit when showSeconds is TRUE! (e.g. "16:16:666" -> "16:16:66")
377
+ if (showSeconds && /^\d{2}:\d{2}:\d{3,}/.test(raw)) {
378
+ raw = raw.slice(0, 8);
379
+ }
380
+
381
+ // 9. Strict Max length capping
382
+ const maxLen = mode === '12h' ? (showSeconds ? 11 : 8) : (showSeconds ? 8 : 5);
383
+ if (raw.length > maxLen) {
384
+ raw = raw.slice(0, maxLen);
385
+ }
386
+
387
+ e.target.value = raw;
388
+ setInputValue(raw);
389
+
390
+ if (!raw) {
391
+ if (onChange) onChange('', { hours: null, minutes: null, seconds: null, ampm: null, format: mode, timezone: tz, variant: activeVariant });
392
+ return;
393
+ }
394
+
395
+ // Emit change if raw matches a complete valid time pattern
396
+ if (/^\d{1,2}:\d{1,2}(:\d{1,2})?(\s*[ap]m)?$/i.test(raw) || /^\d{4}$/.test(raw)) {
397
+ const p = parseTime(raw, mode === '12h', showSeconds);
398
+ const formatted = formatOutput(p);
399
+ if (onChange) {
400
+ onChange(formatted, {
401
+ hours: parseInt(p.h24, 10),
402
+ minutes: parseInt(p.m, 10),
403
+ seconds: parseInt(p.s, 10),
404
+ ampm: p.ampm,
405
+ format: mode,
406
+ timezone: tz,
407
+ variant: activeVariant,
408
+ });
409
+ }
410
+ }
411
+ };
412
+
413
+ const handleInputBlur = () => {
414
+ isTypingRef.current = false;
415
+ if (!inputValue) return;
416
+ const p = parseTime(inputValue, mode === '12h', showSeconds);
417
+ emitChange(p);
418
+ };
419
+
420
+ // Stepper Up/Down (+/-) handlers
421
+ const stepHour = (delta) => {
422
+ isTypingRef.current = false;
423
+ let curH = mode === '12h' ? parseInt(parsed.h12, 10) : parseInt(parsed.h24, 10);
424
+ if (isNaN(curH)) curH = mode === '12h' ? 12 : 0;
425
+
426
+ if (mode === '12h') {
427
+ let nextH12 = curH + delta;
428
+ let nextAmPm = parsed.ampm;
429
+ if (nextH12 > 12) { nextH12 = 1; }
430
+ else if (nextH12 < 1) { nextH12 = 12; }
431
+ const h12 = pad(nextH12);
432
+ const h24 = to24h(h12, nextAmPm);
433
+ emitChange({ ...parsed, h12, h24, ampm: nextAmPm });
434
+ } else {
435
+ let nextH24 = (curH + delta + 24) % 24;
436
+ const h24 = pad(nextH24);
437
+ const { h12, ampm } = to12h(h24);
438
+ emitChange({ ...parsed, h24, h12, ampm });
439
+ }
440
+ };
441
+
442
+ const stepMinute = (delta) => {
443
+ isTypingRef.current = false;
444
+ let curM = parseInt(parsed.m, 10);
445
+ if (isNaN(curM)) curM = 0;
446
+ let nextM = (curM + delta + 60) % 60;
447
+ const m = pad(nextM);
448
+ emitChange({ ...parsed, m });
449
+ };
450
+
451
+ const stepSecond = (delta) => {
452
+ isTypingRef.current = false;
453
+ let curS = parseInt(parsed.s, 10);
454
+ if (isNaN(curS)) curS = 0;
455
+ let nextS = (curS + delta + 60) % 60;
456
+ const s = pad(nextS);
457
+ emitChange({ ...parsed, s });
458
+ };
459
+
460
+ // List direct selection handlers
461
+ const selectH24 = (hNum) => {
462
+ isTypingRef.current = false;
463
+ const h24 = pad(hNum);
464
+ const { h12, ampm } = to12h(h24);
465
+ emitChange({ ...parsed, h24, h12, ampm });
466
+ };
467
+
468
+ const selectH12 = (hNum12) => {
469
+ isTypingRef.current = false;
470
+ const h12 = pad(hNum12);
471
+ const h24 = to24h(h12, parsed.ampm);
472
+ emitChange({ ...parsed, h24, h12 });
473
+ };
474
+
475
+ const selectMin = (mNum) => {
476
+ isTypingRef.current = false;
477
+ const m = pad(mNum);
478
+ emitChange({ ...parsed, m });
479
+ };
480
+
481
+ const selectSec = (sNum) => {
482
+ isTypingRef.current = false;
483
+ const s = pad(sNum);
484
+ emitChange({ ...parsed, s });
485
+ };
486
+
487
+ const toggleAmPm = (targetAmPm) => {
488
+ isTypingRef.current = false;
489
+ const ampm = targetAmPm;
490
+ const h24 = to24h(parsed.h12, ampm);
491
+ emitChange({ ...parsed, ampm, h24 });
492
+ };
493
+
494
+ const switchMode = (newMode) => {
495
+ isTypingRef.current = false;
496
+ setMode(newMode);
497
+ emitChange(parsed, newMode);
498
+ };
499
+
500
+ const clearTime = (e) => {
501
+ e?.stopPropagation();
502
+ isTypingRef.current = false;
503
+ setInputValue('');
504
+ if (onChange) onChange('', { hours: null, minutes: null, seconds: null, ampm: null, format: mode, timezone: tz, variant: activeVariant });
505
+ closePicker();
506
+ };
507
+
508
+ const defaultPlaceholder = mode === '12h' && showPlaceholder
509
+ ? (showSeconds ? 'hh:mm:ss AM' : 'hh:mm AM')
510
+ : (showSeconds ? 'HH:MM:SS' : 'HH:MM');
511
+ const ph = placeholder || defaultPlaceholder;
512
+
513
+ const hours24List = Array.from({ length: 24 }, (_, i) => i);
514
+ const hours12List = Array.from({ length: 12 }, (_, i) => i + 1);
515
+ const fullMinutesList = Array.from({ length: 60 }, (_, i) => i);
516
+
517
+ const presets24 = ['00:00', '07:00', '08:00', '09:00', '12:00', '18:00', '23:00'];
518
+ const presets12 = ['07:00 AM', '09:00 AM', '12:00 PM', '03:00 PM', '06:00 PM', '11:00 PM'];
519
+
520
+ const inputHeight = size === 'sm' ? '32px' : '38px';
521
+ const fontSize = size === 'sm' ? '.78rem' : '.85rem';
522
+
523
+ return (
524
+ <div
525
+ className={`cynx-timepicker-wrap ${className}`}
526
+ style={{ display: 'inline-flex', flexDirection: 'column', position: 'relative', width: style?.width || 'auto', boxSizing: 'border-box', minWidth: 0, ...style }}
527
+ >
528
+ {label && (
529
+ <label
530
+ style={{ fontSize: 11, fontWeight: 600, color: 'var(--t2, var(--muted))', textTransform: 'uppercase', letterSpacing: '.08em', display: 'block', marginBottom: 4 }}
531
+ >
532
+ {label}
533
+ </label>
534
+ )}
535
+
536
+ {/* Input Trigger Field */}
537
+ <div ref={btnRef} style={{ position: 'relative', display: 'flex', alignItems: 'center', width: '100%', boxSizing: 'border-box', minWidth: 0 }}>
538
+ {displayClockIcon && (
539
+ <button
540
+ type="button"
541
+ onClick={togglePicker}
542
+ disabled={disabled || readOnly}
543
+ title="Выбрать время"
544
+ style={{
545
+ position: 'absolute',
546
+ left: 10,
547
+ background: 'none',
548
+ border: 'none',
549
+ cursor: (disabled || readOnly) ? 'default' : 'pointer',
550
+ padding: 0,
551
+ display: 'flex',
552
+ alignItems: 'center',
553
+ justifyContent: 'center',
554
+ color: open ? 'var(--accent, var(--blue))' : 'var(--muted, var(--t2))',
555
+ zIndex: 1,
556
+ transition: 'color .15s ease',
557
+ }}
558
+ >
559
+ <Clock size={size === 'sm' ? 14 : 16} />
560
+ </button>
561
+ )}
562
+
563
+ <input
564
+ type="text"
565
+ value={inputValue}
566
+ onChange={handleInputChange}
567
+ onKeyDown={handleKeyDown}
568
+ onBlur={handleInputBlur}
569
+ onFocus={openPicker}
570
+ maxLength={mode === '12h' ? (showSeconds ? 11 : 8) : (showSeconds ? 8 : 5)}
571
+ placeholder={ph}
572
+ disabled={disabled}
573
+ readOnly={readOnly}
574
+ style={{
575
+ display: 'flex',
576
+ width: '100%',
577
+ minWidth: 0,
578
+ maxWidth: '100%',
579
+ boxSizing: 'border-box',
580
+ height: inputHeight,
581
+ paddingLeft: displayClockIcon ? 28 : 10,
582
+ paddingRight: 26,
583
+ border: `1px solid ${open ? 'var(--accent, var(--blue))' : 'var(--border-light, var(--border))'}`,
584
+ borderRadius: 'var(--rads, 6px)',
585
+ fontSize: fontSize,
586
+ background: disabled ? 'var(--grey-1, var(--surface-hover))' : 'var(--bg, var(--surface))',
587
+ color: 'var(--fg, var(--t1, var(--primary-text)))',
588
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
589
+ letterSpacing: '0.03em',
590
+ fontWeight: 600,
591
+ fontVariantNumeric: 'tabular-nums',
592
+ textAlign: 'left',
593
+ outline: 'none',
594
+ boxShadow: open ? 'var(--pd, rgba(37, 99, 235, 0.12))' : 'var(--shadow, none)',
595
+ transition: 'all .15s cubic-bezier(.16,1,.3,1)',
596
+ }}
597
+ />
598
+
599
+ <div style={{ position: 'absolute', right: 8, display: 'flex', alignItems: 'center' }}>
600
+ {inputValue && !disabled && !readOnly ? (
601
+ <button
602
+ type="button"
603
+ onClick={clearTime}
604
+ title="Очистить"
605
+ style={{
606
+ background: 'none',
607
+ border: 'none',
608
+ cursor: 'pointer',
609
+ color: 'var(--muted, var(--t2))',
610
+ padding: 2,
611
+ display: 'flex',
612
+ alignItems: 'center',
613
+ justifyContent: 'center',
614
+ transition: 'color .14s ease',
615
+ }}
616
+ onMouseEnter={e => e.currentTarget.style.color = 'var(--danger)'}
617
+ onMouseLeave={e => e.currentTarget.style.color = 'var(--muted, var(--t2))'}
618
+ >
619
+ <X size={13} />
620
+ </button>
621
+ ) : (
622
+ <ChevronDown
623
+ size={13}
624
+ style={{
625
+ color: open ? 'var(--accent, var(--blue))' : 'var(--muted, var(--t2))',
626
+ transform: open ? 'rotate(180deg)' : 'none',
627
+ transition: 'transform .2s ease, color .15s ease',
628
+ pointerEvents: 'none',
629
+ }}
630
+ />
631
+ )}
632
+ </div>
633
+ </div>
634
+
635
+ {/* PORTAL DROPDOWN POPUP */}
636
+ {rendered && createPortal(
637
+ <div
638
+ ref={dropRef}
639
+ className={`cynx-timepicker-drop ${closing ? 'closing' : ''}`}
640
+ style={{
641
+ position: 'fixed',
642
+ top: pos.top,
643
+ left: pos.left,
644
+ zIndex: 999999,
645
+ background: 'var(--surface, var(--bg))',
646
+ border: '1px solid var(--border-light, var(--border))',
647
+ borderRadius: 'var(--rad, 10px)',
648
+ boxShadow: 'var(--shadow-lg, var(--shadow))',
649
+ padding: 10,
650
+ width: mode === '12h' ? 290 : 240,
651
+ maxWidth: 'calc(100vw - 24px)',
652
+ boxSizing: 'border-box',
653
+ fontSize: '.78rem',
654
+ userSelect: 'none',
655
+ fontFamily: 'var(--sans, inherit)',
656
+ }}
657
+ >
658
+ {/* Header Bar: Variant Switcher */}
659
+ {showVariantToggle && (
660
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, paddingBottom: 6, borderBottom: '1px solid var(--border-light, var(--border))' }}>
661
+ <span style={{ fontSize: '.68rem', fontWeight: 700, color: 'var(--fg, var(--t1))', textTransform: 'uppercase', letterSpacing: '.05em' }}>
662
+ Режим
663
+ </span>
664
+
665
+ <div style={{ display: 'inline-flex', background: 'var(--grey-1, var(--surface-hover))', padding: 2, borderRadius: 6, border: '1px solid var(--border-light, var(--border))' }}>
666
+ <button
667
+ type="button"
668
+ onClick={() => setActiveVariant('stepper')}
669
+ className="cynx-timepicker-cell"
670
+ style={{
671
+ padding: '2px 8px',
672
+ borderRadius: 4,
673
+ border: 'none',
674
+ fontSize: '.68rem',
675
+ fontWeight: activeVariant === 'stepper' ? 700 : 500,
676
+ background: activeVariant === 'stepper' ? 'var(--surface, var(--bg))' : 'transparent',
677
+ color: activeVariant === 'stepper' ? 'var(--accent, var(--blue))' : 'var(--muted, var(--t2))',
678
+ boxShadow: activeVariant === 'stepper' ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
679
+ cursor: 'pointer',
680
+ display: 'flex',
681
+ alignItems: 'center',
682
+ gap: 3,
683
+ }}
684
+ >
685
+ <Sliders size={11} /> Спиннер (+/-)
686
+ </button>
687
+
688
+ <button
689
+ type="button"
690
+ onClick={() => setActiveVariant('list')}
691
+ className="cynx-timepicker-cell"
692
+ style={{
693
+ padding: '2px 8px',
694
+ borderRadius: 4,
695
+ border: 'none',
696
+ fontSize: '.68rem',
697
+ fontWeight: activeVariant === 'list' ? 700 : 500,
698
+ background: activeVariant === 'list' ? 'var(--surface, var(--bg))' : 'transparent',
699
+ color: activeVariant === 'list' ? 'var(--accent, var(--blue))' : 'var(--muted, var(--t2))',
700
+ boxShadow: activeVariant === 'list' ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
701
+ cursor: 'pointer',
702
+ display: 'flex',
703
+ alignItems: 'center',
704
+ gap: 3,
705
+ }}
706
+ >
707
+ <List size={11} /> Список (00-59)
708
+ </button>
709
+ </div>
710
+ </div>
711
+ )}
712
+
713
+ {/* Optional Format Switcher (24h / 12h) */}
714
+ {showFormatToggle && (
715
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, paddingBottom: 6, borderBottom: '1px solid var(--border-light, var(--border))' }}>
716
+ <span style={{ fontSize: '.68rem', fontWeight: 700, color: 'var(--fg, var(--t1))', textTransform: 'uppercase', letterSpacing: '.05em' }}>
717
+ Формат
718
+ </span>
719
+
720
+ <div style={{ display: 'inline-flex', background: 'var(--grey-1, var(--surface-hover))', padding: 2, borderRadius: 6, border: '1px solid var(--border-light, var(--border))' }}>
721
+ <button
722
+ type="button"
723
+ onClick={() => switchMode('24h')}
724
+ className="cynx-timepicker-cell"
725
+ style={{
726
+ padding: '2px 8px',
727
+ borderRadius: 4,
728
+ border: 'none',
729
+ fontSize: '.68rem',
730
+ fontWeight: mode === '24h' ? 700 : 500,
731
+ background: mode === '24h' ? 'var(--surface, var(--bg))' : 'transparent',
732
+ color: mode === '24h' ? 'var(--accent, var(--blue))' : 'var(--muted, var(--t2))',
733
+ boxShadow: mode === '24h' ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
734
+ cursor: 'pointer',
735
+ }}
736
+ >
737
+ 24h
738
+ </button>
739
+ <button
740
+ type="button"
741
+ onClick={() => switchMode('12h')}
742
+ className="cynx-timepicker-cell"
743
+ style={{
744
+ padding: '2px 8px',
745
+ borderRadius: 4,
746
+ border: 'none',
747
+ fontSize: '.68rem',
748
+ fontWeight: mode === '12h' ? 700 : 500,
749
+ background: mode === '12h' ? 'var(--surface, var(--bg))' : 'transparent',
750
+ color: mode === '12h' ? 'var(--accent, var(--blue))' : 'var(--muted, var(--t2))',
751
+ boxShadow: mode === '12h' ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
752
+ cursor: 'pointer',
753
+ }}
754
+ >
755
+ 12h AM/PM
756
+ </button>
757
+ </div>
758
+ </div>
759
+ )}
760
+
761
+ {/* Optional Timezone Selector Header */}
762
+ {showTimezoneSelect && (
763
+ <div style={{ marginBottom: 8 }}>
764
+ <button
765
+ type="button"
766
+ onClick={() => setShowTzPicker(prev => !prev)}
767
+ className="cynx-timepicker-cell"
768
+ style={{
769
+ display: 'flex',
770
+ alignItems: 'center',
771
+ justifyContent: 'space-between',
772
+ width: '100%',
773
+ padding: '4px 8px',
774
+ borderRadius: 6,
775
+ border: '1px solid var(--border-light, var(--border))',
776
+ background: 'var(--grey-1, var(--surface-hover))',
777
+ fontSize: '.68rem',
778
+ color: 'var(--fg, var(--t1))',
779
+ cursor: 'pointer',
780
+ fontWeight: 500,
781
+ }}
782
+ >
783
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
784
+ <Globe size={12} color="var(--accent, var(--blue))" />
785
+ {COMMON_TIMEZONES.find(t => t.value === tz)?.label || tz}
786
+ </span>
787
+ <ChevronDown size={12} color="var(--muted, var(--t2))" />
788
+ </button>
789
+
790
+ {showTzPicker && (
791
+ <div style={{ marginTop: 4, background: 'var(--surface, var(--bg))', border: '1px solid var(--border-light, var(--border))', borderRadius: 6, boxShadow: 'var(--shadow)', padding: 4, display: 'flex', flexDirection: 'column', gap: 2 }}>
792
+ {COMMON_TIMEZONES.map(t => (
793
+ <button
794
+ key={t.value}
795
+ type="button"
796
+ onClick={() => { setTz(t.value); setShowTzPicker(false); }}
797
+ className="cynx-timepicker-cell"
798
+ style={{
799
+ padding: '4px 6px',
800
+ borderRadius: 4,
801
+ border: 'none',
802
+ textAlign: 'left',
803
+ fontSize: '.68rem',
804
+ background: tz === t.value ? 'var(--accent, var(--blue))' : 'transparent',
805
+ color: tz === t.value ? '#ffffff' : 'var(--fg, var(--t1))',
806
+ cursor: 'pointer',
807
+ fontWeight: tz === t.value ? 600 : 400,
808
+ }}
809
+ >
810
+ {t.label}
811
+ </button>
812
+ ))}
813
+ </div>
814
+ )}
815
+ </div>
816
+ )}
817
+
818
+ {/* Optional Quick Presets Bar */}
819
+ {showPresets && (
820
+ <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 8, paddingBottom: 6, borderBottom: '1px solid var(--border-light, var(--border))' }}>
821
+ {(mode === '12h' ? presets12 : presets24).map(p => (
822
+ <button
823
+ key={p}
824
+ type="button"
825
+ onClick={() => {
826
+ const newP = parseTime(p, mode === '12h', showSeconds);
827
+ emitChange(newP);
828
+ }}
829
+ className="cynx-timepicker-cell"
830
+ style={{
831
+ padding: '3px 6px',
832
+ borderRadius: 4,
833
+ fontSize: '.68rem',
834
+ border: '1px solid var(--border-light, var(--border))',
835
+ background: inputValue === p ? 'var(--accent, var(--blue))' : 'var(--grey-1, var(--surface-hover))',
836
+ color: inputValue === p ? '#ffffff' : 'var(--fg, var(--t1))',
837
+ cursor: 'pointer',
838
+ fontWeight: 500,
839
+ }}
840
+ >
841
+ {p}
842
+ </button>
843
+ ))}
844
+ </div>
845
+ )}
846
+
847
+ {/* AM / PM Segment Selector (Only for 12h mode) */}
848
+ {mode === '12h' && (
849
+ <div style={{ display: 'flex', gap: 6, marginBottom: 8, justifyContent: 'center' }}>
850
+ <button
851
+ type="button"
852
+ onClick={() => toggleAmPm('AM')}
853
+ className="cynx-timepicker-cell"
854
+ style={{
855
+ flex: 1,
856
+ padding: '4px 0',
857
+ borderRadius: 6,
858
+ border: '1px solid var(--border-light, var(--border))',
859
+ fontSize: '.75rem',
860
+ fontWeight: parsed.ampm === 'AM' ? 700 : 500,
861
+ background: parsed.ampm === 'AM' ? 'var(--accent, var(--blue))' : 'var(--grey-1, var(--surface-hover))',
862
+ color: parsed.ampm === 'AM' ? '#ffffff' : 'var(--fg, var(--t1))',
863
+ cursor: 'pointer',
864
+ textAlign: 'center',
865
+ }}
866
+ >
867
+ AM (Утро)
868
+ </button>
869
+ <button
870
+ type="button"
871
+ onClick={() => toggleAmPm('PM')}
872
+ className="cynx-timepicker-cell"
873
+ style={{
874
+ flex: 1,
875
+ padding: '4px 0',
876
+ borderRadius: 6,
877
+ border: '1px solid var(--border-light, var(--border))',
878
+ fontSize: '.75rem',
879
+ fontWeight: parsed.ampm === 'PM' ? 700 : 500,
880
+ background: parsed.ampm === 'PM' ? 'var(--accent, var(--blue))' : 'var(--grey-1, var(--surface-hover))',
881
+ color: parsed.ampm === 'PM' ? '#ffffff' : 'var(--fg, var(--t1))',
882
+ cursor: 'pointer',
883
+ textAlign: 'center',
884
+ }}
885
+ >
886
+ PM (Вечер)
887
+ </button>
888
+ </div>
889
+ )}
890
+
891
+ {/* VARIANT 1: STEPPER MODE (Up/Down +/- Buttons) */}
892
+ {activeVariant === 'stepper' ? (
893
+ <div style={{ display: 'grid', gridTemplateColumns: showSeconds ? '1fr 1fr 1fr' : '1fr 1fr', gap: 10, padding: '4px 0' }}>
894
+ {/* Hours Stepper */}
895
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
896
+ <div style={{ fontSize: '.65rem', fontWeight: 600, color: 'var(--muted, var(--t2))' }}>
897
+ {mode === '12h' ? 'Час (1-12)' : 'Час (0-23)'}
898
+ </div>
899
+ <button type="button" className="cynx-timepicker-stepper-btn" onClick={() => stepHour(1)} title="+1 Час">
900
+ <ChevronUp size={14} />
901
+ </button>
902
+ <div style={{ fontSize: '1.2rem', fontWeight: 700, fontVariantNumeric: 'tabular-nums', padding: '6px 0', color: 'var(--accent, var(--blue))' }}>
903
+ {mode === '12h' ? parsed.h12 : parsed.h24}
904
+ </div>
905
+ <button type="button" className="cynx-timepicker-stepper-btn" onClick={() => stepHour(-1)} title="-1 Час">
906
+ <ChevronDown size={14} />
907
+ </button>
908
+ </div>
909
+
910
+ {/* Minutes Stepper */}
911
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
912
+ <div style={{ fontSize: '.65rem', fontWeight: 600, color: 'var(--muted, var(--t2))' }}>Мин (0-59)</div>
913
+ <button type="button" className="cynx-timepicker-stepper-btn" onClick={() => stepMinute(1)} title="+1 Мин">
914
+ <ChevronUp size={14} />
915
+ </button>
916
+ <div style={{ fontSize: '1.2rem', fontWeight: 700, fontVariantNumeric: 'tabular-nums', padding: '6px 0', color: 'var(--accent, var(--blue))' }}>
917
+ {parsed.m}
918
+ </div>
919
+ <button type="button" className="cynx-timepicker-stepper-btn" onClick={() => stepMinute(-1)} title="-1 Мин">
920
+ <ChevronDown size={14} />
921
+ </button>
922
+ </div>
923
+
924
+ {/* Seconds Stepper */}
925
+ {showSeconds && (
926
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
927
+ <div style={{ fontSize: '.65rem', fontWeight: 600, color: 'var(--muted, var(--t2))' }}>Сек (0-59)</div>
928
+ <button type="button" className="cynx-timepicker-stepper-btn" onClick={() => stepSecond(1)} title="+1 Сек">
929
+ <ChevronUp size={14} />
930
+ </button>
931
+ <div style={{ fontSize: '1.2rem', fontWeight: 700, fontVariantNumeric: 'tabular-nums', padding: '6px 0', color: 'var(--accent, var(--blue))' }}>
932
+ {parsed.s}
933
+ </div>
934
+ <button type="button" className="cynx-timepicker-stepper-btn" onClick={() => stepSecond(-1)} title="-1 Сек">
935
+ <ChevronDown size={14} />
936
+ </button>
937
+ </div>
938
+ )}
939
+ </div>
940
+ ) : (
941
+ /* VARIANT 2: LIST MODE (Full List 00..59) */
942
+ <div style={{ display: 'grid', gridTemplateColumns: showSeconds ? '1fr 1fr 1fr' : '1fr 1fr', gap: 6 }}>
943
+ {/* Hours Column */}
944
+ <div>
945
+ <div style={{ fontSize: '.65rem', fontWeight: 600, color: 'var(--muted, var(--t2))', marginBottom: 4, textAlign: 'center' }}>
946
+ {mode === '12h' ? 'Час (1-12)' : 'Час (0-23)'}
947
+ </div>
948
+ <div style={{ maxHeight: 150, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2, paddingRight: 2 }}>
949
+ {(mode === '12h' ? hours12List : hours24List).map(hNum => {
950
+ const isSelected = mode === '12h' ? parsed.h12 === pad(hNum) : parsed.h24 === pad(hNum);
951
+ return (
952
+ <button
953
+ key={hNum}
954
+ type="button"
955
+ onClick={() => (mode === '12h' ? selectH12(hNum) : selectH24(hNum))}
956
+ className="cynx-timepicker-cell"
957
+ style={{
958
+ padding: '3px 6px',
959
+ borderRadius: 4,
960
+ border: 'none',
961
+ textAlign: 'center',
962
+ fontSize: '.75rem',
963
+ cursor: 'pointer',
964
+ fontWeight: isSelected ? 700 : 400,
965
+ background: isSelected ? 'var(--accent, var(--blue))' : 'transparent',
966
+ color: isSelected ? '#ffffff' : 'var(--fg, var(--t1))',
967
+ }}
968
+ >
969
+ {pad(hNum)}
970
+ </button>
971
+ );
972
+ })}
973
+ </div>
974
+ </div>
975
+
976
+ {/* Minutes Column: FULL LIST 00 to 59! */}
977
+ <div>
978
+ <div style={{ fontSize: '.65rem', fontWeight: 600, color: 'var(--muted, var(--t2))', marginBottom: 4, textAlign: 'center' }}>Мин (00-59)</div>
979
+ <div style={{ maxHeight: 150, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2, paddingRight: 2 }}>
980
+ {fullMinutesList.map(mNum => {
981
+ const mStr = pad(mNum);
982
+ const isSelected = parsed.m === mStr;
983
+ return (
984
+ <button
985
+ key={mNum}
986
+ type="button"
987
+ onClick={() => selectMin(mNum)}
988
+ className="cynx-timepicker-cell"
989
+ style={{
990
+ padding: '3px 6px',
991
+ borderRadius: 4,
992
+ border: 'none',
993
+ textAlign: 'center',
994
+ fontSize: '.75rem',
995
+ cursor: 'pointer',
996
+ fontWeight: isSelected ? 700 : 400,
997
+ background: isSelected ? 'var(--accent, var(--blue))' : 'transparent',
998
+ color: isSelected ? '#ffffff' : 'var(--fg, var(--t1))',
999
+ }}
1000
+ >
1001
+ {mStr}
1002
+ </button>
1003
+ );
1004
+ })}
1005
+ </div>
1006
+ </div>
1007
+
1008
+ {/* Seconds Column (if showSeconds): FULL LIST 00 to 59! */}
1009
+ {showSeconds && (
1010
+ <div>
1011
+ <div style={{ fontSize: '.65rem', fontWeight: 600, color: 'var(--muted, var(--t2))', marginBottom: 4, textAlign: 'center' }}>Сек (00-59)</div>
1012
+ <div style={{ maxHeight: 150, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }}>
1013
+ {fullMinutesList.map(sNum => {
1014
+ const sStr = pad(sNum);
1015
+ const isSelected = parsed.s === sStr;
1016
+ return (
1017
+ <button
1018
+ key={sNum}
1019
+ type="button"
1020
+ onClick={() => selectSec(sNum)}
1021
+ className="cynx-timepicker-cell"
1022
+ style={{
1023
+ padding: '3px 6px',
1024
+ borderRadius: 4,
1025
+ border: 'none',
1026
+ textAlign: 'center',
1027
+ fontSize: '.75rem',
1028
+ cursor: 'pointer',
1029
+ fontWeight: isSelected ? 700 : 400,
1030
+ background: isSelected ? 'var(--accent, var(--blue))' : 'transparent',
1031
+ color: isSelected ? '#ffffff' : 'var(--fg, var(--t1))',
1032
+ }}
1033
+ >
1034
+ {sStr}
1035
+ </button>
1036
+ );
1037
+ })}
1038
+ </div>
1039
+ </div>
1040
+ )}
1041
+ </div>
1042
+ )}
1043
+
1044
+ {/* Footer Actions */}
1045
+ <div style={{ marginTop: 8, paddingTop: 6, borderTop: '1px solid var(--border-light, var(--border))', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
1046
+ <button
1047
+ type="button"
1048
+ onClick={clearTime}
1049
+ className="cynx-timepicker-cell"
1050
+ style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--danger)', fontSize: '.7rem', fontWeight: 500 }}
1051
+ >
1052
+ Сбросить
1053
+ </button>
1054
+ <button
1055
+ type="button"
1056
+ onClick={closePicker}
1057
+ className="cynx-timepicker-cell"
1058
+ style={{ padding: '3px 10px', borderRadius: 4, border: 'none', background: 'var(--accent, var(--blue))', color: '#ffffff', fontSize: '.7rem', cursor: 'pointer', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 4 }}
1059
+ >
1060
+ <Check size={12} /> Готово
1061
+ </button>
1062
+ </div>
1063
+ </div>,
1064
+ document.body
1065
+ )}
1066
+ </div>
1067
+ );
1068
+ }
1069
+
1070
+ export default TimePicker;
package/index.js CHANGED
@@ -28,6 +28,7 @@ import { ConfirmRoot as _ConfirmRoot, confirm as _confirm } from './components/C
28
28
  import { SearchableDropdown as _SearchableDropdown } from './components/SearchableDropdown.jsx';
29
29
  import { Pagination as _Pagination } from './components/Pagination.jsx';
30
30
  import { DatePicker as _DatePicker } from './components/DatePicker.jsx';
31
+ import { TimePicker as _TimePicker } from './components/TimePicker.jsx';
31
32
  import { ActionsMenu as _ActionsMenu } from './components/ActionsMenu.jsx';
32
33
  import { BrandingCard as _BrandingCard } from './components/BrandingCard.jsx';
33
34
  import { Dropzone as _Dropzone } from './components/Dropzone.jsx';
@@ -72,6 +73,7 @@ export const confirm = _confirm;
72
73
  export const SearchableDropdown = _SearchableDropdown;
73
74
  export const Pagination = _Pagination;
74
75
  export const DatePicker = _DatePicker;
76
+ export const TimePicker = _TimePicker;
75
77
  export const ActionsMenu = _ActionsMenu;
76
78
  export const BrandingCard = _BrandingCard;
77
79
  export const Dropzone = _Dropzone;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.2.48",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"