cynx-lib 1.3.67 → 1.3.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.67",
3
+ "version": "1.3.69",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
- import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
3
+ import { ChevronLeft, ChevronRight, Calendar, Clock } from 'lucide-react';
4
+ import { TimePicker } from './TimePicker.jsx';
4
5
 
5
6
  // DatePicker — reusable date picker component
6
7
  // Props:
@@ -12,9 +13,28 @@ import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
12
13
  // maxDate – Date object, days after this are disabled
13
14
  // disabled – boolean
14
15
  // style – container style overrides
15
-
16
- const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
17
- const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
16
+ // showTime – boolean, enables integrated TimePicker in dropdown
17
+ // timeValue – string, time string e.g. "14:30"
18
+ // onTimeChange – function(timeString)
19
+ // timeFormat – "24h" | "12h"
20
+ // showSeconds – boolean
21
+
22
+ const LOCALES = {
23
+ en: {
24
+ days: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
25
+ months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
26
+ today: 'Today',
27
+ time: 'Time:',
28
+ ok: 'OK',
29
+ },
30
+ ru: {
31
+ days: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'],
32
+ months: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
33
+ today: 'Сегодня',
34
+ time: 'Время:',
35
+ ok: 'OK',
36
+ },
37
+ };
18
38
 
19
39
  function pad(n) { return String(n).padStart(2, '0'); }
20
40
  function toKey(y, m, d) { return `${y}-${pad(m+1)}-${pad(d)}`; }
@@ -42,11 +62,48 @@ function isSameDay(a, b) {
42
62
  }
43
63
  function isToday(y, m, d) { const t = new Date(); return t.getFullYear()===y && t.getMonth()===m && t.getDate()===d; }
44
64
 
45
- export function DatePicker({ value, onChange, label, placeholder = 'Select date', badges = {}, minDate, maxDate, disabled, showStepper = true, stepperPosition = 'around', style }) {
65
+ const SIZES = {
66
+ xs: { height: 24, fontSize: 11, paddingH: 6, iconSize: 11, btnSize: 24, closeSize: 11 },
67
+ sm: { height: 28, fontSize: 11.5, paddingH: 8, iconSize: 12, btnSize: 28, closeSize: 12 },
68
+ md: { height: 32, fontSize: 13, paddingH: 10, iconSize: 13, btnSize: 32, closeSize: 13 },
69
+ lg: { height: 36, fontSize: 14, paddingH: 12, iconSize: 14, btnSize: 36, closeSize: 14 },
70
+ };
71
+
72
+ export function DatePicker({
73
+ value,
74
+ onChange,
75
+ label,
76
+ placeholder = 'Select date',
77
+ badges = {},
78
+ minDate,
79
+ maxDate,
80
+ disabled,
81
+ size = 'md',
82
+ showStepper = false,
83
+ stepperPosition = 'off',
84
+ locale = 'en',
85
+ style,
86
+ fullWidth = false,
87
+ showTime = false,
88
+ timeValue,
89
+ onTimeChange,
90
+ timeFormat = '24h',
91
+ showSeconds = false,
92
+ timePlaceholder = '00:00',
93
+ timeProps = {},
94
+ range = false,
95
+ }) {
96
+ const loc = typeof locale === 'object' && locale !== null ? locale : (LOCALES[locale] || LOCALES.en);
97
+ const DAYS = loc.days;
98
+ const MONTHS = loc.months;
99
+ const s = SIZES[size] || SIZES.md;
46
100
  const [open, setOpen] = useState(false);
47
101
  const [view, setView] = useState('day'); // 'day' | 'month' | 'year'
48
- const [cursor, setCursor] = useState(() => toDate(value) || new Date());
102
+ const [cursor, setCursor] = useState(() => new Date());
49
103
  const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
104
+ const [rangeStart, setRangeStart] = useState(null);
105
+ const [rangeEnd, setRangeEnd] = useState(null);
106
+ const [hoverDate, setHoverDate] = useState(null);
50
107
  const ref = useRef(null);
51
108
  const btnRef = useRef(null);
52
109
  const dropRef = useRef(null);
@@ -77,10 +134,24 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
77
134
  function close(e) {
78
135
  if (ref.current?.contains(e.target)) return;
79
136
  if (dropRef.current?.contains(e.target)) return;
137
+ if (e.target?.closest?.('.cynx-timepicker-drop') || e.target?.closest?.('.cynx-timepicker-cell')) return;
138
+ if (document.querySelector('.cynx-timepicker-drop')) return;
139
+ if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
80
140
  setOpen(false); setView('day');
81
141
  }
142
+ function onKey(e) {
143
+ if (e.key === 'Escape') {
144
+ if (document.querySelector('.cynx-timepicker-drop')) return;
145
+ if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
146
+ setOpen(false); setView('day');
147
+ }
148
+ }
82
149
  document.addEventListener('mousedown', close, true);
83
- return () => document.removeEventListener('mousedown', close, true);
150
+ document.addEventListener('keydown', onKey);
151
+ return () => {
152
+ document.removeEventListener('mousedown', close, true);
153
+ document.removeEventListener('keydown', onKey);
154
+ };
84
155
  }, [open]);
