cynx-lib 1.3.77 → 1.3.79

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.77",
3
+ "version": "1.3.79",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -97,8 +97,7 @@ export function DatePicker({
97
97
  maxDate,
98
98
  disabled,
99
99
  size = 'md',
100
- showStepper = false,
101
- stepperPosition = 'off',
100
+ stepper = 'off',
102
101
  locale = 'en',
103
102
  style,
104
103
  fullWidth = false,
@@ -833,12 +832,6 @@ export function DatePicker({
833
832
  </button>
834
833
  );
835
834
 
836
- const actualPos = stepperPosition || 'around';
837
- const showStepperEffective = showStepper && actualPos !== 'off';
838
- const posMode = actualPos === 'left' || actualPos === 'start' ? 'left'
839
- : actualPos === 'right' || actualPos === 'end' ? 'right'
840
- : 'around';
841
-
842
835
  return (
843
836
  <div style={{
844
837
  display: fullWidth || style?.display === 'flex' ? 'flex' : 'inline-flex',
@@ -864,15 +857,23 @@ export function DatePicker({
864
857
  maxWidth: '100%',
865
858
  minWidth: 0,
866
859
  }}>
867
- {showStepperEffective && posMode === 'around' && renderPrevBtn()}
868
- {showStepperEffective && posMode === 'left' && renderPrevBtn()}
869
- {showStepperEffective && posMode === 'left' && renderNextBtn()}
860
+ {stepper === 'around' && renderPrevBtn()}
861
+ {stepper === 'left' && (
862
+ <>
863
+ {renderPrevBtn()}
864
+ {renderNextBtn()}
865
+ </>
866
+ )}
870
867
 
871
868
  {renderTriggerBtn()}
872
869
 
873
- {showStepperEffective && posMode === 'around' && renderNextBtn()}
874
- {showStepperEffective && posMode === 'right' && renderPrevBtn()}
875
- {showStepperEffective && posMode === 'right' && renderNextBtn()}
870
+ {stepper === 'around' && renderNextBtn()}
871
+ {stepper === 'right' && (
872
+ <>
873
+ {renderPrevBtn()}
874
+ {renderNextBtn()}
875
+ </>
876
+ )}
876
877
  </div>
877
878
 
878
879
  {/* Dropdown via portal */}
@@ -142,6 +142,8 @@ ensureStyles();
142
142
 
143
143
  const props = withDefaults(
144
144
  defineProps<{
145
+ type?: "date" | "month" | "year";
146
+ mode?: "date" | "month" | "year";
145
147
  modelValue?: any;
146
148
  value?: any;
147
149
  label?: string;
@@ -151,8 +153,7 @@ const props = withDefaults(
151
153
  maxDate?: any;
152
154
  disabled?: boolean;
153
155
  size?: "xs" | "sm" | "md" | "lg";
154
- showStepper?: boolean;
155
- stepperPosition?: "off" | "around" | "left" | "right" | "start" | "end";
156
+ stepper?: "off" | "around" | "left" | "right";
156
157
  locale?: "en" | "ru" | DatePickerLocale | string;
157
158
  clearable?: boolean;
158
159
  fullWidth?: boolean;
@@ -165,17 +166,18 @@ const props = withDefaults(
165
166
  range?: boolean;
166
167
  }>(),
167
168
  {
169
+ type: "date",
170
+ mode: undefined,
168
171
  modelValue: undefined,
169
172
  value: undefined,
170
173
  label: "",
171
- placeholder: "Select date",
174
+ placeholder: undefined,
172
175
  badges: () => ({}),
173
176
  minDate: undefined,
174
177
  maxDate: undefined,
175
178
  disabled: false,
176
179
  size: "md",
177
- showStepper: false,
178
- stepperPosition: "off",
180
+ stepper: "off",
179
181
  locale: "en",
180
182
  clearable: true,
181
183
  fullWidth: false,
@@ -198,10 +200,34 @@ const emit = defineEmits<{
198
200
  (e: "clear"): void;
199
201
  }>();
200
202
 
203
+ const emitValue = (val: any) => {
204
+ emit("update:modelValue", val);
205
+ emit("update:value", val);
206
+ emit("change", val);
207
+ };
208
+
201
209
  const currentVal = computed(() => {
202
210
  return props.modelValue !== undefined ? props.modelValue : props.value;
203
211
  });
204
212
 
213
+ const pickerType = computed(() => props.type || props.mode || "date");
214
+ const initialView = computed<"day" | "month" | "year">(() => {
215
+ if (pickerType.value === "month") return "month";
216
+ if (pickerType.value === "year") return "year";
217
+ return "day";
218
+ });
219
+
220
+ const defaultPlaceholder = computed(() =>
221
+ pickerType.value === "month"
222
+ ? "Select month"
223
+ : pickerType.value === "year"
224
+ ? "Select year"
225
+ : "Select date"
226
+ );
227
+ const effectivePlaceholder = computed(() =>
228
+ props.placeholder !== undefined ? props.placeholder : defaultPlaceholder.value
229
+ );
230
+
205
231
  const loc = computed<DatePickerLocale>(() => {
206
232
  if (typeof props.locale === "object" && props.locale !== null) {
207
233
  return props.locale;
@@ -211,7 +237,10 @@ const loc = computed<DatePickerLocale>(() => {
211
237
 
212
238
  const s = computed(() => SIZES[props.size] || SIZES.md);
213
239
  const open = ref(false);
214
- const view = ref<"day" | "month" | "year">("day");
240
+ const view = ref<"day" | "month" | "year">(initialView.value);
241
+ watch(pickerType, (pt) => {
242
+ view.value = pt === "month" ? "month" : pt === "year" ? "year" : "day";
243
+ });
215
244
  const cursor = ref<Date>(new Date());
216
245
  const pos = ref({ top: 0, left: 0, width: 0 });
217
246
 
@@ -284,13 +313,13 @@ const handleWindowClick = (e: MouseEvent) => {
284
313
  if (dropRef.value?.contains(target)) return;
285
314
  if (target.closest?.(".cynx-timepicker-drop")) return;
286
315
  open.value = false;
287
- view.value = "day";
316
+ view.value = initialView.value;
288
317
  };
289
318
 
290
319
  const handleKeyDown = (e: KeyboardEvent) => {
291
320
  if (e.key === "Escape" && open.value) {
292
321
  open.value = false;
293
- view.value = "day";
322
+ view.value = initialView.value;
294
323
  }
295
324
  };
296
325
 
@@ -319,7 +348,10 @@ onBeforeUnmount(() => {
319
348
 
320
349
  const toggleOpen = () => {
321
350
  if (props.disabled) return;
322
- if (!open.value) calcPos();
351
+ if (!open.value) {
352
+ calcPos();
353
+ view.value = initialView.value;
354
+ }
323
355
  open.value = !open.value;
324
356
  };
325
357
 
@@ -346,12 +378,10 @@ function pick(date: Date) {
346
378
  rangeEnd.value = end;
347
379
 
348
380
  const result = [formatDateStr(start), formatDateStr(end)];
349
- emit("update:modelValue", result);
350
- emit("update:value", result);
351
- emit("change", result);
381
+ emitValue(result);
352
382
 
353
383
  open.value = false;
354
- view.value = "day";
384
+ view.value = initialView.value;
355
385
  }
356
386
  return;
357
387
  }
@@ -362,18 +392,14 @@ function pick(date: Date) {
362
392
  const strVal = `${y}-${m}-${d}`;
363
393
 
364
394
  if (typeof currentVal.value === "string" || currentVal.value === undefined) {
365
- emit("update:modelValue", strVal);
366
- emit("update:value", strVal);
367
- emit("change", strVal);
395
+ emitValue(strVal);
368
396
  } else {
369
- emit("update:modelValue", date);
370
- emit("update:value", date);
371
- emit("change", date);
397
+ emitValue(date);
372
398
  }
373
399
 
374
400
  if (!props.showTime) {
375
401
  open.value = false;
376
- view.value = "day";
402
+ view.value = initialView.value;
377
403
  }
378
404
  }
379
405
 
@@ -383,27 +409,91 @@ function handleClear(e: MouseEvent) {
383
409
  rangeStart.value = null;
384
410
  rangeEnd.value = null;
385
411
  }
386
- emit("update:modelValue", null);
387
- emit("update:value", null);
388
- emit("change", null);
412
+ emitValue(null);
389
413
  emit("clear");
390
414
  }
391
415
 
416
+ const isPrevDayDisabled = () => {
417
+ const minD = toDate(props.minDate);
418
+ if (!minD) return false;
419
+ const current = toDate(currentVal.value) || new Date();
420
+ if (pickerType.value === "year") {
421
+ return current.getFullYear() - 1 < minD.getFullYear();
422
+ }
423
+ if (pickerType.value === "month") {
424
+ const prevMonthEnd = new Date(current.getFullYear(), current.getMonth(), 0, 23, 59, 59, 999);
425
+ return prevMonthEnd < new Date(new Date(minD).setHours(0, 0, 0, 0));
426
+ }
427
+ const prev = new Date(current.getFullYear(), current.getMonth(), current.getDate() - 1);
428
+ return prev < new Date(new Date(minD).setHours(0, 0, 0, 0));
429
+ };
430
+
431
+ const isNextDayDisabled = () => {
432
+ const maxD = toDate(props.maxDate);
433
+ if (!maxD) return false;
434
+ const current = toDate(currentVal.value) || new Date();
435
+ if (pickerType.value === "year") {
436
+ return current.getFullYear() + 1 > maxD.getFullYear();
437
+ }
438
+ if (pickerType.value === "month") {
439
+ const nextMonthStart = new Date(current.getFullYear(), current.getMonth() + 1, 1);
440
+ return nextMonthStart > new Date(new Date(maxD).setHours(23, 59, 59, 999));
441
+ }
442
+ const next = new Date(current.getFullYear(), current.getMonth(), current.getDate() + 1);
443
+ return next > new Date(new Date(maxD).setHours(23, 59, 59, 999));
444
+ };
445
+
392
446
  const changeDay = (delta: number) => {
393
447
  if (props.disabled) return;
394
448
  const current = toDate(currentVal.value) || new Date();
449
+ const minD = toDate(props.minDate);
450
+ const maxD = toDate(props.maxDate);
451
+
452
+ if (pickerType.value === "year") {
453
+ const next = new Date(current.getFullYear() + delta, 0, 1);
454
+ if (minD && next.getFullYear() < minD.getFullYear()) return;
455
+ if (maxD && next.getFullYear() > maxD.getFullYear()) return;
456
+ if (typeof currentVal.value === "number") {
457
+ emitValue(next.getFullYear());
458
+ } else if (typeof currentVal.value === "string") {
459
+ emitValue(`${next.getFullYear()}`);
460
+ } else {
461
+ emitValue(next);
462
+ }
463
+ return;
464
+ }
465
+
466
+ if (pickerType.value === "month") {
467
+ const next = new Date(current.getFullYear(), current.getMonth() + delta, 1);
468
+ const nextEnd = new Date(next.getFullYear(), next.getMonth() + 1, 0, 23, 59, 59, 999);
469
+ if (minD && nextEnd < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
470
+ if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
471
+ if (typeof currentVal.value === "string") {
472
+ const y = next.getFullYear();
473
+ const m = pad(next.getMonth() + 1);
474
+ emitValue(`${y}-${m}`);
475
+ } else {
476
+ emitValue(next);
477
+ }
478
+ return;
479
+ }
480
+
395
481
  const next = new Date(
396
482
  current.getFullYear(),
397
483
  current.getMonth(),
398
484
  current.getDate() + delta
399
485
  );
400
-
401
- const minD = toDate(props.minDate);
402
- const maxD = toDate(props.maxDate);
403
486
  if (minD && next < new Date(new Date(minD).setHours(0, 0, 0, 0))) return;
404
487
  if (maxD && next > new Date(new Date(maxD).setHours(23, 59, 59, 999))) return;
405
488
 
406
- pick(next);
489
+ if (typeof currentVal.value === "string") {
490
+ const y = next.getFullYear();
491
+ const m = pad(next.getMonth() + 1);
492
+ const d = pad(next.getDate());
493
+ emitValue(`${y}-${m}-${d}`);
494
+ } else {
495
+ emitValue(next);
496
+ }
407
497
  };
408
498
 
409
499
  const isNavNextDisabled = () => {
@@ -458,9 +548,6 @@ function nav(dir: number) {
458
548
  cursor.value = c;
459
549
  }
460
550
 
461
- function cycleView() {
462
- view.value = view.value === "day" ? "month" : "year";
463
- }
464
551
 
465
552
  const valDate = computed(() => toDate(currentVal.value));
466
553
 
@@ -561,6 +648,21 @@ const formatted = computed(() => {
561
648
  const vd = valDate.value;
562
649
  const formattedTime =
563
650
  props.showTime && props.timeValue ? ` ${props.timeValue}` : "";
651
+
652
+ if (pickerType.value === "year") {
653
+ return vd
654
+ ? `${vd.getFullYear()}`
655
+ : currentVal.value !== null && currentVal.value !== undefined && currentVal.value !== ""
656
+ ? `${currentVal.value}`
657
+ : null;
658
+ }
659
+ if (pickerType.value === "month") {
660
+ return vd
661
+ ? `${loc.value.months[vd.getMonth()]} ${vd.getFullYear()}`
662
+ : typeof currentVal.value === "string" && currentVal.value
663
+ ? `${currentVal.value}`
664
+ : null;
665
+ }
564
666
  if (vd) {
565
667
  const mName = loc.value.months[vd.getMonth()] || "";
566
668
  return `${mName} ${vd.getDate()}, ${vd.getFullYear()}${formattedTime}`;
@@ -571,18 +673,6 @@ const formatted = computed(() => {
571
673
  return null;
572
674
  });
573
675
 
574
- const headerLabel = computed(() => {
575
- const y = cursor.value.getFullYear();
576
- const m = cursor.value.getMonth();
577
- if (view.value === "day") {
578
- return `${loc.value.months[m]} ${y}`;
579
- }
580
- if (view.value === "month") {
581
- return `${y}`;
582
- }
583
- const base = Math.floor(y / 12) * 12;
584
- return `${base} – ${base + 11}`;
585
- });
586
676
 
587
677
  const daysCells = computed(() => {
588
678
  const y = cursor.value.getFullYear();
@@ -644,15 +734,6 @@ const isYearDisabled = (yr: number) => {
644
734
  return Boolean(disMin || disMax);
645
735
  };
646
736
 
647
- const showStepperEffective = computed(
648
- () => props.showStepper && props.stepperPosition !== "off"
649
- );
650
- const posMode = computed(() => {
651
- const p = props.stepperPosition;
652
- if (p === "left" || p === "start") return "left";
653
- if (p === "right" || p === "end") return "right";
654
- return "around";
655
- });
656
737
  </script>
657
738
 
658
739
  <template>
@@ -698,9 +779,9 @@ const posMode = computed(() => {
698
779
  >
699
780
  <!-- Left stepper buttons -->
700
781
  <button
701
- v-if="showStepperEffective && (posMode === 'around' || posMode === 'left')"
782
+ v-if="props.stepper === 'around' || props.stepper === 'left'"
702
783
  type="button"
703
- :disabled="disabled || isCellDisabled(new Date(new Date(valDate || new Date()).setDate((valDate || new Date()).getDate() - 1)))"
784
+ :disabled="disabled || isPrevDayDisabled()"
704
785
  style="
705
786
  display: flex;
706
787
  align-items: center;
@@ -725,6 +806,34 @@ const posMode = computed(() => {
725
806
  <ChevronLeft :size="s.iconSize + 2" />
726
807
  </button>
727
808
 
809
+ <button
810
+ v-if="props.stepper === 'left'"
811
+ type="button"
812
+ :disabled="disabled || isNextDayDisabled()"
813
+ style="
814
+ display: flex;
815
+ align-items: center;
816
+ justify-content: center;
817
+ border-radius: var(--rads, 8px);
818
+ border: 1px solid var(--border, #e2e8f0);
819
+ background: var(--surface, #ffffff);
820
+ color: var(--muted, #797b8d);
821
+ box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
822
+ flex-shrink: 0;
823
+ cursor: pointer;
824
+ transition: all .15s;
825
+ "
826
+ :style="{
827
+ width: `${s.btnSize}px`,
828
+ height: `${s.btnSize}px`,
829
+ opacity: disabled ? 0.35 : 1,
830
+ }"
831
+ title="Next day (+1)"
832
+ @click.stop="changeDay(1)"
833
+ >
834
+ <ChevronRight :size="s.iconSize + 2" />
835
+ </button>
836
+
728
837
  <!-- Trigger button -->
729
838
  <button
730
839
  ref="btnRef"
@@ -770,7 +879,7 @@ const posMode = computed(() => {
770
879
  min-width: 0;
771
880
  "
772
881
  >
773
- {{ formatted || placeholder }}
882
+ {{ formatted || effectivePlaceholder }}
774
883
  </span>
775
884
  <span
776
885
  v-if="currentVal && clearable"
@@ -794,9 +903,37 @@ const posMode = computed(() => {
794
903
 
795
904
  <!-- Right stepper buttons -->
796
905
  <button
797
- v-if="showStepperEffective && (posMode === 'around' || posMode === 'right')"
906
+ v-if="props.stepper === 'right'"
907
+ type="button"
908
+ :disabled="disabled || isPrevDayDisabled()"
909
+ style="
910
+ display: flex;
911
+ align-items: center;
912
+ justify-content: center;
913
+ border-radius: var(--rads, 8px);
914
+ border: 1px solid var(--border, #e2e8f0);
915
+ background: var(--surface, #ffffff);
916
+ color: var(--muted, #797b8d);
917
+ box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.08));
918
+ flex-shrink: 0;
919
+ cursor: pointer;
920
+ transition: all .15s;
921
+ "
922
+ :style="{
923
+ width: `${s.btnSize}px`,
924
+ height: `${s.btnSize}px`,
925
+ opacity: disabled ? 0.35 : 1,
926
+ }"
927
+ title="Previous day (-1)"
928
+ @click.stop="changeDay(-1)"
929
+ >
930
+ <ChevronLeft :size="s.iconSize + 2" />
931
+ </button>
932
+
933
+ <button
934
+ v-if="props.stepper === 'around' || props.stepper === 'right'"
798
935
  type="button"
799
- :disabled="disabled || isCellDisabled(new Date(new Date(valDate || new Date()).setDate((valDate || new Date()).getDate() + 1)))"
936
+ :disabled="disabled || isNextDayDisabled()"
800
937
  style="
801
938
  display: flex;
802
939
  align-items: center;
@@ -876,23 +1013,84 @@ const posMode = computed(() => {
876
1013
  <ChevronLeft :size="15" />
877
1014
  </button>
878
1015
 
879
- <button
880
- type="button"
881
- style="
882
- background: none;
883
- border: none;
884
- cursor: pointer;
885
- font-size: .82rem;
886
- font-weight: 700;
887
- color: var(--primary-text, #0f172a);
888
- padding: 4px 8px;
889
- border-radius: var(--radxs, 6px);
890
- font-family: var(--sans, inherit);
891
- "
892
- @click="cycleView"
893
- >
894
- {{ headerLabel }}
895
- </button>
1016
+ <div style="display: flex; align-items: center; gap: 4px; min-width: 0;">
1017
+ <template v-if="view === 'day'">
1018
+ <button
1019
+ type="button"
1020
+ style="
1021
+ background: none;
1022
+ border: none;
1023
+ cursor: pointer;
1024
+ font-size: .82rem;
1025
+ font-weight: 700;
1026
+ color: var(--primary-text, #0f172a);
1027
+ padding: 4px 6px;
1028
+ border-radius: var(--radxs, 6px);
1029
+ font-family: var(--sans, inherit);
1030
+ transition: background .12s;
1031
+ "
1032
+ @click="view = 'month'"
1033
+ @mouseenter="(e: MouseEvent) => ((e.currentTarget as HTMLElement).style.background = 'var(--grey-1, #f5f5f8)')"
1034
+ @mouseleave="(e: MouseEvent) => ((e.currentTarget as HTMLElement).style.background = 'none')"
1035
+ >
1036
+ {{ loc.months[cursor.getMonth()] }}
1037
+ </button>
1038
+ <button
1039
+ type="button"
1040
+ style="
1041
+ background: none;
1042
+ border: none;
1043
+ cursor: pointer;
1044
+ font-size: .82rem;
1045
+ font-weight: 700;
1046
+ color: var(--primary-text, #0f172a);
1047
+ padding: 4px 6px;
1048
+ border-radius: var(--radxs, 6px);
1049
+ font-family: var(--sans, inherit);
1050
+ transition: background .12s;
1051
+ "
1052
+ @click="view = 'year'"
1053
+ @mouseenter="(e: MouseEvent) => ((e.currentTarget as HTMLElement).style.background = 'var(--grey-1, #f5f5f8)')"
1054
+ @mouseleave="(e: MouseEvent) => ((e.currentTarget as HTMLElement).style.background = 'none')"
1055
+ >
1056
+ {{ cursor.getFullYear() }}
1057
+ </button>
1058
+ </template>
1059
+
1060
+ <button
1061
+ v-else-if="view === 'month'"
1062
+ type="button"
1063
+ style="
1064
+ background: none;
1065
+ border: none;
1066
+ cursor: pointer;
1067
+ font-size: .82rem;
1068
+ font-weight: 700;
1069
+ color: var(--primary-text, #0f172a);
1070
+ padding: 4px 8px;
1071
+ border-radius: var(--radxs, 6px);
1072
+ font-family: var(--sans, inherit);
1073
+ transition: background .12s;
1074
+ "
1075
+ @click="view = 'year'"
1076
+ @mouseenter="(e: MouseEvent) => ((e.currentTarget as HTMLElement).style.background = 'var(--grey-1, #f5f5f8)')"
1077
+ @mouseleave="(e: MouseEvent) => ((e.currentTarget as HTMLElement).style.background = 'none')"
1078
+ >
1079
+ {{ cursor.getFullYear() }}
1080
+ </button>
1081
+
1082
+ <span
1083
+ v-else-if="view === 'year'"
1084
+ style="
1085
+ font-size: .82rem;
1086
+ font-weight: 700;
1087
+ color: var(--primary-text, #0f172a);
1088
+ padding: 4px 8px;
1089
+ "
1090
+ >
1091
+ {{ Math.floor(cursor.getFullYear() / 12) * 12 }} – {{ Math.floor(cursor.getFullYear() / 12) * 12 + 11 }}
1092
+ </span>
1093
+ </div>
896
1094
 
897
1095
  <button
898
1096
  type="button"
@@ -1039,7 +1237,18 @@ const posMode = computed(() => {
1039
1237
  const c = new Date(cursor);
1040
1238
  c.setMonth(idx);
1041
1239
  cursor = c;
1042
- view = 'day';
1240
+ if (pickerType === 'month') {
1241
+ if (typeof currentVal === 'string') {
1242
+ const ym = `${c.getFullYear()}-${pad(idx + 1)}`;
1243
+ emitValue(ym);
1244
+ } else {
1245
+ emitValue(c);
1246
+ }
1247
+ open = false;
1248
+ view = 'month';
1249
+ } else {
1250
+ view = 'day';
1251
+ }
1043
1252
  }
1044
1253
  "
1045
1254
  >
@@ -1082,7 +1291,21 @@ const posMode = computed(() => {
1082
1291
  const c = new Date(cursor);
1083
1292
  c.setFullYear(yr);
1084
1293
  cursor = c;
1085
- view = 'month';
1294
+ if (pickerType === 'year') {
1295
+ if (typeof currentVal === 'number') {
1296
+ emitValue(yr);
1297
+ } else if (typeof currentVal === 'string') {
1298
+ emitValue(`${yr}`);
1299
+ } else {
1300
+ emitValue(c);
1301
+ }
1302
+ open = false;
1303
+ view = 'year';
1304
+ } else if (pickerType === 'month') {
1305
+ view = 'month';
1306
+ } else {
1307
+ view = 'day';
1308
+ }
1086
1309
  }
1087
1310
  "
1088
1311
  >