@athoscommerce/snap-preact 1.5.1-beta.114 → 1.5.1-beta.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/dist/cjs/components/src/components/Atoms/Dropdown/Dropdown.d.ts.map +1 -1
  2. package/dist/cjs/components/src/components/Atoms/Dropdown/Dropdown.js +15 -3
  3. package/dist/cjs/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.d.ts.map +1 -1
  4. package/dist/cjs/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.js +22 -2
  5. package/dist/cjs/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.d.ts.map +1 -1
  6. package/dist/cjs/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.js +96 -66
  7. package/dist/cjs/components/src/components/Organisms/Chat/Chat.d.ts.map +1 -1
  8. package/dist/cjs/components/src/components/Organisms/Chat/Chat.js +218 -43
  9. package/dist/cjs/components/src/components/Organisms/Chat/Chat.styles.d.ts.map +1 -1
  10. package/dist/cjs/components/src/components/Organisms/Chat/Chat.styles.js +211 -175
  11. package/dist/esm/components/src/components/Atoms/Dropdown/Dropdown.d.ts.map +1 -1
  12. package/dist/esm/components/src/components/Atoms/Dropdown/Dropdown.js +14 -3
  13. package/dist/esm/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.d.ts.map +1 -1
  14. package/dist/esm/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.js +22 -2
  15. package/dist/esm/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.d.ts.map +1 -1
  16. package/dist/esm/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.js +94 -65
  17. package/dist/esm/components/src/components/Organisms/Chat/Chat.d.ts.map +1 -1
  18. package/dist/esm/components/src/components/Organisms/Chat/Chat.js +194 -23
  19. package/dist/esm/components/src/components/Organisms/Chat/Chat.styles.d.ts.map +1 -1
  20. package/dist/esm/components/src/components/Organisms/Chat/Chat.styles.js +67 -5
  21. package/package.json +12 -12
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ import { useEffect } from 'preact/hooks';
2
3
  import { observer } from 'mobx-react-lite';
3
4
  import { css } from '@emotion/react';
4
5
  import classnames from 'classnames';
@@ -14,36 +15,44 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
14
15
  display: 'flex',
15
16
  flexDirection: 'column',
16
17
  gap: '1em',
18
+ paddingBottom: '1em',
19
+ '.ss__chat-product-query-message__header__back': {
20
+ // Overlay banner pinned to the top of the secondary chat's scrollable messages
21
+ // container so it stays visible while the product details scroll underneath.
22
+ // Styling mirrors `.ss__chat__session-feedback` (dark primary bg, primary text,
23
+ // 8px/15px padding, 10px gap, 14px font). Rendered as a direct child of the
24
+ // component root (not the header) so sticky holds across the full scroll range.
25
+ // Negative marginBottom cancels the root's `gap: 1em` so the banner sits flush
26
+ // against the header below it.
27
+ position: 'sticky',
28
+ top: 0,
29
+ zIndex: 2,
30
+ marginBottom: '-1em',
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ gap: '10px',
34
+ padding: '8px 15px',
35
+ background: new Colour(colorPrimary).darkenHex(0.1),
36
+ color: colorPrimaryText,
37
+ fontSize: '14px',
38
+ cursor: 'pointer',
39
+ border: 'none',
40
+ font: 'inherit',
41
+ width: 'auto',
42
+ '&:focus-visible': {
43
+ outline: `2px solid ${colorPrimaryText}`,
44
+ outlineOffset: '-2px',
45
+ },
46
+ svg: {
47
+ fill: colorPrimaryText,
48
+ stroke: colorPrimaryText,
49
+ },
50
+ },
17
51
  '.ss__chat-product-query-message__header': {
18
52
  background: new Colour(colorPrimary).lightenHex(0.05),
19
53
  padding: '1em',
20
54
  display: 'flex',
21
55
  flexDirection: 'column',
22
- '.ss__chat-product-query-message__header__back': {
23
- display: 'flex',
24
- alignItems: 'center',
25
- gap: '0.35em',
26
- color: colorPrimaryText,
27
- cursor: 'pointer',
28
- alignSelf: 'flex-start',
29
- padding: '0.5em 0',
30
- fontSize: '0.85em',
31
- opacity: 0.85,
32
- background: 'transparent',
33
- border: 'none',
34
- font: 'inherit',
35
- '&:hover': {
36
- opacity: 1,
37
- },
38
- '&:focus-visible': {
39
- outline: `2px solid ${colorPrimaryText}`,
40
- outlineOffset: '2px',
41
- },
42
- svg: {
43
- fill: colorPrimaryText,
44
- stroke: colorPrimaryText,
45
- },
46
- },
47
56
  '.ss__price': {
48
57
  color: colorPrimaryText,
49
58
  },
@@ -67,7 +76,7 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
67
76
  flex: 1,
68
77
  '.ss__chat-product-query-message__header__product__details__name': {
69
78
  fontWeight: 'bold',
70
- fontSize: '0.95em',
79
+ fontSize: '1.2em',
71
80
  a: {
72
81
  color: colorPrimaryText,
73
82
  textDecoration: 'none',
@@ -76,10 +85,6 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
76
85
  },
77
86
  },
78
87
  },