85
156
 
86
157
  const [stepperPosState, setStepperPosState] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('datepicker_stepper_position') : null);
@@ -93,11 +164,31 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
93
164
  return () => window.removeEventListener('datepicker-pos-changed', onPosChanged);
94
165
  }, []);
95
166
 
96
- // Sync cursor when value changes externally
167
+ // Sync cursor and range when value changes externally
97
168
  useEffect(() => {
98
- const d = toDate(value);
99
- if (d) setCursor(d);
100
- }, [value]);
169
+ if (range) {
170
+ if (Array.isArray(value)) {
171
+ const sD = toDate(value[0]);
172
+ const eD = toDate(value[1]);
173
+ setRangeStart(sD);
174
+ setRangeEnd(eD);
175
+ if (sD) setCursor(sD);
176
+ } else if (typeof value === 'string' && value.includes(',')) {
177
+ const parts = value.split(',');
178
+ const sD = toDate(parts[0]);
179
+ const eD = toDate(parts[1]);
180
+ setRangeStart(sD);
181
+ setRangeEnd(eD);
182
+ if (sD) setCursor(sD);
183
+ } else {
184
+ setRangeStart(null);
185
+ setRangeEnd(null);
186
+ }
187
+ } else {
188
+ const d = toDate(value);
189
+ if (d) setCursor(d);
190
+ }
191
+ }, [value, range]);
101
192
 
