cynx-lib 1.3.34 → 1.3.36

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/utils/utils/tz.js CHANGED
@@ -592,6 +592,112 @@ export function tashkentMonth() {
592
592
  return getCurrentMonth('Asia/Tashkent');
593
593
  }
594
594
 
595
+ export function tashkentTimeStr() {
596
+ const p = getTimezoneParts(new Date(), 'Asia/Tashkent');
597
+ return `${pad(p.hour)}:${pad(p.minute)}`;
598
+ }
599
+
600
+ // ─── COMPATIBILITY & REGIONAL HELPERS ──────────────────────────────────────────
601
+
602
+ export const DEFAULT_TIMEZONE = 'Asia/Tashkent';
603
+ export const DEFAULT_OFFSET = '+05:00';
604
+ export const DEFAULT_TZ_LABEL = 'GMT+5';
605
+
606
+ /**
607
+ * Format date in standard custom format with timezone label.
608
+ */
609
+ export function formatCustomDateTz(date, timeZone = DEFAULT_TIMEZONE) {
610
+ if (!date) return '';
611
+ const d = new Date(date);
612
+ if (isNaN(d.getTime())) return '';
613
+
614
+ const dtf = new Intl.DateTimeFormat('en-US', {
615
+ timeZone,
616
+ month: 'short',
617
+ day: 'numeric',
618
+ year: 'numeric',
619
+ hour: 'numeric',
620
+ minute: '2-digit',
621
+ hour12: true,
622
+ });
623
+
624
+ const parts = dtf.formatToParts(d);
625
+ const partMap = {};
626
+ for (const p of parts) {
627
+ partMap[p.type] = p.value;
628
+ }
629
+
630
+ return `${partMap.month} ${partMap.day}, ${partMap.year}, ${partMap.hour}:${partMap.minute} ${partMap.dayPeriod || ''} ${DEFAULT_TZ_LABEL}`.replace(/\s+/g, ' ').trim();
631
+ }
632
+
633
+ /**
634
+ * Format a Date to YYYY-MM-DD in the configured timezone.
635
+ */
636
+ export function formatDateKeyTz(date = new Date(), timeZone = DEFAULT_TIMEZONE) {
637
+ const d = new Date(date);
638
+ if (isNaN(d.getTime())) return '';
639
+ const dtf = new Intl.DateTimeFormat('en-CA', {
640
+ timeZone,
641
+ year: 'numeric',
642
+ month: '2-digit',
643
+ day: '2-digit',
644
+ });
645
+ return dtf.format(d);
646
+ }
647
+
648
+ /**
649
+ * Extract date (YYYY-MM-DD) and time (HH:mm) parts in the configured timezone.
650
+ */
651
+ export function getPartsInTz(date, timeZone = DEFAULT_TIMEZONE) {
652
+ if (!date) return { date: '', time: '' };
653
+ const d = new Date(date);
654
+ if (isNaN(d.getTime())) return { date: '', time: '' };
655
+
656
+ const dtf = new Intl.DateTimeFormat('en-GB', {
657
+ timeZone,
658
+ year: 'numeric',
659
+ month: '2-digit',
660
+ day: '2-digit',
661
+ hour: '2-digit',
662
+ minute: '2-digit',
663
+ hour12: false,
664
+ });
665
+
666
+ const parts = dtf.formatToParts(d);
667
+ const map = {};
668
+ for (const p of parts) map[p.type] = p.value;
669
+
670
+ return {
671
+ date: `${map.year}-${map.month}-${map.day}`,
672
+ time: `${map.hour}:${map.minute}`,
673
+ };
674
+ }
675
+
676
+ /**
677
+ * Combine date (YYYY-MM-DD) and time (HH:mm) into an ISO string with timezone offset.
678
+ */
679
+ export function combineToTzIso(dateStr, timeStr = '00:00', offset = DEFAULT_OFFSET) {
680
+ if (!dateStr || !dateStr.trim()) return null;
681
+ const t = timeStr && timeStr.trim() ? timeStr.trim() : '00:00';
682
+ const fullTime = t.length === 5 ? `${t}:00` : t;
683
+ return `${dateStr.trim()}T${fullTime}${offset}`;
684
+ }
685
+
686
+ /**
687
+ * Safely parse an incoming date input ensuring timezone offset is respected.
688
+ */
689
+ export function parseInputDate(input, defaultOffset = DEFAULT_OFFSET) {
690
+ if (!input) return null;
691
+ if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
692
+ let s = String(input).trim();
693
+ if (!s) return null;
694
+ if (!/[zZ]$/.test(s) && !/[+-]\d{2}(:?\d{2})?$/.test(s)) {
695
+ s = `${s}${defaultOffset}`;
696
+ }
697
+ const d = new Date(s);
698
+ return isNaN(d.getTime()) ? null : d;
699
+ }
700
+
595
701
  // ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
