cynx-lib 1.3.28

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.
Files changed (46) hide show
  1. package/index.js +3 -0
  2. package/package.json +36 -0
  3. package/ui/components/ActionsMenu.jsx +158 -0
  4. package/ui/components/ActivityCard.jsx +184 -0
  5. package/ui/components/AuthOverlay.jsx +94 -0
  6. package/ui/components/Badge.jsx +186 -0
  7. package/ui/components/BrandingCard.jsx +146 -0
  8. package/ui/components/Button.jsx +148 -0
  9. package/ui/components/Card.jsx +132 -0
  10. package/ui/components/Checkbox.jsx +59 -0
  11. package/ui/components/ChipSelect.jsx +54 -0
  12. package/ui/components/CircularProgress.jsx +216 -0
  13. package/ui/components/Confirm.jsx +79 -0
  14. package/ui/components/DatePicker.jsx +605 -0
  15. package/ui/components/Divider.jsx +96 -0
  16. package/ui/components/Dropzone.jsx +203 -0
  17. package/ui/components/FilePicker.jsx +205 -0
  18. package/ui/components/HoverCard.jsx +81 -0
  19. package/ui/components/ImageUpload.jsx +147 -0
  20. package/ui/components/InfoBadge.jsx +204 -0
  21. package/ui/components/Input.jsx +229 -0
  22. package/ui/components/LineProgress.jsx +133 -0
  23. package/ui/components/LinearProgress.jsx +132 -0
  24. package/ui/components/Modal.jsx +212 -0
  25. package/ui/components/Pagination.jsx +52 -0
  26. package/ui/components/PulseDot.jsx +77 -0
  27. package/ui/components/Select.jsx +415 -0
  28. package/ui/components/Skeleton.jsx +118 -0
  29. package/ui/components/Spinner.jsx +108 -0
  30. package/ui/components/StatBarCard.jsx +25 -0
  31. package/ui/components/StatsBar.jsx +88 -0
  32. package/ui/components/StatsCard.jsx +171 -0
  33. package/ui/components/TabBar.jsx +404 -0
  34. package/ui/components/Tabs.jsx +29 -0
  35. package/ui/components/TimePicker.jsx +1069 -0
  36. package/ui/components/Toast.jsx +106 -0
  37. package/ui/components/Toggle.jsx +67 -0
  38. package/ui/components/UptimeBar.jsx +135 -0
  39. package/ui/components/XTable.jsx +44 -0
  40. package/ui/index.js +97 -0
  41. package/utils/index.js +11 -0
  42. package/utils/utils/oidc.js +209 -0
  43. package/utils/utils/realtime.js +1040 -0
  44. package/utils/utils/storage.js +393 -0
  45. package/utils/utils/tz.js +625 -0
  46. package/utils/utils/uid.js +252 -0
