@sonic-equipment/ui 123.0.0 → 124.0.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.
@@ -72,7 +72,7 @@ function SpinnerState({ isDisabled, onChange, onManualInput, quantity, }) {
72
72
  useEffect(() => {
73
73
  setInternalQuantity(quantity);
74
74
  }, [quantity]);
75
- return (jsx(NumberField, { withButtons: true, autoGrow: true, formatOptions: {
75
+ return (jsx(NumberField, { withButtons: true, autoGrow: true, "data-test-selector": "quantity", formatOptions: {
76
76
  maximumFractionDigits: 0,
77
77
  style: 'decimal',
78
78
  useGrouping: false,
@@ -95,7 +95,7 @@ function ManualInputState({ isDisabled, onCancel, onConfirm, quantity, }) {
95
95
  if (e.key === 'Escape')
96
96
  onCancel();
97
97
  };
98
- return (jsxs("div", { className: styles['manual-input-container'], children: [jsx("div", { className: styles['left-button-spacer'] }), jsx(NumberField, { autoFocus: true, autoGrow: true, defaultValue: quantity ? ensureNumber(quantity) : undefined, formatOptions: {
98
+ return (jsxs("div", { className: styles['manual-input-container'], children: [jsx("div", { className: styles['left-button-spacer'] }), jsx(NumberField, { autoFocus: true, autoGrow: true, "data-test-selector": "quantity", defaultValue: quantity ? ensureNumber(quantity) : undefined, formatOptions: {
99
99
  maximumFractionDigits: 0,
100
100
  style: 'decimal',
101
101
  useGrouping: false,
@@ -5,11 +5,13 @@ import { useAddProductToCurrentCart } from '../../shared/api/storefront/hooks/ca
5
5
  import { useDeleteCartLineById } from '../../shared/api/storefront/hooks/cart/use-delete-cart-line-by-id.js';
6
6
  import { useFetchCurrentCartLines } from '../../shared/api/storefront/hooks/cart/use-fetch-current-cart-lines.js';
7
7
  import { useUpdateCartLineById } from '../../shared/api/storefront/hooks/cart/use-update-cart-line-by-id.js';
8
+ import { useDataLayer } from '../../shared/ga/use-data-layer.js';
8
9
  import { useCartEvents } from '../../shared/providers/cart-provider.js';
9
10
  import { useToast } from '../../toast/use-toast.js';
10
11
  import { AddToCartButton } from './add-to-cart-button.js';
11
12
 
12
13
  const ConnectedAddToCartButton = ({ onAddToCart, productId }) => {
14
+ const { createEcommerceEvent, dataLayer } = useDataLayer();
13
15
  const { isPending: isPendingAddToCart, mutate: addToCart } = useAddProductToCurrentCart();
14
16
  const { data: cartLines, isLoading: isLoadingCartLines } = useFetchCurrentCartLines();
15
17
  const { isPending: isPendingDeleteCartLine, mutate: deleteCartLine } = useDeleteCartLineById();
@@ -77,6 +79,12 @@ const ConnectedAddToCartButton = ({ onAddToCart, productId }) => {
77
79
  onSuccess: cartLine => {
78
80
  onAddToCart?.({ cartLine });
79
81
  onCartLineAdded?.(cartLine);
82
+ dataLayer.push(createEcommerceEvent({
83
+ cartLine,
84
+ event: {
85
+ event: 'add_to_cart',
86
+ },
87
+ }));
80
88
  addToast({
81
89
  body: (jsx(FormattedMessage, { id: "The product has been added to your cart." })),
82
90
  isUserDismissable: false,
@@ -9,10 +9,11 @@ export interface ButtonProps {
9
9
  icon?: React.ReactNode;
10
10
  iconPosition?: 'left' | 'right';
11
11
  isDisabled?: boolean;
12
+ isLoading?: string | ReactNode | boolean;
12
13
  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
13
14
  size?: 'sm' | 'md' | 'lg';
14
15
  type?: 'button' | 'submit' | 'reset';
15
16
  variant?: 'solid' | 'outline' | 'ghost';
16
17
  withArrow?: boolean;
17
18
  }
18
- export declare function Button({ _pseudo, children, className, color, condensed, 'data-test-selector': dataTestSelector, icon, iconPosition, isDisabled, onClick: _onClick, size, type, variant, withArrow, }: ButtonProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function Button({ _pseudo, children, className, color, condensed, 'data-test-selector': dataTestSelector, icon, iconPosition, isDisabled, isLoading, onClick: _onClick, size, type, variant, withArrow, }: ButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,12 @@
1
1
  "use client";
2
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { isValidElement } from 'react';
3
4
  import clsx from 'clsx';
5
+ import { ProgressCircle } from '../../loading/progress-circle.js';
4
6
  import { GlyphsArrowBoldCapsRightIcon } from '../../icons/glyph/glyphs-arrow-boldcaps-right-icon.js';
5
7
  import buttonStyles from './button.module.css.js';
6
8
 
7
- function Button({ _pseudo = 'none', children, className, color = 'primary', condensed, 'data-test-selector': dataTestSelector, icon, iconPosition = 'left', isDisabled, onClick: _onClick, size = 'lg', type = 'button', variant = 'solid', withArrow = false, }) {
9
+ function Button({ _pseudo = 'none', children, className, color = 'primary', condensed, 'data-test-selector': dataTestSelector, icon, iconPosition = 'left', isDisabled, isLoading = false, onClick: _onClick, size = 'lg', type = 'button', variant = 'solid', withArrow = false, }) {
8
10
  const showIconOnLeft = icon && iconPosition === 'left';
9
11
  const showIconOnRight = icon && iconPosition === 'right';
10
12
  const onClick = (e) => {
@@ -15,7 +17,10 @@ function Button({ _pseudo = 'none', children, className, color = 'primary', cond
15
17
  return;
16
18
  e.preventDefault();
17
19
  };
18
- return (jsxs("button", { className: clsx({ [buttonStyles.condensed]: condensed }, { [buttonStyles.icon]: icon }, buttonStyles.button, buttonStyles[variant], buttonStyles[size], buttonStyles[color], buttonStyles[_pseudo], className), "data-disabled": isDisabled ? true : undefined, "data-test-selector": dataTestSelector, disabled: isDisabled, onClick: onClick, type: type, children: [jsx(Fragment, { children: showIconOnLeft && jsx("span", { className: buttonStyles.icon, children: icon }) }), children, withArrow && (jsx(GlyphsArrowBoldCapsRightIcon, { className: buttonStyles['right-arrow-icon'] })), showIconOnRight && jsx("span", { className: buttonStyles.icon, children: icon })] }));
20
+ return (jsx("button", { className: clsx({ [buttonStyles.condensed]: condensed }, { [buttonStyles.icon]: icon }, { [buttonStyles['loading-uninformative']]: isLoading === true }, {
21
+ [buttonStyles['loading-informative']]: isLoading &&
22
+ (typeof isLoading === 'string' || isValidElement(isLoading)),
23
+ }, buttonStyles.button, buttonStyles[variant], buttonStyles[size], buttonStyles[color], buttonStyles[_pseudo], className), "data-disabled": isDisabled ? true : undefined, "data-test-selector": dataTestSelector, disabled: isDisabled, onClick: onClick, type: type, children: jsxs(Fragment, { children: [showIconOnLeft && jsx("span", { className: buttonStyles.icon, children: icon }), jsx("span", { className: buttonStyles.children, children: isLoading ? (isLoading === true ? children : isLoading) : children }), withArrow && (jsx(GlyphsArrowBoldCapsRightIcon, { className: buttonStyles['right-arrow-icon'] })), showIconOnRight && jsx("span", { className: buttonStyles.icon, children: icon }), isLoading && (jsx(ProgressCircle, { className: buttonStyles.spinner, size: "sm", variant: color === 'primary' ? 'white' : 'gray' }))] }) }));
19
24
  }
20
25
 
21
26
  export { Button };
@@ -1,3 +1,3 @@
1
- var buttonStyles = {"button":"button-module-V4meK","icon":"button-module-XaNWz","sm":"button-module-Pbwz7","md":"button-module-GVTEW","condensed":"button-module-GKHQc","lg":"button-module-nyNY8","primary":"button-module-tmyk8","outline":"button-module-vq9GI","solid":"button-module-AjvlY","hover":"button-module-YzPAr","focus":"button-module--xzsY","active":"button-module-XMFzj","ghost":"button-module-f4UVe","right-arrow-icon":"button-module-ydQAo","secondary":"button-module--1bCH"};
1
+ var buttonStyles = {"button":"button-module-V4meK","icon":"button-module-XaNWz","loading-uninformative":"button-module-LwuW2","loading-informative":"button-module-U5IxM","spinner":"button-module-13ndF","children":"button-module-vqRq-","primary":"button-module-tmyk8","secondary":"button-module--1bCH","sm":"button-module-Pbwz7","md":"button-module-GVTEW","condensed":"button-module-GKHQc","lg":"button-module-nyNY8","outline":"button-module-vq9GI","solid":"button-module-AjvlY","hover":"button-module-YzPAr","focus":"button-module--xzsY","active":"button-module-XMFzj","ghost":"button-module-f4UVe","right-arrow-icon":"button-module-ydQAo"};
2
2
 
3
3
  export { buttonStyles as default };
package/dist/exports.d.ts CHANGED
@@ -217,6 +217,10 @@ export * from './shared/api/storefront/services/website-service';
217
217
  export * from './shared/api/storefront/services/wishlist-service';
218
218
  export * from './shared/feature-flags/use-feature-flags';
219
219
  export * from './shared/fetch/request';
220
+ export * from './shared/ga/data-layer';
221
+ export * from './shared/ga/google-analytics-provider';
222
+ export * from './shared/ga/types';
223
+ export * from './shared/ga/use-data-layer';
220
224
  export * from './shared/hooks/use-breakpoint';
221
225
  export * from './shared/hooks/use-cookie';
222
226
  export * from './shared/hooks/use-debounced-callback';
@@ -4,6 +4,7 @@ export type NumberFieldSize = 'md' | 'lg';
4
4
  interface NumberFieldProps {
5
5
  autoFocus?: boolean;
6
6
  autoGrow?: boolean;
7
+ 'data-test-selector'?: string;
7
8
  defaultValue?: number;
8
9
  formatOptions?: Intl.NumberFormatOptions;
9
10
  isDisabled?: boolean;
@@ -29,5 +30,5 @@ interface NumberFieldProps {
29
30
  * This component is used to create a number field.
30
31
  * This field can also grow when a user types in text.
31
32
  */
32
- export declare function NumberField({ autoFocus, autoGrow, defaultValue, formatOptions, isDisabled, isInvalid, isReadOnly, isRequired, label, maxLength, maxValue, minValue, name, onChange, onInput, onKeyUp, placeholder, showLabel, size, validate, value, withButtons, }: NumberFieldProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function NumberField({ autoFocus, autoGrow, 'data-test-selector': dataTestSelector, defaultValue, formatOptions, isDisabled, isInvalid, isReadOnly, isRequired, label, maxLength, maxValue, minValue, name, onChange, onInput, onKeyUp, placeholder, showLabel, size, validate, value, withButtons, }: NumberFieldProps): import("react/jsx-runtime").JSX.Element;
33
34
  export {};
@@ -15,12 +15,12 @@ import styles from './number-field.module.css.js';
15
15
  * This component is used to create a number field.
16
16
  * This field can also grow when a user types in text.
17
17
  */
18
- function NumberField({ autoFocus, autoGrow, defaultValue, formatOptions = { style: 'decimal', useGrouping: false }, isDisabled, isInvalid, isReadOnly, isRequired, label, maxLength, maxValue, minValue, name, onChange, onInput, onKeyUp, placeholder, showLabel = false, size = 'lg', validate, value, withButtons, }) {
18
+ function NumberField({ autoFocus, autoGrow, 'data-test-selector': dataTestSelector, defaultValue, formatOptions = { style: 'decimal', useGrouping: false }, isDisabled, isInvalid, isReadOnly, isRequired, label, maxLength, maxValue, minValue, name, onChange, onInput, onKeyUp, placeholder, showLabel = false, size = 'lg', validate, value, withButtons, }) {
19
19
  const inputRef = useRef(null);
20
- return (jsxs(NumberField$1, { "aria-label": label, autoFocus: autoFocus, className: clsx(styles.field, styles[size]), defaultValue: defaultValue, formatOptions: formatOptions, isDisabled: isDisabled, isInvalid: isInvalid, isReadOnly: isReadOnly, isRequired: isRequired, maxValue: maxValue, minValue: minValue, name: name, onChange: onChange, onInput: onInput, validate: validate, value: value, children: [showLabel && jsx(Label, { isRequired: isRequired, children: label }), jsxs("div", { className: styles['button-input-container'], children: [withButtons && (jsx(Button, { "data-test-selector": "decrement", isDisabled: isDisabled,
20
+ return (jsxs(NumberField$1, { "aria-label": label, autoFocus: autoFocus, className: clsx(styles.field, styles[size]), "data-test-selector": dataTestSelector, defaultValue: defaultValue, formatOptions: formatOptions, isDisabled: isDisabled, isInvalid: isInvalid, isReadOnly: isReadOnly, isRequired: isRequired, maxValue: maxValue, minValue: minValue, name: name, onChange: onChange, onInput: onInput, validate: validate, value: value, children: [showLabel && jsx(Label, { isRequired: isRequired, children: label }), jsxs("div", { className: styles['button-input-container'], children: [withButtons && (jsx(Button, { "data-test-selector": "decrement", isDisabled: isDisabled,
21
21
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
22
22
  // @ts-expect-error
23
- onClick: e => e.preventDefault(), onPressStart: e => e.target.focus(), slot: "decrement", children: (value || 0) <= 1 ? jsx(StrokeTrashIcon, {}) : jsx(StrokeCollapseIcon, {}) })), jsx(Input, { ref: inputRef, autoGrow: autoGrow, label: label, maxLength: maxLength, onFocus: e => (e.target.selectionStart = e.target.value.length || 0), onKeyUp: e => onKeyUp?.(e), placeholder: placeholder, size: size }), withButtons && (jsx(Button, { "data-test-selector": "increment", isDisabled: isDisabled,
23
+ onClick: e => e.preventDefault(), onPressStart: e => e.target.focus(), slot: "decrement", children: (value || 0) <= 1 ? jsx(StrokeTrashIcon, {}) : jsx(StrokeCollapseIcon, {}) })), jsx(Input, { ref: inputRef, autoGrow: autoGrow, "data-test-selector": "value", label: label, maxLength: maxLength, onFocus: e => (e.target.selectionStart = e.target.value.length || 0), onKeyUp: e => onKeyUp?.(e), placeholder: placeholder, size: size }), withButtons && (jsx(Button, { "data-test-selector": "increment", isDisabled: isDisabled,
24
24
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25
25
  // @ts-expect-error
26
26
  onClick: e => e.preventDefault(), onPressStart: e => e.target.focus(), slot: "increment", children: jsx(StrokeExpandIcon, {}) }))] }), jsx(FieldError, {})] }));
@@ -1,7 +1,10 @@
1
1
  export interface SelectProps<T> {
2
2
  'data-test-selector'?: string;
3
+ defaultSelectedOption?: keyof T;
3
4
  isDisabled?: boolean;
5
+ isRequired?: boolean;
4
6
  label: string;
7
+ name?: string;
5
8
  onChange: (value: keyof T) => void;
6
9
  options: T;
7
10
  placeholder?: string;
@@ -11,4 +14,4 @@ export interface SelectProps<T> {
11
14
  size?: 'sm' | 'md';
12
15
  variant?: 'outline' | 'solid';
13
16
  }
14
- export declare function Select<T extends object>({ 'data-test-selector': dataTestSelector, isDisabled, label, onChange, options, placeholder, selectedOption, showLabel, showPlaceholder, size, variant, }: SelectProps<T>): import("react/jsx-runtime").JSX.Element;
17
+ export declare function Select<T extends Record<string, string>>({ 'data-test-selector': dataTestSelector, defaultSelectedOption, isDisabled, isRequired, label, name, onChange, options, placeholder, selectedOption, showLabel, showPlaceholder, size, variant, }: SelectProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -5,10 +5,11 @@ import { Select as Select$1, Button, SelectValue, Popover, ListBox, Section, Hea
5
5
  import clsx from 'clsx';
6
6
  import { GlyphsChevronsSlimDownIcon } from '../../icons/glyph/glyphs-chevrons-slim-down-icon.js';
7
7
  import { StrokeCheckmarkIcon } from '../../icons/stroke/stroke-checkmark-icon.js';
8
+ import { FieldError } from '../field-error/field-error.js';
8
9
  import { Label } from '../label/label.js';
9
10
  import styles from './select.module.css.js';
10
11
 
11
- function Select({ 'data-test-selector': dataTestSelector, isDisabled = false, label, onChange, options, placeholder, selectedOption, showLabel = true, showPlaceholder = true, size = 'md', variant = 'outline', }) {
12
+ function Select({ 'data-test-selector': dataTestSelector, defaultSelectedOption, isDisabled = false, isRequired = false, label, name, onChange, options, placeholder, selectedOption, showLabel = true, showPlaceholder = true, size = 'md', variant = 'outline', }) {
12
13
  const selectRef = useRef(null);
13
14
  useEffect(() => {
14
15
  const updateWidth = () => {
@@ -23,9 +24,11 @@ function Select({ 'data-test-selector': dataTestSelector, isDisabled = false, la
23
24
  window.addEventListener('resize', updateWidth);
24
25
  return () => window?.removeEventListener('resize', updateWidth);
25
26
  }, []);
26
- return (jsxs(Select$1, { ref: selectRef, "aria-label": label, className: clsx(styles.select, styles[size], styles[variant]), "data-test-selector": dataTestSelector, isDisabled: isDisabled, onSelectionChange: selected => onChange(selected), placeholder: placeholder || label, selectedKey: String(selectedOption), children: [showLabel && jsx(Label, { children: label }), jsxs(Button, { className: styles.button, children: [jsx(SelectValue, { "data-test-selector": "value" }), jsx(GlyphsChevronsSlimDownIcon, { "aria-hidden": "true", className: styles.chevron })] }), jsx(Popover, { ref: ref =>
27
+ return (jsxs(Select$1, { ref: selectRef, "aria-label": label, className: clsx(styles.select, styles[size], styles[variant]), "data-test-selector": dataTestSelector, defaultSelectedKey: defaultSelectedOption === undefined
28
+ ? undefined
29
+ : String(defaultSelectedOption), isDisabled: isDisabled, isRequired: isRequired, name: name, onSelectionChange: selected => onChange(selected), placeholder: placeholder || label, selectedKey: selectedOption === undefined ? undefined : String(selectedOption), children: [showLabel && jsx(Label, { isRequired: isRequired, children: label }), jsxs(Button, { className: styles.button, children: [jsx(SelectValue, { "data-test-selector": "value" }), jsx(GlyphsChevronsSlimDownIcon, { "aria-hidden": "true", className: styles.chevron })] }), jsx(FieldError, {}), jsx(Popover, { ref: ref =>
27
30
  // Workaround for react/react-aria #1513
28
- ref?.addEventListener('touchend', e => e.preventDefault()), className: clsx(styles.popover, styles[variant]), placement: "bottom left", triggerRef: selectRef, children: jsx(ListBox, { className: styles.listbox, "data-test-selector": dataTestSelector ? `${dataTestSelector}_options` : undefined, children: jsxs(Section, { children: [showPlaceholder && (jsx(Header, { className: styles.header, children: placeholder || label })), Object.entries(options).map(([key, value]) => (jsxs(ListBoxItem, { "aria-label": value, className: styles.item, id: key, textValue: value, children: [selectedOption === key && (jsx("span", { slot: "description", children: jsx(StrokeCheckmarkIcon, { className: styles.check }) })), jsx("span", { slot: "label", children: value })] }, key)))] }) }) })] }));
31
+ ref?.addEventListener('touchend', e => e.preventDefault()), className: clsx(styles.popover, styles[variant]), placement: "bottom left", children: jsx(ListBox, { className: styles.listbox, "data-test-selector": dataTestSelector ? `${dataTestSelector}_options` : undefined, children: jsxs(Section, { children: [showPlaceholder && (jsx(Header, { className: styles.header, children: placeholder || label })), Object.entries(options).map(([key, value]) => (jsxs(ListBoxItem, { "aria-label": value, className: styles.item, id: key, textValue: value, children: [selectedOption === key && (jsx("span", { slot: "description", children: jsx(StrokeCheckmarkIcon, { className: styles.check }) })), jsx("span", { slot: "label", children: value })] }, key)))] }) }) })] }));
29
32
  }
30
33
 
31
34
  export { Select };
package/dist/index.js CHANGED
@@ -219,6 +219,10 @@ export { fetchCountries, fetchCountriesLanguages, fetchCountriesWithLanguages, f
219
219
  export { WishListNameAlreadyExistsError, addWishListItemToWishList, createWishList, deleteWishList, deleteWishListItemFromWishList, getWishList, getWishListItemsByWishListId, getWishLists } from './shared/api/storefront/services/wishlist-service.js';
220
220
  export { useFeatureFlags } from './shared/feature-flags/use-feature-flags.js';
221
221
  export { BadRequestError, ForbiddenRequestError, InternalServerErrorRequestError, NotFoundRequestError, RequestError, TimeoutRequestError, UnauthorizedRequestError, UnprocessableContentRequestError, isRequestError, request } from './shared/fetch/request.js';
222
+ export { dataLayer } from './shared/ga/data-layer.js';
223
+ export { GoogleAnalyticsProvider, useGoogleAnalyticsProvider } from './shared/ga/google-analytics-provider.js';
224
+ export { isGAEvent } from './shared/ga/types.js';
225
+ export { useDataLayer } from './shared/ga/use-data-layer.js';
222
226
  export { useBreakpoint } from './shared/hooks/use-breakpoint.js';
223
227
  export { useCookie } from './shared/hooks/use-cookie.js';
224
228
  export { useDebouncedCallback } from './shared/hooks/use-debounced-callback.js';
@@ -1,4 +1,6 @@
1
1
  export interface ProgressCircleProps {
2
2
  className?: string;
3
+ size?: 'sm' | 'lg';
4
+ variant?: 'white' | 'gray';
3
5
  }
4
- export declare function ProgressCircle({ className }: ProgressCircleProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function ProgressCircle({ className, size, variant, }: ProgressCircleProps): import("react/jsx-runtime").JSX.Element;
@@ -1,9 +1,9 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { jsx } from 'react/jsx-runtime';
2
2
  import clsx from 'clsx';
3
3
  import styles from './progress-circle.module.css.js';
4
4
 
5
- function ProgressCircle({ className }) {
6
- return (jsxs("svg", { className: clsx(styles['progress-circle'], className), viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", xmlnsXlink: "http://www.w3.org/1999/xlink", children: [jsx("title", { children: "Spinner" }), jsxs("defs", { children: [jsx("pattern", { height: "100%", id: "pattern-1", patternUnits: "objectBoundingBox", width: "100%", children: jsx("use", { xlinkHref: "#image-2" }) }), jsx("image", { height: "24", id: "image-2", width: "24", xlinkHref: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAGKADAAQAAAABAAAAGAAAAADiNXWtAAAB7UlEQVRIDZ2VSaoCQQyG01rOoBtBr+YZXXsFL+BGEHcqDuCE89SPL480hdpiGYil3an/S1KDUavViq/Xq1wul8RPp5Ocz2dhPB6PiR8OB42RAHO5XE7D4zgW/PF4CM/u97tks1n1TCYjURSpB2hraALglw8BQGXOuQQSKk68y+fzL9lRBQAqAUIlVkUoxBUKBZ3sC1irbrdbUoX/PgTyAqDXmFVBBVRCFfYuGMBk67UvRCUA2FFAfgKUSiWhFTgQA9ESjLVgC7NWwEPNFYtFFUEICJnigAxCFbvd7jcAFVi/gbDoCALmOzCMAzebzUILEFculxVgOwcIDqRSqaiTBDadToMhziYDwBgNRmW0rdFoSLPZ1PeDwUDHbz+iTqejyv5VYHuekbVggavVqkLq9fq32hqnLTJxdgmijPQeYSqs1Woa3G63g8QJ1haZKMJkjDgLjLi/Nbvd7m8ARCxry9x2jyn2ej2ZTCb28+tRK/Azpy3vrN/vy2azeffq4zM9aLTFP1jPM4bDoWbPH06o6WXn9/lZgDMxGo1kuVwG/5uhlfkkTsB4PJb5fC7b7VYPH89C7P9GS5nBLYr4er2W/X6vhy4lNPXxR8BisdDWkD13Ee0KtVQAgqvVSncO2XNlc3WEWirAxLmmgf0K+ANZ6DTlvO5jwwAAAABJRU5ErkJggg==" })] }), jsx("g", { fill: "none", fillRule: "evenodd", id: "Page-1", stroke: "none", strokeWidth: "1", children: jsx("path", { d: "M12,0 C18.627417,0 24,5.372583 24,12 C24,14.7277828 23.0855773,17.3196292 21.4324752,19.4188392 C19.1717866,22.2895997 15.7255176,24 12,24 C11.2636203,24 10.6666667,23.4030463 10.6666667,22.6666667 C10.6666667,21.930287 11.2636203,21.3333333 12,21.3333333 C14.8994206,21.3333333 17.5771113,20.0043823 19.3374325,17.7690188 C20.6234737,16.1359252 21.3333333,14.1238938 21.3333333,12 C21.3333333,6.84534234 17.1546577,2.66666667 12,2.66666667 C6.84534234,2.66666667 2.66666667,6.84534234 2.66666667,12 C2.66666667,14.4546154 3.61656005,16.756214 5.28844833,18.485859 C5.80023235,19.015323 5.78589988,19.8594213 5.25643588,20.3712053 C4.72697187,20.8829893 3.88287357,20.8686569 3.37108955,20.3391928 C1.22326178,18.1171666 0,15.1531945 0,12 C0,5.372583 5.372583,0 12,0 Z", fill: "url(#pattern-1)", fillRule: "nonzero", id: "Spinner" }) })] }));
5
+ function ProgressCircle({ className, size = 'lg', variant = 'white', }) {
6
+ return (jsx("span", { className: clsx(styles['progress-circle'], styles[variant], styles[size], className) }));
7
7
  }
8
8
 
9
9
  export { ProgressCircle };
@@ -1,3 +1,3 @@
1
- var styles = {"progress-circle":"progress-circle-module-4nweP","spin":"progress-circle-module-kCf7K"};
1
+ var styles = {"progress-circle":"progress-circle-module-4nweP","spin":"progress-circle-module-kCf7K","white":"progress-circle-module-SHNFy","gray":"progress-circle-module-TRZWO","sm":"progress-circle-module--Gspu","md":"progress-circle-module-4tRZd"};
2
2
 
3
3
  export { styles as default };
@@ -4,6 +4,7 @@ import { ConnectedProductCard } from '../../cards/product-card/connected-product
4
4
  import { CardCarousel } from '../../carousel/card-carousel/card-carousel.js';
5
5
  import { ProductUSPCarousel } from '../../carousel/usp-carousel/product-usp-carousel.js';
6
6
  import { FormattedMessage } from '../../intl/formatted-message.js';
7
+ import { useDataLayer } from '../../shared/ga/use-data-layer.js';
7
8
  import { Heading } from '../../typography/heading/heading.js';
8
9
  import { ProductDetailsPageLayout } from '../layouts/product-details-page-layout/product-details-page-layout.js';
9
10
  import { Page } from '../page/page.js';
@@ -13,6 +14,12 @@ import { ProductDetailsRecentlyViewed } from './components/product-details-recen
13
14
 
14
15
  function ProductDetails({ data, priceComponent, recentlyViewedComponent, }) {
15
16
  const { breadCrumb, included, product, recentlyViewed, usps } = data;
17
+ useDataLayer({
18
+ event: {
19
+ event: 'view_item',
20
+ },
21
+ product,
22
+ });
16
23
  return (jsx(Page, { breadCrumb: breadCrumb, children: jsx(ProductDetailsPageLayout, { imageGallery: jsx(ProductDetailImages, { images: product.images }), included: included !== undefined &&
17
24
  included.length > 0 && (jsxs(Fragment, { children: [jsx(Heading, { size: "s", tag: "h2", children: jsx(FormattedMessage, { id: "Includes" }) }), jsx(CardCarousel, { allowExpandToGrid: true, hasOverflow: true, cards: included.map(product => (jsx(ConnectedProductCard, { href: product.href, id: product.productId, image: {
18
25
  fit: 'contain',
@@ -120,6 +120,7 @@ export interface ProductDetails {
120
120
  storefrontId: string;
121
121
  tags?: string[];
122
122
  unitListPrice: number;
123
+ unitListPriceDisplay: string;
123
124
  }
124
125
  export interface Usp {
125
126
  heading: string;
@@ -0,0 +1,2 @@
1
+ import type { GADataLayerEvent } from './types';
2
+ export declare const dataLayer: GADataLayerEvent[];
@@ -0,0 +1,13 @@
1
+ const _dataLayer = [];
2
+ _dataLayer.push = (function (_push) {
3
+ return (...items) => {
4
+ // eslint-disable-next-line no-console
5
+ console.log('dataLayer.push', items.length === 1 ? items[0] : items);
6
+ return _push(...items);
7
+ };
8
+ })(_dataLayer.push.bind(_dataLayer));
9
+ const dataLayer =
10
+ // eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
11
+ typeof window === 'undefined' ? _dataLayer : (window.dataLayer ?? _dataLayer);
12
+
13
+ export { dataLayer };
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+ export declare function useGoogleAnalyticsProvider(): {
3
+ customerNumber: string | undefined;
4
+ isFetching: boolean;
5
+ };
6
+ export declare function GoogleAnalyticsProvider({ children }?: {
7
+ children?: ReactNode;
8
+ }): string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | null;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { useFetchSession } from '../api/storefront/hooks/authentication/use-fetch-session.js';
3
+
4
+ function useGoogleAnalyticsProvider() {
5
+ const { data: session, isFetching } = useFetchSession();
6
+ return { customerNumber: session?.billTo?.customerNumber, isFetching };
7
+ }
8
+ function GoogleAnalyticsProvider({ children = null } = { children: null }) {
9
+ const { isFetching } = useGoogleAnalyticsProvider();
10
+ if (isFetching)
11
+ return null;
12
+ return children;
13
+ }
14
+
15
+ export { GoogleAnalyticsProvider, useGoogleAnalyticsProvider };
@@ -0,0 +1,34 @@
1
+ export interface GABaseEvent {
2
+ [key: string]: any;
3
+ event: string;
4
+ eventAction?: string;
5
+ eventCategory?: string;
6
+ eventLabel?: string;
7
+ eventValue?: number;
8
+ }
9
+ export declare function isGAEvent(event: any): event is GABaseEvent;
10
+ export interface GAEcommerceEvent extends GABaseEvent {
11
+ ecommerce?: {
12
+ [key: string]: any;
13
+ currencyCode?: string;
14
+ items?: Array<{
15
+ [key: string]: any;
16
+ item_category?: string;
17
+ item_id?: string;
18
+ item_name?: string;
19
+ price?: number;
20
+ quantity?: number;
21
+ }>;
22
+ };
23
+ }
24
+ export interface GAPageviewEvent extends GABaseEvent {
25
+ page_location?: string;
26
+ page_path?: string;
27
+ page_title?: string;
28
+ }
29
+ export type GADataLayerEvent = GABaseEvent | GAEcommerceEvent | GAPageviewEvent;
30
+ declare global {
31
+ interface Window {
32
+ dataLayer?: GADataLayerEvent[];
33
+ }
34
+ }
@@ -0,0 +1,8 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ function isGAEvent(event) {
3
+ return (typeof event === 'object' &&
4
+ 'event' in event &&
5
+ typeof event.event === 'string');
6
+ }
7
+
8
+ export { isGAEvent };
@@ -0,0 +1,50 @@
1
+ import { CartModel } from '../api/storefront/model/shop.model';
2
+ import { type GADataLayerEvent, type GAEcommerceEvent } from './types';
3
+ interface GAProductModel {
4
+ price?: number;
5
+ productNumber: string;
6
+ productTitle: string;
7
+ unitListPrice: number;
8
+ unitListPriceDisplay: string;
9
+ }
10
+ interface GACartModel {
11
+ cartLines: CartModel['cartLines'];
12
+ currencySymbol: string;
13
+ orderSubTotal: number;
14
+ }
15
+ interface GACartLineModel {
16
+ erpNumber: string;
17
+ pricing: {
18
+ actualPrice: number;
19
+ actualPriceDisplay: string;
20
+ } | null;
21
+ productName: string;
22
+ qtyOrdered: number | null;
23
+ }
24
+ interface CreateEcommerceEventArgsWithCartLine {
25
+ cartLine: GACartLineModel;
26
+ event: GAEcommerceEvent;
27
+ }
28
+ interface CreateEcommerceEventArgsWithCart {
29
+ event: GAEcommerceEvent;
30
+ product: GAProductModel;
31
+ }
32
+ interface CreateEcommerceEventArgsWithProduct {
33
+ cart: GACartModel;
34
+ event: GAEcommerceEvent;
35
+ }
36
+ interface CreateEcommerceEvent {
37
+ (args: CreateEcommerceEventArgsWithCart): GAEcommerceEvent;
38
+ (args: CreateEcommerceEventArgsWithCartLine): GAEcommerceEvent;
39
+ (args: CreateEcommerceEventArgsWithProduct): GAEcommerceEvent;
40
+ }
41
+ interface UseDataLayerReturnValue {
42
+ createEcommerceEvent: CreateEcommerceEvent;
43
+ dataLayer: GADataLayerEvent[];
44
+ }
45
+ export declare function useDataLayer(): UseDataLayerReturnValue;
46
+ export declare function useDataLayer(event: GADataLayerEvent): UseDataLayerReturnValue;
47
+ export declare function useDataLayer(args: CreateEcommerceEventArgsWithProduct): UseDataLayerReturnValue;
48
+ export declare function useDataLayer(args: CreateEcommerceEventArgsWithCartLine): UseDataLayerReturnValue;
49
+ export declare function useDataLayer(args: CreateEcommerceEventArgsWithCart): UseDataLayerReturnValue;
50
+ export {};
@@ -0,0 +1,89 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ import { currencySymbolToISO } from '../model/currency.js';
3
+ import { dataLayer } from './data-layer.js';
4
+ import { useGoogleAnalyticsProvider } from './google-analytics-provider.js';
5
+ import { isGAEvent } from './types.js';
6
+
7
+ function useDataLayer(eventOrArgs) {
8
+ const { customerNumber } = useGoogleAnalyticsProvider();
9
+ const createEcommerceEvent = useCallback(({ cart, cartLine, event, product, }) => {
10
+ if (cart) {
11
+ return {
12
+ ...event,
13
+ ecommerce: {
14
+ ...event.ecommerce,
15
+ currency: event.ecommerce?.currency ?? cart.currencySymbol,
16
+ customer: event.ecommerce?.customer ?? customerNumber,
17
+ items: cart.cartLines?.map(cartLine => ({
18
+ item_id: cartLine.erpNumber,
19
+ item_name: cartLine.shortDescription,
20
+ price: cartLine.pricing?.unitNetPrice,
21
+ quantity: cartLine.qtyOrdered ?? 0,
22
+ })) ?? [],
23
+ },
24
+ value: event.value ?? cart.orderSubTotal ?? 0, // TODO: Why not grandTotal?
25
+ };
26
+ }
27
+ else if (product) {
28
+ return {
29
+ ...event,
30
+ ecommerce: {
31
+ ...event.ecommerce,
32
+ currency: event.ecommerce?.currency ??
33
+ currencySymbolToISO[product.unitListPriceDisplay[0] || ''],
34
+ customer: event.ecommerce?.customer ?? customerNumber,
35
+ items: [
36
+ {
37
+ item_id: product.productNumber,
38
+ item_name: product.productTitle,
39
+ price: product.price || product.unitListPrice,
40
+ },
41
+ ],
42
+ },
43
+ value: 0,
44
+ };
45
+ }
46
+ else if (cartLine) {
47
+ return {
48
+ ...event,
49
+ ecommerce: {
50
+ ...event.ecommerce,
51
+ currency: event.ecommerce?.currency ??
52
+ currencySymbolToISO[cartLine.pricing?.actualPriceDisplay[0] || ''],
53
+ customer: event.ecommerce?.customer ?? customerNumber,
54
+ items: [
55
+ {
56
+ item_id: cartLine.erpNumber,
57
+ item_name: cartLine.productName,
58
+ price: cartLine.pricing?.actualPrice,
59
+ quantity: cartLine.qtyOrdered ?? undefined,
60
+ },
61
+ ],
62
+ },
63
+ value: cartLine.pricing?.actualPrice,
64
+ };
65
+ }
66
+ else {
67
+ return event;
68
+ }
69
+ }, [customerNumber]);
70
+ useEffect(() => {
71
+ if (!eventOrArgs)
72
+ return;
73
+ if (isGAEvent(eventOrArgs)) {
74
+ dataLayer.push(eventOrArgs);
75
+ }
76
+ else if ('cart' in eventOrArgs) {
77
+ dataLayer.push(createEcommerceEvent(eventOrArgs));
78
+ }
79
+ else if ('product' in eventOrArgs) {
80
+ dataLayer.push(createEcommerceEvent(eventOrArgs));
81
+ }
82
+ else if ('cartLine' in eventOrArgs) {
83
+ dataLayer.push(createEcommerceEvent(eventOrArgs));
84
+ }
85
+ }, [eventOrArgs, createEcommerceEvent]);
86
+ return { createEcommerceEvent, dataLayer };
87
+ }
88
+
89
+ export { useDataLayer };
@@ -1,4 +1,5 @@
1
1
  export declare const currencySymbolToISO: {
2
+ readonly '': "EUR";
2
3
  readonly $: "USD";
3
4
  readonly 'Bs.': "VES";
4
5
  readonly Ft: "HUF";
@@ -1,4 +1,5 @@
1
1
  const currencySymbolToISO = {
2
+ '': 'EUR', // Euro
2
3
  $: 'USD', // US Dollar
3
4
  'Bs.': 'VES', // Venezuelan Bolívar Soberano
4
5
  Ft: 'HUF', // Hungarian Forint
package/dist/styles.css CHANGED
@@ -629,6 +629,64 @@
629
629
  margin-right: -16px;
630
630
  }
631
631
 
632
+ .progress-circle-module-4nweP {
633
+ --width: var(--space-24);
634
+
635
+ position: relative;
636
+ display: block;
637
+ width: var(--width);
638
+ height: var(--width);
639
+ border: 4px solid transparent;
640
+ border-radius: var(--width);
641
+ animation: progress-circle-module-kCf7K 0.6s infinite linear;
642
+ -webkit-mask:
643
+ linear-gradient(#000 0 0) padding-box,
644
+ linear-gradient(#000 0 0);
645
+ mask:
646
+ linear-gradient(#000 0 0) padding-box,
647
+ linear-gradient(#000 0 0);
648
+ -webkit-mask-composite: xor;
649
+ mask-composite: exclude;
650
+ }
651
+
652
+ .progress-circle-module-4nweP:where(.progress-circle-module-SHNFy) {
653
+ background: conic-gradient(
654
+ from 0.5turn,
655
+ var(--color-white),
656
+ rgb(0 0 0 / 0%) 0.08turn,
657
+ rgb(0 0 0 / 0%) 0.17turn,
658
+ var(--color-white) 0.5turn,
659
+ var(--color-white)
660
+ )
661
+ border-box;
662
+ }
663
+
664
+ .progress-circle-module-4nweP:where(.progress-circle-module-TRZWO) {
665
+ background: conic-gradient(
666
+ from 0.5turn,
667
+ var(--color-black),
668
+ var(--color-brand-light-gray) 0.08turn,
669
+ var(--color-brand-light-gray) 0.17turn,
670
+ var(--color-black) 0.5turn,
671
+ var(--color-black)
672
+ )
673
+ border-box;
674
+ }
675
+
676
+ .progress-circle-module-4nweP:where(.progress-circle-module--Gspu) {
677
+ --width: var(--space-20);
678
+ }
679
+
680
+ .progress-circle-module-4nweP:where(.progress-circle-module-4tRZd) {
681
+ --width: var(--space-24);
682
+ }
683
+
684
+ @keyframes progress-circle-module-kCf7K {
685
+ to {
686
+ transform: rotate(360deg);
687
+ }
688
+ }
689
+
632
690
  .button-module-V4meK {
633
691
  all: unset;
634
692
  display: inline-flex;
@@ -660,6 +718,48 @@
660
718
  height: 24px;
661
719
  }
662
720
 
721
+ .button-module-V4meK.button-module-LwuW2,
722
+ .button-module-V4meK.button-module-U5IxM {
723
+ cursor: default;
724
+ pointer-events: none;
725
+ }
726
+
727
+ .button-module-V4meK.button-module-LwuW2 > :not(.button-module-13ndF) {
728
+ visibility: hidden;
729
+ }
730
+
731
+ .button-module-V4meK.button-module-LwuW2 .button-module-13ndF {
732
+ position: absolute;
733
+ margin: auto;
734
+ inset: 0;
735
+ }
736
+
737
+ .button-module-V4meK.button-module-U5IxM {
738
+ position: relative;
739
+ display: inline-grid;
740
+ padding-right: var(--space-48);
741
+ gap: var(--space-12);
742
+ }
743
+
744
+ .button-module-V4meK.button-module-U5IxM .button-module-vqRq- {
745
+ overflow: hidden;
746
+ text-overflow: ellipsis;
747
+ white-space: nowrap;
748
+ }
749
+
750
+ .button-module-V4meK.button-module-U5IxM .button-module-13ndF {
751
+ position: absolute;
752
+ right: var(--space-8);
753
+ }
754
+
755
+ .button-module-V4meK.button-module-U5IxM:where(.button-module-tmyk8) {
756
+ color: color-mix(in srgb, var(--color-white) 40%, transparent 60%);
757
+ }
758
+
759
+ .button-module-V4meK.button-module-U5IxM:where(.button-module--1bCH) {
760
+ color: var(--color-brand-medium-gray);
761
+ }
762
+
663
763
  /*********************************************************
664
764
  *
665
765
  * Sizes
@@ -726,11 +826,11 @@
726
826
  transform: translateY(-1px);
727
827
  }
728
828
 
729
- .button-module-tmyk8:where([data-disabled]) {
730
- border-color: var(--color-brand-light-gray);
731
- background-color: var(--color-red-100);
732
- color: var(--color-red-50);
733
- }
829
+ .button-module-tmyk8:where([data-disabled]):where(:not(.button-module-U5IxM, .button-module-LwuW2)) {
830
+ border-color: var(--color-brand-light-gray);
831
+ background-color: var(--color-red-100);
832
+ color: var(--color-red-50);
833
+ }
734
834
 
735
835
  .button-module--1bCH:where(.button-module-vq9GI, .button-module-AjvlY) {
736
836
  background-color: var(--color-brand-white);
@@ -959,7 +1059,7 @@
959
1059
  position: relative;
960
1060
  display: inline-block;
961
1061
  overflow: hidden;
962
- border: none;
1062
+ border: 1px solid transparent;
963
1063
  border-radius: var(--border-radius-8);
964
1064
  background-color: var(--color-brand-light-gray);
965
1065
  }
@@ -1005,16 +1105,19 @@
1005
1105
  box-shadow: var(--shadow-focus-outline);
1006
1106
  outline: 0;
1007
1107
  }
1008
- .input-module-2woJR:has([data-invalid])::after {
1009
- position: absolute;
1010
- right: 0;
1011
- bottom: 0;
1012
- left: 0;
1013
- display: block;
1014
- height: 2px;
1015
- background-color: var(--color-brand-red);
1016
- content: '';
1108
+ .input-module-2woJR:has([data-invalid]) {
1109
+ border-bottom-color: var(--color-brand-red);
1017
1110
  }
1111
+ .input-module-2woJR:has([data-invalid])::after {
1112
+ position: absolute;
1113
+ right: 0;
1114
+ bottom: 0;
1115
+ left: 0;
1116
+ display: block;
1117
+ height: 1px;
1118
+ background-color: var(--color-brand-red);
1119
+ content: '';
1120
+ }
1018
1121
  .input-module-2woJR .input-module-6HwY4 {
1019
1122
  display: inline-grid;
1020
1123
  }
@@ -1168,6 +1271,7 @@
1168
1271
  all: unset;
1169
1272
  position: relative;
1170
1273
  display: flex;
1274
+ overflow: hidden;
1171
1275
  width: 100%;
1172
1276
  height: var(--height);
1173
1277
  box-sizing: border-box;
@@ -1207,7 +1311,24 @@
1207
1311
  opacity: 0.4;
1208
1312
  }
1209
1313
 
1210
- .select-module-ui-Wc.select-module-IRd4F .select-module-aMQIQ {
1314
+ .select-module-ui-Wc[data-invalid] .select-module-aMQIQ {
1315
+ border-bottom-color: var(--color-brand-red);
1316
+ }
1317
+
1318
+ .select-module-ui-Wc[data-invalid] .select-module-aMQIQ::after {
1319
+ position: absolute;
1320
+ right: 0;
1321
+ bottom: 0;
1322
+ left: 0;
1323
+ display: block;
1324
+ height: 1px;
1325
+ background-color: var(--color-brand-red);
1326
+ content: '';
1327
+ }
1328
+
1329
+ /* stylelint-disable-next-line no-descending-specificity */
1330
+
1331
+ .select-module-ui-Wc:where(.select-module-IRd4F) .select-module-aMQIQ {
1211
1332
  border-color: var(--color-brand-light-gray);
1212
1333
  background-color: var(--color-brand-light-gray);
1213
1334
  }
@@ -2345,20 +2466,6 @@
2345
2466
  }
2346
2467
  }
2347
2468
 
2348
- .progress-circle-module-4nweP {
2349
- --width: var(--space-24);
2350
-
2351
- width: var(--width);
2352
- height: var(--width);
2353
- animation: progress-circle-module-kCf7K 0.6s infinite linear;
2354
- }
2355
-
2356
- @keyframes progress-circle-module-kCf7K {
2357
- to {
2358
- transform: rotate(360deg);
2359
- }
2360
- }
2361
-
2362
2469
  .product-overview-grid-module-bzys- {
2363
2470
  --amount-of-columns: 1;
2364
2471
  --column-gap: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonic-equipment/ui",
3
- "version": "123.0.0",
3
+ "version": "124.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "engines": {