596
702
 
597
703
  export default {
@@ -622,4 +728,13 @@ export default {
622
728
  tashkentNow,
623
729
  tashkentToday,
624
730
  tashkentMonth,
731
+ tashkentTimeStr,
732
+ DEFAULT_TIMEZONE,
733
+ DEFAULT_OFFSET,
734
+ DEFAULT_TZ_LABEL,
735
+ formatCustomDateTz,
736
+ formatDateKeyTz,
737
+ getPartsInTz,
738
+ combineToTzIso,
739
+ parseInputDate,
625
740
  };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * cynx/utils — UI and Formatting Helpers (ui.js)
3
+ *
4
+ * Universal classname merger (cn), clipboard helper, and friendly date/byte formatters.
5
+ */
6
+
7
+ /**
8
+ * Universal class names combiner / merger (zero-dependency clsx replacement).
9
+ * Handles strings, arrays, objects, booleans, and null/undefined values.
10
+ *
11
+ * @param {...any} inputs
12
+ * @returns {string} Combined class string
13
+ */
14
+ export function cn(...inputs) {
15
+ const classes = [];
16
+
17
+ for (let i = 0; i < inputs.length; i++) {
18
+ const arg = inputs[i];
19
+ if (!arg) continue;
20
+
21
+ const argType = typeof arg;
22
+ if (argType === 'string' || argType === 'number') {
23
+ classes.push(arg);
24
+ } else if (Array.isArray(arg)) {
25
+ if (arg.length) {
26
+ const inner = cn(...arg);
27
+ if (inner) classes.push(inner);
28
+ }
29
+ } else if (argType === 'object') {
30
+ for (const key in arg) {
31
+ if (Object.prototype.hasOwnProperty.call(arg, key) && arg[key]) {
32
+ classes.push(key);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ return classes.join(' ');
39
+ }
40
+
41
+ /**
42
+ * Safely copy text to clipboard with modern Clipboard API and legacy fallback.
43
+ *
44
+ * @param {string} text
45
+ * @returns {Promise<boolean>}
46
+ */
47
+ export async function copyToClipboard(text) {
48
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
49
+ try {
50
+ await navigator.clipboard.writeText(text);
51
+ return true;
52
+ } catch {}
53
+ }
54
+
55
+ if (typeof document !== 'undefined') {
56
+ try {
57
+ const el = document.createElement('textarea');
58
+ el.value = text;
59
+ el.setAttribute('readonly', '');
60
+ el.style.position = 'absolute';
61
+ el.style.left = '-9999px';
62
+ document.body.appendChild(el);
63
+ el.select();
64
+ const successful = document.execCommand('copy');
65
+ document.body.removeChild(el);
66
+ return successful;
67
+ } catch {}
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * Format relative time (e.g. "5s ago", "12m ago", "3h ago", "2d ago").
75
+ *
76
+ * @param {Date|string|number} d
77
+ * @returns {string}
78
+ */
79
+ export function fmtAgo(d) {
80
+ if (!d) return '—';
81
+ const s = Math.floor((Date.now() - new Date(d).getTime()) / 1000);
82
+ if (isNaN(s)) return '—';
83
+ if (s < 60) return `${Math.max(0, s)}s ago`;
84
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
85
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
86
+ return `${Math.floor(s / 86400)}d ago`;
87
+ }
88
+
89
+ /**
90
+ * Format localized date or return fallback dash.
91
+ *
92
+ * @param {Date|string|number} d
93
+ * @param {string} [locale='ru-RU']
94
+ * @returns {string}
95
+ */
96
+ export function fmtDate(d, locale = 'ru-RU') {
97
+ if (!d) return '—';
98
+ try {
99
+ const date = new Date(d);
100
+ return isNaN(date.getTime()) ? '—' : date.toLocaleString(locale);
101
+ } catch {
102
+ return '—';
103
+ }
104
+ }
105
+
106
+ export default {
107
+ cn,
108
+ copyToClipboard,
109
+ fmtAgo,
110
+ fmtDate,
111
+ };