102
193
  const calcPos = useCallback(() => {
103
194
  if (!btnRef.current) return;
@@ -138,6 +229,92 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
138
229
  setOpen(o => !o);
139
230
  };
140
231
 
232
+ function formatDateStr(d) {
233
+ if (!d) return '';
234
+ const y = d.getFullYear();
235
+ const m = pad(d.getMonth() + 1);
236
+ const day = pad(d.getDate());
237
+ return `${y}-${m}-${day}`;
238
+ }
239
+
240
+ function isRangeStartActive(d) {
241
+ if (!range || !rangeStart) return false;
242
+ if (rangeEnd) {
243
+ const sTime = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()).getTime();
244
+ const eTime = new Date(rangeEnd.getFullYear(), rangeEnd.getMonth(), rangeEnd.getDate()).getTime();
245
+ const minD = sTime <= eTime ? rangeStart : rangeEnd;
246
+ return isSameDay(d, minD);
247
+ }
248
+ if (hoverDate) {
249
+ const sTime = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()).getTime();
250
+ const hTime = new Date(hoverDate.getFullYear(), hoverDate.getMonth(), hoverDate.getDate()).getTime();
251
+ const minD = sTime <= hTime ? rangeStart : hoverDate;
252
+ return isSameDay(d, minD);
253
+ }
254
+ return isSameDay(d, rangeStart);
255
+ }
256
+
257
+ function isRangeEndActive(d) {
258
+ if (!range || !rangeStart) return false;
259
+ if (rangeEnd) {
260
+ const sTime = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()).getTime();
261
+ const eTime = new Date(rangeEnd.getFullYear(), rangeEnd.getMonth(), rangeEnd.getDate()).getTime();
262
+ const maxD = sTime <= eTime ? rangeEnd : rangeStart;
263
+ return isSameDay(d, maxD);
264
+ }
265
+ if (hoverDate) {
266
+ const sTime = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()).getTime();
267
+ const hTime = new Date(hoverDate.getFullYear(), hoverDate.getMonth(), hoverDate.getDate()).getTime();
268
+ const maxD = sTime <= hTime ? hoverDate : rangeStart;
269
+ return isSameDay(d, maxD);
270
+ }
271
+ return false;
272
+ }
273
+
274
+ function isRangeEndpoint(d) {
275
+ return isRangeStartActive(d) || isRangeEndActive(d);
276
+ }
277
+
278
+ function isRangeBetween(d) {
279
+ if (!range || !rangeStart) return false;
280
+ const dTime = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
281
+ if (rangeEnd) {
282
+ const sTime = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()).getTime();
283
+ const eTime = new Date(rangeEnd.getFullYear(), rangeEnd.getMonth(), rangeEnd.getDate()).getTime();
284
+ const minT = Math.min(sTime, eTime);
285
+ const maxT = Math.max(sTime, eTime);
286
+ return dTime > minT && dTime < maxT;
287
+ }
288
+ if (hoverDate) {
289
+ const sTime = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()).getTime();
290
+ const hTime = new Date(hoverDate.getFullYear(), hoverDate.getMonth(), hoverDate.getDate()).getTime();
291
+ const minT = Math.min(sTime, hTime);
292
+ const maxT = Math.max(sTime, hTime);
293
+ return dTime > minT && dTime < maxT;
294
+ }
295
+ return false;
296
+ }
297
+
298
+ function getRangeRadius(d) {
299
+ if (!range) return 'var(--radxs, 6px)';
300
+ const isStart = isRangeStartActive(d);
301
+ const isEnd = isRangeEndActive(d);
302
+ const dayOfWeek = d.getDay();
303
+ const isRowStart = dayOfWeek === 1;
304
+ const isRowEnd = dayOfWeek === 0;
305
+
306
+ if (isStart && isEnd) return 'var(--radxs, 6px)';
307
+ if (isStart) return 'var(--radxs, 6px) 0 0 var(--radxs, 6px)';
308
+ if (isEnd) return '0 var(--radxs, 6px) var(--radxs, 6px) 0';
309
+ if (isRangeBetween(d)) {
310
+ if (isRowStart && isRowEnd) return 'var(--radxs, 6px)';
311
+ if (isRowStart) return 'var(--radxs, 6px) 0 0 var(--radxs, 6px)';
312
+ if (isRowEnd) return '0 var(--radxs, 6px) var(--radxs, 6px) 0';
313
+ return '0';
314
+ }
315
+ return 'var(--radxs, 6px)';
316
+ }
317
+
141
318
  function pick(date) {
142
319
  if (!date) return;
143
320
  const minD = toDate(minDate);
@@ -145,6 +322,29 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
145
322
  if (minD && date < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
146
323
  if (maxD && date > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
147
324
 
325
+ if (range) {
326
+ if (!rangeStart || (rangeStart && rangeEnd)) {
327
+ setRangeStart(date);
328
+ setRangeEnd(null);
329
+ } else {
330
+ let start = rangeStart;
331
+ let end = date;
332
+ if (start > end) {
333
+ const tmp = start;
334
+ start = end;
335
+ end = tmp;
336
+ }
337
+ setRangeStart(start);
338
+ setRangeEnd(end);
339
+
340
+ const result = [formatDateStr(start), formatDateStr(end)];
341
+ onChange?.(result);
342
+ setOpen(false);
343
+ setView('day');
344
+ }
345
+ return;
346
+ }
347
+
148
348
  if (typeof value === 'string') {
149
349
  const y = date.getFullYear();
150
350
  const m = pad(date.getMonth() + 1);
@@ -153,8 +353,10 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
153
353
  } else {
154
354
  onChange?.(date);
155
355
  }
156
- setOpen(false);
157
- setView('day');
356
+ if (!showTime) {
357
+ setOpen(false);
358
+ setView('day');
359
+ }
158
360
  }
159
361
 
160
362
  const isNavNextDisabled = () => {
@@ -212,26 +414,26 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
212
414
  }
213
415
 
214
416
  const valDate = toDate(value);
215
- const formatted = valDate
216
- ? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}`
217
- : (typeof value === 'string' ? value : null);
417
+ const formattedTime = showTime && timeValue ? ` ${timeValue}` : '';
418
+ const formatted = range
419
+ ? (rangeStart && rangeEnd ? `${formatDateStr(rangeStart)} — ${formatDateStr(rangeEnd)}` : rangeStart ? `${formatDateStr(rangeStart)} — ...` : null)
420
+ : valDate
421
+ ? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}${formattedTime}`
422
+ : (typeof value === 'string' && value ? `${value}${formattedTime}` : null);
218
423
 
219
424
  // ── Day grid ─────────────────────────────────────────────────
220
425
  function renderDays() {
221
426
  const y = cursor.getFullYear(), m = cursor.getMonth();
222
- const first = new Date(y, m, 1).getDay(); // 0=Sun
223
- const offset = first === 0 ? 6 : first === 1 ? 7 : first - 1; // 0=Sun→6, 1=Mon→7(show prev week), else Mon-start
427
+ const first = new Date(y, m, 1).getDay();
428
+ const offset = first === 0 ? 6 : first === 1 ? 7 : first - 1;
224
429
  const daysInMonth = new Date(y, m + 1, 0).getDate();
225
430
  const prevDays = new Date(y, m, 0).getDate();
226
431
 
227
432
  const cells = [];
228
- // Prev month filler
229
433
  for (let i = offset - 1; i >= 0; i--)
230
434
  cells.push({ day: prevDays - i, cur: false, next: false });
231
- // Current month
232
435
  for (let d = 1; d <= daysInMonth; d++)
233
436
  cells.push({ day: d, cur: true, next: false });
234
- // Next month filler — complete last row + always show at least one full next month week
235
437
  const totalSoFar = cells.length;
236
438
  let remaining = (7 - (totalSoFar % 7)) % 7;
237
439
  if (remaining === 0) remaining = 7;
@@ -250,7 +452,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
250
452
  ))}