79
- '.ss__chat-product-query-message__header__product__details__brand': {
80
- fontSize: '0.8em',
81
- color: 'rgba(255, 255, 255, 0.7)',
82
- },
83
88
  '.ss__chat-product-query-message__header__product__details__price': {
84
89
  fontWeight: 'bold',
85
90
  fontSize: '1.1em',
@@ -97,18 +102,6 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
97
102
  justifyContent: 'center',
98
103
  gap: '0.35em',
99
104
  flexShrink: 0,
100
- '.ss__chat-product-query-message__header__product__actions__go-to-product': {
101
- a: {
102
- color: colorPrimaryText,
103
- opacity: 0.85,
104
- fontSize: '0.8em',
105
- textDecoration: 'underline',
106
- cursor: 'pointer',
107
- '&:hover': {
108
- opacity: 1,
109
- },
110
- },
111
- },
112
105
  '.ss__chat-product-query-message__header__product__actions__add-to-cart .ss__button': {
113
106
  flexDirection: 'row-reverse',
114
107
  borderRadius: '0.5em',
@@ -154,6 +147,9 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
154
147
  },
155
148
  '.ss__chat-product-query-message__variants': {
156
149
  padding: '0 2em',
150
+ display: 'flex',
151
+ flexDirection: 'column',
152
+ gap: '1em',
157
153
  '.ss__chat-product-query-message__variants__label': {
158
154
  fontWeight: '600',
159
155
  fontSize: '0.9em',
@@ -196,6 +192,16 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
196
192
  '&.ss__chat-product-query-message__variants__swatch--unavailable': {
197
193
  opacity: 0.4,
198
194
  },
195
+ '&.ss__chat-product-query-message__variants__swatch--text-only': {
196
+ padding: '0.4em 0.75em',
197
+ '&.ss__chat-product-query-message__variants__swatch--selected': {
198
+ padding: '0.3em 0.65em',
199
+ },
200
+ '.ss__chat-product-query-message__variants__swatch__value': {
201
+ fontSize: '1em',
202
+ maxWidth: 'none',
203
+ },
204
+ },
199
205
  '.ss__chat-product-query-message__variants__swatch__image': {
200
206
  width: '48px',
201
207
  height: '48px',
@@ -232,7 +238,7 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
232
238
  borderCollapse: 'separate',
233
239
  borderSpacing: 0,
234
240
  fontSize: '0.9em',
235
- border: `3px solid ${colorPrimary}`,
241
+ border: `1px solid ${colorPrimary}`,
236
242
  borderTop: 'none',
237
243
  borderBottomLeftRadius: '0.5em',
238
244
  borderBottomRightRadius: '0.5em',
@@ -273,16 +279,6 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
273
279
  justifyContent: 'flex-end',
274
280
  },
275
281
  },
276
- '.ss__chat-product-query-message__section__description': {
277
- padding: '0.75em',
278
- fontSize: '0.9em',
279
- color: '#374151',
280
- lineHeight: '1.5',
281
- border: `3px solid ${colorPrimary}`,
282
- borderTop: 'none',
283
- borderBottomLeftRadius: '0.5em',
284
- borderBottomRightRadius: '0.5em',
285
- },
286
282
  '.ss__chat-product-query-message__section__features': {
287
283
  margin: 0,
288
284
  padding: '0.5em 1.25em',
@@ -293,6 +289,12 @@ const defaultStyles = ({ primaryColor, primaryColorText }) => {
293
289
  },
294
290
  },
295
291
  },
292
+ '.ss__chat-product-query-message__description': {
293
+ padding: '0 1em',
294
+ fontSize: '0.9em',
295
+ color: '#374151',
296
+ lineHeight: '1.5',
297
+ },
296
298
  });
297
299
  };
298
300
  // fields that have dedicated rendering in the product card; skip them in the info table
