cynx-lib 1.3.28

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