251
453
  </div>
252
454
  {/* Day cells */}
253
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 1 }}>
455
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px 0' }}>
254
456
  {cells.map((c, i) => {
255
457
  if (!c.cur) {
256
458
  const isPrev = !c.next;
@@ -264,18 +466,22 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
264
466
  <div
265
467
  key={i}
266
468
  onClick={() => { if (!dis) pick(date); }}
469
+ onMouseEnter={() => { if (range && rangeStart && !rangeEnd) setHoverDate(date); }}
267
470
  style={{
268
471
  textAlign: 'center',
269
472
  padding: '6px 2px',
270
473
  fontSize: '.78rem',
271
474
  color: dis ? 'var(--border, #e5e7eb)' : 'var(--muted, #797b8d)',
272
475
  cursor: dis ? 'not-allowed' : 'pointer',
273
- borderRadius: 'var(--radxs, 6px)',
476
+ borderRadius: getRangeRadius(date),
477
+ background: (range ? isRangeEndpoint(date) : false)
478
+ ? 'var(--accent, #f99e2c)'
479
+ : isRangeBetween(date)
480
+ ? 'rgba(18, 103, 93, 0.14)'
481
+ : 'transparent',
274
482
  transition: 'background .12s',
275
483
  opacity: dis ? 0.35 : 0.7,
276
484
  }}
277
- onMouseEnter={e => { if (!dis) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'; }}
278
- onMouseLeave={e => { if (!dis) e.currentTarget.style.background = 'transparent'; }}
279
485
  >
280
486
  {c.day}
281
487
  </div>
@@ -284,30 +490,37 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
284
490
  const date = new Date(y, m, c.day);
285
491
  const key = toKey(y, m, c.day);
286
492
  const badge = badges[key];
287
- const sel = isSameDay(date, value);
493
+ const sel = range ? isRangeEndpoint(date) : isSameDay(date, value);
288
494
  const today = isToday(y, m, c.day);
289
495
  const disMin = minD && date < new Date(new Date(minD).setHours(0,0,0,0));
290
496
  const disMax = maxD && date > new Date(new Date(maxD).setHours(23,59,59,999));
291
497
  const dis = disMin || disMax;
292
498
  return (
293
- <div key={i} onClick={() => !dis && pick(date)} style={{
294
- position: 'relative', textAlign: 'center', padding: '6px 2px',
295
- fontSize: '.8rem', fontWeight: sel ? 700 : today ? 600 : 400,
296
- borderRadius: 'var(--radxs, 6px)', cursor: dis ? 'not-allowed' : 'pointer',
297
- background: sel ? 'var(--accent, #f99e2c)' : 'transparent',
298
- color: sel ? 'var(--def-btn-text,#fff)' : dis ? 'var(--border, #e2e8f0)' : today ? 'var(--accent, #f99e2c)' : 'var(--primary-text, #0f172a)',
299
- border: today && !sel ? '1.5px solid var(--accent, #f99e2c)' : '1.5px solid transparent',
300
- transition: 'background .12s, color .12s',
301
- opacity: dis ? 0.35 : 1,
302
- }}
303
- onMouseEnter={e => { if (!sel && !dis) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'; }}
304
- onMouseLeave={e => { if (!sel && !dis) e.currentTarget.style.background = 'transparent'; }}>
499
+ <div
500
+ key={i}
501
+ onClick={() => !dis && pick(date)}
502
+ onMouseEnter={() => { if (range && rangeStart && !rangeEnd) setHoverDate(date); }}
503
+ style={{
504
+ position: 'relative', textAlign: 'center', padding: '6px 2px',
505
+ fontSize: '.8rem', fontWeight: sel ? 700 : today ? 600 : 400,
506
+ borderRadius: getRangeRadius(date), cursor: dis ? 'not-allowed' : 'pointer',
507
+ background: sel
508
+ ? 'var(--accent, #12675D)'
509
+ : isRangeBetween(date)
510
+ ? 'rgba(18, 103, 93, 0.14)'
511
+ : 'transparent',
512
+ color: sel ? '#ffffff' : dis ? 'var(--border, #e2e8f0)' : today ? 'var(--accent, #12675D)' : 'var(--primary-text, #0f172a)',
513
+ border: today && !sel ? '1.5px solid var(--accent, #12675D)' : '1.5px solid transparent',
514
+ transition: 'background .12s, color .12s',
515
+ opacity: dis ? 0.35 : 1,
516
+ }}
517
+ >
305
518
  {c.day}
306
519
  {badge && (
307
520
  <span style={{
308
521
  position: 'absolute', bottom: 2, left: '50%', transform: 'translateX(-50%)',
309
522
  width: 4, height: 4, borderRadius: '50%',
310
- background: badge.color || 'var(--accent, #f99e2c)',
523
+ background: badge.color || 'var(--accent, #12675D)',
311
524
  }} />
312
525
  )}
313
526
  </div>
@@ -450,7 +663,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
450
663
  onClick={(e) => { e.stopPropagation(); changeDay(-1); }}
451
664
  style={{
452
665
  display: 'flex', alignItems: 'center', justifyContent: 'center',
453
- width: 32, height: 32, borderRadius: 'var(--rads, 8px)',
666
+ width: s.btnSize, height: s.btnSize, borderRadius: 'var(--rads, 8px)',
454
667
  border: '1px solid var(--border, #e2e8f0)', background: 'var(--surface, #ffffff)',
455
668
  cursor: dis ? 'not-allowed' : 'pointer', opacity: dis ? 0.35 : 1,
456
669
  color: 'var(--muted, #797b8d)', transition: 'all .15s', boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
@@ -460,7 +673,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
460
673
  onMouseEnter={e => { if (!dis) { e.currentTarget.style.borderColor = 'var(--accent, #f99e2c)'; e.currentTarget.style.color = 'var(--accent, #f99e2c)'; } }}
461
674
  onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; e.currentTarget.style.color = 'var(--muted, #797b8d)'; }}
462
675
  >
463
- <ChevronLeft size={15} />
676
+ <ChevronLeft size={s.iconSize + 2} />
464
677
  </button>
465
678
  );
466
679
  };
@@ -474,7 +687,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
474
687
  onClick={(e) => { e.stopPropagation(); changeDay(1); }}
475
688
  style={{
476
689
  display: 'flex', alignItems: 'center', justifyContent: 'center',
477
- width: 32, height: 32, borderRadius: 'var(--rads, 8px)',
690
+ width: s.btnSize, height: s.btnSize, borderRadius: 'var(--rads, 8px)',
478
691
  border: '1px solid var(--border, #e2e8f0)', background: 'var(--surface, #ffffff)',
479
692
  cursor: dis ? 'not-allowed' : 'pointer', opacity: dis ? 0.35 : 1,
480
693
  color: 'var(--muted, #797b8d)', transition: 'all .15s', boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
@@ -484,7 +697,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
484
697
  onMouseEnter={e => { if (!dis) { e.currentTarget.style.borderColor = 'var(--accent, #f99e2c)'; e.currentTarget.style.color = 'var(--accent, #f99e2c)'; } }}
485
698
  onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; e.currentTarget.style.color = 'var(--muted, #797b8d)'; }}
486
699
  >
487
- <ChevronRight size={15} />
700
+ <ChevronRight size={s.iconSize + 2} />
488
701
  </button>
489
702
  );
490
703
  };
@@ -492,21 +705,24 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
492
705
  const renderTriggerBtn = () => (
493
706
  <button ref={btnRef} type="button" onClick={toggleOpen} style={{
494
707
  display: 'flex', alignItems: 'center', gap: 7,
495
- padding: '5px 10px', borderRadius: 'var(--rads, 8px)',
708
+ padding: `0 ${s.paddingH}px`, borderRadius: 'var(--rads, 8px)',
496
709
  border: '1px solid var(--border, #e2e8f0)', background: 'var(--surface, #ffffff)',
497
710
  cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
498
- fontSize: '.8rem', color: formatted ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)',
711
+ fontSize: s.fontSize, color: formatted ? 'var(--primary-text, #0f172a)' : 'var(--muted, #797b8d)',
499
712
  fontFamily: 'var(--sans, inherit)', transition: 'border-color .15s',
500
- whiteSpace: 'nowrap', minWidth: 140, height: 32, boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
713
+ whiteSpace: 'nowrap', minWidth: 0, flex: 1, height: s.height, boxShadow: 'var(--shadow, 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06))',
714
+ overflow: 'hidden'
501
715
  }}
502
716
  onMouseEnter={e => { if (!disabled) e.currentTarget.style.borderColor = 'var(--accent, #f99e2c)'; }}
503
717
  onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; }}>
504
- <Calendar size={13} style={{ color: 'var(--muted, #797b8d)', flexShrink: 0 }} />
505
- <span style={{ flex: 1, textAlign: 'left' }}>{formatted || placeholder}</span>
718
+ <Calendar size={s.iconSize} style={{ color: 'var(--muted, #797b8d)', flexShrink: 0 }} />
719
+ <span style={{ flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>
720
+ {formatted || placeholder}
721
+ </span>
506
722
  {value && (
507
723
  <span
508
724
  onClick={e => { e.stopPropagation(); onChange?.(null); }}
509
- style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted, #797b8d)', cursor: 'pointer', flexShrink: 0, fontSize: 13, lineHeight: 1, padding: '0 2px' }}
725
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted, #797b8d)', cursor: 'pointer', flexShrink: 0, fontSize: s.closeSize, lineHeight: 1, padding: '0 2px' }}
510
726
  onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger, #f64747)'; }}
511
727
  onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted, #797b8d)'; }}>
512
728
  ✕
@@ -522,13 +738,30 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
522
738
  : 'around';
523
739
 
524
740
  return (
525
- <div style={{ display: 'inline-flex', flexDirection: 'column', position: 'relative', width: style?.width || 'auto', boxSizing: 'border-box', minWidth: 0, ...style }}>
741
+ <div style={{
742
+ display: fullWidth || style?.display === 'flex' ? 'flex' : 'inline-flex',
743
+ flexDirection: 'column',
744
+ position: 'relative',
745
+ width: style?.width || (fullWidth ? '100%' : 'auto'),
746
+ maxWidth: style?.maxWidth || (fullWidth ? '100%' : 320),
747
+ boxSizing: 'border-box',
748
+ minWidth: 0,
749
+ ...style,
750
+ }}>
526
751
  {label && (
527
752
  <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--t2, var(--muted, #797b8d))', textTransform: 'uppercase', letterSpacing: '.08em', display: 'block', marginBottom: 4 }}>
528
753
  {label}
529
754
  </label>
530
755
  )}
531
- <div ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, width: '100%' }}>
756
+ <div ref={ref} style={{
757
+ position: 'relative',
758
+ display: 'inline-flex',
759
+ alignItems: 'center',
760
+ gap: 4,
761
+ width: '100%',
762
+ maxWidth: '100%',
763
+ minWidth: 0,
764
+ }}>
532
765
  {showStepperEffective && posMode === 'around' && renderPrevBtn()}
533
766
  {showStepperEffective && posMode === 'left' && renderPrevBtn()}
534
767
  {showStepperEffective && posMode === 'left' && renderNextBtn()}
@@ -542,7 +775,7 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
542
775
 
543
776
  {/* Dropdown via portal */}
544
777
  {open && createPortal(
545
- <div ref={dropRef} style={{
778
+ <div ref={dropRef} className="cynx-datepicker-drop" style={{
546
779
  position: 'fixed', top: pos.top, left: pos.left, width: pos.width, zIndex: 9000,
547
780
  background: 'var(--surface, #ffffff)', border: '1px solid var(--border, #e2e8f0)',
548
781
  borderRadius: 'var(--rad, 10px)', boxShadow: 'var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.06))',
@@ -595,6 +828,56 @@ export function DatePicker({ value, onChange, label, placeholder = 'Select date'
595
828
  {view === 'month' && renderMonths()}
596
829
  {view === 'year' && renderYears()}
597
830
 
831
+ {/* Integrated Time Picker footer */}
832
+ {showTime && (
833
+ <div
834
+ style={{
835
+ borderTop: '1px solid var(--border-light, #ebebf0)',
836
+ marginTop: 8,
837
+ paddingTop: 8,
838
+ display: 'flex',
839
+ alignItems: 'center',
840
+ justifyContent: 'space-between',
841
+ gap: 8,
842
+ }}
843
+ >
844
+ <div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: '.76rem', color: 'var(--muted, #797b8d)', fontWeight: 600 }}>
845
+ <Clock size={13} style={{ opacity: 0.85 }} />
846
+ <span>{loc.time || 'Time:'}</span>
847
+ </div>
848
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
849
+ <TimePicker
850
+ value={timeValue || ''}
851
+ onChange={(t) => onTimeChange?.(t)}
852
+ format={timeFormat}
853
+ showSeconds={showSeconds}
854
+ placeholder={timePlaceholder}
855
+ size={timeProps.size || size || 'md'}
856
+ {...timeProps}
857
+ />
858
+ <button
859
+ type="button"
860
+ onClick={() => { setOpen(false); setView('day'); }}
861
+ style={{
862
+ padding: '4px 10px',
863
+ borderRadius: 'var(--radxs, 6px)',
864
+ background: 'var(--accent, #f99e2c)',
865
+ color: 'var(--def-btn-text, #ffffff)',
866
+ border: 'none',
867
+ fontSize: '11.5px',
868
+ fontWeight: 600,
869
+ cursor: 'pointer',
870
+ transition: 'opacity 0.15s ease',
871
+ }}
872
+ onMouseEnter={(e) => { e.currentTarget.style.opacity = '0.9'; }}
873
+ onMouseLeave={(e) => { e.currentTarget.style.opacity = '1'; }}
874
+ >
875
+ {loc.ok || 'OK'}
876
+ </button>
877
+ </div>
878
+ </div>
879
+ )}
880
+
598
881
  </div>,
599
882
  document.body
600
883
  )}
@@ -3,11 +3,19 @@ import { computed } from "vue";
3
3
 
4
4
  const VARIANTS: Record<string, any> = {
5
5
  ok: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
6
+ success: { bg: "var(--success-bg, rgba(39,174,96,.1))", color: "var(--success, #16a34a)", border: "var(--sd, rgba(39,174,96,.2))", dot: "var(--success, #16a34a)" },
6
7
  err: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
8
+ danger: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
9
+ error: { bg: "var(--danger-bg, rgba(246,71,71,.1))", color: "var(--danger, #ef4444)", border: "var(--dd, rgba(246,71,71,.2))", dot: "var(--danger, #ef4444)" },
7
10
  warn: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
11
+ warning: { bg: "var(--accent-bg, rgba(249,158,44,.1))", color: "var(--accent, #f59e0b)", border: "var(--accent-border, rgba(249,158,44,.25))", dot: "var(--accent, #f59e0b)" },
8
12
  blue: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
13
+ info: { bg: "var(--blue-bg, rgba(39,130,228,.1))", color: "var(--blue, #2563eb)", border: "var(--infd, rgba(39,130,228,.2))", dot: "var(--blue, #2563eb)" },
9
14
  teal: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
15
+ primary: { bg: "rgba(18,103,93,.1)", color: "var(--accent, #12675D)", border: "rgba(18,103,93,.25)", dot: "var(--accent, #12675D)" },
10
16
  dim: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
17
+ neutral: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
18
+ secondary: { bg: "var(--grey-1, #f5f5f8)", color: "var(--grey-4, #64748b)", border: "var(--border, #e2e8f0)", dot: "var(--grey-4, #64748b)" },
11
19
  ghost: { bg: "transparent", color: "var(--t2, #64748b)", border: "var(--bd, #e2e8f0)", dot: "var(--t3, #94a3b8)" },
12
20
  };
13
21