@@ -428,6 +430,7 @@ export const ChatProductQueryMessage = observer((properties) => {
428
430
  const chatMessages = controller?.store.currentChat?.chat || [];
429
431
  const sourceMessage = chatItem.sourceMessageId ? chatMessages.find((m) => m.id === chatItem.sourceMessageId) : null;
430
432
  const cameFromInspiration = sourceMessage?.messageType === 'inspirationResult';
433
+ const cameFromComparison = sourceMessage?.messageType === 'productComparison';
431
434
  if (!product) {
432
435
  return (_jsx(CacheProvider, { children: _jsx("div", { className: classnames('ss__chat-product-query-message', className, internalClassName), ...styling, children: _jsx("div", { className: classnames('ss__chat-product-query-message__header'), children: _jsx("div", { className: classnames('ss__chat-product-query-message__header__product'), children: _jsx("div", { className: classnames('ss__chat-product-query-message__header__product__details'), children: _jsx("div", { className: classnames('ss__chat-product-query-message__header__product__details__name'), children: productQuickviewError || 'Loading product details...' }) }) }) }) }) }));
433
436
  }
@@ -435,6 +438,24 @@ export const ChatProductQueryMessage = observer((properties) => {
435
438
  const displayedCore = displayed.mappings?.core || {};
436
439
  const variants = product.variants;
437
440
  const selections = variants?.selections || [];
441
+ // Ensure every selection has an initial value picked. Without this, entering
442
+ // the quickview from inspiration leaves all selections empty, and the first
443
+ // variant click can't narrow `refineSelections` to a single variant — so the
444
+ // active variant (and hero image) doesn't update until a second selection is
445
+ // made. Pre-selecting the first available value for each selection on mount
446
+ // matches the carousel behavior. Single-value selections also get hidden from
447
+ // the UI (handled by `visibleSelections` below).
448
+ useEffect(() => {
449
+ selections.forEach((selection) => {
450
+ if (selection.selected)
451
+ return;
452
+ const target = selection.values.find((v) => v.available) || selection.values[0];
453
+ if (target) {
454
+ selection.select(target.value);
455
+ }
456
+ });
457
+ }, [selections]);
458
+ const visibleSelections = selections.filter((selection) => selection.values.length > 1);
438
459
  const displayedData = {
439
460
  mappings: displayed.mappings,
440
461
  attributes: displayed.attributes || {},
@@ -447,18 +468,26 @@ export const ChatProductQueryMessage = observer((properties) => {
447
468
  controller?.store.currentChat?.popProductQueryMessage(chatItem.sourceMessageId);
448
469
  controller?.store.clearProductQuickview();
449
470
  };
450
- return (_jsx(CacheProvider, { children: _jsxs("div", { className: classnames('ss__chat-product-query-message', className, internalClassName), ...styling, children: [_jsxs("div", { className: classnames('ss__chat-product-query-message__header'), children: [cameFromInspiration && (_jsxs("button", { type: "button", className: classnames('ss__chat-product-query-message__header__back'), "aria-label": "Back to inspiration", onClick: handleBack, children: [_jsx(Icon, { icon: "angle-left", size: "14px" }), _jsx("span", { children: "Back to inspiration" })] })), _jsxs("div", { className: classnames('ss__chat-product-query-message__header__product'), children: [(displayedCore.imageUrl || displayedCore.parentImageUrl) && (_jsx(Image, { className: classnames('ss__chat-product-query-message__header__product__image'), src: (displayedCore.imageUrl || displayedCore.parentImageUrl), alt: displayedCore.name || '' })), _jsxs("div", { className: classnames('ss__chat-product-query-message__header__product__details'), children: [displayedCore.name && (_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__details__name'), children: displayedCore.url ? (_jsx("a", { href: displayedCore.url, onClick: (e) => controller?.track.product.click(e, product), target: "_blank", rel: "noopener noreferrer", children: displayedCore.name })) : (displayedCore.name) })), displayedCore.brand && (_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__details__brand'), children: displayedCore.brand })), displayedCore.price != null && (_jsxs("div", { className: classnames('ss__chat-product-query-message__header__product__details__price'), children: [displayedCore.msrp != null && Number(displayedCore.msrp) > Number(displayedCore.price) && (_jsx(Price, { value: displayedCore.msrp, lineThrough: true })), _jsx(Price, { value: displayedCore.price })] }))] }), _jsxs("div", { className: classnames('ss__chat-product-query-message__header__product__actions'), children: [displayedCore.url && (_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__go-to-product'), children: _jsx("a", { href: displayedCore.url, onClick: (e) => controller?.track.product.click(e, product), target: "_blank", rel: "noopener noreferrer", children: "Go to product" }) })), _jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__add-to-cart'), children: _jsx(Button, { icon: "cart", content: 'Add to Cart', onClick: () => {
451
- controller?.track.product.addToCart(product);
452
- controller?.addToCart(product);
453
- } }) }), controller?.store.features.similarProducts.enabled && (_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__show-similar'), children: _jsx(Button, { icon: "search-thin", content: 'Show Similar', onClick: () => controller?.productSimilar(sourceProduct) }) })), _jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__discuss-product'), children: _jsx(Button, { icon: "chat", content: 'Discuss Product', onClick: () => controller?.productQuery(sourceProduct) }) })] })] })] }), selections.length > 0 && (_jsx("div", { className: classnames('ss__chat-product-query-message__variants'), role: "group", "aria-label": "Variant selection", children: selections.map((selection) => (_jsxs("div", { children: [_jsxs("div", { className: classnames('ss__chat-product-query-message__variants__label'), id: `ss__chat-pq-variant-${selection.field}`, children: [formatLabel(selection.field), " (", selection.values.length, ")"] }), _jsx("div", { className: classnames('ss__chat-product-query-message__variants__swatches'), role: "radiogroup", "aria-labelledby": `ss__chat-pq-variant-${selection.field}`, children: selection.values.map((selectionValue) => {
454
- const isUnavailable = !selectionValue.available;
455
- const isSelected = selection.selected?.value === selectionValue.value;
456
- return (_jsxs("button", { type: "button", className: classnames('ss__chat-product-query-message__variants__swatch', {
457
- 'ss__chat-product-query-message__variants__swatch--selected': isSelected,
458
- 'ss__chat-product-query-message__variants__swatch--unavailable': isUnavailable,
459
- }), title: selectionValue.value, role: "radio", "aria-checked": isSelected, "aria-disabled": isUnavailable, "aria-label": `${formatLabel(selection.field)}: ${selectionValue.value}${isUnavailable ? ' (unavailable)' : ''}`, onClick: () => selection.select(selectionValue.value), children: [selectionValue.thumbnailImageUrl ? (_jsx(Image, { className: classnames('ss__chat-product-query-message__variants__swatch__image'), src: selectionValue.thumbnailImageUrl, alt: selectionValue.value })) : null, _jsx("span", { className: classnames('ss__chat-product-query-message__variants__swatch__value'), children: selectionValue.value })] }, selectionValue.value));
460
- }) })] }, selection.field))) })), infoRows.length > 0 && (_jsxs("div", { className: classnames('ss__chat-product-query-message__section'), children: [_jsx("div", { className: classnames('ss__chat-product-query-message__section__title'), children: "Product Information" }), _jsx("table", { className: classnames('ss__chat-product-query-message__section__table'), "aria-label": "Product information", children: _jsx("tbody", { children: infoRows.map((row) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), _jsx("td", { children: row.rawKey === 'availability' ? (_jsx("span", { className: classnames({
471
+ return (_jsx(CacheProvider, { children: _jsxs("div", { className: classnames('ss__chat-product-query-message', className, internalClassName), ...styling, children: [(cameFromInspiration || cameFromComparison) && (_jsxs("button", { type: "button", className: classnames('ss__chat-product-query-message__header__back'), "aria-label": cameFromComparison ? 'Back to comparison' : 'Back to inspiration', onClick: handleBack, children: [_jsx(Icon, { icon: "angle-left", size: "14px" }), _jsx("span", { children: cameFromComparison ? 'Back to comparison' : 'Back to inspiration' })] })), _jsx("div", { className: classnames('ss__chat-product-query-message__header'), children: _jsxs("div", { className: classnames('ss__chat-product-query-message__header__product'), children: [(displayedCore.imageUrl || displayedCore.parentImageUrl) && (_jsx(Image, { className: classnames('ss__chat-product-query-message__header__product__image'), src: (displayedCore.imageUrl || displayedCore.parentImageUrl), alt: displayedCore.name || '' })), _jsxs("div", { className: classnames('ss__chat-product-query-message__header__product__details'), children: [displayedCore.name && (_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__details__name'), children: displayedCore.url ? (_jsx("a", { href: displayedCore.url, onClick: (e) => controller?.track.product.click(e, product), target: "_blank", rel: "noopener noreferrer", children: displayedCore.name })) : (displayedCore.name) })), displayedCore.price != null && (_jsxs("div", { className: classnames('ss__chat-product-query-message__header__product__details__price'), children: [displayedCore.msrp != null && Number(displayedCore.msrp) > Number(displayedCore.price) && (_jsx(Price, { value: displayedCore.msrp, lineThrough: true })), _jsx(Price, { value: displayedCore.price })] }))] }), _jsxs("div", { className: classnames('ss__chat-product-query-message__header__product__actions'), children: [_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__add-to-cart'), children: _jsx(Button, { icon: "cart", content: 'Add to Cart', onClick: () => {
472
+ controller?.track.product.addToCart(product);
473
+ controller?.addToCart(product);
474
+ } }) }), controller?.store.features.similarProducts.enabled && (_jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__show-similar'), children: _jsx(Button, { icon: "search-thin", content: 'Show Similar', onClick: () => controller?.productSimilar(sourceProduct) }) })), _jsx("div", { className: classnames('ss__chat-product-query-message__header__product__actions__discuss-product'), children: _jsx(Button, { icon: "chat", content: 'Discuss Product', onClick: () => controller?.productQuery(sourceProduct) }) })] })] }) }), visibleSelections.length > 0 && (_jsx("div", { className: classnames('ss__chat-product-query-message__variants'), role: "group", "aria-label": "Variant selection", children: visibleSelections.map((selection) => {
475
+ // If every value in this selection shares the same thumbnail (or none have
476
+ // one), the image carries no signal — show text only.
477
+ const firstThumb = selection.values[0]?.thumbnailImageUrl;
478
+ const allSameThumb = selection.values.every((v) => v.thumbnailImageUrl === firstThumb);
479
+ const showThumbs = !allSameThumb;
480
+ return (_jsxs("div", { children: [_jsxs("div", { className: classnames('ss__chat-product-query-message__variants__label'), id: `ss__chat-pq-variant-${selection.field}`, children: [formatLabel(selection.field), " (", selection.values.length, ")"] }), _jsx("div", { className: classnames('ss__chat-product-query-message__variants__swatches'), role: "radiogroup", "aria-labelledby": `ss__chat-pq-variant-${selection.field}`, children: selection.values.map((selectionValue) => {
481
+ const isUnavailable = !selectionValue.available;
482
+ const isSelected = selection.selected?.value === selectionValue.value;
483
+ return (_jsxs("button", { type: "button", className: classnames('ss__chat-product-query-message__variants__swatch', {
484
+ 'ss__chat-product-query-message__variants__swatch--selected': isSelected,
485
+ 'ss__chat-product-query-message__variants__swatch--unavailable': isUnavailable,
486
+ 'ss__chat-product-query-message__variants__swatch--text-only': !showThumbs,
487
+ }), title: selectionValue.value, role: "radio", "aria-checked": isSelected, "aria-disabled": isUnavailable, "aria-label": `${formatLabel(selection.field)}: ${selectionValue.value}${isUnavailable ? ' (unavailable)' : ''}`, onClick: () => selection.select(selectionValue.value), children: [showThumbs && selectionValue.thumbnailImageUrl ? (_jsx(Image, { className: classnames('ss__chat-product-query-message__variants__swatch__image'), src: selectionValue.thumbnailImageUrl, alt: selectionValue.value })) : null, _jsx("span", { className: classnames('ss__chat-product-query-message__variants__swatch__value'), children: selectionValue.value })] }, selectionValue.value));
488
+ }) })] }, selection.field));
489
+ }) })), descriptionRow && _jsx("div", { className: classnames('ss__chat-product-query-message__description'), children: descriptionRow.value }), infoRows.length > 0 && (_jsxs("div", { className: classnames('ss__chat-product-query-message__section'), children: [_jsx("div", { className: classnames('ss__chat-product-query-message__section__title'), children: "Product Information" }), _jsx("table", { className: classnames('ss__chat-product-query-message__section__table'), "aria-label": "Product information", children: _jsx("tbody", { children: infoRows.map((row) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), _jsx("td", { children: row.rawKey === 'availability' ? (_jsx("span", { className: classnames({
461
490
  'ss__chat-product-query-message__section__table__availability--in-stock': row.value === 'In Stock',
462
491
  'ss__chat-product-query-message__section__table__availability--out-of-stock': row.value === 'Out of Stock',
463
- }), children: row.value === 'In Stock' ? `\u2713 ${row.value}` : row.value })) : row.rawKey.toLowerCase() === 'price' ? (_jsxs("span", { className: "ss__chat-product-query-message__section__table__price", children: [displayedCore.msrp != null && Number(displayedCore.msrp) > Number(row.value) && (_jsx(Price, { value: Number(displayedCore.msrp), lineThrough: true })), _jsx(Price, { value: Number(row.value) })] })) : (row.value) })] }, row.key))) }) })] })), descriptionRow && (_jsxs("div", { className: classnames('ss__chat-product-query-message__section'), children: [_jsx("div", { className: classnames('ss__chat-product-query-message__section__title'), children: "Description" }), _jsx("div", { className: classnames('ss__chat-product-query-message__section__description'), children: descriptionRow.value })] })), features.length > 0 && (_jsxs("div", { className: classnames('ss__chat-product-query-message__section'), children: [_jsx("div", { className: classnames('ss__chat-product-query-message__section__title'), children: "Features" }), _jsx("ul", { className: classnames('ss__chat-product-query-message__section__features'), children: features.map((feature, idx) => (_jsx("li", { children: feature }, idx))) })] }))] }) }));
492
+ }), children: row.value === 'In Stock' ? `\u2713 ${row.value}` : row.value })) : row.rawKey.toLowerCase() === 'price' ? (_jsxs("span", { className: "ss__chat-product-query-message__section__table__price", children: [displayedCore.msrp != null && Number(displayedCore.msrp) > Number(row.value) && (_jsx(Price, { value: Number(displayedCore.msrp), lineThrough: true })), _jsx(Price, { value: Number(row.value) })] })) : (row.value) })] }, row.key))) }) })] })), features.length > 0 && (_jsxs("div", { className: classnames('ss__chat-product-query-message__section'), children: [_jsx("div", { className: classnames('ss__chat-product-query-message__section__title'), children: "Features" }), _jsx("ul", { className: classnames('ss__chat-product-query-message__section__features'), children: features.map((feature, idx) => (_jsx("li", { children: feature }, idx))) })] }))] }) }));
464
493
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../../../../../components/src/components/Organisms/Chat/Chat.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AA6BrE,eAAO,MAAM,YAAY,gBAAyB,iBAAiB,KAAG,GAAG,CAAC,OAAO;;CAs7C/E,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC/B,UAAU,EAAE,cAAc,CAAC;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CACzB,GAAG,uBAAuB,GAC1B,cAAc,CAAC,iBAAiB,CAAC,CAAC;AAEnC,MAAM,MAAM,uBAAuB,GAAG;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,+BAA+B,CAAC,EAAE,OAAO,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,QAAQ;CAExB"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../../../../../components/src/components/Organisms/Chat/Chat.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AA6BrE,eAAO,MAAM,YAAY,gBAAyB,iBAAiB,KAAG,GAAG,CAAC,OAAO;;CAkmD/E,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC/B,UAAU,EAAE,cAAc,CAAC;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CACzB,GAAG,uBAAuB,GAC1B,cAAc,CAAC,iBAAiB,CAAC,CAAC;AAEnC,MAAM,MAAM,uBAAuB,GAAG;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,+BAA+B,CAAC,EAAE,OAAO,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,QAAQ;CAExB"}
@@ -235,6 +235,116 @@ export const ChatOrganism = observer((properties) => {
235
235
  observer.observe(el);
236
236
  return () => observer.disconnect();
237
237
  }, [store.open, store.currentChat?.isExpired]);
