cynx-ui 1.2.39 → 1.2.40

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.
@@ -42,7 +42,7 @@ function isSameDay(a, b) {
42
42
  }
43
43
  function isToday(y, m, d) { const t = new Date(); return t.getFullYear()===y && t.getMonth()===m && t.getDate()===d; }
44
44
 
45
- export function DatePicker({ value, onChange, placeholder = 'Select date', badges = {}, minDate, maxDate, disabled, style }) {
45
+ export function DatePicker({ value, onChange, placeholder = 'Select date', badges = {}, minDate, maxDate, disabled, showStepper = true, stepperPosition = 'around', style }) {
46
46
  const [open, setOpen] = useState(false);
47
47
  const [view, setView] = useState('day'); // 'day' | 'month' | 'year'
48
48
  const [cursor, setCursor] = useState(() => toDate(value) || new Date());
@@ -51,6 +51,24 @@ export function DatePicker({ value, onChange, placeholder = 'Select date', badge
51
51
  const btnRef = useRef(null);
52
52
  const dropRef = useRef(null);
53
53
 
54
+ const changeDay = (delta) => {
55
+ if (disabled) return;
56
+ const current = toDate(value) || new Date();
57
+ const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
58
+
59
+ if (minDate && next < new Date(new Date(minDate).setHours(0, 0, 0, 0))) return;
60
+ if (maxDate && next > new Date(new Date(maxDate).setHours(23, 59, 59, 999))) return;
61
+
62
+ if (typeof value === 'string') {
63
+ const y = next.getFullYear();
64
+ const m = pad(next.getMonth() + 1);
65
+ const d = pad(next.getDate());
66
+ onChange?.(`${y}-${m}-${d}`);
67
+ } else {
68
+ onChange?.(next);
69
+ }
70
+ };
71
+
54
72
  // Close on outside click
55
73
  useEffect(() => {
56
74
  if (!open) return;
@@ -272,32 +290,89 @@ export function DatePicker({ value, onChange, placeholder = 'Select date', badge
272
290
  ? `${cursor.getFullYear()}`
273
291
  : `${Math.floor(cursor.getFullYear()/12)*12} – ${Math.floor(cursor.getFullYear()/12)*12+11}`;
274
292
 
275
- return (
276
- <div ref={ref} style={{ position: 'relative', display: 'inline-block', ...style }}>
277
- {/* Trigger */}
278
- <button ref={btnRef} type="button" onClick={toggleOpen} style={{
279
- display: 'flex', alignItems: 'center', gap: 7,
280
- padding: '5px 10px', borderRadius: 'var(--rads)',
293
+ const renderPrevBtn = () => (
294
+ <button
295
+ type="button"
296
+ disabled={disabled}
297
+ onClick={(e) => { e.stopPropagation(); changeDay(-1); }}
298
+ style={{
299
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
300
+ width: 32, height: 32, borderRadius: 'var(--rads)',
301
+ border: '1px solid var(--border)', background: 'var(--surface)',
302
+ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
303
+ color: 'var(--muted)', transition: 'all .15s', boxShadow: 'var(--shadow)',
304
+ flexShrink: 0
305
+ }}
306
+ title="Previous day (-1)"
307
+ onMouseEnter={e => { if (!disabled) { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent)'; } }}
308
+ onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--muted)'; }}
309
+ >
310
+ <ChevronLeft size={15} />
311
+ </button>
312
+ );
313
+
314
+ const renderNextBtn = () => (
315
+ <button
316
+ type="button"
317
+ disabled={disabled}
318
+ onClick={(e) => { e.stopPropagation(); changeDay(1); }}
319
+ style={{
320
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
321
+ width: 32, height: 32, borderRadius: 'var(--rads)',
281
322
  border: '1px solid var(--border)', background: 'var(--surface)',
282
323
  cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
283
- fontSize: '.8rem', color: formatted ? 'var(--primary-text)' : 'var(--muted)',
284
- fontFamily: 'var(--sans)', transition: 'border-color .15s',
285
- whiteSpace: 'nowrap', minWidth: 140, height: 32, boxShadow: 'var(--shadow)',
324
+ color: 'var(--muted)', transition: 'all .15s', boxShadow: 'var(--shadow)',
325
+ flexShrink: 0
286
326
  }}
287
- onMouseEnter={e => { if (!disabled) e.currentTarget.style.borderColor = 'var(--accent)'; }}
288
- onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; }}>
289
- <Calendar size={13} style={{ color: 'var(--muted)', flexShrink: 0 }} />
290
- <span style={{ flex: 1, textAlign: 'left' }}>{formatted || placeholder}</span>
291
- {value && (
292
- <span
293
- onClick={e => { e.stopPropagation(); onChange?.(null); }}
294
- style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted)', cursor: 'pointer', flexShrink: 0, fontSize: 13, lineHeight: 1, padding: '0 2px' }}
295
- onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger)'; }}
296
- onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted)'; }}>
297
-
298
- </span>
299
- )}
300
- </button>
327
+ title="Next day (+1)"
328
+ onMouseEnter={e => { if (!disabled) { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent)'; } }}
329
+ onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--muted)'; }}
330
+ >
331
+ <ChevronRight size={15} />
332
+ </button>
333
+ );
334
+
335
+ const renderTriggerBtn = () => (
336
+ <button ref={btnRef} type="button" onClick={toggleOpen} style={{
337
+ display: 'flex', alignItems: 'center', gap: 7,
338
+ padding: '5px 10px', borderRadius: 'var(--rads)',
339
+ border: '1px solid var(--border)', background: 'var(--surface)',
340
+ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
341
+ fontSize: '.8rem', color: formatted ? 'var(--primary-text)' : 'var(--muted)',
342
+ fontFamily: 'var(--sans)', transition: 'border-color .15s',
343
+ whiteSpace: 'nowrap', minWidth: 140, height: 32, boxShadow: 'var(--shadow)',
344
+ }}
345
+ onMouseEnter={e => { if (!disabled) e.currentTarget.style.borderColor = 'var(--accent)'; }}
346
+ onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; }}>
347
+ <Calendar size={13} style={{ color: 'var(--muted)', flexShrink: 0 }} />
348
+ <span style={{ flex: 1, textAlign: 'left' }}>{formatted || placeholder}</span>
349
+ {value && (
350
+ <span
351
+ onClick={e => { e.stopPropagation(); onChange?.(null); }}
352
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted)', cursor: 'pointer', flexShrink: 0, fontSize: 13, lineHeight: 1, padding: '0 2px' }}
353
+ onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger)'; }}
354
+ onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted)'; }}>
355
+
356
+ </span>
357
+ )}
358
+ </button>
359
+ );
360
+
361
+ const posMode = stepperPosition === 'left' || stepperPosition === 'start' ? 'left'
362
+ : stepperPosition === 'right' || stepperPosition === 'end' ? 'right'
363
+ : 'around';
364
+
365
+ return (
366
+ <div ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, ...style }}>
367
+ {showStepper && posMode === 'around' && renderPrevBtn()}
368
+ {showStepper && posMode === 'left' && renderPrevBtn()}
369
+ {showStepper && posMode === 'left' && renderNextBtn()}
370
+
371
+ {renderTriggerBtn()}
372
+
373
+ {showStepper && posMode === 'around' && renderNextBtn()}
374
+ {showStepper && posMode === 'right' && renderPrevBtn()}
375
+ {showStepper && posMode === 'right' && renderNextBtn()}
301
376
 
302
377
  {/* Dropdown via portal */}
303
378
  {open && createPortal(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-ui",
3
- "version": "1.2.39",
3
+ "version": "1.2.40",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js"