@pushwoosh/dumb-components 2.1.2 → 2.2.1
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/Calendar/__tests__/CalendarMonthSlider.test.d.ts +1 -0
- package/Calendar/__tests__/CalendarMonthSlider.test.js +85 -0
- package/Calendar/__tests__/helpers.test.js +20 -1
- package/Calendar/helpers.js +10 -3
- package/helpers/__tests__/timezones.test.js +49 -1
- package/helpers/timezones.d.ts +4 -0
- package/helpers/timezones.js +47 -8
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { CalendarMonthSlider } from '../CalendarMonthSlider/CalendarMonthSlider';
|
|
4
|
+
const LAST_DAY_START = {
|
|
5
|
+
year: 2026,
|
|
6
|
+
month: 1,
|
|
7
|
+
day: 31
|
|
8
|
+
};
|
|
9
|
+
function renderSlider(startMonth = LAST_DAY_START) {
|
|
10
|
+
return render(_jsx(CalendarMonthSlider, {
|
|
11
|
+
startMonth: startMonth,
|
|
12
|
+
onDaySelect: jest.fn()
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
function selectsOf(kind) {
|
|
16
|
+
const selects = screen.getAllByRole('combobox');
|
|
17
|
+
const isYear = select => /^\d+$/.test(select.options[0].text);
|
|
18
|
+
return selects.filter(select => kind === 'year' ? isYear(select) : !isYear(select));
|
|
19
|
+
}
|
|
20
|
+
function visibleMonths() {
|
|
21
|
+
const years = selectsOf('year');
|
|
22
|
+
return selectsOf('month').map((month, i) => `${years[i].value}-${month.value.padStart(2, '0')}`);
|
|
23
|
+
}
|
|
24
|
+
function step(direction) {
|
|
25
|
+
const [prev, next] = screen.getAllByRole('button');
|
|
26
|
+
fireEvent.click(direction === 'prev' ? prev : next);
|
|
27
|
+
act(() => {
|
|
28
|
+
jest.runAllTimers();
|
|
29
|
+
});
|
|
30
|
+
fireEvent.transitionEnd(selectsOf('month')[0]);
|
|
31
|
+
}
|
|
32
|
+
describe('CalendarMonthSlider', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.useFakeTimers();
|
|
35
|
+
jest.setSystemTime(new Date(2026, 5, 15));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
jest.useRealTimers();
|
|
39
|
+
});
|
|
40
|
+
it('renders the start month regardless of the day it was opened on', () => {
|
|
41
|
+
renderSlider();
|
|
42
|
+
expect(visibleMonths()).toEqual(['2026-01']);
|
|
43
|
+
});
|
|
44
|
+
it('steps to the adjacent month in both directions without skipping one', () => {
|
|
45
|
+
renderSlider();
|
|
46
|
+
step('next');
|
|
47
|
+
expect(visibleMonths()).toEqual(['2026-02']);
|
|
48
|
+
step('next');
|
|
49
|
+
expect(visibleMonths()).toEqual(['2026-03']);
|
|
50
|
+
step('prev');
|
|
51
|
+
expect(visibleMonths()).toEqual(['2026-02']);
|
|
52
|
+
});
|
|
53
|
+
it('crosses the year boundary in both directions', () => {
|
|
54
|
+
renderSlider({
|
|
55
|
+
year: 2026,
|
|
56
|
+
month: 12,
|
|
57
|
+
day: 31
|
|
58
|
+
});
|
|
59
|
+
step('next');
|
|
60
|
+
expect(visibleMonths()).toEqual(['2027-01']);
|
|
61
|
+
step('prev');
|
|
62
|
+
step('prev');
|
|
63
|
+
expect(visibleMonths()).toEqual(['2026-11']);
|
|
64
|
+
});
|
|
65
|
+
it('shows the adjacent month, not the one after, while the step animates', () => {
|
|
66
|
+
renderSlider();
|
|
67
|
+
fireEvent.click(screen.getAllByRole('button')[1]);
|
|
68
|
+
expect(visibleMonths()).toEqual(['2026-01', '2026-02']);
|
|
69
|
+
});
|
|
70
|
+
it('jumps to the month and year picked in the selects', () => {
|
|
71
|
+
renderSlider();
|
|
72
|
+
fireEvent.change(selectsOf('month')[0], {
|
|
73
|
+
target: {
|
|
74
|
+
value: '2'
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
expect(visibleMonths()).toEqual(['2026-02']);
|
|
78
|
+
fireEvent.change(selectsOf('year')[0], {
|
|
79
|
+
target: {
|
|
80
|
+
value: '2027'
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
expect(visibleMonths()).toEqual(['2027-02']);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addPeriod, setPeriodStruct } from '../helpers';
|
|
1
|
+
import { addPeriod, getTimezoneOffset, setPeriodStruct } from '../helpers';
|
|
2
2
|
describe('addPeriod', () => {
|
|
3
3
|
it('clamps the day when the target month is shorter', () => {
|
|
4
4
|
expect(addPeriod({
|
|
@@ -121,4 +121,23 @@ describe('setPeriodStruct', () => {
|
|
|
121
121
|
day: 28
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
|
+
});
|
|
125
|
+
describe('getTimezoneOffset', () => {
|
|
126
|
+
it('follows DST at the given instant', () => {
|
|
127
|
+
expect(getTimezoneOffset('Australia/Melbourne', new Date('2026-06-15T12:00:00Z'))).toBe(600);
|
|
128
|
+
expect(getTimezoneOffset('Australia/Melbourne', new Date('2026-10-10T12:00:00Z'))).toBe(660);
|
|
129
|
+
expect(getTimezoneOffset('Asia/Kolkata', new Date('2026-01-15T12:00:00Z'))).toBe(330);
|
|
130
|
+
});
|
|
131
|
+
it('builds one Intl.DateTimeFormat per zone, however many instants it is asked about', () => {
|
|
132
|
+
const construct = jest.spyOn(Intl, 'DateTimeFormat');
|
|
133
|
+
try {
|
|
134
|
+
getTimezoneOffset('Pacific/Chatham', new Date('2026-01-15T12:00:00Z'));
|
|
135
|
+
const afterFirst = construct.mock.calls.length;
|
|
136
|
+
getTimezoneOffset('Pacific/Chatham', new Date('2026-06-15T12:00:00Z'));
|
|
137
|
+
getTimezoneOffset('Pacific/Chatham', new Date('2026-10-10T12:00:00Z'));
|
|
138
|
+
expect(construct.mock.calls.length).toBe(afterFirst);
|
|
139
|
+
} finally {
|
|
140
|
+
construct.mockRestore();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
124
143
|
});
|
package/Calendar/helpers.js
CHANGED
|
@@ -163,8 +163,10 @@ export const structToDate = (ds, tzOffsetMinutes) => {
|
|
|
163
163
|
const utcTime = Date.UTC(ds.year, ds.month - 1, ds.day, hour, minute, second, millisecond);
|
|
164
164
|
return new Date(utcTime - tzOffsetMinutes * 60_000);
|
|
165
165
|
};
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
const offsetFormatters = new Map();
|
|
167
|
+
function offsetFormatter(timezoneName) {
|
|
168
|
+
const cached = offsetFormatters.get(timezoneName);
|
|
169
|
+
if (cached) return cached;
|
|
168
170
|
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
169
171
|
timeZone: timezoneName,
|
|
170
172
|
hour12: false,
|
|
@@ -175,7 +177,12 @@ export function getTimezoneOffset(timezoneName, at = new Date()) {
|
|
|
175
177
|
minute: '2-digit',
|
|
176
178
|
second: '2-digit'
|
|
177
179
|
});
|
|
178
|
-
|
|
180
|
+
offsetFormatters.set(timezoneName, dtf);
|
|
181
|
+
return dtf;
|
|
182
|
+
}
|
|
183
|
+
/** Returns the offset in minutes (east of UTC) for an IANA `timezoneName` at the given instant `at`. */
|
|
184
|
+
export function getTimezoneOffset(timezoneName, at = new Date()) {
|
|
185
|
+
const parts = offsetFormatter(timezoneName).formatToParts(at);
|
|
179
186
|
const y = Number(parts.find(p => p.type === 'year')?.value);
|
|
180
187
|
const mo = Number(parts.find(p => p.type === 'month')?.value) - 1;
|
|
181
188
|
const d = Number(parts.find(p => p.type === 'day')?.value);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalizeTimezone, findTimezoneOption, formatTimezoneLabel, formatTimezoneOffset, formatTimezoneShort, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption } from '../timezones';
|
|
1
|
+
import { canonicalizeTimezone, findTimezoneOption, formatTimezoneLabel, formatTimezoneOffset, formatTimezoneAbbreviation, formatTimezoneBadge, formatTimezoneShort, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption } from '../timezones';
|
|
2
2
|
const winter = new Date('2026-01-15T12:00:00Z');
|
|
3
3
|
const melbourneDst = new Date('2026-10-10T12:00:00Z');
|
|
4
4
|
const melbourneStandard = new Date('2026-06-15T12:00:00Z');
|
|
@@ -24,6 +24,41 @@ describe('formatTimezoneShort', () => {
|
|
|
24
24
|
expect(formatTimezoneShort('Not/AZone', winter)).toBe('Not/AZone');
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
+
describe('formatTimezoneAbbreviation', () => {
|
|
28
|
+
it('returns the letter abbreviation where the engine has one', () => {
|
|
29
|
+
expect(formatTimezoneAbbreviation('America/New_York', winter)).toBe('EST');
|
|
30
|
+
expect(formatTimezoneAbbreviation('America/Los_Angeles', winter)).toBe('PST');
|
|
31
|
+
});
|
|
32
|
+
it('returns null where the engine only offers a GMT-style name', () => {
|
|
33
|
+
expect(formatTimezoneAbbreviation('Asia/Karachi', winter)).toBeNull();
|
|
34
|
+
expect(formatTimezoneAbbreviation('Asia/Kolkata', winter)).toBeNull();
|
|
35
|
+
expect(formatTimezoneAbbreviation('Europe/Moscow', winter)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it('returns null for UTC and for a zone it does not know', () => {
|
|
38
|
+
expect(formatTimezoneAbbreviation('UTC', winter)).toBeNull();
|
|
39
|
+
expect(formatTimezoneAbbreviation('Not/AZone', winter)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
it('follows DST at the given instant', () => {
|
|
42
|
+
expect(formatTimezoneAbbreviation('America/New_York', new Date('2026-07-01T12:00:00Z'))).toBe('EDT');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('formatTimezoneBadge', () => {
|
|
46
|
+
it('pairs the abbreviation with the offset', () => {
|
|
47
|
+
expect(formatTimezoneBadge('America/New_York', winter)).toBe('EST (UTC-05:00)');
|
|
48
|
+
});
|
|
49
|
+
it('falls back to the bare offset for a zone without an abbreviation', () => {
|
|
50
|
+
expect(formatTimezoneBadge('Asia/Karachi', winter)).toBe('UTC+05:00');
|
|
51
|
+
expect(formatTimezoneBadge('Asia/Kolkata', winter)).toBe('UTC+05:30');
|
|
52
|
+
expect(formatTimezoneBadge('UTC', winter)).toBe('UTC+00:00');
|
|
53
|
+
});
|
|
54
|
+
it('follows DST at the given instant', () => {
|
|
55
|
+
expect(formatTimezoneBadge('Australia/Melbourne', melbourneStandard)).toBe('UTC+10:00');
|
|
56
|
+
expect(formatTimezoneBadge('Australia/Melbourne', melbourneDst)).toBe('UTC+11:00');
|
|
57
|
+
});
|
|
58
|
+
it('keeps an unknown zone readable', () => {
|
|
59
|
+
expect(formatTimezoneBadge('Not/AZone', winter)).toBe('Not/AZone');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
27
62
|
describe('formatTimezoneLabel', () => {
|
|
28
63
|
it('uses the current tzdata: Kazakhstan is UTC+5 since 2024', () => {
|
|
29
64
|
expect(formatTimezoneLabel('Asia/Almaty', winter)).toBe('(UTC+05:00) Asia/Almaty');
|
|
@@ -89,6 +124,19 @@ describe('listTimezones', () => {
|
|
|
89
124
|
expect(withStored.some(option => option.value === '')).toBe(false);
|
|
90
125
|
expect(withStored[withStored.length - 1].value).toBe('Not/A_Zone');
|
|
91
126
|
});
|
|
127
|
+
it('answers an equal but fresh list when called again for the same minute', () => {
|
|
128
|
+
const again = listTimezones(winter);
|
|
129
|
+
expect(again).toEqual(options);
|
|
130
|
+
expect(again).not.toBe(options);
|
|
131
|
+
again.length = 0;
|
|
132
|
+
expect(listTimezones(winter)).toEqual(options);
|
|
133
|
+
});
|
|
134
|
+
it('re-resolves offsets when the instant moves across a DST boundary', () => {
|
|
135
|
+
const standard = listTimezones(melbourneStandard).find(option => option.value === 'Australia/Melbourne');
|
|
136
|
+
const dst = listTimezones(melbourneDst).find(option => option.value === 'Australia/Melbourne');
|
|
137
|
+
expect(standard?.label).toBe('(UTC+10:00) Australia/Melbourne');
|
|
138
|
+
expect(dst?.label).toBe('(UTC+11:00) Australia/Melbourne');
|
|
139
|
+
});
|
|
92
140
|
it('falls back to the browser zone and UTC when the engine has no zone list', () => {
|
|
93
141
|
const original = Intl.supportedValuesOf;
|
|
94
142
|
Object.defineProperty(Intl, 'supportedValuesOf', {
|
package/helpers/timezones.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export declare function formatTimezoneOffset(minutes: number): string;
|
|
|
15
15
|
export declare function formatTimezoneLabel(timezone: string, at?: Date): string;
|
|
16
16
|
/** Compact badge for a zone, `UTC+05:00`; a zone the engine doesn't know is shown by its bare name. */
|
|
17
17
|
export declare function formatTimezoneShort(timezone: string, at?: Date): string;
|
|
18
|
+
/** Letter abbreviation of the zone at `at` (`EST`, `PKT`), or `null` when the engine only offers a `GMT+5`-style name or does not know the zone. */
|
|
19
|
+
export declare function formatTimezoneAbbreviation(timezone: string, at?: Date): string | null;
|
|
20
|
+
/** Badge naming the zone a time is shown in: `PKT (UTC+05:00)`, or the bare offset when the zone has no letter abbreviation. */
|
|
21
|
+
export declare function formatTimezoneBadge(timezone: string, at?: Date): string;
|
|
18
22
|
/** Option for any stored zone name — aliases, `Etc/GMT±N`, POSIX abbreviations — kept verbatim as the value. */
|
|
19
23
|
export declare function resolveTimezoneOption(timezone: string, at?: Date): TimezoneOption;
|
|
20
24
|
/** 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. */
|
package/helpers/timezones.js
CHANGED
|
@@ -30,16 +30,37 @@ export function formatTimezoneOffset(minutes) {
|
|
|
30
30
|
const mins = String(absolute % 60).padStart(2, '0');
|
|
31
31
|
return `UTC${sign}${hours}:${mins}`;
|
|
32
32
|
}
|
|
33
|
+
function labelForOffset(timezone, offset) {
|
|
34
|
+
return offset === null ? timezone : `(${formatTimezoneOffset(offset)}) ${timezone}`;
|
|
35
|
+
}
|
|
33
36
|
/** The shared picker label, `(UTC+05:00) Asia/Almaty` at instant `at`; a zone the engine doesn't know is labelled by its bare name. */
|
|
34
37
|
export function formatTimezoneLabel(timezone, at = new Date()) {
|
|
35
|
-
|
|
36
|
-
return offset === null ? timezone : `(${formatTimezoneOffset(offset)}) ${timezone}`;
|
|
38
|
+
return labelForOffset(timezone, getTimezoneOffsetSafe(timezone, at));
|
|
37
39
|
}
|
|
38
40
|
/** Compact badge for a zone, `UTC+05:00`; a zone the engine doesn't know is shown by its bare name. */
|
|
39
41
|
export function formatTimezoneShort(timezone, at = new Date()) {
|
|
40
42
|
const offset = getTimezoneOffsetSafe(timezone, at);
|
|
41
43
|
return offset === null ? timezone : formatTimezoneOffset(offset);
|
|
42
44
|
}
|
|
45
|
+
/** Letter abbreviation of the zone at `at` (`EST`, `PKT`), or `null` when the engine only offers a `GMT+5`-style name or does not know the zone. */
|
|
46
|
+
export function formatTimezoneAbbreviation(timezone, at = new Date()) {
|
|
47
|
+
try {
|
|
48
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
49
|
+
timeZone: timezone,
|
|
50
|
+
timeZoneName: 'short'
|
|
51
|
+
}).formatToParts(at);
|
|
52
|
+
const name = parts.find(part => part.type === 'timeZoneName')?.value ?? '';
|
|
53
|
+
return /^[A-Z]{2,5}$/.test(name) && name !== 'UTC' ? name : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Badge naming the zone a time is shown in: `PKT (UTC+05:00)`, or the bare offset when the zone has no letter abbreviation. */
|
|
59
|
+
export function formatTimezoneBadge(timezone, at = new Date()) {
|
|
60
|
+
const short = formatTimezoneShort(timezone, at);
|
|
61
|
+
const abbreviation = formatTimezoneAbbreviation(timezone, at);
|
|
62
|
+
return abbreviation ? `${abbreviation} (${short})` : short;
|
|
63
|
+
}
|
|
43
64
|
/** Option for any stored zone name — aliases, `Etc/GMT±N`, POSIX abbreviations — kept verbatim as the value. */
|
|
44
65
|
export function resolveTimezoneOption(timezone, at = new Date()) {
|
|
45
66
|
return {
|
|
@@ -61,18 +82,36 @@ function compareZones(a, b) {
|
|
|
61
82
|
if (b.name === UTC) return 1;
|
|
62
83
|
return a.name.localeCompare(b.name);
|
|
63
84
|
}
|
|
85
|
+
function buildTimezoneList(names, at) {
|
|
86
|
+
return names.map(name => ({
|
|
87
|
+
name,
|
|
88
|
+
offset: getTimezoneOffsetSafe(name, at)
|
|
89
|
+
})).sort(compareZones).map(({
|
|
90
|
+
name,
|
|
91
|
+
offset
|
|
92
|
+
}) => ({
|
|
93
|
+
value: name,
|
|
94
|
+
label: labelForOffset(name, offset)
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
function listCacheKey(names, at) {
|
|
98
|
+
return `${Math.floor(at.getTime() / 60_000)}|${names.join(',')}`;
|
|
99
|
+
}
|
|
100
|
+
let cachedListKey = null;
|
|
101
|
+
let cachedList = [];
|
|
64
102
|
/** 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. */
|
|
65
103
|
export function listTimezones(at = new Date(), include = []) {
|
|
66
104
|
const names = new Set([UTC, ...supportedTimezones()]);
|
|
67
105
|
include.forEach(name => {
|
|
68
106
|
if (name) names.add(name);
|
|
69
107
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
108
|
+
const zones = [...names];
|
|
109
|
+
const key = listCacheKey(zones, at);
|
|
110
|
+
if (key !== cachedListKey) {
|
|
111
|
+
cachedList = buildTimezoneList(zones, at);
|
|
112
|
+
cachedListKey = key;
|
|
113
|
+
}
|
|
114
|
+
return cachedList.slice();
|
|
76
115
|
}
|
|
77
116
|
/** 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`. */
|
|
78
117
|
export function findTimezoneOption(options, value, at = new Date()) {
|
package/index.d.ts
CHANGED
|
@@ -59,4 +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, formatTimezoneShort, getBrowserTimezone, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption, type TimezoneOption, } from './helpers/timezones';
|
|
62
|
+
export { canonicalizeTimezone, findTimezoneOption, formatTimezoneAbbreviation, formatTimezoneBadge, formatTimezoneLabel, formatTimezoneOffset, formatTimezoneShort, getBrowserTimezone, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption, type TimezoneOption, } from './helpers/timezones';
|
package/index.js
CHANGED
|
@@ -57,4 +57,4 @@ export { ApplicationPreloader } from './ApplicationPreloader';
|
|
|
57
57
|
export { NotificationInbox, NotificationBar, NotificationBell } from './NotificationInbox';
|
|
58
58
|
export { LeafletTheme } from './LeafletTheme';
|
|
59
59
|
export { buildBarcodeUrl, barcodePlaceholderSafeEscape, isBarcodePlaceholder } from './helpers/barcodeUrl';
|
|
60
|
-
export { canonicalizeTimezone, findTimezoneOption, formatTimezoneLabel, formatTimezoneOffset, formatTimezoneShort, getBrowserTimezone, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption } from './helpers/timezones';
|
|
60
|
+
export { canonicalizeTimezone, findTimezoneOption, formatTimezoneAbbreviation, formatTimezoneBadge, formatTimezoneLabel, formatTimezoneOffset, formatTimezoneShort, getBrowserTimezone, getTimezoneOffsetSafe, listTimezones, resolveTimezoneOption } from './helpers/timezones';
|