238
+ // Prevent the background page from scrolling while the chat is open on touch
239
+ // devices. iOS Safari (real iPad/iPhone) rubber-band-scrolls the body when a
240
+ // touchmove inside `.ss__chat__messages` reaches the top/bottom — and
241
+ // `overscroll-behavior: contain` is ignored there.
242
+ //
243
+ // We don't touch body styles (locking via `position: fixed` on `<body>` leaves
244
+ // iPad Safari in a stuck viewport state where the page can't scroll after the
245
+ // lock is released). Instead we install a `touchmove` handler that:
246
+ // - allows the touchmove if it's inside a scrollable region of the chat
247
+ // and that region isn't at its scroll edge in the swipe direction;
248
+ // - calls `preventDefault()` otherwise — which is what blocks both
249
+ // background scrolling and iOS rubber-band overscroll.
250
+ // Cleanup just removes the listeners, so nothing can leak into the page.
251
+ useEffect(() => {
252
+ if (!store.open)
253
+ return;
254
+ if (typeof document === 'undefined')
255
+ return;
256
+ let startX = 0;
257
+ let startY = 0;
258
+ let scrollEl = null;
259
+ // Walk up from the touch target to find the first ancestor inside the chat
260
+ // that is actually scrollable in either axis. Stop at the chat root so we
261
+ // never consider elements outside the chat.
262
+ const findScrollableInChat = (start) => {
263
+ const chatRoot = chatRef.current;
264
+ if (!chatRoot)
265
+ return null;
266
+ let el = start;
267
+ while (el && chatRoot.contains(el)) {
268
+ const style = getComputedStyle(el);
269
+ const scrollableY = (style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight;
270
+ const scrollableX = (style.overflowX === 'auto' || style.overflowX === 'scroll') && el.scrollWidth > el.clientWidth;
271
+ if (scrollableY || scrollableX) {
272
+ return el;
273
+ }
274
+ if (el === chatRoot)
275
+ break;
276
+ el = el.parentElement;
277
+ }
278
+ return null;
279
+ };
280
+ const onTouchStart = (e) => {
281
+ if (!e.touches[0])
282
+ return;
283
+ startX = e.touches[0].clientX;
284
+ startY = e.touches[0].clientY;
285
+ scrollEl = findScrollableInChat(e.target);
286
+ };
287
+ const onTouchMove = (e) => {
288
+ const target = e.target;
289
+ const chatRoot = chatRef.current;
290
+ const inChat = !!(chatRoot && target && chatRoot.contains(target));
291
+ // Touches outside the chat shouldn't scroll the background.
292
+ if (!inChat) {
293
+ if (e.cancelable)
294
+ e.preventDefault();
295
+ return;
296
+ }
297
+ // Inside the chat but no scrollable container — block to prevent body scroll.
298
+ if (!scrollEl) {
299
+ if (e.cancelable)
300
+ e.preventDefault();
301
+ return;
302
+ }
303
+ // Inside a scrollable container — only block when at the edge in the
304
+ // swipe direction, so internal scrolling continues to work. Detect the
305
+ // dominant axis so horizontally-scrollable rows (e.g. the facets row)
306
+ // also get to handle their own touch gestures.
307
+ const currentX = e.touches[0]?.clientX ?? startX;
308
+ const currentY = e.touches[0]?.clientY ?? startY;
309
+ const deltaX = currentX - startX;
310
+ const deltaY = currentY - startY;
311
+ const horizontal = Math.abs(deltaX) > Math.abs(deltaY);
312
+ if (horizontal) {
313
+ const scrollsX = scrollEl.scrollWidth > scrollEl.clientWidth;
314
+ if (!scrollsX) {
315
+ if (e.cancelable)
316
+ e.preventDefault();
317
+ return;
318
+ }
319
+ const atLeft = scrollEl.scrollLeft <= 0;
320
+ const atRight = scrollEl.scrollLeft + scrollEl.clientWidth >= scrollEl.scrollWidth - 1;
321
+ if ((atLeft && deltaX > 0) || (atRight && deltaX < 0)) {
322
+ if (e.cancelable)
323
+ e.preventDefault();
324
+ }
325
+ }
326
+ else {
327
+ const scrollsY = scrollEl.scrollHeight > scrollEl.clientHeight;
328
+ if (!scrollsY) {
329
+ if (e.cancelable)
330
+ e.preventDefault();
331
+ return;
332
+ }
333
+ const atTop = scrollEl.scrollTop <= 0;
334
+ const atBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 1;
335
+ if ((atTop && deltaY > 0) || (atBottom && deltaY < 0)) {
336
+ if (e.cancelable)
337
+ e.preventDefault();
338
+ }
339
+ }
340
+ };
341
+ document.addEventListener('touchstart', onTouchStart, { passive: true });
342
+ document.addEventListener('touchmove', onTouchMove, { passive: false });
343
+ return () => {
344
+ document.removeEventListener('touchstart', onTouchStart);
345
+ document.removeEventListener('touchmove', onTouchMove);
346
+ };
347
+ }, [store.open]);
238
348
  const HistoryButton = (props) => (_jsx(Button, { className: "ss__chat__header__button--history", disabled: props.disabled, icon: {
239
349
  icon: 'history',
240
350
  title: 'Chat History',
@@ -315,9 +425,13 @@ export const ChatOrganism = observer((properties) => {
315
425
  }, children: _jsx("div", { className: "ss__chat__actions__facet__options", children: _jsx("div", { className: "ss__chat__actions__facet__slider", style: { padding: '12px 16px' }, children: _jsx(FacetSlider, { facet: props.facet, onChange: () => {
316
426
  if (!multiselectFacets) {
317
427
  // FacetSlider fires onChange before its urlManager update; defer
318
- // so controller.search() reads the updated filter state and
319
- // promotes the request to productSearch.
320
- queueMicrotask(() => controller.search());
428
+ // so controller.search() reads the updated filter state. Force
429
+ // productSearch with the current filters so a slider tweak doesn't
430
+ // fall through to `general` when hasPendingFacetChanges happens to
431
+ // be false (e.g. the slider returned to the seeded range).
432
+ queueMicrotask(() => controller.search({
433
+ data: { requestType: 'productSearch', searchFilters: controller.store.searchFilters },
434
+ }));
321
435
  }
322
436
  } }) }) }) }) }));
323
437
  };
