@blotoutio/providers-evo-search-sdk 1.70.0 → 1.70.2

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/core.cjs.js CHANGED
@@ -1,5 +1,269 @@
1
1
  'use strict';
2
2
 
3
+ // Per-tenant filter vocabulary discovered from a catalog's structured product
4
+ // attributes (see buildProductAttributes in catalog.ts). The slot-fill LLM is
5
+ // constrained to this vocabulary and its output is validated against it, so
6
+ // filters reflect what the brand actually has and can't be hallucinated. Built
7
+ // at sync time, persisted to KV, read at query time. Deterministic — per-brand
8
+ // relevance emerges because the vocabulary is derived from that tenant's data.
9
+ // What a store that has never opened the Facets page gets. Availability is the
10
+ // only one: it reads a structural field every catalog has, so it needs no sync
11
+ // work and no merchant mapping, and it is the filter shoppers reach for most.
12
+ // Everything else stays opt-in — inventing Brand/Category groups for a catalog
13
+ // we haven't been told about is what the discovered-facet design got wrong.
14
+ //
15
+ // This is a fallback for an ABSENT config, never an injection into a present
16
+ // one: a merchant who saves a list without Availability has removed it, and a
17
+ // merchant who clears every facet gets `{ facets: [] }` and no groups at all.
18
+ // `resolveFacetsConfig` is the single place that distinction is made.
19
+ const DEFAULT_FACETS_CONFIG = {
20
+ facets: [
21
+ {
22
+ id: 'availability',
23
+ label: 'Availability',
24
+ source: { kind: 'availability' },
25
+ display: 'list',
26
+ enabled: true,
27
+ },
28
+ ],
29
+ };
30
+ /** Compatibility order consumed by renderer versions that predate embedded facets. */
31
+ const deriveFacetIds = (config) => [
32
+ 'price',
33
+ ...config.facets
34
+ .filter((facet) => facet.enabled && facet.id !== 'price')
35
+ .map((facet) => facet.id),
36
+ ];
37
+
38
+ const SEARCH_PAGE_DESIGN_VERSION = 1;
39
+ /**
40
+ * Sentinel accepted by color tokens, `controlRadius` and `containerWidth`:
41
+ * resolve the value from the merchant's live theme at mount (storefront) or
42
+ * fall back to the preset value below (editor preview, headless, resolution
43
+ * failure).
44
+ */
45
+ const AUTO_DESIGN_TOKEN = 'auto';
46
+ const SEARCH_PAGE_PRODUCT_CARD_DEFAULTS = {
47
+ textAlignment: 'start',
48
+ imageFit: 'cover',
49
+ imagePosition: 'center',
50
+ hoverEffect: 'zoom',
51
+ contentPaddingTop: 0,
52
+ contentPaddingRight: 0,
53
+ contentPaddingBottom: 0,
54
+ contentPaddingLeft: 0,
55
+ };
56
+ const labelDefaults = {
57
+ searchPlaceholder: 'Search products',
58
+ filters: 'Filters',
59
+ sort: 'Sort',
60
+ clearAll: 'Clear all',
61
+ loadMore: 'Load more',
62
+ loading: 'Loading…',
63
+ retry: 'Try again',
64
+ emptyTitle: 'No products found',
65
+ emptyMessage: 'Check the spelling or try a broader search.',
66
+ resultsFor: 'Results for',
67
+ products: 'Products',
68
+ soldOut: 'Sold out',
69
+ sale: 'Sale',
70
+ chooseOptions: 'Choose options',
71
+ selectOption: 'Select {option}',
72
+ checkingAvailability: 'Checking availability…',
73
+ unavailable: 'Unavailable',
74
+ // The renderer composes '+<n> <label>', so this is the tail of "+3 more" and
75
+ // not a whole sentence. 'Show more options' read as "+3 Show more options".
76
+ showMoreOptions: 'more',
77
+ showFewerOptions: 'Show fewer options',
78
+ searching: 'Searching…',
79
+ emptyFilteredTitle: 'No products match these filters',
80
+ emptyFilteredMessage: 'Try removing a filter to see more results.',
81
+ clearFilters: 'Clear filters',
82
+ errorTitle: 'We couldn’t load your search',
83
+ errorMessage: 'Check your connection and try again.',
84
+ resultsStale: 'Results could not be updated.',
85
+ adding: 'Adding…',
86
+ added: 'Added',
87
+ };
88
+ // The stock palette every preset falls back to. Presets keep their own colors
89
+ // slot so one can diverge later without touching the others.
90
+ const stockPalette = {
91
+ text: '#121212',
92
+ muted: '#6b7280',
93
+ background: '#ffffff',
94
+ surface: '#f7f7f7',
95
+ border: '#e5e7eb',
96
+ accent: '#121212',
97
+ sale: '#b42318',
98
+ soldOut: '#667085',
99
+ };
100
+ const SEARCH_PAGE_TOKEN_FALLBACKS = {
101
+ dawn: { colors: { ...stockPalette }, controlRadius: 2, containerWidth: 1200 },
102
+ horizon: {
103
+ colors: { ...stockPalette },
104
+ controlRadius: 999,
105
+ containerWidth: 1440,
106
+ },
107
+ 'utility-grid': {
108
+ colors: { ...stockPalette },
109
+ controlRadius: 6,
110
+ containerWidth: 1280,
111
+ },
112
+ 'editorial-grid': {
113
+ colors: { ...stockPalette },
114
+ controlRadius: 2,
115
+ containerWidth: 1360,
116
+ },
117
+ };
118
+ const common = {
119
+ version: SEARCH_PAGE_DESIGN_VERSION,
120
+ typography: {
121
+ fontFamily: 'inherit',
122
+ headingSize: 28,
123
+ productTitleSize: 15,
124
+ priceSize: 14,
125
+ headingWeight: 600,
126
+ productTitleWeight: 500,
127
+ },
128
+ // Colors adopt the live theme by default; merchants pin explicit hex values
129
+ // in the editor when they want to diverge from the theme.
130
+ colors: {
131
+ text: AUTO_DESIGN_TOKEN,
132
+ muted: AUTO_DESIGN_TOKEN,
133
+ background: AUTO_DESIGN_TOKEN,
134
+ surface: AUTO_DESIGN_TOKEN,
135
+ border: AUTO_DESIGN_TOKEN,
136
+ accent: AUTO_DESIGN_TOKEN,
137
+ sale: AUTO_DESIGN_TOKEN,
138
+ soldOut: AUTO_DESIGN_TOKEN,
139
+ },
140
+ shape: {
141
+ borderWidth: 1,
142
+ cardRadius: 0,
143
+ imageRadius: 0,
144
+ controlRadius: AUTO_DESIGN_TOKEN,
145
+ controlHeight: 44,
146
+ },
147
+ fields: {
148
+ image: true,
149
+ title: true,
150
+ price: true,
151
+ compareAtPrice: true,
152
+ discount: true,
153
+ soldOut: true,
154
+ },
155
+ productCard: { ...SEARCH_PAGE_PRODUCT_CARD_DEFAULTS },
156
+ labels: labelDefaults,
157
+ mount: {},
158
+ facets: {
159
+ facets: DEFAULT_FACETS_CONFIG.facets.map((facet) => ({ ...facet })),
160
+ },
161
+ facetIds: deriveFacetIds(DEFAULT_FACETS_CONFIG),
162
+ advancedCss: '',
163
+ };
164
+ const definePreset = (preset, overrides) => {
165
+ var _a, _b;
166
+ return ({
167
+ ...common,
168
+ ...overrides,
169
+ version: SEARCH_PAGE_DESIGN_VERSION,
170
+ preset,
171
+ typography: { ...common.typography, ...overrides.typography },
172
+ colors: { ...common.colors, ...overrides.colors },
173
+ shape: { ...common.shape, ...overrides.shape },
174
+ fields: { ...common.fields, ...overrides.fields },
175
+ productCard: {
176
+ ...SEARCH_PAGE_PRODUCT_CARD_DEFAULTS,
177
+ ...overrides.productCard,
178
+ },
179
+ labels: { ...common.labels, ...overrides.labels },
180
+ mount: { ...common.mount, ...overrides.mount },
181
+ facets: overrides.facets
182
+ ? { facets: overrides.facets.facets.map((facet) => ({ ...facet })) }
183
+ : common.facets
184
+ ? { facets: common.facets.facets.map((facet) => ({ ...facet })) }
185
+ : undefined,
186
+ facetIds: [...((_a = overrides.facetIds) !== null && _a !== void 0 ? _a : common.facetIds)],
187
+ advancedCss: (_b = overrides.advancedCss) !== null && _b !== void 0 ? _b : '',
188
+ });
189
+ };
190
+ const SEARCH_PAGE_DESIGN_PRESETS = {
191
+ dawn: definePreset('dawn', {
192
+ layout: {
193
+ filterPlacement: 'sidebar',
194
+ containerWidth: AUTO_DESIGN_TOKEN,
195
+ columns: { desktop: 4, tablet: 3, mobile: 2 },
196
+ columnGap: 24,
197
+ rowGap: 36,
198
+ imageRatio: '1 / 1.2',
199
+ },
200
+ }),
201
+ horizon: definePreset('horizon', {
202
+ layout: {
203
+ filterPlacement: 'toolbar',
204
+ containerWidth: AUTO_DESIGN_TOKEN,
205
+ columns: { desktop: 4, tablet: 3, mobile: 2 },
206
+ columnGap: 16,
207
+ rowGap: 28,
208
+ imageRatio: '4 / 5',
209
+ },
210
+ // controlRadius stays 'auto' (from common): pill controls only when the
211
+ // theme itself uses them; the preset fallback keeps the stock pill look.
212
+ shape: { ...common.shape, imageRadius: 8 },
213
+ }),
214
+ 'utility-grid': definePreset('utility-grid', {
215
+ layout: {
216
+ filterPlacement: 'sidebar',
217
+ containerWidth: AUTO_DESIGN_TOKEN,
218
+ columns: { desktop: 4, tablet: 3, mobile: 2 },
219
+ columnGap: 12,
220
+ rowGap: 20,
221
+ imageRatio: '1 / 1',
222
+ },
223
+ typography: {
224
+ ...common.typography,
225
+ headingSize: 24,
226
+ productTitleSize: 14,
227
+ productTitleWeight: 600,
228
+ },
229
+ shape: { ...common.shape, cardRadius: 8, imageRadius: 8, controlRadius: 6 },
230
+ }),
231
+ 'editorial-grid': definePreset('editorial-grid', {
232
+ layout: {
233
+ filterPlacement: 'toolbar',
234
+ containerWidth: AUTO_DESIGN_TOKEN,
235
+ columns: { desktop: 3, tablet: 2, mobile: 1 },
236
+ columnGap: 32,
237
+ rowGap: 48,
238
+ imageRatio: '3 / 4',
239
+ },
240
+ typography: {
241
+ ...common.typography,
242
+ fontFamily: 'serif',
243
+ headingSize: 36,
244
+ productTitleSize: 18,
245
+ },
246
+ // A deliberate editorial look keeps its concrete radius rather than
247
+ // adopting the theme's controls.
248
+ shape: { ...common.shape, controlRadius: 2 },
249
+ }),
250
+ };
251
+ /**
252
+ * Served to every enabled store that has published nothing — including
253
+ * storefronts running an SDK bundle pinned before `'auto'` existed, which
254
+ * would emit `--evs-max-width:autopx`, drop the declaration and render
255
+ * full-bleed. So the blind default keeps a concrete width: `'auto'` reaches a
256
+ * storefront only once a merchant picks a preset in the designer, by which
257
+ * point their tag is serving a bundle that understands it.
258
+ */
259
+ ({
260
+ ...SEARCH_PAGE_DESIGN_PRESETS.dawn,
261
+ layout: {
262
+ ...SEARCH_PAGE_DESIGN_PRESETS.dawn.layout,
263
+ containerWidth: SEARCH_PAGE_TOKEN_FALLBACKS.dawn.containerWidth,
264
+ },
265
+ });
266
+
3
267
  /**
4
268
  * Known ad-network click ID query parameters and the human-readable provider
5
269
  * label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
@@ -886,6 +1150,7 @@ const downscaleImage = async (source) => {
886
1150
  // Ask can't reuse withSearchResilience: a half-consumed stream isn't retryable.
887
1151
  // Idle timeout resets per chunk; both reject with TimeoutError so submitAsk
888
1152
  // routes them to the search fallback rather than treating them as cancellation.
1153
+ const QUICK_ADD_VALIDATE_TIMEOUT_MS = 7000;
889
1154
  const ASK_REQUEST_TIMEOUT_MS = 7000;
890
1155
  const ASK_SSE_IDLE_TIMEOUT_MS = 10000;
891
1156
  // Keep-warm cadence for `warm()`. Chrome reaps used-idle sockets after ~5
@@ -914,7 +1179,30 @@ const readShopifyCurrency = () => {
914
1179
  return undefined;
915
1180
  }
916
1181
  };
917
- const resolveDisplayCurrency = (uiPreferences) => { var _a, _b; return (_b = (_a = readShopifyCurrency()) !== null && _a !== void 0 ? _a : parseCurrencyCode(uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.currency)) !== null && _b !== void 0 ? _b : 'USD'; };
1182
+ const parseCountryCode = (value) => typeof value === 'string' && /^[a-z]{2}$/i.test(value)
1183
+ ? value.toUpperCase()
1184
+ : undefined;
1185
+ const readShopifyCountry = () => {
1186
+ var _a;
1187
+ try {
1188
+ return parseCountryCode((_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.country);
1189
+ }
1190
+ catch {
1191
+ return undefined;
1192
+ }
1193
+ };
1194
+ /**
1195
+ * A host-supplied market wins over `window.Shopify`: nothing sets it on a
1196
+ * Shopify theme (where the storefront's own globals are the truth), and a
1197
+ * headless host that does set it is telling us the market its own page is
1198
+ * rendering — which is the only source that knows, since `window.Shopify`
1199
+ * does not exist there. `uiPreferences.currency` stays the merchant-level
1200
+ * fallback for a headless host that supplies nothing.
1201
+ */
1202
+ const resolveDisplayCurrency = (uiPreferences, market) => {
1203
+ var _a, _b, _c;
1204
+ return (_c = (_b = (_a = market.currency) !== null && _a !== void 0 ? _a : readShopifyCurrency()) !== null && _b !== void 0 ? _b : parseCurrencyCode(uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.currency)) !== null && _c !== void 0 ? _c : 'USD';
1205
+ };
918
1206
  const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, uiPreferences, }) => {
919
1207
  if (!baseURL) {
920
1208
  throw new Error('baseURL missing');
@@ -925,6 +1213,17 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
925
1213
  if (!sendTag) {
926
1214
  throw new Error('sendTag missing');
927
1215
  }
1216
+ // Replaced wholesale rather than merged, so a host that drops one field
1217
+ // (leaving a market) does not keep that field's stale value.
1218
+ let market = {};
1219
+ const setMarket = (next) => {
1220
+ const currency = parseCurrencyCode(next === null || next === void 0 ? void 0 : next.currency);
1221
+ const country = parseCountryCode(next === null || next === void 0 ? void 0 : next.country);
1222
+ market = {
1223
+ ...(currency ? { currency } : {}),
1224
+ ...(country ? { country } : {}),
1225
+ };
1226
+ };
928
1227
  const getChannel = () => { var _a; return (((_a = window[registryKey]) === null || _a === void 0 ? void 0 : _a.ui) ? 'widget' : 'headless'); };
929
1228
  const getHeaders = () => {
930
1229
  const headers = new Headers({ EdgeTagUserId: userId });
@@ -965,7 +1264,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
965
1264
  const getURL = (path) => {
966
1265
  const url = new URL(baseOrigin);
967
1266
  url.pathname = `${PROVIDER_PATH_PREFIX}${path}`;
968
- url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences));
1267
+ url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences, market));
969
1268
  return url;
