cynx-lib 1.3.74 → 1.3.76

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.74",
3
+ "version": "1.3.76",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -41,8 +41,24 @@ function toKey(y, m, d) { return `${y}-${pad(m+1)}-${pad(d)}`; }
41
41
  function toDate(val) {
42
42
  if (!val) return null;
43
43
  if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
44
+ if (typeof val === 'number') {
45
+ if (val >= 1000 && val <= 9999) return new Date(val, 0, 1);
46
+ const d = new Date(val);
47
+ return isNaN(d.getTime()) ? null : d;
48
+ }
44
49
  if (typeof val === 'string') {
45
- const parts = val.split('-');
50
+ const trimmed = val.trim();
51
+ if (/^\d{4}$/.test(trimmed)) {
52
+ const y = parseInt(trimmed, 10);
53
+ if (!isNaN(y)) return new Date(y, 0, 1);
54
+ }
55
+ const ym = trimmed.split('-');
56
+ if (ym.length === 2) {
57
+ const y = parseInt(ym[0], 10);
58
+ const m = parseInt(ym[1], 10) - 1;
59
+ if (!isNaN(y) && !isNaN(m)) return new Date(y, m, 1);
60
+ }
61
+ const parts = trimmed.split('-');
46
62
  if (parts.length === 3) {
47
63
  const y = parseInt(parts[0], 10);
48
64
  const m = parseInt(parts[1], 10) - 1;
@@ -72,8 +88,10 @@ const SIZES = {
72
88
  export function DatePicker({
73
89
  value,
74
90
  onChange,
91
+ type = 'date',
92
+ mode,
75
93
  label,
76
- placeholder = 'Select date',
94
+ placeholder,
77
95
  badges = {},
78
96
  minDate,
79
97
  maxDate,
@@ -97,8 +115,13 @@ export function DatePicker({
97
115
  const DAYS = loc.days;
98
116
  const MONTHS = loc.months;
99
117
  const s = SIZES[size] || SIZES.md;
118
+ const pickerType = type || mode || 'date';
119
+ const initialView = pickerType === 'month' ? 'month' : pickerType === 'year' ? 'year' : 'day';
120
+ const defaultPlaceholder = pickerType === 'month' ? 'Select month' : pickerType === 'year' ? 'Select year' : 'Select date';
121
+ const effectivePlaceholder = placeholder !== undefined ? placeholder : defaultPlaceholder;
122
+
100
123
  const [open, setOpen] = useState(false);
101
- const [view, setView] = useState('day'); // 'day' | 'month' | 'year'
124
+ const [view, setView] = useState(initialView); // 'day' | 'month' | 'year'
102
125
  const [cursor, setCursor] = useState(() => new Date());
103
126
  const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
104
127
  const [rangeStart, setRangeStart] = useState(null);
@@ -108,11 +131,46 @@ export function DatePicker({
108
131
  const btnRef = useRef(null);
109
132
  const dropRef = useRef(null);
110
133
 
134
+ useEffect(() => {
135
+ setView(initialView);
136
+ }, [pickerType]);
137
+
111
138
  const changeDay = (delta) => {
112
139
  if (disabled) return;
113
140
  const current = toDate(value) || new Date();
114
- const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
141
+ if (pickerType === 'year') {
142
+ const next = new Date(current.getFullYear() + delta, 0, 1);
143
+ const minD = toDate(minDate);
144
+ const maxD = toDate(maxDate);
145
+ if (minD && next.getFullYear() < minD.getFullYear()) return;
146
+ if (maxD && next.getFullYear() > maxD.getFullYear()) return;
147
+ if (typeof value === 'number') {
148
+ onChange?.(next.getFullYear());
149
+ } else if (typeof value === 'string') {
150
+ onChange?.(`${next.getFullYear()}`);
151
+ } else {
152
+ onChange?.(next);
153
+ }
154
+ return;
155
+ }
156
+ if (pickerType === 'month') {
157
+ const next = new Date(current.getFullYear(), current.getMonth() + delta, 1);
158
+ const minD = toDate(minDate);
159
+ const maxD = toDate(maxDate);
160
+ const nextEnd = new Date(next.getFullYear(), next.getMonth() + 1, 0, 23, 59, 59, 999);
161
+ if (minD && nextEnd < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
162
+ if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
163
+ if (typeof value === 'string') {
164
+ const y = next.getFullYear();
165
+ const m = pad(next.getMonth() + 1);
166
+ onChange?.(`${y}-${m}`);
167
+ } else {
168
+ onChange?.(next);
169
+ }
170
+ return;
171
+ }
115
172
 
173
+ const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + delta);
116
174
  const minD = toDate(minDate);
117
175
  const maxD = toDate(maxDate);
118
176
  if (minD && next < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
@@ -137,13 +195,13 @@ export function DatePicker({
137
195
  if (e.target?.closest?.('.cynx-timepicker-drop') || e.target?.closest?.('.cynx-timepicker-cell')) return;
138
196
  if (document.querySelector('.cynx-timepicker-drop')) return;
139
197
  if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
140
- setOpen(false); setView('day');
198
+ setOpen(false); setView(initialView);
141
199
  }
142
200
  function onKey(e) {
143
201
  if (e.key === 'Escape') {
144
202
  if (document.querySelector('.cynx-timepicker-drop')) return;
145
203
  if (typeof window !== 'undefined') window.__cynx_last_portal_closed_at = Date.now();
146
- setOpen(false); setView('day');
204
+ setOpen(false); setView(initialView);
147
205
  }
148
206
  }
149
207
  document.addEventListener('mousedown', close, true);
@@ -152,7 +210,7 @@ export function DatePicker({
152
210
  document.removeEventListener('mousedown', close, true);
153
211
  document.removeEventListener('keydown', onKey);
154
212
  };
155
- }, [open]);
213
+ }, [open, initialView]);
156
214
 
157
215
  const [stepperPosState, setStepperPosState] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('datepicker_stepper_position') : null);
158
216
 
@@ -225,6 +283,7 @@ export function DatePicker({
225
283
  if (disabled) return;
226
284
  if (!open) {
227
285
  calcPos();
286
+ setView(initialView);
228
287
  }
229
288
  setOpen(o => !o);
230
289
  };
@@ -417,6 +476,10 @@ export function DatePicker({
417
476
  const formattedTime = showTime && timeValue ? ` ${timeValue}` : '';
418
477
  const formatted = range
419
478
  ? (rangeStart && rangeEnd ? `${formatDateStr(rangeStart)} — ${formatDateStr(rangeEnd)}` : rangeStart ? `${formatDateStr(rangeStart)} — ...` : null)
479
+ : pickerType === 'year'
480
+ ? (valDate ? `${valDate.getFullYear()}` : (value !== null && value !== undefined && value !== '' ? `${value}` : null))
481
+ : pickerType === 'month'
482
+ ? (valDate ? `${MONTHS[valDate.getMonth()]} ${valDate.getFullYear()}` : (typeof value === 'string' && value ? `${value}` : null))
420
483
  : valDate
421
484
  ? `${MONTHS[valDate.getMonth()]} ${valDate.getDate()}, ${valDate.getFullYear()}${formattedTime}`
422
485
  : (typeof value === 'string' && value ? `${value}${formattedTime}` : null);
@@ -555,7 +618,18 @@ export function DatePicker({
555
618
  const c = new Date(cursor);
556
619
  c.setMonth(i);
557
620
  setCursor(c);
558
- setView('day');
621
+ if (pickerType === 'month') {
622
+ if (typeof value === 'string') {
623
+ const ym = `${y}-${pad(i + 1)}`;
624
+ onChange?.(ym);
625
+ } else {
626
+ onChange?.(c);
627
+ }
628
+ setOpen(false);
629
+ setView('month');
630
+ } else {
631
+ setView('day');
632
+ }
559
633
  }}
560
634
  style={{
561
635
  textAlign: 'center',
@@ -605,7 +679,21 @@ export function DatePicker({
605
679
  const c = new Date(cursor);
606
680
  c.setFullYear(yr);
607
681
  setCursor(c);
608
- setView('month');
682
+ if (pickerType === 'year') {
683
+ if (typeof value === 'number') {
684
+ onChange?.(yr);
685
+ } else if (typeof value === 'string') {
686
+ onChange?.(`${yr}`);
687
+ } else {
688
+ onChange?.(c);
689
+ }
690
+ setOpen(false);
691
+ setView('year');
692
+ } else if (pickerType === 'month') {
693
+ setView('month');
694
+ } else {
695
+ setView('day');
696
+ }
609
697
  }}
610
698
  style={{
611
699
  textAlign: 'center',
@@ -641,6 +729,13 @@ export function DatePicker({
641
729
  const maxD = toDate(maxDate);
642
730
  if (!maxD) return false;
643
731
  const current = toDate(value) || new Date();
732
+ if (pickerType === 'year') {
733
+ return (current.getFullYear() + 1) > maxD.getFullYear();
734
+ }
735
+ if (pickerType === 'month') {
736
+ const nextMonthStart = new Date(current.getFullYear(), current.getMonth() + 1, 1);
737
+ return nextMonthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
738
+ }
644
739
  const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + 1);
645
740
  return next > new Date(new Date(maxD).setHours(23, 59, 59, 999));
646
741
  };
@@ -650,6 +745,13 @@ export function DatePicker({
650
745
  const minD = toDate(minDate);
651
746
  if (!minD) return false;
652
747
  const current = toDate(value) || new Date();
748
+ if (pickerType === 'year') {
749
+ return (current.getFullYear() - 1) < minD.getFullYear();
750
+ }
751
+ if (pickerType === 'month') {
752
+ const prevMonthEnd = new Date(current.getFullYear(), current.getMonth(), 0, 23, 59, 59, 999);
753
+ return prevMonthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
754
+ }
653
755
  const prev = new Date(current.getFullYear(), current.getMonth(), current.getDate() - 1);
654
756
  return prev < new Date(new Date(minD).setHours(0, 0, 0, 0));
655
757
  };
@@ -717,7 +819,7 @@ export function DatePicker({
717
819
  onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border, #e2e8f0)'; }}>
718
820
  <Calendar size={s.iconSize} style={{ color: 'var(--muted, #797b8d)', flexShrink: 0 }} />
719
821
  <span style={{ flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }}>
720
- {formatted || placeholder}
822
+ {formatted || effectivePlaceholder}
721
823
  </span>
722
824
  {value && (
723
825
  <span
@@ -783,7 +885,7 @@ export function DatePicker({
783
885
  animation: 'dp-in .12s ease',
784
886
  }}>
785
887
  {/* Calendar header */}
786
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
888
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, gap: 4 }}>
787
889
  <button
788
890
  type="button"
789
891
  disabled={isNavPrevDisabled()}
@@ -800,11 +902,65 @@ export function DatePicker({
800
902
  >
801
903
  <ChevronLeft size={15} />
802
904
  </button>
803
- <button type="button" onClick={cycleView} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)', padding: '4px 8px', borderRadius: 'var(--radxs, 6px)', fontFamily: 'var(--sans, inherit)' }}
804
- onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
805
- onMouseLeave={e => e.currentTarget.style.background = 'none'}>
806
- {headerLabel}
807
- </button>
905
+
906
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4, minWidth: 0 }}>
907
+ {view === 'day' && (
908
+ <>
909
+ <button
910
+ type="button"
911
+ onClick={() => setView('month')}
912
+ style={{
913
+ background: 'none', border: 'none', cursor: 'pointer',
914
+ fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)',
915
+ padding: '4px 6px', borderRadius: 'var(--radxs, 6px)',
916
+ fontFamily: 'var(--sans, inherit)', transition: 'background .12s'
917
+ }}
918
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
919
+ onMouseLeave={e => e.currentTarget.style.background = 'none'}
920
+ >
921
+ {MONTHS[cursor.getMonth()]}
922
+ </button>
923
+ <button
924
+ type="button"
925
+ onClick={() => setView('year')}
926
+ style={{
927
+ background: 'none', border: 'none', cursor: 'pointer',
928
+ fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)',
929
+ padding: '4px 6px', borderRadius: 'var(--radxs, 6px)',
930
+ fontFamily: 'var(--sans, inherit)', transition: 'background .12s'
931
+ }}
932
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
933
+ onMouseLeave={e => e.currentTarget.style.background = 'none'}
934
+ >
935
+ {cursor.getFullYear()}
936
+ </button>
937
+ </>
938
+ )}
939
+
940
+ {view === 'month' && (
941
+ <button
942
+ type="button"
943
+ onClick={() => setView('year')}
944
+ style={{
945
+ background: 'none', border: 'none', cursor: 'pointer',
946
+ fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)',
947
+ padding: '4px 8px', borderRadius: 'var(--radxs, 6px)',
948
+ fontFamily: 'var(--sans, inherit)', transition: 'background .12s'
949
+ }}
950
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--grey-1, #f5f5f8)'}
951
+ onMouseLeave={e => e.currentTarget.style.background = 'none'}
952
+ >
953
+ {cursor.getFullYear()}
954
+ </button>
955
+ )}
956
+
957
+ {view === 'year' && (
958
+ <span style={{ fontSize: '.82rem', fontWeight: 700, color: 'var(--primary-text, #0f172a)', padding: '4px 8px' }}>
959
+ {Math.floor(cursor.getFullYear() / 12) * 12} – {Math.floor(cursor.getFullYear() / 12) * 12 + 11}
960
+ </span>
961
+ )}
962
+ </div>
963
+
808
964
  <button
809
965
  type="button"
810
966
  disabled={isNavNextDisabled()}
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ import { computed, useSlots } from "vue";
3
+
4
+ const SIZE_CONFIG: Record<string, any> = {
5
+ xs: { hMargin: "8px 0", vMargin: "0 4px", vHeight: "14px", fontSize: ".7rem", padding: "0 8px" },
6
+ sm: { hMargin: "12px 0", vMargin: "0 8px", vHeight: "18px", fontSize: ".75rem", padding: "0 10px" },
7
+ md: { hMargin: "16px 0", vMargin: "0 12px", vHeight: "24px", fontSize: ".82rem", padding: "0 12px" },
8
+ lg: { hMargin: "24px 0", vMargin: "0 16px", vHeight: "32px", fontSize: ".9rem", padding: "0 16px" },
9
+ xl: { hMargin: "32px 0", vMargin: "0 24px", vHeight: "48px", fontSize: "1rem", padding: "0 20px" },
10
+ };
11
+
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ label?: string;
15
+ orientation?: "horizontal" | "vertical";
16
+ align?: "left" | "center" | "right";
17
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
18
+ color?: string;
19
+ thickness?: number | string;
20
+ variant?: string;
21
+ }>(),
22
+ {
23
+ orientation: "horizontal",
24
+ align: "center",
25
+ size: "md",
26
+ color: "var(--border-light, #ebebf0)",
27
+ thickness: 1,
28
+ variant: "solid",
29
+ }
30
+ );
31
+
32
+ const slots = useSlots();
33
+ const hasContent = computed(() => Boolean(props.label || slots.default));
34
+ const cfg = computed(() => SIZE_CONFIG[props.size] || SIZE_CONFIG.md);
35
+ const thickStr = computed(() =>
36
+ typeof props.thickness === "number" ? `${props.thickness}px` : props.thickness
37
+ );
38
+ const lineStyle = computed(() => `${thickStr.value} ${props.variant} ${props.color}`);
39
+ const leftFlex = computed(() => (props.align === "left" ? "0 0 16px" : "1"));
40
+ const rightFlex = computed(() => (props.align === "right" ? "0 0 16px" : "1"));
41
+ </script>
42
+
43
+ <template>
44
+ <div
45
+ v-if="orientation === 'vertical'"
46
+ class="cynx-divider cynx-divider-vertical"
47
+ :style="{
48
+ display: 'inline-block',
49
+ width: 0,
50
+ height: cfg.vHeight,
51
+ margin: cfg.vMargin,
52
+ borderLeft: lineStyle,
53
+ verticalAlign: 'middle',
54
+ alignSelf: 'center',
55
+ flexShrink: 0,
56
+ }"
57
+ />
58
+
59
+ <div
60
+ v-else-if="hasContent"
61
+ class="cynx-divider cynx-divider-horizontal"
62
+ :style="{
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ width: '100%',
66
+ margin: cfg.hMargin,
67
+ }"
68
+ >
69
+ <div :style="{ flex: leftFlex, borderBottom: lineStyle }" />
70
+ <span
71
+ :style="{
72
+ padding: cfg.padding,
73
+ fontSize: cfg.fontSize,
74
+ color: 'var(--muted, #9b9daa)',
75
+ fontWeight: 500,
76
+ whiteSpace: 'nowrap',
77
+ lineHeight: 1,
78
+ }"
79
+ >
80
+ <slot>{{ label }}</slot>
81
+ </span>
82
+ <div :style="{ flex: rightFlex, borderBottom: lineStyle }" />
83
+ </div>
84
+
85
+ <div
86
+ v-else
87
+ class="cynx-divider cynx-divider-horizontal"
88
+ :style="{
89
+ width: '100%',
90
+ margin: cfg.hMargin,
91
+ borderBottom: lineStyle,
92
+ }"
93
+ />
94
+ </template>
package/vue/index.d.ts CHANGED
@@ -58,6 +58,7 @@ export declare const LinearProgress: DefineComponent<any, any, any>;
58
58
  export declare const CircularProgress: DefineComponent<any, any, any>;
59
59
  export declare const StatusTag: DefineComponent<any, any, any>;
60
60
  export declare const InfoBadge: DefineComponent<any, any, any>;
61
+ export declare const Divider: DefineComponent<any, any, any>;
61
62
  export declare const XTable: DefineComponent<any, any, any>;
62
63
  export declare const XThead: DefineComponent<any, any, any>;
63
64
  export declare const XTbody: DefineComponent<any, any, any>;
package/vue/index.js CHANGED
@@ -19,6 +19,7 @@ import LinearProgress from "./components/LinearProgress.vue";
19
19
  import CircularProgress from "./components/CircularProgress.vue";
20
20
  import StatusTag from "./components/StatusTag.vue";
21
21
  import InfoBadge from "./components/InfoBadge.vue";
22
+ import Divider from "./components/Divider.vue";
22
23
  import { XTable, XThead, XTbody, XTr, XTh, XTd } from "./components/XTable.vue";
23
24
  import { useAutoSkeletonRows } from "./composables/useAutoSkeletonRows";
24
25
 
@@ -43,6 +44,7 @@ export {
43
44
  CircularProgress,
44
45
  StatusTag,
45
46
  InfoBadge,
47
+ Divider,
46
48
  XTable,
47
49
  XThead,
48
50
  XTbody,
@@ -73,6 +75,7 @@ export default {
73
75
  CircularProgress,
74
76
  StatusTag,
75
77
  InfoBadge,
78
+ Divider,
76
79
  XTable,
77
80
  XThead,
78
81
  XTbody,