@@ -357,10 +471,12 @@ export const ChatOrganism = observer((properties) => {
357
471
  return undefined;
358
472
  }
359
473
  }, [requestType]);
474
+ const isWelcomeState = (!store.currentChat?.chat || store.currentChat.chat.length === 0) && !!store.welcomeMessage && !store.currentChat?.isExpired;
360
475
  return (_jsx(CacheProvider, { children: _jsxs(_Fragment, { children: [_jsxs("div", { ref: chatRef, className: classnames('ss__chat', {
361
476
  'ss__chat--open': store.open,
362
477
  'ss__chat--minimized': !store.open,
363
478
  'ss__chat--mobile': isMobile,
479
+ 'ss__chat--welcome': isWelcomeState,
364
480
  }, className, internalClassName), ...styling, children: [!disableBubbleSuggestedQuestions && !store.open && !store.currentChat && store.suggestedQuestions?.length > 0 && (_jsx("div", { className: "ss__chat__suggested-questions", children: store.suggestedQuestions.map((question, index) => (_jsx("div", { className: "ss__chat__suggested-questions__item", onClick: () => {
365
481
  controller.openChat(question);
366
482
  }, children: question }, index))) })), _jsx("button", { type: "button", className: 'ss__chat__bubble', "aria-label": store.open ? 'Close chat' : 'Open chat', "aria-expanded": store.open, onClick: () => controller.handlers.button.click(), children: _jsx(Icon, { icon: "chat", title: "Open Chat" }) }), store.open && shouldShowSideChat && activeMessage ? (_jsxs("div", { ref: secondaryRef, className: classnames('ss__chat__secondary'), role: "region", "aria-label": "Conversation details", style: isMobile
@@ -531,6 +647,9 @@ export const ChatOrganism = observer((properties) => {
531
647
  '&:hover': {
532
648
  background: new Colour(colorPrimary).lightenHex(0.95),
533
649
  },
650
+ '&.ss__chat__actions__facet__option--selected': {
651
+ fontWeight: 'bold',
652
+ },
534
653
  },
535
654
  }),
536
655
  },
@@ -554,12 +673,28 @@ export const ChatOrganism = observer((properties) => {
554
673
  'ss__chat__actions__facet__option__checkbox--checked': isSelected,
555
674
  }), children: isSelected && _jsx(Icon, { icon: "check-thin", size: "10px" }) }), _jsx("span", { className: "ss__chat__actions__facet__option__label", children: rangeOption.label })] }, optionValue));
556
675
  }
557
- return (_jsx(Button, { onClick: () => {
558
- controller.store.addFacet({
559
- key: facet.field,
560
- value: optionValue,
676
+ return (_jsx(Button, { className: classnames({
677
+ 'ss__chat__actions__facet__option--selected': controller.store.isFacetSelected(facet.field, optionValue),
678
+ }), onClick: () => {
679
+ // Non-multiselect: clicking an already-selected value toggles it off.
680
+ // `addFacet` is idempotent against an existing urlManager value, so without
681
+ // the toggle the state wouldn't change, `hasPendingFacetChanges` would stay
682
+ // false, and the params getter would fall through to `general`.
683
+ if (controller.store.isFacetSelected(facet.field, optionValue)) {
684
+ controller.store.removeFacet(facet.field, optionValue);
685
+ }
686
+ else {
687
+ controller.store.addFacet({
688
+ key: facet.field,
689
+ value: optionValue,
690
+ });
691
+ }
692
+ // Force productSearch with the current filter state rather than relying
693
+ // on the params getter's hasPendingFacetChanges promotion — a facet click
694
+ // in the chat is unambiguously a search intent.
695
+ controller.search({
696
+ data: { requestType: 'productSearch', searchFilters: controller.store.searchFilters },
561
697
  });
562
- controller.search();
563
698
  }, children: rangeOption.label }, optionValue));
564
699
  })
565
700
  : facet.values.map((option) => {
@@ -581,12 +716,28 @@ export const ChatOrganism = observer((properties) => {
581
716
  'ss__chat__actions__facet__option__checkbox--checked': isSelected,
582
717
  }), children: isSelected && _jsx(Icon, { icon: "check-thin", size: "10px" }) }), _jsx("span", { className: "ss__chat__actions__facet__option__label", children: valueOption.label })] }, optionValue));
583
718
  }
584
- return (_jsx(Button, { onClick: () => {
585
- controller.store.addFacet({
586
- key: facet.field,
587
- value: optionValue,
719
+ return (_jsx(Button, { className: classnames({
720
+ 'ss__chat__actions__facet__option--selected': controller.store.isFacetSelected(facet.field, optionValue),
721
+ }), onClick: () => {
722
+ // Non-multiselect: clicking an already-selected value toggles it off.
723
+ // `addFacet` is idempotent against an existing urlManager value, so without
724
+ // the toggle the state wouldn't change, `hasPendingFacetChanges` would stay
725
+ // false, and the params getter would fall through to `general`.
726
+ if (controller.store.isFacetSelected(facet.field, optionValue)) {
727
+ controller.store.removeFacet(facet.field, optionValue);
728
+ }
729
+ else {
730
+ controller.store.addFacet({
731
+ key: facet.field,
732
+ value: optionValue,
733
+ });
734
+ }
735
+ // Force productSearch with the current filter state rather than relying
736
+ // on the params getter's hasPendingFacetChanges promotion — a facet click
737
+ // in the chat is unambiguously a search intent.
738
+ controller.search({
739
+ data: { requestType: 'productSearch', searchFilters: controller.store.searchFilters },
588
740
  });
589
- controller.search();
590
741
  }, children: valueOption.label }, optionValue));
591
742
  }) }) }, facet.field) }, idx));