@@ -0,0 +1,625 @@
1
+ /**
2
+ * cynx/utils — Universal Time Zone & Date Engine (tz.js)
3
+ *
4
+ * Lightweight, zero-dependency timezone and date utility powered by native Intl API.
5
+ * Compatible with Browser, Node.js, Web Workers, and SSR.
6
+ */
7
+
8
+ // ─── CACHE & HELPERS ────────────────────────────────────────────────────────────
9
+
10
+ const formatterCache = new Map();
11
+
12
+ function getCachedFormatter(locale, options) {
13
+ const key = `${locale}_${JSON.stringify(options)}`;
14
+ if (!formatterCache.has(key)) {
15
+ formatterCache.set(key, new Intl.DateTimeFormat(locale, options));
16
+ }
17
+ return formatterCache.get(key);
18
+ }
19
+
20
+ function parseDate(input) {
21
+ if (input instanceof Date) return new Date(input.getTime());
22
+ if (typeof input === 'number') return new Date(input);
23
+ if (typeof input === 'string') {
24
+ const d = new Date(input);
25
+ if (!isNaN(d.getTime())) return d;
26
+ }
27
+ return new Date();
28
+ }
29
+
30
+ /**
31
+ * Left-pad a number with leading zeros.
32
+ *
33
+ * @param {number|string} n
34
+ * @param {number} [length=2]
35
+ * @returns {string}
36
+ */
37
+ export function pad(n, length = 2) {
38
+ return String(n).padStart(length, '0');
39
+ }
40
+
41
+ // ─── 1. TIMEZONE DISCOVERY & VALIDATION ──────────────────────────────────────────
42
+
43
+ /**
44
+ * Get the current user/system IANA timezone.
45
+ * Example: "Asia/Tashkent", "Europe/London", "America/New_York"
46
+ *
47
+ * @returns {string}
48
+ */
49
+ export function getUserTimezone() {
50
+ try {
51
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
52
+ } catch {
53
+ return 'UTC';
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check if a given string is a valid IANA timezone name.
59
+ *
60
+ * @param {string} timezone
61
+ * @returns {boolean}
62
+ */
63
+ export function isValidTimezone(timezone) {
64
+ if (!timezone || typeof timezone !== 'string') return false;
65
+ try {
66
+ Intl.DateTimeFormat(undefined, { timeZone: timezone });
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ // ─── 2. TIMEZONE CONVERSION & OFFSETS ───────────────────────────────────────────
74
+
75
+ /**
76
+ * Extract date/time parts for a given date in a specific timezone.
77
+ *
78
+ * @param {Date|number|string} date
79
+ * @param {string} [timezone] Target IANA timezone (default: user timezone)
80
+ * @returns {{ year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, weekday: number }}
81
+ */
82
+ export function getTimezoneParts(date, timezone = getUserTimezone()) {
83
+ const d = parseDate(date);
84
+ const formatter = getCachedFormatter('en-US', {
85
+ timeZone: timezone,
86
+ year: 'numeric',
87
+ month: 'numeric',
88
+ day: 'numeric',
89
+ hour: 'numeric',
90
+ minute: 'numeric',
91
+ second: 'numeric',
92
+ fractionalSecondDigits: 3,
93
+ hour12: false,
94
+ weekday: 'short',
95
+ });
96
+
97
+ const parts = formatter.formatToParts(d);
98
+ const result = {
99
+ year: 0,
100
+ month: 1,
101
+ day: 1,
102
+ hour: 0,
103
+ minute: 0,
104
+ second: 0,
105
+ millisecond: d.getMilliseconds(),
106
+ weekday: d.getDay(),
107
+ };
108
+
109
+ for (const { type, value } of parts) {
110
+ if (type === 'year') result.year = parseInt(value, 10);
111
+ else if (type === 'month') result.month = parseInt(value, 10);
112
+ else if (type === 'day') result.day = parseInt(value, 10);
113
+ else if (type === 'hour') result.hour = parseInt(value, 10) === 24 ? 0 : parseInt(value, 10);
114
+ else if (type === 'minute') result.minute = parseInt(value, 10);
115
+ else if (type === 'second') result.second = parseInt(value, 10);
116
+ else if (type === 'fractionalSecond') result.millisecond = parseInt(value, 10);
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Get the UTC offset for a specific timezone and date.
124
+ *
125
+ * @param {string} [timezone] Target IANA timezone
126
+ * @param {Date|number|string} [date] Reference date
127
+ * @returns {{ offsetMinutes: number, offsetString: string, formatted: string }}
128
+ * Example: { offsetMinutes: 300, offsetString: "+05:00", formatted: "UTC+05:00" }
129
+ */
130
+ export function getTimezoneOffset(timezone = getUserTimezone(), date = new Date()) {
131
+ const d = parseDate(date);
132
+ try {
133
+ const parts = getTimezoneParts(d, timezone);
134
+ const utcParts = getTimezoneParts(d, 'UTC');
135
+
136
+ const tzDate = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
137
+ const utcDate = Date.UTC(utcParts.year, utcParts.month - 1, utcParts.day, utcParts.hour, utcParts.minute, utcParts.second);
138
+
139
+ const diffMinutes = Math.round((tzDate - utcDate) / (1000 * 60));
140
+ const sign = diffMinutes >= 0 ? '+' : '-';
141
+ const absMin = Math.abs(diffMinutes);
142
+ const hours = String(Math.floor(absMin / 60)).padStart(2, '0');
143
+ const mins = String(absMin % 60).padStart(2, '0');
144
+ const offsetString = `${sign}${hours}:${mins}`;
145
+
146
+ return {
147
+ offsetMinutes: diffMinutes,
148
+ offsetString,
149
+ formatted: `UTC${offsetString}`,
150
+ };
151
+ } catch {
152
+ return { offsetMinutes: 0, offsetString: '+00:00', formatted: 'UTC+00:00' };
153
+ }
154
+ }
155
+
156
+ // ─── 3. DATE & TIME FORMATTING (WITH TOKENS) ────────────────────────────────────
157
+
158
+ const MONTH_NAMES = {
159
+ en: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
160
+ ru: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
161
+ };
162
+
163
+ const MONTH_SHORT = {
164
+ en: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
165
+ ru: ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'],
166
+ };
167
+
168
+ /**
169
+ * Format date in a specified timezone using custom format pattern tokens.
170
+ *
171
+ * Supported tokens:
172
+ * - YYYY / YY : Full / 2-digit year
173
+ * - MMMM / MMM / MM / M : Full / short / padded / unpadded month
174
+ * - DD / D : 2-digit / unpadded day
175
+ * - HH / H : 24-hour padded / unpadded
176
+ * - hh / h : 12-hour padded / unpadded
177
+ * - mm / m : Minutes padded / unpadded
178
+ * - ss / s : Seconds padded / unpadded
179
+ * - SSS : Milliseconds
180
+ * - A / a : AM/PM / am/pm
181
+ * - ZZ / Z : +05:00 / +0500
182
+ * - z : Timezone offset (e.g. UTC+05:00)
183
+ *
184
+ * @param {Date|number|string} date
185
+ * @param {string} [pattern='YYYY-MM-DD HH:mm:ss']
186
+ * @param {object} [options]
187
+ * @param {string} [options.timezone] Target timezone (default: user timezone)
188
+ * @param {string} [options.locale='en'] Locale for month/day names ('en' | 'ru')
189
+ * @returns {string}
190
+ */
191
+ export function formatTZ(date, pattern = 'YYYY-MM-DD HH:mm:ss', { timezone = getUserTimezone(), locale = 'en' } = {}) {
192
+ const d = parseDate(date);
193
+ const p = getTimezoneParts(d, timezone);
194
+ const offset = getTimezoneOffset(timezone, d);
195
+
196
+ const lang = MONTH_NAMES[locale] ? locale : 'en';
197
+ const monthIdx = p.month - 1;
198
+ const hour12 = p.hour % 12 || 12;
199
+ const isPM = p.hour >= 12;
200
+
201
+ const tokens = {
202
+ YYYY: String(p.year),
203
+ YY: String(p.year).slice(-2),
204
+ MMMM: MONTH_NAMES[lang][monthIdx],
205
+ MMM: MONTH_SHORT[lang][monthIdx],
206
+ MM: String(p.month).padStart(2, '0'),
207
+ M: String(p.month),
208
+ DD: String(p.day).padStart(2, '0'),
209
+ D: String(p.day),
210
+ HH: String(p.hour).padStart(2, '0'),
211
+ H: String(p.hour),
212
+ hh: String(hour12).padStart(2, '0'),
213
+ h: String(hour12),
214
+ mm: String(p.minute).padStart(2, '0'),
215
+ m: String(p.minute),
216
+ ss: String(p.second).padStart(2, '0'),
217
+ s: String(p.second),
218
+ SSS: String(p.millisecond).padStart(3, '0'),
219
+ A: isPM ? 'PM' : 'AM',
220
+ a: isPM ? 'pm' : 'am',
221
+ ZZ: offset.offsetString,
222
+ Z: offset.offsetString.replace(':', ''),
223
+ z: offset.formatted,
224
+ };
225
+
226
+ return pattern.replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|SSS|A|a|ZZ|Z|z/g, (match) => tokens[match] || match);
227
+ }
228
+
229
+ // ─── 4. RELATIVE TIME («TIME AGO») ──────────────────────────────────────────────
230
+
231
+ /**
232
+ * Format relative time (e.g. "5 minutes ago", "just now", "in 2 days").
233
+ *
234
+ * @param {Date|number|string} date Target date
235
+ * @param {object} [options]
236
+ * @param {Date|number|string} [options.baseDate=Date.now()] Base comparison date
237
+ * @param {string} [options.locale='en'] Locale (e.g. 'en', 'ru')
238
+ * @returns {string}
239
+ */
240
+ export function timeAgo(date, { baseDate = Date.now(), locale = 'en' } = {}) {
241
+ const d = parseDate(date).getTime();
242
+ const base = parseDate(baseDate).getTime();
243
+ const diffSec = Math.round((d - base) / 1000);
244
+
245
+ const rtf = typeof Intl !== 'undefined' && Intl.RelativeTimeFormat
246
+ ? new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
247
+ : null;
248
+
249
+ const absSec = Math.abs(diffSec);
250
+
251
+ if (absSec < 10) {
252
+ return locale.startsWith('ru') ? 'только что' : 'just now';
253
+ }
254
+
255
+ const units = [
256
+ { name: 'year', seconds: 31536000 },
257
+ { name: 'month', seconds: 2592000 },
258
+ { name: 'day', seconds: 86400 },
259
+ { name: 'hour', seconds: 3600 },
260
+ { name: 'minute', seconds: 60 },
261
+ { name: 'second', seconds: 1 },
262
+ ];
263
+
264
+ for (const { name, seconds } of units) {
265
+ if (absSec >= seconds || name === 'second') {
266
+ const val = Math.round(diffSec / seconds);
267
+ if (rtf) {
268
+ return rtf.format(val, name);
269
+ }
270
+ return `${Math.abs(val)} ${name}${Math.abs(val) > 1 ? 's' : ''} ${diffSec < 0 ? 'ago' : 'from now'}`;
271
+ }
272
+ }
273
+
274
+ return '';
275
+ }
276
+
277
+ // ─── 5. TIMEZONE-AWARE CURRENT TIME & STRING DATES ──────────────────────────────
278
+
279
+ /**
280
+ * Returns a Date object shifted to the wall-clock time of the target timezone.
281
+ *
282
+ * @param {string} [timezone] Target IANA timezone (default: user timezone)
283
+ * @returns {Date}
284
+ */
285
+ export function getNow(timezone = getUserTimezone()) {
286
+ const d = new Date();
287
+ const offset = getTimezoneOffset(timezone, d);
288
+ return new Date(d.getTime() + offset.offsetMinutes * 60 * 1000);
289
+ }
290
+
291
+ /**
292
+ * Get current date string in 'YYYY-MM-DD' format for a given timezone.
293
+ *
294
+ * @param {string} [timezone] Target IANA timezone (default: user timezone)
295
+ * @returns {string} E.g. "2026-09-08"
296
+ */
297
+ export function getToday(timezone = getUserTimezone()) {
298
+ const p = getTimezoneParts(new Date(), timezone);
299
+ return `${p.year}-${pad(p.month)}-${pad(p.day)}`;
300
+ }
301
+
302
+ /**
303
+ * Get current month string in 'YYYY-MM' format for a given timezone.
304
+ *
305
+ * @param {string} [timezone] Target IANA timezone (default: user timezone)
306
+ * @returns {string} E.g. "2026-09"
307
+ */
308
+ export function getCurrentMonth(timezone = getUserTimezone()) {
309
+ const p = getTimezoneParts(new Date(), timezone);
310
+ return `${p.year}-${pad(p.month)}`;
311
+ }
312
+
313
+ // ─── 6. STRING-BASED DATE ARITHMETIC & RANGES ───────────────────────────────────
314
+
315
+ /**
316
+ * Shift a date string ('YYYY-MM-DD' or ISO) by an amount of days, months, or years.
317
+ * Leap-year safe and timezone-independent.
318
+ *
319
+ * @param {string|Date} dateInput
320
+ * @param {number} [amount=0] Amount to shift (negative for past, positive for future)
321
+ * @param {'days'|'months'|'years'} [unit='days']
322
+ * @returns {string} Date string in 'YYYY-MM-DD' format
323
+ */
324
+ export function shiftDateStr(dateInput, amount = 0, unit = 'days') {
325
+ let y, m, d;
326
+ if (typeof dateInput === 'string' && /^\d{4}-\d{2}-\d{2}/.test(dateInput)) {
327
+ const parts = dateInput.slice(0, 10).split('-').map(Number);
328
+ y = parts[0];
329
+ m = parts[1];
330
+ d = parts[2];
331
+ } else {
332
+ const parsed = parseDate(dateInput);
333
+ y = parsed.getUTCFullYear();
334
+ m = parsed.getUTCMonth() + 1;
335
+ d = parsed.getUTCDate();
336
+ }
337
+
338
+ const dt = new Date(Date.UTC(y, m - 1, d));
339
+ switch (unit) {
340
+ case 'years':
341
+ dt.setUTCFullYear(dt.getUTCFullYear() + amount);
342
+ break;
343
+ case 'months':
344
+ dt.setUTCMonth(dt.getUTCMonth() + amount);
345
+ break;
346
+ case 'days':
347
+ default:
348
+ dt.setUTCDate(dt.getUTCDate() + amount);
349
+ break;
350
+ }
351
+
352
+ return `${dt.getUTCFullYear()}-${pad(dt.getUTCMonth() + 1)}-${pad(dt.getUTCDate())}`;
353
+ }
354
+
355
+ /**
356
+ * Shift a date string by N days into the past.
357
+ *
358
+ * @param {string|Date} dateStr
359
+ * @param {number} [count=1] Number of days to go back
360
+ * @returns {string} Date string in 'YYYY-MM-DD' format
361
+ */
362
+ export function prevDayStr(dateStr, count = 1) {
363
+ return shiftDateStr(dateStr, -count, 'days');
364
+ }
365
+
366
+ /**
367
+ * Shift a date string by N days into the future.
368
+ *
369
+ * @param {string|Date} dateStr
370
+ * @param {number} [count=1] Number of days to advance
371
+ * @returns {string} Date string in 'YYYY-MM-DD' format
372
+ */
373
+ export function nextDayStr(dateStr, count = 1) {
374
+ return shiftDateStr(dateStr, count, 'days');
375
+ }
376
+
377
+ /**
378
+ * Calculate the number of calendar days between two dates (date2 - date1).
379
+ *
380
+ * @param {string|Date} date1
381
+ * @param {string|Date} date2
382
+ * @returns {number} Difference in days (positive if date2 is after date1)
383
+ */
384
+ export function daysBetween(date1, date2) {
385
+ const parseToUtcMidnight = (val) => {
386
+ if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val)) {
387
+ const [y, m, d] = val.slice(0, 10).split('-').map(Number);
388
+ return Date.UTC(y, m - 1, d);
389
+ }
390
+ const dt = parseDate(val);
391
+ return Date.UTC(dt.getUTCFullYear(), dt.getUTCMonth(), dt.getUTCDate());
392
+ };
393
+
394
+ const ms1 = parseToUtcMidnight(date1);
395
+ const ms2 = parseToUtcMidnight(date2);
396
+ return Math.round((ms2 - ms1) / (1000 * 60 * 60 * 24));
397
+ }
398
+
399
+ /**
400
+ * Generate an array of date strings ('YYYY-MM-DD') for every day in the range [startDate, endDate].
401
+ *
402
+ * @param {string|Date} startDate
403
+ * @param {string|Date} endDate
404
+ * @returns {string[]} Array of date strings
405
+ */
406
+ export function getDateRange(startDate, endDate) {
407
+ const startStr = typeof startDate === 'string' && /^\d{4}-\d{2}-\d{2}/.test(startDate)
408
+ ? startDate.slice(0, 10)
409
+ : formatTZ(startDate, 'YYYY-MM-DD');
410
+ const endStr = typeof endDate === 'string' && /^\d{4}-\d{2}-\d{2}/.test(endDate)
411
+ ? endDate.slice(0, 10)
412
+ : formatTZ(endDate, 'YYYY-MM-DD');
413
+
414
+ if (startStr > endStr) return [];
415
+
416
+ const range = [];
417
+ let curr = startStr;
418
+ while (curr <= endStr) {
419
+ range.push(curr);
420
+ curr = nextDayStr(curr);
421
+ }
422
+ return range;
423
+ }
424
+
425
+ /**
426
+ * Get start of month date string ('YYYY-MM-01').
427
+ *
428
+ * @param {string|Date} [date=new Date()]
429
+ * @param {string} [timezone]
430
+ * @returns {string} E.g. "2026-09-01"
431
+ */
432
+ export function getStartOfMonth(date = new Date(), timezone = getUserTimezone()) {
433
+ const p = getTimezoneParts(date, timezone);
434
+ return `${p.year}-${pad(p.month)}-01`;
435
+ }
436
+
437
+ /**
438
+ * Get end of month date string (e.g. '2026-02-28', '2026-09-30').
439
+ *
440
+ * @param {string|Date} [date=new Date()]
441
+ * @param {string} [timezone]
442
+ * @returns {string}
443
+ */
444
+ export function getEndOfMonth(date = new Date(), timezone = getUserTimezone()) {
445
+ const p = getTimezoneParts(date, timezone);
446
+ const dt = new Date(Date.UTC(p.year, p.month, 0));
447
+ return `${p.year}-${pad(p.month)}-${pad(dt.getUTCDate())}`;
448
+ }
449
+
450
+ /**
451
+ * Get exact Start of Day (00:00:00.000) as a Date object for a given date and timezone.
452
+ *
453
+ * @param {string|Date} [date=new Date()]
454
+ * @param {string} [timezone]
455
+ * @returns {Date}
456
+ */
457
+ export function getStartOfDay(date = new Date(), timezone = getUserTimezone()) {
458
+ const p = getTimezoneParts(date, timezone);
459
+ const offset = getTimezoneOffset(timezone, new Date(Date.UTC(p.year, p.month - 1, p.day, 12, 0, 0)));
460
+ return new Date(Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0, 0) - offset.offsetMinutes * 60 * 1000);
461
+ }
462
+
463
+ /**
464
+ * Get exact End of Day (23:59:59.999) as a Date object for a given date and timezone.
465
+ *
466
+ * @param {string|Date} [date=new Date()]
467
+ * @param {string} [timezone]
468
+ * @returns {Date}
469
+ */
470
+ export function getEndOfDay(date = new Date(), timezone = getUserTimezone()) {
471
+ const p = getTimezoneParts(date, timezone);
472
+ const offset = getTimezoneOffset(timezone, new Date(Date.UTC(p.year, p.month - 1, p.day, 12, 0, 0)));
473
+ return new Date(Date.UTC(p.year, p.month - 1, p.day, 23, 59, 59, 999) - offset.offsetMinutes * 60 * 1000);
474
+ }
475
+
476
+ // ─── 7. DATE ARITHMETIC & COMPARISONS ───────────────────────────────────────────
477
+
478
+ /**
479
+ * Add or subtract time from a Date object.
480
+ *
481
+ * @param {Date|number|string} date
482
+ * @param {number} amount Amount to add (negative to subtract)
483
+ * @param {'years'|'months'|'days'|'hours'|'minutes'|'seconds'} unit
484
+ * @returns {Date}
485
+ */
486
+ export function addTime(date, amount, unit = 'days') {
487
+ const d = parseDate(date);
488
+ switch (unit) {
489
+ case 'years':
490
+ d.setFullYear(d.getFullYear() + amount);
491
+ break;
492
+ case 'months':
493
+ d.setMonth(d.getMonth() + amount);
494
+ break;
495
+ case 'days':
496
+ d.setDate(d.getDate() + amount);
497
+ break;
498
+ case 'hours':
499
+ d.setHours(d.getHours() + amount);
500
+ break;
501
+ case 'minutes':
502
+ d.setMinutes(d.getMinutes() + amount);
503
+ break;
504
+ case 'seconds':
505
+ d.setSeconds(d.getSeconds() + amount);
506
+ break;
507
+ }
508
+ return d;
509
+ }
510
+
511
+ /**
512
+ * Check if two dates fall on the same day in a given timezone.
513
+ */
514
+ export function isSameDay(date1, date2, timezone = getUserTimezone()) {
515
+ const p1 = getTimezoneParts(date1, timezone);
516
+ const p2 = getTimezoneParts(date2, timezone);
517
+ return p1.year === p2.year && p1.month === p2.month && p1.day === p2.day;
518
+ }
519
+
520
+ /**
521
+ * Check if a date is today in a given timezone.
522
+ */
523
+ export function isToday(date, timezone = getUserTimezone()) {
524
+ return isSameDay(date, new Date(), timezone);
525
+ }
526
+
527
+ // ─── 8. TIMEZONE LIST GENERATOR (FOR SELECT/DROPDOWNS) ──────────────────────────
528
+
529
+ export const COMMON_TIMEZONES = [
530
+ { value: 'Pacific/Midway', label: '(UTC-11:00) Midway Island, Samoa' },
531
+ { value: 'Pacific/Honolulu', label: '(UTC-10:00) Hawaii' },
532
+ { value: 'America/Anchorage', label: '(UTC-09:00) Alaska' },
533
+ { value: 'America/Los_Angeles', label: '(UTC-08:00) Pacific Time (US & Canada)' },
534
+ { value: 'America/Denver', label: '(UTC-07:00) Mountain Time (US & Canada)' },
535
+ { value: 'America/Chicago', label: '(UTC-06:00) Central Time (US & Canada)' },
536
+ { value: 'America/New_York', label: '(UTC-05:00) Eastern Time (US & Canada)' },
537
+ { value: 'America/Sao_Paulo', label: '(UTC-03:00) Brasilia, Sao Paulo' },
538
+ { value: 'UTC', label: '(UTC+00:00) Universal Coordinated Time, London' },
539
+ { value: 'Europe/London', label: '(UTC+00:00) London, Dublin, Edinburgh' },
540
+ { value: 'Europe/Paris', label: '(UTC+01:00) Paris, Berlin, Rome, Madrid' },
541
+ { value: 'Europe/Kyiv', label: '(UTC+02:00) Kyiv, Helsinki, Athens' },
542
+ { value: 'Europe/Moscow', label: '(UTC+03:00) Moscow, St. Petersburg, Istanbul' },
543
+ { value: 'Asia/Dubai', label: '(UTC+04:00) Dubai, Abu Dhabi, Baku' },
544
+ { value: 'Asia/Tashkent', label: '(UTC+05:00) Tashkent, Samarkand, Islamabad' },
545
+ { value: 'Asia/Almaty', label: '(UTC+05:00) Almaty, Astana' },
546
+ { value: 'Asia/Dhaka', label: '(UTC+06:00) Dhaka, Novosibirsk' },
547
+ { value: 'Asia/Bangkok', label: '(UTC+07:00) Bangkok, Hanoi, Jakarta' },
548
+ { value: 'Asia/Singapore', label: '(UTC+08:00) Singapore, Beijing, Hong Kong' },
549
+ { value: 'Asia/Tokyo', label: '(UTC+09:00) Tokyo, Seoul' },
550
+ { value: 'Australia/Sydney', label: '(UTC+10:00) Sydney, Melbourne' },
551
+ { value: 'Pacific/Auckland', label: '(UTC+12:00) Auckland, Wellington' },
552
+ ];
553
+
554
+ /**
555
+ * Return a list of major timezones with their current real-time UTC offsets.
556
+ * Ideal for populating Select/Dropdown components.
557
+ *
558
+ * @returns {Array<{ value: string, label: string, offset: string, offsetMinutes: number }>}
559
+ */
560
+ export function getTimezoneList() {
561
+ return COMMON_TIMEZONES.map((tz) => {
562
+ const off = getTimezoneOffset(tz.value);
563
+ return {
564
+ value: tz.value,
565
+ label: tz.label,
566
+ offset: off.offsetString,
567
+ offsetMinutes: off.offsetMinutes,
568
+ };
569
+ });
570
+ }
571
+
572
+ // ─── 9. BACKWARD COMPATIBILITY (TASHKENT SHORTCUTS) ─────────────────────────────
573
+
574
+ /**
575
+ * @deprecated Use getNow('Asia/Tashkent')
576
+ */
577
+ export function tashkentNow() {
578
+ return getNow('Asia/Tashkent');
579
+ }
580
+
581
+ /**
582
+ * @deprecated Use getToday('Asia/Tashkent')
583
+ */
584
+ export function tashkentToday() {
585
+ return getToday('Asia/Tashkent');
586
+ }
587
+
588
+ /**
589
+ * @deprecated Use getCurrentMonth('Asia/Tashkent')
590
+ */
591
+ export function tashkentMonth() {
592
+ return getCurrentMonth('Asia/Tashkent');
593
+ }
594
+
595
+ // ─── MAIN EXPORT ────────────────────────────────────────────────────────────────
596
+
597
+ export default {
598
+ pad,
599
+ getUserTimezone,
600
+ isValidTimezone,
601
+ getTimezoneParts,
602
+ getTimezoneOffset,
603
+ formatTZ,
604
+ timeAgo,
605
+ getNow,
606
+ getToday,
607
+ getCurrentMonth,
608
+ shiftDateStr,
609
+ prevDayStr,
610
+ nextDayStr,
611
+ daysBetween,
612
+ getDateRange,
613
+ getStartOfMonth,
614
+ getEndOfMonth,
615
+ getStartOfDay,
616
+ getEndOfDay,
617
+ addTime,
618
+ isSameDay,
619
+ isToday,
620
+ getTimezoneList,
621
+ COMMON_TIMEZONES,
622
+ tashkentNow,
623
+ tashkentToday,
624
+ tashkentMonth,
625
+ };