970
1269
  };
971
1270
  // Only `/user/*` endpoints return per-user PII (recents, profile-bound
@@ -975,10 +1274,20 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
975
1274
  // (`Allow-Credentials: true` + non-wildcard origin echo) on every
976
1275
  // response — so default to `omit` and opt in below.
977
1276
  const needsCredentials = (path) => path.startsWith('/user/');
978
- const getJSON = async (path, label, params, signal) => {
1277
+ const getJSON = async (path, label,
1278
+ // An array value appends one param per entry (repeatable keys, e.g. the
1279
+ // results page's `f.<key>` filters).
1280
+ params, signal) => {
979
1281
  const url = getURL(path);
980
1282
  for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
981
- url.searchParams.set(key, value);
1283
+ if (Array.isArray(value)) {
1284
+ for (const entry of value) {
1285
+ url.searchParams.append(key, entry);
1286
+ }
1287
+ }
1288
+ else {
1289
+ url.searchParams.set(key, value);
1290
+ }
982
1291
  }
983
1292
  const credentialed = needsCredentials(path);
984
1293
  if (!credentialed) {
@@ -1007,6 +1316,14 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
1007
1316
  logger.info('EvoSearch API: Fetching ask config');
1008
1317
  return getJSON('/config', 'Config', undefined, signal);
1009
1318
  };
1319
+ const searchPageDesign = (signal, previewToken) => {
1320
+ logger.info(`EvoSearch API: Fetching ${previewToken ? 'preview' : 'published'} search page design`);
1321
+ return getJSON('/search-page-design', 'SearchPageDesign',
1322
+ // Not `preview`: that name is the identity param every preview session
1323
+ // sets to `1`, and identity params are written after these, so the token
1324
+ // would be overwritten and the worker would see the session flag instead.
1325
+ previewToken ? { designPreview: previewToken } : undefined, signal);
1326
+ };
1010
1327
  // Isolate warmer doubling as connection establisher: primes the worker's
1011
1328
  // search index off the request path AND opens the h2/h3 connection before
1012
1329
  // the first real keystroke needs it. Runs under the resilience wrapper so a
@@ -1077,6 +1394,109 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
1077
1394
  logger.info(`EvoSearch API: Search success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
1078
1395
  return data;
1079
1396
  };
1397
+ // Results-page fetch: same /search endpoint with the opt-in extensions
1398
+ // (facets, filters, pagination, explicit sort). Kept separate from the
1399
+ // autocomplete `search()` so its hot path stays untouched.
1400
+ const searchResults = (query, options = {}) => {
1401
+ var _a, _b, _c, _d, _e, _f;
1402
+ const params = {
1403
+ q: query,
1404
+ facets: '1',
1405
+ };
1406
+ if (options.limit != null) {
1407
+ params['limit'] = String(options.limit);
1408
+ }
1409
+ if (options.cursor) {
1410
+ params['cursor'] = options.cursor;
1411
+ }
1412
+ if (options.offset) {
1413
+ params['offset'] = String(options.offset);
1414
+ }
1415
+ if (options.sort && options.sort !== 'relevance') {
1416
+ params['sort'] = options.sort;
1417
+ }
1418
+ if ((_a = options.previewToken) === null || _a === void 0 ? void 0 : _a.trim()) {
1419
+ params['designPreview'] = options.previewToken.trim();
1420
+ }
1421
+ const filters = options.filters;
1422
+ for (const [key, values] of Object.entries((_b = filters === null || filters === void 0 ? void 0 : filters.terms) !== null && _b !== void 0 ? _b : {})) {
1423
+ if (values.length) {
1424
+ params[`f.${key}`] = values;
1425
+ }
1426
+ }
1427
+ for (const [key, window] of Object.entries((_c = filters === null || filters === void 0 ? void 0 : filters.ranges) !== null && _c !== void 0 ? _c : {})) {
1428
+ if (window.min != null || window.max != null) {
1429
+ params[`f.${key}`] = `${(_d = window.min) !== null && _d !== void 0 ? _d : ''}..${(_e = window.max) !== null && _e !== void 0 ? _e : ''}`;
1430
+ }
1431
+ }
1432
+ if ((filters === null || filters === void 0 ? void 0 : filters.priceMin) != null) {
1433
+ params['priceMin'] = String(filters.priceMin);
1434
+ }
1435
+ if ((filters === null || filters === void 0 ? void 0 : filters.priceMax) != null) {
1436
+ params['priceMax'] = String(filters.priceMax);
1437
+ }
1438
+ return withSearchResilience((attemptSignal) => getJSON('/search', 'SearchResults', params, attemptSignal), (_f = options.signal) !== null && _f !== void 0 ? _f : NEVER_ABORTED);
1439
+ };
1440
+ // Same deadline as `validateQuickAdd`, and for the same reason: the only
1441
+ // caller passes no signal, so without one a stalled request never settles —
1442
+ // and its promise is memoized, so the swatch picker it backs stays pending
1443
+ // for the life of the page rather than retrying.
1444
+ const productVariants = async (productId, signal) => {
1445
+ const controller = new AbortController();
1446
+ const onCallerAbort = () => controller.abort();
1447
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort);
1448
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
1449
+ controller.abort();
1450
+ }
1451
+ const timer = setTimeout(() => controller.abort(), QUICK_ADD_VALIDATE_TIMEOUT_MS);
1452
+ try {
1453
+ return await getJSON(`/products/${encodeURIComponent(productId)}/variants`, 'ProductVariants', undefined, controller.signal);
1454
+ }
1455
+ finally {
1456
+ clearTimeout(timer);
1457
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
1458
+ }
1459
+ };
1460
+ const validateQuickAdd = async (productId, variantId, signal) => {
1461
+ var _a;
1462
+ const url = getURL('/quick-add/validate');
1463
+ setIdentityParams(url);
1464
+ const headers = getHeaders();
1465
+ headers.set('Content-Type', 'application/json');
1466
+ // Same deadline pattern as the other POSTs: a caller that passes no signal
1467
+ // would otherwise have no timeout, and a stalled request leaves the
1468
+ // quick-add control pending forever.
1469
+ const controller = new AbortController();
1470
+ const onCallerAbort = () => controller.abort();
1471
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort);
1472
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
1473
+ controller.abort();
1474
+ }
1475
+ const timer = setTimeout(() => controller.abort(), QUICK_ADD_VALIDATE_TIMEOUT_MS);
1476
+ const country = (_a = market.country) !== null && _a !== void 0 ? _a : readShopifyCountry();
1477
+ let response;
1478
+ try {
1479
+ response = await fetchImpl(url, {
1480
+ method: 'POST',
1481
+ headers,
1482
+ credentials: 'omit',
1483
+ signal: controller.signal,
1484
+ body: JSON.stringify({
1485
+ productId,
1486
+ variantId,
1487
+ ...(country ? { country } : {}),
1488
+ }),
1489
+ });
1490
+ }
1491
+ finally {
1492
+ clearTimeout(timer);
1493
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
1494
+ }
1495
+ if (!response.ok) {
1496
+ return { eligible: false, reason: 'context-unavailable' };
1497
+ }
1498
+ return (await response.json());
1499
+ };
1080
1500
  // Image search. Unlike `/search` there is no response cache on the worker
1081
1501
  // side and every call costs a multimodal embedding, so callers must debounce
1082
1502
  // and abort rather than fire per keystroke. A 403 `{disabled:true}` means the
@@ -1290,6 +1710,8 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
1290
1710
  return {
1291
1711
  trending,
1292
1712
  config,
1713
+ setMarket,
1714
+ searchPageDesign,
1293
1715
  warm,
1294
1716
  cancelWarm,
1295
1717
  user: {
@@ -1298,6 +1720,9 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
1298
1720
  clearRecents,
1299
1721
  },
1300
1722
  search,
1723
+ searchResults,
1724
+ productVariants,
1725
+ validateQuickAdd,
1301
1726
  searchImage,
1302
1727
  askStream,
1303
1728
  sendEvent,
@@ -1367,7 +1792,7 @@ const startHeadlessKeepWarm = (api) => {
1367
1792
  };
1368
1793
  };
1369
1794
  const init = (params) => {
1370
- var _a, _b, _c, _d, _e, _f, _g, _h;
1795
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1371
1796
  if (typeof window == 'undefined' || typeof document == 'undefined') {
1372
1797
  return;
1373
1798
  }
@@ -1377,8 +1802,13 @@ const init = (params) => {
1377
1802
  // A re-init can land on a different channel, or bail out entirely, so drop
1378
1803
  // any previous cadence before deciding which one this run owns.
1379
1804
  disposeKeepWarm === null || disposeKeepWarm === void 0 ? void 0 : disposeKeepWarm();
1380
- const evsParam = new URLSearchParams(window.location.search).get('evs');
1381
- if (evsParam === 'preview') {
1805
+ const urlParams = new URLSearchParams(window.location.search);
1806
+ const evsParam = urlParams.get('evs');
1807
+ // A designer draft link carries only its token; in preview mode the SDK
1808
+ // would otherwise never initialize on it, so the token doubles as the
1809
+ // preview-session trigger. Only `?evs` verbs can turn preview off.
1810
+ const designerPreview = evsParam === null && ((_c = (_b = urlParams.get('evs-preview')) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '') !== '';
1811
+ if (evsParam === 'preview' || designerPreview) {
1382
1812
  try {
1383
1813
  sessionStorage.removeItem(disableKeyName);
1384
1814
  sessionStorage.setItem(previewKeyName, '1');
@@ -1408,7 +1838,7 @@ const init = (params) => {
1408
1838
  error('EvoSearch SDK: Failed to clear evoSearch sessionStorage');
1409
1839
  }
1410
1840
  }
1411
- const { enabled, uiPreferences, askAi } = (_c = (_b = params.manifest) === null || _b === void 0 ? void 0 : _b.variables) !== null && _c !== void 0 ? _c : {};
1841
+ const { enabled, uiPreferences, askAi } = (_e = (_d = params.manifest) === null || _d === void 0 ? void 0 : _d.variables) !== null && _e !== void 0 ? _e : {};
1412
1842
  const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
1413
1843
  const askAiEnabled = (askAi === null || askAi === void 0 ? void 0 : askAi.enabled) === true;
1414
1844
  const visualSearchEnabled = (uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.visualSearch) === true;
@@ -1424,7 +1854,7 @@ const init = (params) => {
1424
1854
  const evoSearchAPI = createEvoSearchAPI({
1425
1855
  baseURL: params.baseUrl,
1426
1856
  userId: params.userId,
1427
- sessionId: (_d = params.session) === null || _d === void 0 ? void 0 : _d.sessionId,
1857
+ sessionId: (_f = params.session) === null || _f === void 0 ? void 0 : _f.sessionId,
1428
1858
  sendTag: params.sendTag,
1429
1859
  uiPreferences,
1430
1860
  });
@@ -1439,7 +1869,7 @@ const init = (params) => {
1439
1869
  options: { destination: params.baseUrl },
1440
1870
  });
1441
1871
  if (!uiImplementation) {
1442
- if (((_f = (_e = params.manifest) === null || _e === void 0 ? void 0 : _e.variables) === null || _f === void 0 ? void 0 : _f.disableDefaultUI) === true) {
1872
+ if (((_h = (_g = params.manifest) === null || _g === void 0 ? void 0 : _g.variables) === null || _h === void 0 ? void 0 : _h.disableDefaultUI) === true) {
1443
1873
  logger.info('EvoSearch SDK: Running in headless mode, no default UI will render');
1444
1874
  // Deliberately keyed on `disableDefaultUI` rather than bare `headless`:
1445
1875
  // in the else branch the UI is missing by mistake, so search isn't
@@ -1454,7 +1884,7 @@ const init = (params) => {
1454
1884
  }
1455
1885
  uiImplementation.init({
1456
1886
  destination: params.baseUrl,
1457
- sessionId: (_g = params.session) === null || _g === void 0 ? void 0 : _g.sessionId,
1887
+ sessionId: (_j = params.session) === null || _j === void 0 ? void 0 : _j.sessionId,
1458
1888
  userId: params.userId,
1459
1889
  trendingSearches: [],
1460
1890
  handpickedProducts: [],
@@ -1475,7 +1905,7 @@ const init = (params) => {
1475
1905
  }
1476
1906
  catch (err) {
1477
1907
  error(`EvoSearch SDK: Initialization failed - ${err}`);
1478
- if ((_h = window[registryKey]) === null || _h === void 0 ? void 0 : _h.api) {
1908
+ if ((_k = window[registryKey]) === null || _k === void 0 ? void 0 : _k.api) {
1479
1909
  notifyReady();
1480
1910
  }
1481
1911
  }
@@ -1484,7 +1914,7 @@ const init = (params) => {
1484
1914
  const tag = ({ data }) => {
1485
1915
  try {
1486
1916
  return {
1487
- sdkVersion: "1.70.0" || '',
1917
+ sdkVersion: "1.70.2" || '',
1488
1918
  searchQuery: data['searchQuery'],
1489
1919
  searchResults: data['searchResults'],
1490
1920
  timestamp: Date.now(),
@@ -1493,7 +1923,7 @@ const tag = ({ data }) => {
1493
1923
  catch (err) {
1494
1924
  console.error(`EvoSearch SDK: Tagging failed - ${err}`);
1495
1925
  return {
1496
- sdkVersion: "1.70.0" ,
1926
+ sdkVersion: "1.70.2" ,
1497
1927
  timestamp: Date.now(),
1498
1928
  };
1499
1929
  }