592
743
  }) })] }) })), (() => {
@@ -611,12 +762,11 @@ export const ChatOrganism = observer((properties) => {
611
762
  id: result?.id,
612
763
  name: d?.mappings?.core?.name || '',
613
764
  imageUrl: d?.mappings?.core?.thumbnailImageUrl || d?.mappings?.core?.imageUrl || d?.mappings?.core?.parentImageUrl || '',
614
- onClick: isMobile
615
- ? () => {
616
- controller.productQuickView(result);
765
+ onClick: () => {
766
+ controller.productQuickView(result);
767
+ if (isMobile)
617
768
  setMobileProductInfoOpen(true);
618
- }
619
- : undefined,
769
+ },
620
770
  };
621
771
  })
622
772
  : showCommittedComparisons
@@ -626,12 +776,11 @@ export const ChatOrganism = observer((properties) => {
626
776
  id: comparisonItem.result?.id,
627
777
  name: d?.mappings?.core?.name || '',
628
778
  imageUrl: d?.mappings?.core?.thumbnailImageUrl || d?.mappings?.core?.imageUrl || d?.mappings?.core?.parentImageUrl || '',
629
- onClick: isMobile
630
- ? () => {
631
- controller.productQuickView(comparisonItem.result);
779
+ onClick: () => {
780
+ controller.productQuickView(comparisonItem.result);
781
+ if (isMobile)
632
782
  setMobileProductInfoOpen(true);
633
- }
634
- : undefined,
783
+ },
635
784
  };
636
785
  })
