@pushwoosh/dumb-components 1.1.234 → 1.1.236

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.
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from 'react';
3
3
  import { Combobox } from '../../../Combobox';
4
- import { getSortedTimezones } from '../../helpers-tz';
4
+ import { findTimezoneOption, listTimezones } from '../../../helpers/timezones';
5
5
  export const CalendarTimezoneSelect = ({
6
6
  timezone,
7
7
  onChange,
8
8
  isDisabled
9
9
  }) => {
10
- const timezonesOptions = useMemo(() => getSortedTimezones(), []);
11
- const value = useMemo(() => timezonesOptions.find(op => op.value === timezone) ?? null, [timezonesOptions, timezone]);
10
+ const timezonesOptions = useMemo(() => listTimezones(new Date(), [timezone]), [timezone]);
11
+ const value = useMemo(() => findTimezoneOption(timezonesOptions, timezone), [timezonesOptions, timezone]);
12
12
  return _jsx(Combobox, {
13
13
  select: true,
14
14
  items: timezonesOptions,
@@ -1,7 +1,2 @@
1
- export type TimezoneOption = {
2
- value: string;
3
- label: string;
4
- };
5
- export declare function getSortedTimezones(): TimezoneOption[];
6
1
  export declare function groupTimezonesByOffset(at?: Date): [number, string[]][];
7
2
  export declare function formatTimezoneShort(timezone: string): string;
@@ -13,37 +13,6 @@ function formatTimezoneOffset(minutes) {
13
13
  return `UTC${sign}${hours}:${formatTwoDigitNumber(mins)}`;
14
14
  }
15
15
  }
16
- export function getSortedTimezones() {
17
- const timezones = getAllTimeZones();
18
- const now = new Date();
19
- return [...timezones, 'UTC'].map(tz => {
20
- const offset = getTimezoneOffset(tz, now);
21
- const offsetFormatted = formatTimezoneOffset(offset);
22
- let displayName;
23
- if (tz === 'UTC') {
24
- displayName = 'Coordinated Universal Time';
25
- } else if (tz.includes('/')) {
26
- const parts = tz.split('/');
27
- if (parts.length >= 2) {
28
- const country = parts[0].replace(/_/g, ' ');
29
- const city = parts[parts.length - 1].replace(/_/g, ' ');
30
- displayName = `${city} / ${country}`;
31
- } else {
32
- displayName = tz.replace(/_/g, ' ');
33
- }
34
- } else {
35
- displayName = tz.replace(/_/g, ' ');
36
- }
37
- return {
38
- value: tz,
39
- label: `${offsetFormatted} – ${displayName}`
40
- };
41
- }).sort((a, b) => {
42
- if (a.value === 'UTC') return -1;
43
- if (b.value === 'UTC') return 1;
44
- return a.label.localeCompare(b.label);
45
- });
46
- }
47
16
  export function groupTimezonesByOffset(at = new Date()) {
48
17
  const zones = getAllTimeZones();
49
18
  const groups = new Map();
@@ -1,5 +1,4 @@
1
1
  export { CalendarSuperForm, type CalendarSuperFormProps } from './CalendarSuperForm';
2
- export { getSortedTimezones, type TimezoneOption } from './helpers-tz';
3
2
  export { CalendarDropdown, stringifyValue, type CalendarDropdownProps, type CalendarDropdownRangeDateProps, } from './CalendarDropdown';
4
3
  export { dateToStruct, structToDate, dateToStructTz, structToDateTz, getTimezoneOffset, addPeriod, compareStructs, getPeriodEnd, getPeriodStart, pickDateStruct, } from './helpers';
5
4
  export type { DateStruct, RangeValue } from './types';
package/Calendar/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { CalendarSuperForm } from './CalendarSuperForm';
2
- export { getSortedTimezones } from './helpers-tz';
3
2
  export { CalendarDropdown, stringifyValue } from './CalendarDropdown';
4
3
  export { dateToStruct, structToDate, dateToStructTz, structToDateTz, getTimezoneOffset, addPeriod, compareStructs, getPeriodEnd, getPeriodStart, pickDateStruct } from './helpers';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ import { canonicalizeTimezone, findTimezoneOption, formatTimezoneLabel, formatTimezoneOffset, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption } from '../timezones';
2
+ const winter = new Date('2026-01-15T12:00:00Z');
3
+ const melbourneDst = new Date('2026-10-10T12:00:00Z');
4
+ describe('formatTimezoneOffset', () => {
5
+ it('renders sign, hours and minutes', () => {
6
+ expect(formatTimezoneOffset(300)).toBe('GMT+05:00');
7
+ expect(formatTimezoneOffset(330)).toBe('GMT+05:30');
8
+ expect(formatTimezoneOffset(-210)).toBe('GMT-03:30');
9
+ expect(formatTimezoneOffset(0)).toBe('GMT+00:00');
10
+ });
11
+ });
12
+ describe('formatTimezoneLabel', () => {
13
+ it('uses the current tzdata: Kazakhstan is UTC+5 since 2024', () => {
14
+ expect(formatTimezoneLabel('Asia/Almaty', winter)).toBe('(GMT+05:00) Asia/Almaty');
15
+ expect(formatTimezoneLabel('Asia/Qostanay', winter)).toBe('(GMT+05:00) Asia/Qostanay');
16
+ });
17
+ it('keeps half-hour zones east of UTC exact', () => {
18
+ expect(formatTimezoneLabel('Asia/Kolkata', winter)).toBe('(GMT+05:30) Asia/Kolkata');
19
+ expect(formatTimezoneLabel('Asia/Kathmandu', winter)).toBe('(GMT+05:45) Asia/Kathmandu');
20
+ expect(formatTimezoneLabel('America/St_Johns', winter)).toBe('(GMT-03:30) America/St_Johns');
21
+ });
22
+ it('resolves the offset at the given instant, not today', () => {
23
+ expect(getTimezoneOffsetSafe('Australia/Melbourne', winter)).toBe(660);
24
+ expect(getTimezoneOffsetSafe('Australia/Melbourne', new Date('2026-07-01T00:00:00Z'))).toBe(600);
25
+ expect(getTimezoneOffsetSafe('Australia/Melbourne', melbourneDst)).toBe(660);
26
+ });
27
+ it('labels Etc and POSIX names the backend accepts but the picker does not list', () => {
28
+ expect(formatTimezoneLabel('Etc/GMT+3', winter)).toBe('(GMT-03:00) Etc/GMT+3');
29
+ expect(formatTimezoneLabel('Etc/GMT-0', winter)).toBe('(GMT+00:00) Etc/GMT-0');
30
+ expect(formatTimezoneLabel('EST', winter)).toBe('(GMT-05:00) EST');
31
+ });
32
+ it('falls back to the bare name for a zone the engine does not know', () => {
33
+ expect(getTimezoneOffsetSafe('Not/A_Zone')).toBeNull();
34
+ expect(formatTimezoneLabel('Not/A_Zone')).toBe('Not/A_Zone');
35
+ });
36
+ });
37
+ describe('canonicalizeTimezone', () => {
38
+ it('maps aliases onto one engine name and rejects unknown zones', () => {
39
+ expect(canonicalizeTimezone('Europe/Kiev')).toBe(canonicalizeTimezone('Europe/Kyiv'));
40
+ expect(canonicalizeTimezone('Asia/Calcutta')).toBe(canonicalizeTimezone('Asia/Kolkata'));
41
+ expect(canonicalizeTimezone('Etc/UTC')).toBe('UTC');
42
+ expect(canonicalizeTimezone('Not/A_Zone')).toBeNull();
43
+ });
44
+ });
45
+ describe('listTimezones', () => {
46
+ const options = listTimezones(winter);
47
+ it('contains UTC exactly once and no duplicates', () => {
48
+ expect(options.filter(option => option.value === 'UTC')).toHaveLength(1);
49
+ expect(new Set(options.map(option => option.value)).size).toBe(options.length);
50
+ });
51
+ it('covers every zone the engine reports', () => {
52
+ const supported = Intl.supportedValuesOf('timeZone');
53
+ expect(options.length).toBeGreaterThanOrEqual(supported.length);
54
+ supported.forEach(name => {
55
+ expect(options.some(option => option.value === name)).toBe(true);
56
+ });
57
+ });
58
+ it('is sorted by offset, UTC first among the zero-offset zones, then by name', () => {
59
+ const offsets = options.map(option => getTimezoneOffsetSafe(option.value, winter) ?? Infinity);
60
+ expect([...offsets].sort((a, b) => a - b)).toEqual(offsets);
61
+ const zeroOffset = options.filter(option => getTimezoneOffsetSafe(option.value, winter) === 0);
62
+ expect(zeroOffset[0].value).toBe('UTC');
63
+ });
64
+ it('labels every option in the shared format', () => {
65
+ options.forEach(option => {
66
+ expect(option.label).toBe(formatTimezoneLabel(option.value, winter));
67
+ });
68
+ });
69
+ it('keeps stored values selectable verbatim, skipping blanks', () => {
70
+ const withStored = listTimezones(winter, ['Asia/Calcutta', 'Etc/GMT-0', 'EST', 'Not/A_Zone', '', null, undefined]);
71
+ ['Asia/Calcutta', 'Etc/GMT-0', 'EST', 'Not/A_Zone'].forEach(name => {
72
+ expect(withStored.filter(option => option.value === name)).toHaveLength(1);
73
+ });
74
+ expect(withStored.some(option => option.value === '')).toBe(false);
75
+ expect(withStored[withStored.length - 1].value).toBe('Not/A_Zone');
76
+ });
77
+ it('falls back to the browser zone and UTC when the engine has no zone list', () => {
78
+ const original = Intl.supportedValuesOf;
79
+ Object.defineProperty(Intl, 'supportedValuesOf', {
80
+ value: undefined,
81
+ configurable: true
82
+ });
83
+ try {
84
+ const fallback = listTimezones(winter).map(option => option.value);
85
+ expect(fallback).toContain('UTC');
86
+ expect(fallback).toContain(Intl.DateTimeFormat().resolvedOptions().timeZone);
87
+ expect(fallback.length).toBeLessThanOrEqual(2);
88
+ } finally {
89
+ Object.defineProperty(Intl, 'supportedValuesOf', {
90
+ value: original,
91
+ configurable: true
92
+ });
93
+ }
94
+ });
95
+ });
96
+ describe('findTimezoneOption', () => {
97
+ const options = listTimezones(winter, ['Asia/Calcutta']);
98
+ it('matches a stored name exactly, aliases included', () => {
99
+ expect(findTimezoneOption(options, 'Asia/Almaty', winter)?.value).toBe('Asia/Almaty');
100
+ expect(findTimezoneOption(options, 'Asia/Calcutta', winter)?.value).toBe('Asia/Calcutta');
101
+ expect(findTimezoneOption(options, 'Not/Stored', winter)).toBeNull();
102
+ });
103
+ it('maps a legacy numeric offset onto a zone with that offset at the instant', () => {
104
+ expect(getTimezoneOffsetSafe(findTimezoneOption(options, 330, winter).value, winter)).toBe(330);
105
+ expect(getTimezoneOffsetSafe(findTimezoneOption(options, '-210', winter).value, winter)).toBe(-210);
106
+ expect(findTimezoneOption(options, 7, winter)).toBeNull();
107
+ });
108
+ it('returns null for an empty value', () => {
109
+ expect(findTimezoneOption(options, '', winter)).toBeNull();
110
+ expect(findTimezoneOption(options, null, winter)).toBeNull();
111
+ expect(findTimezoneOption(options, undefined, winter)).toBeNull();
112
+ });
113
+ });
114
+ describe('resolveTimezoneOption', () => {
115
+ it('never rewrites the stored value', () => {
116
+ expect(resolveTimezoneOption('Europe/Kiev', winter)).toEqual({
117
+ value: 'Europe/Kiev',
118
+ label: '(GMT+02:00) Europe/Kiev'
119
+ });
120
+ expect(resolveTimezoneOption('Not/A_Zone', winter)).toEqual({
121
+ value: 'Not/A_Zone',
122
+ label: 'Not/A_Zone'
123
+ });
124
+ });
125
+ });
@@ -1,2 +1,3 @@
1
1
  export * from './assignRef';
2
2
  export * from './barcodeUrl';
3
+ export * from './timezones';
package/helpers/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './assignRef';
2
- export * from './barcodeUrl';
2
+ export * from './barcodeUrl';
3
+ export * from './timezones';
@@ -0,0 +1,21 @@
1
+ /** A timezone picker option: `value` is the IANA name the backend receives, `label` is what the user reads. */
2
+ export type TimezoneOption = {
3
+ value: string;
4
+ label: string;
5
+ };
6
+ /** IANA name of the zone the browser runs in. */
7
+ export declare function getBrowserTimezone(): string;
8
+ /** The engine's own primary name for `timezone` (an alias like `Europe/Kyiv` may map to `Europe/Kiev`), or `null` for a zone it doesn't know. */
9
+ export declare function canonicalizeTimezone(timezone: string): string | null;
10
+ /** Offset of `timezone` in minutes east of UTC at instant `at`, or `null` for a zone the engine doesn't know. */
11
+ export declare function getTimezoneOffsetSafe(timezone: string, at?: Date): number | null;
12
+ /** Minutes east of UTC rendered as `GMT+05:00` / `GMT-03:30`. */
13
+ export declare function formatTimezoneOffset(minutes: number): string;
14
+ /** The shared picker label, `(GMT+05:00) Asia/Almaty` at instant `at`; a zone the engine doesn't know is labelled by its bare name. */
15
+ export declare function formatTimezoneLabel(timezone: string, at?: Date): string;
16
+ /** Option for any stored zone name — aliases, `Etc/GMT±N`, POSIX abbreviations — kept verbatim as the value. */
17
+ export declare function resolveTimezoneOption(timezone: string, at?: Date): TimezoneOption;
18
+ /** Zones the browser knows plus `UTC` and every non-empty `include` value, sorted by offset at `at` then by name. Pass the stored value as `include` so an old alias stays selectable and is re-saved unchanged. */
19
+ export declare function listTimezones(at?: Date, include?: Array<string | null | undefined>): TimezoneOption[];
20
+ /** Option matching a stored value: by zone name, or for a legacy numeric offset (minutes east of UTC) the first zone with that offset at `at`. */
21
+ export declare function findTimezoneOption(options: TimezoneOption[], value: string | number | null | undefined, at?: Date): TimezoneOption | null;
@@ -0,0 +1,80 @@
1
+ import { getTimezoneOffset } from '../Calendar/helpers';
2
+ const UTC = 'UTC';
3
+ /** IANA name of the zone the browser runs in. */
4
+ export function getBrowserTimezone() {
5
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || UTC;
6
+ }
7
+ /** The engine's own primary name for `timezone` (an alias like `Europe/Kyiv` may map to `Europe/Kiev`), or `null` for a zone it doesn't know. */
8
+ export function canonicalizeTimezone(timezone) {
9
+ try {
10
+ return new Intl.DateTimeFormat('en-US', {
11
+ timeZone: timezone
12
+ }).resolvedOptions().timeZone;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ /** Offset of `timezone` in minutes east of UTC at instant `at`, or `null` for a zone the engine doesn't know. */
18
+ export function getTimezoneOffsetSafe(timezone, at = new Date()) {
19
+ try {
20
+ return getTimezoneOffset(timezone, at);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** Minutes east of UTC rendered as `GMT+05:00` / `GMT-03:30`. */
26
+ export function formatTimezoneOffset(minutes) {
27
+ const sign = minutes < 0 ? '-' : '+';
28
+ const absolute = Math.abs(minutes);
29
+ const hours = String(Math.floor(absolute / 60)).padStart(2, '0');
30
+ const mins = String(absolute % 60).padStart(2, '0');
31
+ return `GMT${sign}${hours}:${mins}`;
32
+ }
33
+ /** The shared picker label, `(GMT+05:00) Asia/Almaty` at instant `at`; a zone the engine doesn't know is labelled by its bare name. */
34
+ export function formatTimezoneLabel(timezone, at = new Date()) {
35
+ const offset = getTimezoneOffsetSafe(timezone, at);
36
+ return offset === null ? timezone : `(${formatTimezoneOffset(offset)}) ${timezone}`;
37
+ }
38
+ /** Option for any stored zone name — aliases, `Etc/GMT±N`, POSIX abbreviations — kept verbatim as the value. */
39
+ export function resolveTimezoneOption(timezone, at = new Date()) {
40
+ return {
41
+ value: timezone,
42
+ label: formatTimezoneLabel(timezone, at)
43
+ };
44
+ }
45
+ function supportedTimezones() {
46
+ const names = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : [];
47
+ return names.length > 0 ? names : [getBrowserTimezone()];
48
+ }
49
+ function compareZones(a, b) {
50
+ if (a.offset !== b.offset) {
51
+ if (a.offset === null) return 1;
52
+ if (b.offset === null) return -1;
53
+ return a.offset - b.offset;
54
+ }
55
+ if (a.name === UTC) return -1;
56
+ if (b.name === UTC) return 1;
57
+ return a.name.localeCompare(b.name);
58
+ }
59
+ /** Zones the browser knows plus `UTC` and every non-empty `include` value, sorted by offset at `at` then by name. Pass the stored value as `include` so an old alias stays selectable and is re-saved unchanged. */
60
+ export function listTimezones(at = new Date(), include = []) {
61
+ const names = new Set([UTC, ...supportedTimezones()]);
62
+ include.forEach(name => {
63
+ if (name) names.add(name);
64
+ });
65
+ return [...names].map(name => ({
66
+ name,
67
+ offset: getTimezoneOffsetSafe(name, at)
68
+ })).sort(compareZones).map(({
69
+ name
70
+ }) => resolveTimezoneOption(name, at));
71
+ }
72
+ /** Option matching a stored value: by zone name, or for a legacy numeric offset (minutes east of UTC) the first zone with that offset at `at`. */
73
+ export function findTimezoneOption(options, value, at = new Date()) {
74
+ if (value === null || value === undefined || value === '') return null;
75
+ const byName = options.find(option => option.value === String(value));
76
+ if (byName) return byName;
77
+ const offset = Number(value);
78
+ if (!Number.isFinite(offset)) return null;
79
+ return options.find(option => getTimezoneOffsetSafe(option.value, at) === offset) ?? null;
80
+ }
package/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { Checkbox } from './Checkbox';
7
7
  export { CrashScreen } from './CrashScreen';
8
8
  export { Divider } from './Divider';
9
9
  export { Drawer, DrawerHeader, DrawerTitle, DrawerBody, DrawerDocs, useDocs, } from './Drawer';
10
- export { CalendarSuperForm, CalendarDropdown, addPeriod, compareStructs, dateToStruct, dateToStructTz, getSortedTimezones, getTimezoneOffset, getPeriodEnd, getPeriodStart, structToDate, structToDateTz, stringifyValue, pickDateStruct, type CalendarDropdownProps, type CalendarDropdownRangeDateProps, type CalendarSuperFormProps, type DateStruct, type RangeValue, type TimezoneOption, } from './Calendar';
10
+ export { CalendarSuperForm, CalendarDropdown, addPeriod, compareStructs, dateToStruct, dateToStructTz, getTimezoneOffset, getPeriodEnd, getPeriodStart, structToDate, structToDateTz, stringifyValue, pickDateStruct, type CalendarDropdownProps, type CalendarDropdownRangeDateProps, type CalendarSuperFormProps, type DateStruct, type RangeValue, } from './Calendar';
11
11
  export { DatePicker, DatePickerField, DateTimePicker, DateTimePickerField, } from './DateTimePicker';
12
12
  export { DropdownMenu, DropdownMenuButton, DropdownMenuSection, DropdownMenuItem, DropdownMenuSubmenu, } from './DropdownMenu';
13
13
  export { GetStartedPage } from './GetStartedPage';
@@ -59,3 +59,4 @@ export { ApplicationPreloader, type ApplicationPreloaderProps, } from './Applica
59
59
  export { NotificationInbox, NotificationBar, NotificationBell, type Notification, type NotificationInboxProps, type NotificationSeverity, type NotificationBarType, type NotificationBarProps, type NotificationBellProps, } from './NotificationInbox';
60
60
  export { LeafletTheme } from './LeafletTheme';
61
61
  export { buildBarcodeUrl, barcodePlaceholderSafeEscape, isBarcodePlaceholder, type BarcodeSymbology, type BarcodeUrlParams, } from './helpers/barcodeUrl';
62
+ export { canonicalizeTimezone, findTimezoneOption, formatTimezoneLabel, formatTimezoneOffset, getBrowserTimezone, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption, type TimezoneOption, } from './helpers/timezones';
package/index.js CHANGED
@@ -7,7 +7,7 @@ export { Checkbox } from './Checkbox';
7
7
  export { CrashScreen } from './CrashScreen';
8
8
  export { Divider } from './Divider';
9
9
  export { Drawer, DrawerHeader, DrawerTitle, DrawerBody, DrawerDocs, useDocs } from './Drawer';
10
- export { CalendarSuperForm, CalendarDropdown, addPeriod, compareStructs, dateToStruct, dateToStructTz, getSortedTimezones, getTimezoneOffset, getPeriodEnd, getPeriodStart, structToDate, structToDateTz, stringifyValue, pickDateStruct } from './Calendar';
10
+ export { CalendarSuperForm, CalendarDropdown, addPeriod, compareStructs, dateToStruct, dateToStructTz, getTimezoneOffset, getPeriodEnd, getPeriodStart, structToDate, structToDateTz, stringifyValue, pickDateStruct } from './Calendar';
11
11
  export { DatePicker, DatePickerField, DateTimePicker, DateTimePickerField } from './DateTimePicker';
12
12
  export { DropdownMenu, DropdownMenuButton, DropdownMenuSection, DropdownMenuItem, DropdownMenuSubmenu } from './DropdownMenu';
13
13
  export { GetStartedPage } from './GetStartedPage';
@@ -56,4 +56,5 @@ export { Alert } from './Alert';
56
56
  export { ApplicationPreloader } from './ApplicationPreloader';
57
57
  export { NotificationInbox, NotificationBar, NotificationBell } from './NotificationInbox';
58
58
  export { LeafletTheme } from './LeafletTheme';
59
- export { buildBarcodeUrl, barcodePlaceholderSafeEscape, isBarcodePlaceholder } from './helpers/barcodeUrl';
59
+ export { buildBarcodeUrl, barcodePlaceholderSafeEscape, isBarcodePlaceholder } from './helpers/barcodeUrl';
60
+ export { canonicalizeTimezone, findTimezoneOption, formatTimezoneLabel, formatTimezoneOffset, getBrowserTimezone, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption } from './helpers/timezones';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.1.234",
3
+ "version": "1.1.236",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",