@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.
- package/dist/cjs/components/src/components/Atoms/Dropdown/Dropdown.d.ts.map +1 -1
- package/dist/cjs/components/src/components/Atoms/Dropdown/Dropdown.js +15 -3
- package/dist/cjs/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.d.ts.map +1 -1
- package/dist/cjs/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.js +22 -2
- package/dist/cjs/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.d.ts.map +1 -1
- package/dist/cjs/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.js +96 -66
- package/dist/cjs/components/src/components/Organisms/Chat/Chat.d.ts.map +1 -1
- package/dist/cjs/components/src/components/Organisms/Chat/Chat.js +218 -43
- package/dist/cjs/components/src/components/Organisms/Chat/Chat.styles.d.ts.map +1 -1
- package/dist/cjs/components/src/components/Organisms/Chat/Chat.styles.js +211 -175
- package/dist/esm/components/src/components/Atoms/Dropdown/Dropdown.d.ts.map +1 -1
- package/dist/esm/components/src/components/Atoms/Dropdown/Dropdown.js +14 -3
- package/dist/esm/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.d.ts.map +1 -1
- package/dist/esm/components/src/components/Molecules/ChatProductComparisonMessage/ChatProductComparisonMessage.js +22 -2
- package/dist/esm/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.d.ts.map +1 -1
- package/dist/esm/components/src/components/Molecules/ChatProductQueryMessage/ChatProductQueryMessage.js +94 -65
- package/dist/esm/components/src/components/Organisms/Chat/Chat.d.ts.map +1 -1
- package/dist/esm/components/src/components/Organisms/Chat/Chat.js +194 -23
- package/dist/esm/components/src/components/Organisms/Chat/Chat.styles.d.ts.map +1 -1
- package/dist/esm/components/src/components/Organisms/Chat/Chat.styles.js +67 -5
- 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: '
|
|
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: `
|
|
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: [
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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))) }) })] })),
|
|
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;;
|
|
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
|
|
319
|
-
//
|
|
320
|
-
|
|
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, {
|
|
558
|
-
controller.store.
|
|
559
|
-
|
|
560
|
-
|
|
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, {
|
|
585
|
-
controller.store.
|
|
586
|
-
|
|
587
|
-
|
|
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:
|
|
615
|
-
|
|
616
|
-
|
|
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:
|
|
630
|
-
|
|
631
|
-
|
|
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,
|
|
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"}
|