cynx-lib 1.3.68 → 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.68",
3
+ "version": "1.3.69",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -91,6 +91,7 @@ export function DatePicker({
91
91
  showSeconds = false,
92
92
  timePlaceholder = '00:00',
93
93
  timeProps = {},
94
+ range = false,
94
95
  }) {
95
96
  const loc = typeof locale === 'object' && locale !== null ? locale : (LOCALES[locale] || LOCALES.en);
96
97
  const DAYS = loc.days;
@@ -98,8 +99,11 @@ export function DatePicker({
98
99
  const s = SIZES[size] || SIZES.md;
99
100
  const [open, setOpen] = useState(false);
100
101
  const [view, setView] = useState('day'); // 'day' | 'month' | 'year'
101
- const [cursor, setCursor] = useState(() => toDate(value) || new Date());
102
+ const [cursor, setCursor] = useState(() => new Date());
102
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);
103
107
  const ref = useRef(null);
104
108
  const btnRef = useRef(null);
105
109
  const dropRef = useRef(null);
@@ -160,11 +164,31 @@ export function DatePicker({
160
164
  return () => window.removeEventListener('datepicker-pos-changed', onPosChanged);
161
165
  }, []);
162
166
 
163
- // Sync cursor when value changes externally
167
+ // Sync cursor and range when value changes externally
164
168
  useEffect(() => {
165
- const d = toDate(value);
166
- if (d) setCursor(d);
167
- }, [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]);
168
192
 
169
193
  const calcPos = useCallback(() => {
170
194
  if (!btnRef.current) return;
@@ -205,6 +229,92 @@ export function DatePicker({
205
229
  setOpen(o => !o);
206
230
  };
207
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
+
208
318
  function pick(date) {
209
319
  if (!date) return;
210
320
  const minD = toDate(minDate);
@@ -212,6 +322,29 @@ export function DatePicker({
212
322
  if (minD && date < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
213
323
  if (maxD && date > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
214
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
+
215
348
  if (typeof value === 'string') {
216
349
  const y = date.getFullYear();
217
350
  const m = pad(date.getMonth() + 1);
@@ -282,26 +415,25 @@ export function DatePicker({
282
415
 
283
416
  const valDate = toDate(value);
284
417
  const formattedTime = showTime && timeValue ? ` ${timeValue}` : '';
285
- const formatted = valDate
418
+ const formatted = range
419
+ ? (rangeStart && rangeEnd ? `${formatDateStr(rangeStart)} — ${formatDateStr(rangeEnd)}` : rangeStart ? `${formatDateStr(rangeStart)} — ...` : null)
420
+ : valDate
286
421
  ? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}${formattedTime}`
287
422
  : (typeof value === 'string' && value ? `${value}${formattedTime}` : null);
288
423
 
289
424
  // ── Day grid ─────────────────────────────────────────────────
290
425
  function renderDays() {
291
426
  const y = cursor.getFullYear(), m = cursor.getMonth();
292
- const first = new Date(y, m, 1).getDay(); // 0=Sun
293
- 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;
294
429
  const daysInMonth = new Date(y, m + 1, 0).getDate();
295
430
  const prevDays = new Date(y, m, 0).getDate();
296
431
 
297
432
  const cells = [];
298
- // Prev month filler
299
433
  for (let i = offset - 1; i >= 0; i--)
300
434
  cells.push({ day: prevDays - i, cur: false, next: false });
301
- // Current month
302
435
  for (let d = 1; d <= daysInMonth; d++)
303
436
  cells.push({ day: d, cur: true, next: false });
304
- // Next month filler — complete last row + always show at least one full next month week
305
437
  const totalSoFar = cells.length;
306
438
  let remaining = (7 - (totalSoFar % 7)) % 7;
307
439
  if (remaining === 0) remaining = 7;
@@ -320,7 +452,7 @@ export function DatePicker({
320
452
  ))}
321
453
  </div>
322
454
  {/* Day cells */}
323
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 1 }}>
455
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px 0' }}>
324
456
  {cells.map((c, i) => {
325
457
  if (!c.cur) {
326
458
  const isPrev = !c.next;
@@ -334,18 +466,22 @@ export function DatePicker({
334
466
  <div
335
467
  key={i}
336
468
  onClick={() => { if (!dis) pick(date); }}
469
+ onMouseEnter={() => { if (range && rangeStart && !rangeEnd) setHoverDate(date); }}
337
470
  style={{
338
471
  textAlign: 'center',
339
472
  padding: '6px 2px',
340
473
  fontSize: '.78rem',
341
474
  color: dis ? 'var(--border, #e5e7eb)' : 'var(--muted, #797b8d)',
342
475
  cursor: dis ? 'not-allowed' : 'pointer',
343
- 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',
344
482
  transition: 'background .12s',
345
483
  opacity: dis ? 0.35 : 0.7,
346
484
  }}
347
- onMouseEnter={e => { if (!dis) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'; }}
348
- onMouseLeave={e => { if (!dis) e.currentTarget.style.background = 'transparent'; }}
349
485
  >
350
486
  {c.day}
351
487
  </div>
@@ -354,30 +490,37 @@ export function DatePicker({
354
490
  const date = new Date(y, m, c.day);
355
491
  const key = toKey(y, m, c.day);
356
492
  const badge = badges[key];
357
- const sel = isSameDay(date, value);
493
+ const sel = range ? isRangeEndpoint(date) : isSameDay(date, value);
358
494
  const today = isToday(y, m, c.day);
359
495
  const disMin = minD && date < new Date(new Date(minD).setHours(0,0,0,0));
360
496
  const disMax = maxD && date > new Date(new Date(maxD).setHours(23,59,59,999));
361
497
  const dis = disMin || disMax;
362
498
  return (
363
- <div key={i} onClick={() => !dis && pick(date)} style={{
364
- position: 'relative', textAlign: 'center', padding: '6px 2px',
365
- fontSize: '.8rem', fontWeight: sel ? 700 : today ? 600 : 400,
366
- borderRadius: 'var(--radxs, 6px)', cursor: dis ? 'not-allowed' : 'pointer',
367
- background: sel ? 'var(--accent, #f99e2c)' : 'transparent',
368
- color: sel ? 'var(--def-btn-text,#fff)' : dis ? 'var(--border, #e2e8f0)' : today ? 'var(--accent, #f99e2c)' : 'var(--primary-text, #0f172a)',
369
- border: today && !sel ? '1.5px solid var(--accent, #f99e2c)' : '1.5px solid transparent',
370
- transition: 'background .12s, color .12s',
371
- opacity: dis ? 0.35 : 1,
372
- }}
373
- onMouseEnter={e => { if (!sel && !dis) e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'; }}
374
- 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
+ >
375
518
  {c.day}
376
519
  {badge && (
377
520
  <span style={{
378
521
  position: 'absolute', bottom: 2, left: '50%', transform: 'translateX(-50%)',
379
522
  width: 4, height: 4, borderRadius: '50%',
380
- background: badge.color || 'var(--accent, #f99e2c)',
523
+ background: badge.color || 'var(--accent, #12675D)',
381
524
  }} />
382
525
  )}
383
526
  </div>