uikit-react-public 0.44.4 → 0.45.0

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.
@@ -0,0 +1,379 @@
1
+ import {
2
+ HTMLAttributes,
3
+ ReactNode,
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useState,
9
+ } from 'react';
10
+ import { css, cx } from '@emotion/css';
11
+ import useTheme from '../../theme/useTheme';
12
+ import marginsStyle, { MarginProps } from '../common/marginsStyle';
13
+ import Heading from '../Heading';
14
+ import Paragraph from '../Paragraph';
15
+ import Button from '../Button';
16
+ import Link from '../Link';
17
+ import BaseDialog from '../Dialog/BaseDialog';
18
+ import CookieSettings from './CookieSettings';
19
+ import {
20
+ DEFAULT_COOKIE_CATEGORIES,
21
+ DEFAULT_DESCRIPTION,
22
+ DEFAULT_DESCRIPTION_SUFFIX,
23
+ DEFAULT_HEADING,
24
+ DEFAULT_POLICY_HREF,
25
+ } from './defaults';
26
+ import { CookieCategory, CookiePreferences } from './types';
27
+
28
+ export const NAME = 'ucl-uikit-cookies-notice';
29
+ const DESKTOP_NOTICE_CONTENT_MAX_WIDTH = 944;
30
+
31
+ export interface CookiesNoticeBaseProps extends HTMLAttributes<HTMLDivElement> {
32
+ heading?: ReactNode;
33
+ description?: ReactNode;
34
+ policyHref?: string;
35
+ policyLabel?: ReactNode;
36
+ cookies?: CookieCategory[];
37
+ preferences?: CookiePreferences;
38
+ modal?: boolean;
39
+ manageCookiesLabel?: ReactNode;
40
+ acceptAllLabel?: ReactNode;
41
+ acceptNecessaryLabel?: ReactNode;
42
+ onAcceptAll?: (preferences: CookiePreferences) => void;
43
+ onAcceptNecessary?: (preferences: CookiePreferences) => void;
44
+ onManageCookies?: () => void;
45
+ onPreferencesChange?: (preferences: CookiePreferences) => void;
46
+ onSavePreferences?: (preferences: CookiePreferences) => void;
47
+ onSettingsClose?: () => void;
48
+ testId?: string;
49
+ }
50
+
51
+ export type CookiesNoticeProps = CookiesNoticeBaseProps & MarginProps;
52
+
53
+ const getAllPreferences = (cookies: CookieCategory[]): CookiePreferences =>
54
+ cookies.reduce<CookiePreferences>((acc, cookie) => {
55
+ acc[cookie.id] = true;
56
+ return acc;
57
+ }, {});
58
+
59
+ const getNecessaryPreferences = (
60
+ cookies: CookieCategory[]
61
+ ): CookiePreferences =>
62
+ cookies.reduce<CookiePreferences>((acc, cookie) => {
63
+ acc[cookie.id] = !!cookie.required;
64
+ return acc;
65
+ }, {});
66
+
67
+ const getRequiredPreferences = (cookies: CookieCategory[]): CookiePreferences =>
68
+ cookies.reduce<CookiePreferences>((acc, cookie) => {
69
+ if (cookie.required) {
70
+ acc[cookie.id] = true;
71
+ }
72
+ return acc;
73
+ }, {});
74
+
75
+ const getDefaultPreferences = (cookies: CookieCategory[]): CookiePreferences =>
76
+ cookies.reduce<CookiePreferences>((acc, cookie) => {
77
+ acc[cookie.id] = cookie.required ? true : (cookie.defaultEnabled ?? false);
78
+ return acc;
79
+ }, {});
80
+
81
+ const CookiesNotice = ({
82
+ heading = DEFAULT_HEADING,
83
+ description,
84
+ policyHref = DEFAULT_POLICY_HREF,
85
+ policyLabel = 'UCL privacy policy',
86
+ cookies = DEFAULT_COOKIE_CATEGORIES,
87
+ preferences,
88
+ modal = false,
89
+ manageCookiesLabel = 'Manage cookies',
90
+ acceptAllLabel = 'Accept all cookies',
91
+ acceptNecessaryLabel = 'Accept necessary only',
92
+ onAcceptAll,
93
+ onAcceptNecessary,
94
+ onManageCookies,
95
+ onPreferencesChange,
96
+ onSavePreferences,
97
+ onSettingsClose,
98
+ testId = NAME,
99
+ className,
100
+ ...props
101
+ }: CookiesNoticeProps) => {
102
+ const [theme] = useTheme();
103
+ const noticeHeadingId = useId();
104
+ const [settingsOpen, setSettingsOpen] = useState(false);
105
+ const initialPreferences = useMemo(
106
+ () => ({
107
+ ...getDefaultPreferences(cookies),
108
+ ...preferences,
109
+ ...getRequiredPreferences(cookies),
110
+ }),
111
+ [cookies, preferences]
112
+ );
113
+ const [savedPreferences, setSavedPreferences] =
114
+ useState<CookiePreferences>(initialPreferences);
115
+ const [draftPreferences, setDraftPreferences] =
116
+ useState<CookiePreferences>(initialPreferences);
117
+
118
+ useEffect(() => {
119
+ setSavedPreferences(initialPreferences);
120
+ setDraftPreferences(initialPreferences);
121
+ }, [initialPreferences]);
122
+
123
+ const handlePreferenceToggle = useCallback(
124
+ (cookie: CookieCategory) => {
125
+ if (cookie.required) return;
126
+
127
+ const nextPreferences = {
128
+ ...draftPreferences,
129
+ [cookie.id]: !draftPreferences[cookie.id],
130
+ };
131
+
132
+ setDraftPreferences(nextPreferences);
133
+ onPreferencesChange?.(nextPreferences);
134
+ },
135
+ [draftPreferences, onPreferencesChange]
136
+ );
137
+
138
+ const handleAcceptAll = useCallback(() => {
139
+ const nextPreferences = getAllPreferences(cookies);
140
+
141
+ setSavedPreferences(nextPreferences);
142
+ setDraftPreferences(nextPreferences);
143
+ onAcceptAll?.(nextPreferences);
144
+ }, [cookies, onAcceptAll]);
145
+
146
+ const handleAcceptNecessary = useCallback(() => {
147
+ const nextPreferences = getNecessaryPreferences(cookies);
148
+
149
+ setSavedPreferences(nextPreferences);
150
+ setDraftPreferences(nextPreferences);
151
+ onAcceptNecessary?.(nextPreferences);
152
+ }, [cookies, onAcceptNecessary]);
153
+
154
+ const handleManageCookies = useCallback(() => {
155
+ onManageCookies?.();
156
+ setDraftPreferences(savedPreferences);
157
+ setSettingsOpen(true);
158
+ }, [onManageCookies, savedPreferences]);
159
+
160
+ const handleSettingsClose = useCallback(() => {
161
+ setSettingsOpen(false);
162
+ onSettingsClose?.();
163
+ }, [onSettingsClose]);
164
+
165
+ const handleSavePreferences = useCallback(() => {
166
+ setSavedPreferences(draftPreferences);
167
+ onSavePreferences?.(draftPreferences);
168
+ setSettingsOpen(false);
169
+ }, [draftPreferences, onSavePreferences]);
170
+
171
+ const baseStyle = css`
172
+ box-sizing: border-box;
173
+ width: 100%;
174
+ padding: ${theme.padding.p24} ${theme.padding.p16};
175
+ background-color: ${theme.colour.surface.primary};
176
+ color: ${theme.colour.text.default};
177
+ border-radius: ${theme.radius.r4} ${theme.radius.r4} 0 0;
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: ${theme.padding.p24};
181
+
182
+ @media (min-width: ${theme.breakpoints.tablet}px) {
183
+ padding: ${theme.padding.p48} ${theme.padding.p32};
184
+ border-radius: ${theme.radius.r4};
185
+ gap: ${theme.padding.p32};
186
+ }
187
+ `;
188
+
189
+ const contentStyle = css`
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: ${theme.padding.p16};
193
+ `;
194
+
195
+ const headingStyle = css`
196
+ text-align: center;
197
+
198
+ @media (min-width: ${theme.breakpoints.tablet}px) {
199
+ text-align: left;
200
+ }
201
+ `;
202
+
203
+ const policyLinkStyle = css`
204
+ color: ${theme.colour.text.default};
205
+
206
+ &:visited {
207
+ color: ${theme.colour.text.default};
208
+ }
209
+ `;
210
+
211
+ const actionsStyle = css`
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: ${theme.padding.p16};
215
+
216
+ @media (min-width: ${theme.breakpoints.tablet}px) {
217
+ flex-direction: row;
218
+ justify-content: space-between;
219
+ align-items: center;
220
+ }
221
+ `;
222
+
223
+ const actionGroupStyle = css`
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: ${theme.padding.p16};
227
+
228
+ @media (min-width: ${theme.breakpoints.tablet}px) {
229
+ flex-direction: row;
230
+ align-items: center;
231
+ }
232
+ `;
233
+
234
+ const manageButtonStyle = css`
235
+ order: 3;
236
+ width: 100%;
237
+ white-space: nowrap;
238
+
239
+ @media (min-width: ${theme.breakpoints.tablet}px) {
240
+ order: 0;
241
+ width: auto;
242
+ }
243
+ `;
244
+
245
+ const actionButtonStyle = css`
246
+ width: 100%;
247
+ white-space: nowrap;
248
+
249
+ @media (min-width: ${theme.breakpoints.tablet}px) {
250
+ width: auto;
251
+ }
252
+ `;
253
+
254
+ const dialogStyle = css`
255
+ height: fit-content;
256
+ max-height: calc(100vh - ${theme.margin.m32});
257
+ margin: auto auto 0;
258
+ background-color: transparent;
259
+ box-shadow: ${theme.boxShadow.x2y4};
260
+
261
+ &:modal {
262
+ max-height: calc(100vh - ${theme.margin.m32});
263
+ }
264
+
265
+ @media (min-width: ${theme.breakpoints.tablet}px) {
266
+ width: calc(100vw - ${theme.margin.m64});
267
+ max-width: calc(
268
+ ${DESKTOP_NOTICE_CONTENT_MAX_WIDTH}px + ${theme.padding.p64}
269
+ );
270
+ margin: auto;
271
+
272
+ &:modal {
273
+ max-width: min(
274
+ calc(${DESKTOP_NOTICE_CONTENT_MAX_WIDTH}px + ${theme.padding.p64}),
275
+ calc(100vw - ${theme.margin.m64})
276
+ );
277
+ }
278
+ }
279
+ `;
280
+
281
+ const style = cx(NAME, baseStyle, marginsStyle(props, theme), className);
282
+
283
+ const notice = (
284
+ <div
285
+ className={style}
286
+ data-testid={testId}
287
+ {...props}
288
+ >
289
+ <div className={cx(`${NAME}__content`, contentStyle)}>
290
+ <Heading
291
+ id={noticeHeadingId}
292
+ level='md'
293
+ as='h2'
294
+ noMargins
295
+ className={cx(`${NAME}__heading`, headingStyle)}
296
+ >
297
+ {heading}
298
+ </Heading>
299
+ <Paragraph
300
+ level='md'
301
+ noMargins
302
+ >
303
+ {description ?? (
304
+ <>
305
+ {DEFAULT_DESCRIPTION}
306
+ <Link
307
+ href={policyHref}
308
+ className={policyLinkStyle}
309
+ noVisited
310
+ >
311
+ {policyLabel}
312
+ </Link>
313
+ .{DEFAULT_DESCRIPTION_SUFFIX}
314
+ </>
315
+ )}
316
+ </Paragraph>
317
+ </div>
318
+
319
+ <div className={cx(`${NAME}__actions`, actionsStyle)}>
320
+ <Button
321
+ variant='secondary'
322
+ fullWidth
323
+ className={cx(`${NAME}__manage-button`, manageButtonStyle)}
324
+ onClick={handleManageCookies}
325
+ >
326
+ {manageCookiesLabel}
327
+ </Button>
328
+ <div className={cx(`${NAME}__accept-actions`, actionGroupStyle)}>
329
+ <Button
330
+ variant='accent'
331
+ fullWidth
332
+ className={cx(
333
+ `${NAME}__accept-necessary-button`,
334
+ actionButtonStyle
335
+ )}
336
+ onClick={handleAcceptNecessary}
337
+ >
338
+ {acceptNecessaryLabel}
339
+ </Button>
340
+ <Button
341
+ variant='accent'
342
+ fullWidth
343
+ className={cx(`${NAME}__accept-all-button`, actionButtonStyle)}
344
+ onClick={handleAcceptAll}
345
+ >
346
+ {acceptAllLabel}
347
+ </Button>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ );
352
+
353
+ return (
354
+ <>
355
+ <BaseDialog
356
+ open
357
+ modal={modal}
358
+ size='large'
359
+ closeOnClickOutside={false}
360
+ className={cx(`${NAME}__dialog`, dialogStyle)}
361
+ aria-labelledby={noticeHeadingId}
362
+ >
363
+ {notice}
364
+ </BaseDialog>
365
+ <CookieSettings
366
+ open={settingsOpen}
367
+ cookies={cookies}
368
+ preferences={draftPreferences}
369
+ policyHref={policyHref}
370
+ policyLabel='UCL privacy policy.'
371
+ onClose={handleSettingsClose}
372
+ onPreferenceToggle={handlePreferenceToggle}
373
+ onSavePreferences={handleSavePreferences}
374
+ />
375
+ </>
376
+ );
377
+ };
378
+
379
+ export default CookiesNotice;
@@ -0,0 +1,180 @@
1
+ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { ThemeContextProvider } from '../../../theme/useTheme';
4
+ import CookiesNotice from '..';
5
+ import { CookieCategory } from '../types';
6
+
7
+ const originalShowModal = HTMLDialogElement.prototype.showModal;
8
+ const originalShow = HTMLDialogElement.prototype.show;
9
+ const originalClose = HTMLDialogElement.prototype.close;
10
+
11
+ beforeAll(() => {
12
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
13
+ this: HTMLDialogElement
14
+ ) {
15
+ this.setAttribute('open', '');
16
+ });
17
+ HTMLDialogElement.prototype.show = vi.fn(function (this: HTMLDialogElement) {
18
+ this.setAttribute('open', '');
19
+ });
20
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
21
+ this.removeAttribute('open');
22
+ });
23
+ });
24
+
25
+ afterAll(() => {
26
+ HTMLDialogElement.prototype.showModal = originalShowModal;
27
+ HTMLDialogElement.prototype.show = originalShow;
28
+ HTMLDialogElement.prototype.close = originalClose;
29
+ });
30
+
31
+ const cookies: CookieCategory[] = [
32
+ {
33
+ id: 'necessary',
34
+ title: 'Necessary',
35
+ description: 'Required for the site to work.',
36
+ required: true,
37
+ },
38
+ {
39
+ id: 'analytics',
40
+ title: 'Analytics',
41
+ description: 'Helps us improve the service.',
42
+ defaultEnabled: true,
43
+ },
44
+ {
45
+ id: 'maps',
46
+ title: 'Maps',
47
+ description: 'Embeds third-party map services.',
48
+ },
49
+ ];
50
+
51
+ const renderWithTheme = (ui: React.ReactNode) =>
52
+ render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
53
+
54
+ describe('CookiesNotice', () => {
55
+ test('renders custom cookie content and calls accept callbacks with preferences', () => {
56
+ const onAcceptAll = vi.fn();
57
+ const onAcceptNecessary = vi.fn();
58
+
59
+ renderWithTheme(
60
+ <CookiesNotice
61
+ cookies={cookies}
62
+ onAcceptAll={onAcceptAll}
63
+ onAcceptNecessary={onAcceptNecessary}
64
+ />
65
+ );
66
+
67
+ expect(screen.getByText('Help us improve your experience')).toBeDefined();
68
+ expect(
69
+ screen.getByRole('dialog', { name: 'Help us improve your experience' })
70
+ ).toHaveAttribute('aria-modal', 'false');
71
+
72
+ fireEvent.click(screen.getByRole('button', { name: 'Accept all cookies' }));
73
+ fireEvent.click(
74
+ screen.getByRole('button', { name: 'Accept necessary only' })
75
+ );
76
+
77
+ expect(onAcceptAll).toHaveBeenCalledWith({
78
+ necessary: true,
79
+ analytics: true,
80
+ maps: true,
81
+ });
82
+ expect(onAcceptNecessary).toHaveBeenCalledWith({
83
+ necessary: true,
84
+ analytics: false,
85
+ maps: false,
86
+ });
87
+ });
88
+
89
+ test('renders modal notice as a dialog', () => {
90
+ renderWithTheme(
91
+ <CookiesNotice
92
+ modal
93
+ cookies={cookies}
94
+ />
95
+ );
96
+
97
+ expect(
98
+ screen.getByRole('dialog', { name: 'Help us improve your experience' })
99
+ ).toBeInTheDocument();
100
+ });
101
+
102
+ test('opens settings and saves selected preferences', () => {
103
+ const onManageCookies = vi.fn();
104
+ const onSavePreferences = vi.fn();
105
+
106
+ renderWithTheme(
107
+ <CookiesNotice
108
+ cookies={cookies}
109
+ onManageCookies={onManageCookies}
110
+ onSavePreferences={onSavePreferences}
111
+ />
112
+ );
113
+
114
+ fireEvent.click(screen.getByRole('button', { name: 'Manage cookies' }));
115
+ fireEvent.click(screen.getByRole('switch', { name: 'Allow Analytics' }));
116
+ fireEvent.click(screen.getByRole('button', { name: 'Save preferences' }));
117
+
118
+ expect(onManageCookies).toHaveBeenCalledTimes(1);
119
+ expect(onSavePreferences).toHaveBeenCalledWith({
120
+ necessary: true,
121
+ analytics: false,
122
+ maps: false,
123
+ });
124
+ });
125
+
126
+ test('keeps saved preferences when settings is reopened', () => {
127
+ const onSavePreferences = vi.fn();
128
+
129
+ renderWithTheme(
130
+ <CookiesNotice
131
+ cookies={cookies}
132
+ onSavePreferences={onSavePreferences}
133
+ />
134
+ );
135
+
136
+ fireEvent.click(screen.getByRole('button', { name: 'Manage cookies' }));
137
+ fireEvent.click(screen.getByRole('switch', { name: 'Allow Analytics' }));
138
+ fireEvent.click(screen.getByRole('button', { name: 'Save preferences' }));
139
+
140
+ fireEvent.click(screen.getByRole('button', { name: 'Manage cookies' }));
141
+ fireEvent.click(screen.getByRole('button', { name: 'Save preferences' }));
142
+
143
+ expect(onSavePreferences).toHaveBeenLastCalledWith({
144
+ necessary: true,
145
+ analytics: false,
146
+ maps: false,
147
+ });
148
+ });
149
+
150
+ test('passes existing preferences into managed settings', () => {
151
+ const onSavePreferences = vi.fn();
152
+
153
+ renderWithTheme(
154
+ <CookiesNotice
155
+ cookies={cookies}
156
+ preferences={{
157
+ necessary: true,
158
+ analytics: false,
159
+ maps: true,
160
+ }}
161
+ onSavePreferences={onSavePreferences}
162
+ />
163
+ );
164
+
165
+ fireEvent.click(screen.getByRole('button', { name: 'Manage cookies' }));
166
+
167
+ expect(screen.getByRole('dialog', { name: 'Privacy preferences' }));
168
+ expect(screen.getByText('Embeds third-party map services.')).toBeDefined();
169
+
170
+ fireEvent.click(screen.getByRole('switch', { name: 'Allow Analytics' }));
171
+ fireEvent.click(screen.getByRole('switch', { name: 'Allow Maps' }));
172
+ fireEvent.click(screen.getByRole('button', { name: 'Save preferences' }));
173
+
174
+ expect(onSavePreferences).toHaveBeenCalledWith({
175
+ necessary: true,
176
+ analytics: true,
177
+ maps: false,
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,40 @@
1
+ import { CookieCategory } from './types';
2
+
3
+ export const DEFAULT_POLICY_HREF =
4
+ 'https://www.ucl.ac.uk/legal-services/privacy/cookie-policy';
5
+
6
+ export const DEFAULT_HEADING = 'Help us improve your experience';
7
+
8
+ export const DEFAULT_DESCRIPTION =
9
+ 'We use cookies for authentication, analytics, and third-party services like Google Maps. Some cookies are required for the site to function and cannot be turned off. By accepting, you agree to the ';
10
+
11
+ export const DEFAULT_DESCRIPTION_SUFFIX =
12
+ ' You can update your preferences any time.';
13
+
14
+ export const DEFAULT_COOKIE_CATEGORIES: CookieCategory[] = [
15
+ {
16
+ id: 'necessary',
17
+ title: 'Necessary Cookies',
18
+ description:
19
+ "These cookies are necessary for the website to function and can't be switched off in our systems.",
20
+ required: true,
21
+ defaultEnabled: true,
22
+ learnMoreHref: DEFAULT_POLICY_HREF,
23
+ },
24
+ {
25
+ id: 'analytics',
26
+ title: 'Analytics',
27
+ description:
28
+ 'These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site.',
29
+ defaultEnabled: true,
30
+ learnMoreHref: DEFAULT_POLICY_HREF,
31
+ },
32
+ {
33
+ id: 'marketing',
34
+ title: 'Marketing',
35
+ description:
36
+ 'These cookies are set through our site by our advertising partners. They may be used to build a profile of your interests and show you relevant ads.',
37
+ defaultEnabled: true,
38
+ learnMoreHref: DEFAULT_POLICY_HREF,
39
+ },
40
+ ];
@@ -0,0 +1,6 @@
1
+ export { default } from './CookiesNotice';
2
+ export type {
3
+ CookiesNoticeBaseProps,
4
+ CookiesNoticeProps,
5
+ } from './CookiesNotice';
6
+ export type { CookieCategory, CookiePreferences } from './types';
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export type CookiePreferences = Record<string, boolean>;
4
+
5
+ export interface CookieCategory {
6
+ id: string;
7
+ title: ReactNode;
8
+ description: ReactNode;
9
+ required?: boolean;
10
+ defaultEnabled?: boolean;
11
+ toggleLabel?: string;
12
+ learnMoreHref?: string;
13
+ learnMoreLabel?: ReactNode;
14
+ }
@@ -41,7 +41,7 @@ export { default as Avatar } from './Avatar';
41
41
  export type { AvatarProps } from './Avatar';
42
42
 
43
43
  export { default as Badge } from './Badge';
44
- export type { BadgeProps } from './Badge';
44
+ export type { BadgeProps, BadgeVariant } from './Badge';
45
45
 
46
46
  export { default as BaseCheckbox } from './BaseCheckbox';
47
47
  export type { BaseCheckboxProps, BaseCheckboxState } from './BaseCheckbox';
@@ -221,9 +221,18 @@ export type { FileInputProps } from './FileInput';
221
221
  export { default as Timepicker } from './Timepicker';
222
222
  export type { TimepickerProps } from './Timepicker';
223
223
 
224
+ /** @deprecated Use CookiesNotice instead. */
224
225
  export { default as CookieNotice } from './CookieNotice';
225
226
  export type { CookieNoticeProps } from './CookieNotice';
226
227
 
228
+ export { default as CookiesNotice } from './CookiesNotice';
229
+ export type {
230
+ CookieCategory,
231
+ CookiePreferences,
232
+ CookiesNoticeBaseProps,
233
+ CookiesNoticeProps,
234
+ } from './CookiesNotice';
235
+
227
236
  export { default as Search } from './Search';
228
237
  export type { SearchProps } from './Search';
229
238
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.44.4",
5
+ "version": "0.45.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",