637
786
  : [];
@@ -688,9 +837,31 @@ export const ChatOrganism = observer((properties) => {
688
837
  store.currentChat?.dismissTopicDrift();
689
838
  return;
690
839
  }
840
+ // Capture image attachments from the current chat before handleTopicDrift /
841
+ // createChat tear down the conversation — an imageSearch that drifted needs
842
+ // the same image attached to the carried-over message in the new session,
843
+ // otherwise params would fall back to a `general` request without the image.
844
+ const previousImageAttachments = (store.currentChat?.attachments.attached || [])
845
+ .filter((item) => item.type === 'image' && item.state !== 'error')
846
+ .map((item) => {
847
+ const img = item;
848
+ return {
849
+ type: 'image',
850
+ base64: img.base64,
851
+ fileName: img.fileName,
852
+ imageId: img.imageId,
853
+ imageUrl: img.imageUrl,
854
+ thumbnailUrl: img.thumbnailUrl,
855
+ // Already uploaded — skip the 'loading' default state.
856
+ state: 'attached',
857
+ };
858
+ });
691
859
  const messageText = store.currentChat?.handleTopicDrift();
692
860
  if (messageText) {
693
861
  controller.store.createChat();
862
+ previousImageAttachments.forEach((cfg) => {
863
+ controller.store.currentChat?.attachments.add(cfg);
864
+ });
694
865
  controller.search({ data: { message: messageText } });
695
866
  }
696
867
  }, children: buttonText })), _jsx("span", { onClick: () => {
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.styles.d.ts","sourceRoot":"","sources":["../../../../../../../components/src/components/Organisms/Chat/Chat.styles.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,cAAc,CA6mCzD,CAAC"}
1
+ {"version":3,"file":"Chat.styles.d.ts","sourceRoot":"","sources":["../../../../../../../components/src/components/Organisms/Chat/Chat.styles.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,cAAc,CA2qCzD,CAAC"}