@blotoutio/providers-evo-search-sdk 1.71.1 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hooks.mjs CHANGED
@@ -3433,6 +3433,443 @@ const useEvoSearchAPI = () => {
3433
3433
  return { ready: ctx.ready, api: ctx.api };
3434
3434
  };
3435
3435
 
3436
+ /**
3437
+ * Storefront-local helpers, deliberately separate from the worker-scoped API
3438
+ * client: `api.ts` injects identity params and headers that must never reach
3439
+ * the merchant's own origin, and its `credentials: omit` policy is the
3440
+ * opposite of what storefront requests (which ride the cart cookie) need.
3441
+ */
3442
+ /**
3443
+ * The storefront's root path. Shopify publishes it (locale prefix included,
3444
+ * e.g. `/en-ca/`) on `window.Shopify.routes.root`; default to `/`.
3445
+ */
3446
+ const storefrontRoot = () => {
3447
+ var _a, _b;
3448
+ if (typeof window === 'undefined') {
3449
+ return '/';
3450
+ }
3451
+ const root = (_b = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.routes) === null || _b === void 0 ? void 0 : _b.root;
3452
+ return typeof root === 'string' && root.startsWith('/') && root.endsWith('/')
3453
+ ? root
3454
+ : '/';
3455
+ };
3456
+ /* ------------------------------------------------------------------ *
3457
+ * Theme quick-add handoff
3458
+ *
3459
+ * Whatever a merchant's quick-add opens — a modal, a side drawer, a plain
3460
+ * navigation — their own product card already knows how to open it. So the
3461
+ * tile borrows that card and presses its button rather than emulating any
3462
+ * one theme's surface. Nothing here is written against a specific theme:
3463
+ * the results section and the trigger are both read off the storefront at
3464
+ * runtime, and a store with no quick-add at all resolves to `false` so the
3465
+ * caller keeps its product link.
3466
+ * ------------------------------------------------------------------ */
3467
+ /**
3468
+ * Our own markup, which must never be mistaken for the theme's card. Every
3469
+ * surface we render is `evs-` prefixed — the custom elements by tag, the
3470
+ * managed page by class — so the prefix is the test rather than a list that
3471
+ * would fall behind the next component.
3472
+ */
3473
+ const isEvoSearchMarkup = (node) => node.tagName.startsWith('EVS-') ||
3474
+ node.hasAttribute('data-evo-search-quick-add') ||
3475
+ Array.from(node.classList).some((name) => name.startsWith('evs-'));
3476
+ const QUICK_ADD_PATTERN = /quick[-_]?(add|shop|view|buy)|choose[-_]?option/i;
3477
+ /** The same words as a selector, so presence is one query rather than a walk. */
3478
+ const QUICK_ADD_HINT = '[class*="quick-add" i],[class*="quickadd" i],[class*="quick-shop" i],' +
3479
+ '[class*="quickshop" i],[class*="quick-view" i],[class*="quickview" i],' +
3480
+ '[class*="quick-buy" i],[class*="quickbuy" i],quick-add,quick-add-component,' +
3481
+ 'quick-add-modal,quick-shop,quick-view';
3482
+ const productHandleOf = (link) => { var _a, _b, _c; return (_c = (_b = /\/products\/([^/?#]+)/.exec((_a = link.getAttribute('href')) !== null && _a !== void 0 ? _a : '')) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : null; };
3483
+ const isThemeOwned = (element) => {
3484
+ for (let node = element; node; node = node.parentElement) {
3485
+ if (isEvoSearchMarkup(node))
3486
+ return false;
3487
+ }
3488
+ return true;
3489
+ };
3490
+ const attributeSignal = (element) => {
3491
+ var _a, _b;
3492
+ return `${element.tagName} ${(_a = element.getAttribute('class')) !== null && _a !== void 0 ? _a : ''} ${(_b = element.getAttribute('data-quick-add-button')) !== null && _b !== void 0 ? _b : ''}`;
3493
+ };
3494
+ const holdsQuickAdd = (element) => element.querySelector(QUICK_ADD_HINT) !== null;
3495
+ /**
3496
+ * Whether this storefront does quick-add at all, read off the theme's own
3497
+ * cards. A tile asks before it names its own call to action: promising an add
3498
+ * on a storefront that can only offer the product page is a worse answer than
3499
+ * naming the detour. Page scope, so a probe reads it once and every tile in
3500
+ * the pass shares the answer; never remembered across passes, because a "no"
3501
+ * from an early render would outlive the markup that made it true.
3502
+ */
3503
+ const pageHasThemeQuickAdd = () => {
3504
+ if (typeof document === 'undefined')
3505
+ return false;
3506
+ try {
3507
+ return Array.from(document.querySelectorAll(QUICK_ADD_HINT)).some(isThemeOwned);
3508
+ }
3509
+ catch {
3510
+ return false;
3511
+ }
3512
+ };
3513
+ /** True until the climb reaches the grid, where a sibling product appears. */
3514
+ const holdsOnlyProduct = (element, handle) => {
3515
+ const links = element.querySelectorAll('a[href*="/products/"]');
3516
+ return (links.length > 0 &&
3517
+ Array.from(links).every((link) => productHandleOf(link) === handle));
3518
+ };
3519
+ /**
3520
+ * The theme's card for one product. Deliberately the OUTERMOST wrapper that
3521
+ * still belongs to this product alone: a theme's quick-add resolves the
3522
+ * product through its own card element (Horizon reads
3523
+ * `closest('product-card')`), so handing it an inner fragment that merely
3524
+ * contains the button leaves it without a product to open.
3525
+ */
3526
+ const themeCardFor = (root, handle) => {
3527
+ const link = Array.from(root.querySelectorAll('a[href*="/products/"]')).find((candidate) => productHandleOf(candidate) === handle && isThemeOwned(candidate));
3528
+ if (!link)
3529
+ return null;
3530
+ let card = null;
3531
+ let node = link.parentElement;
3532
+ for (let depth = 0; node && depth < 10; depth += 1) {
3533
+ if (node.id.startsWith('shopify-section-') ||
3534
+ !holdsOnlyProduct(node, handle)) {
3535
+ break;
3536
+ }
3537
+ if (holdsQuickAdd(node))
3538
+ card = node;
3539
+ node = node.parentElement;
3540
+ }
3541
+ return card;
3542
+ };
3543
+ /**
3544
+ * The section that renders the theme's own product grid, read off the page
3545
+ * rather than configured: Shopify stamps every section as
3546
+ * `shopify-section-<id>`, and the one holding the most product links is the
3547
+ * grid. A storefront with no sections at all (headless) yields null, which
3548
+ * is what keeps the handoff from firing there.
3549
+ */
3550
+ const themeGridSectionId = () => {
3551
+ var _a;
3552
+ let best = null;
3553
+ for (const section of Array.from(document.querySelectorAll('[id^="shopify-section-"]'))) {
3554
+ const links = Array.from(section.querySelectorAll('a[href*="/products/"]')).filter(isThemeOwned).length;
3555
+ if (links > 0 && (!best || links > best.links)) {
3556
+ best = { id: section.id.slice('shopify-section-'.length), links };
3557
+ }
3558
+ }
3559
+ return (_a = best === null || best === void 0 ? void 0 : best.id) !== null && _a !== void 0 ? _a : null;
3560
+ };
3561
+ const themeCardCache = new Map();
3562
+ /**
3563
+ * Handles the theme's own grid section came back without a card for. Worth
3564
+ * remembering, because it is a fact about the theme's results rather than a
3565
+ * transient failure — a refused or failed fetch records nothing — and a card
3566
+ * that mounts on the page later is found before this set is consulted.
3567
+ */
3568
+ const themeCardMisses = new Set();
3569
+ /** Ask the storefront to render its own grid for this product alone. */
3570
+ const fetchThemeCard = async (request, handle) => {
3571
+ const cached = themeCardCache.get(handle);
3572
+ if (cached)
3573
+ return cached;
3574
+ if (themeCardMisses.has(handle))
3575
+ return null;
3576
+ const sectionId = themeGridSectionId();
3577
+ if (!sectionId)
3578
+ return null;
3579
+ const response = await request(`${storefrontRoot()}search?q=${encodeURIComponent(handle)}&section_id=${encodeURIComponent(sectionId)}`, { credentials: 'same-origin' });
3580
+ if (!response.ok)
3581
+ return null;
3582
+ const document_ = new DOMParser().parseFromString(await response.text(), 'text/html');
3583
+ const card = themeCardFor(document_, handle);
3584
+ if (!card) {
3585
+ themeCardMisses.add(handle);
3586
+ return null;
3587
+ }
3588
+ const imported = document.importNode(card, true);
3589
+ themeCardCache.set(handle, imported);
3590
+ return imported;
3591
+ };
3592
+ /**
3593
+ * A borrowed card has to be genuinely laid out: `display:none`,
3594
+ * `visibility:hidden` and `inert` all leave the trigger unclickable, and a
3595
+ * theme that renders its quick-add surface inside the card would render it
3596
+ * hidden too. Off-screen it is, with no `transform` — that would make this
3597
+ * host the containing block for the theme's own fixed-position dialog.
3598
+ */
3599
+ const quickAddHost = () => {
3600
+ const existing = document.querySelector('[data-evo-search-quick-add]');
3601
+ if (existing instanceof HTMLElement)
3602
+ return existing;
3603
+ const host = document.createElement('div');
3604
+ host.setAttribute('data-evo-search-quick-add', '');
3605
+ host.style.cssText =
3606
+ 'position:fixed;top:0;left:-10000px;width:360px;height:1px;overflow:visible';
3607
+ document.body.appendChild(host);
3608
+ return host;
3609
+ };
3610
+ /**
3611
+ * A card commonly carries more than one trigger and hides all but the live
3612
+ * one — Horizon ships a direct-add submit beside a "choose options" button
3613
+ * and drops one out of the layout in CSS. Computed style is the discriminator
3614
+ * (rather than geometry, which an off-screen host would answer for), read up
3615
+ * to the card so a hidden wrapper counts.
3616
+ */
3617
+ const isRendered = (element, root) => {
3618
+ const view = element.ownerDocument.defaultView;
3619
+ if (!view)
3620
+ return true;
3621
+ for (let node = element; node; node = node.parentElement) {
3622
+ const style = view.getComputedStyle(node);
3623
+ if (style.display === 'none' || style.visibility === 'hidden')
3624
+ return false;
3625
+ if (node === root)
3626
+ break;
3627
+ }
3628
+ return true;
3629
+ };
3630
+ const ancestorsWithin = (element, root) => {
3631
+ const chain = [];
3632
+ for (let node = element.parentElement; node && node !== root.parentElement; node = node.parentElement) {
3633
+ chain.push(node);
3634
+ }
3635
+ return chain;
3636
+ };
3637
+ /**
3638
+ * Every control that can submit a form: a button or an `<input>` whose type is
3639
+ * `submit` or `image` — `<button>` submits by default, `<input>` does not.
3640
+ */
3641
+ const isSubmitter = (candidate) => (candidate instanceof HTMLButtonElement && candidate.type !== 'button') ||
3642
+ (candidate instanceof HTMLInputElement &&
3643
+ (candidate.type === 'submit' || candidate.type === 'image'));
3644
+ /**
3645
+ * Whether clicking this element posts the native cart form. `formaction` is
3646
+ * read off the attribute rather than the property, which resolves against the
3647
+ * document URL and so is never empty.
3648
+ */
3649
+ const submitsCartForm = (candidate) => {
3650
+ if (!isSubmitter(candidate))
3651
+ return false;
3652
+ const override = candidate.getAttribute('formaction');
3653
+ if (override)
3654
+ return override.includes('/cart/add');
3655
+ return candidate.closest('form[action*="/cart/add"]') !== null;
3656
+ };
3657
+ /**
3658
+ * Whether clicking this element does anything of its own. `click()` on a
3659
+ * `<div>` dispatches a real bubbling event, so a theme listening higher up
3660
+ * would still open — but a bare div carries no activation behaviour, and the
3661
+ * two are indistinguishable from here. Declining trades that delegated
3662
+ * listener for a product link the shopper can always complete, which is the
3663
+ * better half of the trade: a detour beats a click that does nothing.
3664
+ *
3665
+ * A theme's own custom element counts, because that is where a theme usually
3666
+ * puts its quick-add. Only the configured path is narrowed by this — the
3667
+ * heuristic already queries nothing but `button` and `a[role="button"]`.
3668
+ */
3669
+ const isActionable = (candidate) => ['BUTTON', 'INPUT', 'SELECT', 'SUMMARY', 'LABEL'].includes(candidate.tagName) ||
3670
+ (candidate instanceof HTMLAnchorElement && candidate.hasAttribute('href')) ||
3671
+ candidate.hasAttribute('role') ||
3672
+ candidate.hasAttribute('tabindex') ||
3673
+ candidate.tagName.includes('-');
3674
+ /**
3675
+ * Whether clicking this element can open a quick-add at all. Four ways it
3676
+ * cannot: nothing happens on a click at all; it is not laid out, so the click
3677
+ * lands nowhere; it is disabled, so the click is swallowed and the caller
3678
+ * would read the silent no-op as a successful handoff and suppress the product
3679
+ * link; or it submits the native cart form, which would add a variant the
3680
+ * shopper never chose.
3681
+ *
3682
+ * A merchant's configured selector is held to the same four, because it
3683
+ * names markup the merchant cannot see the state of — a selector matching
3684
+ * Horizon's card picks up the hidden direct-add submit as readily as the
3685
+ * "choose options" button beside it.
3686
+ */
3687
+ const isUsableTrigger = (candidate, card) => {
3688
+ if (!isActionable(candidate))
3689
+ return false;
3690
+ if (!isRendered(candidate, card))
3691
+ return false;
3692
+ if (candidate.disabled === true ||
3693
+ candidate.getAttribute('aria-disabled') === 'true') {
3694
+ return false;
3695
+ }
3696
+ return !submitsCartForm(candidate);
3697
+ };
3698
+ /**
3699
+ * The rendered trigger. A card commonly carries several — Horizon ships both
3700
+ * a direct-add submit and a "choose options" button and hides one of them in
3701
+ * CSS — so visibility is the discriminator, and a submit inside the native
3702
+ * cart form is refused outright: it would silently add a variant the shopper
3703
+ * never chose.
3704
+ */
3705
+ const quickAddTrigger = (card, configured) => {
3706
+ var _a;
3707
+ if (configured) {
3708
+ // Every match, not the first: a broad selector over a card that ships a
3709
+ // hidden direct-add ahead of the live button would otherwise resolve to
3710
+ // the hidden one and decline a handoff that was there to make.
3711
+ return ((_a = Array.from(card.querySelectorAll(configured)).find((candidate) => candidate instanceof HTMLElement && isUsableTrigger(candidate, card))) !== null && _a !== void 0 ? _a : null);
3712
+ }
3713
+ for (const candidate of Array.from(card.querySelectorAll('button,a[role="button"]'))) {
3714
+ if (!(candidate instanceof HTMLElement))
3715
+ continue;
3716
+ if (!isUsableTrigger(candidate, card))
3717
+ continue;
3718
+ const signals = [candidate, ...ancestorsWithin(candidate, card)];
3719
+ if (signals.some((node) => QUICK_ADD_PATTERN.test(attributeSignal(node)))) {
3720
+ return candidate;
3721
+ }
3722
+ }
3723
+ return null;
3724
+ };
3725
+ /**
3726
+ * Whether this storefront has a quick-add to hand off to, answered
3727
+ * synchronously so a caller can keep its product link's native behaviour
3728
+ * when the answer is no. Three things have to hold: the theme does quick-add
3729
+ * somewhere on this page, and either its card for this product is already
3730
+ * here or there is a grid section to render one from that has not already come
3731
+ * back without it. A headless storefront fails the first test and never
3732
+ * reaches the network.
3733
+ */
3734
+ const themeQuickAddAvailable = (handle) => themeQuickAddProbe().available(handle);
3735
+ /**
3736
+ * One render pass's worth of those answers. The two page-level scans — does
3737
+ * the theme do quick-add, and which section is its grid — come back the same
3738
+ * for every tile, and the per-handle lookup walks the document, so a grid of
3739
+ * tiles asking one at a time rescans the page once per tile. A probe reads the
3740
+ * page once and remembers each handle it is asked about; a renderer builds one
3741
+ * per pass and lets it fall out of scope, so nothing is remembered past the
3742
+ * markup it was read from.
3743
+ */
3744
+ const themeQuickAddProbe = () => {
3745
+ if (typeof document === 'undefined')
3746
+ return { available: () => false };
3747
+ let pageAnswer;
3748
+ let sectionAnswer;
3749
+ const perHandle = new Map();
3750
+ const resolve = (handle) => {
3751
+ pageAnswer !== null && pageAnswer !== void 0 ? pageAnswer : (pageAnswer = pageHasThemeQuickAdd());
3752
+ if (!pageAnswer)
3753
+ return false;
3754
+ if (themeCardFor(document, handle) !== null)
3755
+ return true;
3756
+ if (themeCardMisses.has(handle))
3757
+ return false;
3758
+ if (sectionAnswer === undefined)
3759
+ sectionAnswer = themeGridSectionId();
3760
+ return sectionAnswer !== null;
3761
+ };
3762
+ return {
3763
+ available: (handle) => {
3764
+ if (!handle)
3765
+ return false;
3766
+ const memo = perHandle.get(handle);
3767
+ if (memo !== undefined)
3768
+ return memo;
3769
+ let answer = false;
3770
+ try {
3771
+ answer = resolve(handle);
3772
+ }
3773
+ catch {
3774
+ answer = false;
3775
+ }
3776
+ perHandle.set(handle, answer);
3777
+ return answer;
3778
+ },
3779
+ };
3780
+ };
3781
+ /**
3782
+ * Open the merchant's own quick-add surface for a product, resolving false
3783
+ * when the storefront has none — headless, or a theme whose cards simply
3784
+ * link to the product page. False is not a degraded state: the caller's
3785
+ * product link IS what that storefront does.
3786
+ *
3787
+ * The theme's own card on the page is pressed where it stands. A clone has no
3788
+ * `shopify-section-` ancestor and no grid controller above it, so a theme that
3789
+ * resolves its product by climbing out of the card — or keys a body-appended
3790
+ * surface off the section id — would find nothing to open. Only a card we had
3791
+ * to fetch is hosted, because that one has no place on the page to be pressed
3792
+ * in; it keeps the section ancestor it never had.
3793
+ *
3794
+ * A hosted card stays in the host after a successful open, because a theme
3795
+ * that builds its surface inside the card would have it torn out from under
3796
+ * the shopper; the next handoff replaces it.
3797
+ */
3798
+ const openThemeQuickAdd = async ({ handle, triggerSelector, fetchImpl, }) => {
3799
+ // Gated on the same predicate the caller uses, so the handoff can never
3800
+ // spend a section render on a storefront that has nothing to hand off to.
3801
+ if (!themeQuickAddAvailable(handle))
3802
+ return false;
3803
+ try {
3804
+ const live = themeCardFor(document, handle);
3805
+ if (live) {
3806
+ // Already upgraded and wired, so no frame to wait for. `isRendered`
3807
+ // stops at the card rather than the document, which lets a trigger in
3808
+ // the theme's hidden duplicate grid through — its listeners are wired
3809
+ // the same, and the surface it opens is not drawn from the card.
3810
+ const trigger = quickAddTrigger(live, triggerSelector);
3811
+ if (!trigger)
3812
+ return false;
3813
+ trigger.click();
3814
+ return true;
3815
+ }
3816
+ const fetched = await fetchThemeCard(fetchImpl !== null && fetchImpl !== void 0 ? fetchImpl : window.fetch.bind(window), handle);
3817
+ if (!fetched)
3818
+ return false;
3819
+ const host = quickAddHost();
3820
+ const card = fetched.cloneNode(true);
3821
+ host.replaceChildren(card);
3822
+ // One frame so the theme's custom elements upgrade and wire their refs.
3823
+ await new Promise((resolve) => {
3824
+ requestAnimationFrame(() => resolve());
3825
+ });
3826
+ const trigger = quickAddTrigger(card, triggerSelector);
3827
+ if (!trigger) {
3828
+ host.replaceChildren();
3829
+ return false;
3830
+ }
3831
+ trigger.click();
3832
+ return true;
3833
+ }
3834
+ catch {
3835
+ // A theme throwing on its own markup is a theme without a usable
3836
+ // quick-add: the caller's product link stands.
3837
+ return false;
3838
+ }
3839
+ };
3840
+ /**
3841
+ * A tile that cannot resolve a variant, clicked. Borrow the storefront's own
3842
+ * quick-add when there is one; otherwise leave the link untouched — on a
3843
+ * headless storefront it belongs to the host router, and hijacking it would
3844
+ * turn a client transition into a full page load.
3845
+ */
3846
+ const handleChooseOptionsClick = (event, { probe, handle, href, triggerSelector, }) => {
3847
+ if (event.defaultPrevented ||
3848
+ event.metaKey ||
3849
+ event.ctrlKey ||
3850
+ event.shiftKey ||
3851
+ event.altKey ||
3852
+ !probe.available(handle)) {
3853
+ return;
3854
+ }
3855
+ event.preventDefault();
3856
+ void openThemeQuickAdd({
3857
+ handle,
3858
+ ...(triggerSelector ? { triggerSelector } : {}),
3859
+ }).then((opened) => {
3860
+ // The theme looked like it had one and then did not: complete the journey
3861
+ // the shopper actually asked for rather than swallowing it.
3862
+ if (!opened)
3863
+ window.location.href = href;
3864
+ });
3865
+ };
3866
+ /**
3867
+ * That tile's call to action: an add wherever the storefront can actually open
3868
+ * one, and the detour named where it cannot. The same predicate the click
3869
+ * gates on, so the promise and the outcome agree.
3870
+ */
3871
+ const chooseOptionsLabel = (probe, handle, labels) => (probe.available(handle) ? labels.add : labels.detour);
3872
+
3436
3873
  const CSS = `
3437
3874
  .evs-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}
3438
3875
  .evs-search-page{box-sizing:border-box;max-width:var(--evs-max-width,1200px);margin:auto;padding:var(--evs-content-padding-top,24px) var(--evs-content-padding-inline,24px) var(--evs-content-padding-bottom,24px);color:var(--evs-ink);background:var(--evs-background);font-family:var(--evs-font-family)}
@@ -3693,7 +4130,14 @@ const VariantRow = ({ product, design, data, selection, onSelect, onRequestMatri
3693
4130
  ? `Selected ${selectionText}`
3694
4131
  : 'No product options selected' })] }));
3695
4132
  };
3696
- const ProductCard = ({ product, design, onClick, data, selection, onSelect, onRequestMatrix, onAddToCart, quickAddState, variantBusy, hrefFor, }) => {
4133
+ /** The link a tile that cannot resolve a variant carries, and its handoff. */
4134
+ const ChooseOptions = ({ className, href, handle, probe, triggerSelector, children, }) => (jsx("a", { className: className, href: href, onClick: (event) => handleChooseOptionsClick(event, {
4135
+ probe,
4136
+ handle,
4137
+ href,
4138
+ ...(triggerSelector ? { triggerSelector } : {}),
4139
+ }), children: children }));
4140
+ const ProductCard = ({ product, design, onClick, data, selection, onSelect, onRequestMatrix, onAddToCart, quickAddState, variantBusy, hrefFor, quickAddProbe, }) => {
3697
4141
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13;
3698
4142
  const lead = product.variants[0];
3699
4143
  const selectedSummary = exactSelectedVariant(data, selection);
@@ -3806,7 +4250,12 @@ const ProductCard = ({ product, design, onClick, data, selection, onSelect, onRe
3806
4250
  ? design.labels.soldOut
3807
4251
  : (quickAddState === null || quickAddState === void 0 ? void 0 : quickAddState.checking)
3808
4252
  ? (_12 = design.labels.checkingAvailability) !== null && _12 !== void 0 ? _12 : 'Checking availability…'
3809
- : atc.label })) : (jsx("a", { className: `evs-atc evs-atc-${atc.style} evs-atc-choose-options`, href: hrefFor(product, cartTarget === null || cartTarget === void 0 ? void 0 : cartTarget.id), children: (_13 = design.labels.chooseOptions) !== null && _13 !== void 0 ? _13 : 'Choose options' })))] }));
4253
+ : atc.label })) : (jsx(ChooseOptions, { className: `evs-atc evs-atc-${atc.style} evs-atc-choose-options`, href: hrefFor(product, cartTarget === null || cartTarget === void 0 ? void 0 : cartTarget.id), handle: product.handle, probe: quickAddProbe, ...(atc.quickAddTriggerSelector
4254
+ ? { triggerSelector: atc.quickAddTriggerSelector }
4255
+ : {}), children: chooseOptionsLabel(quickAddProbe, product.handle, {
4256
+ add: atc.label,
4257
+ detour: (_13 = design.labels.chooseOptions) !== null && _13 !== void 0 ? _13 : 'Choose options',
4258
+ }) })))] }));
3810
4259
  };
3811
4260
  const PromoCard = ({ card }) => {
3812
4261
  var _a, _b, _c, _d;
@@ -4135,11 +4584,14 @@ const EvoSearchSearchPage = ({ initialSearch, onUrlChange, fallback = null, base
4135
4584
  }));
4136
4585
  }
4137
4586
  };
4587
+ // One probe for the whole pass: its two page-level reads and its per-handle
4588
+ // card lookups walk the document, and every tile asks the same questions.
4589
+ const quickAddProbe = themeQuickAddProbe();
4138
4590
  // Product tiles plus merchant promo cards spliced into their grid slots
4139
4591
  // (first page only — a promo card is a landing moment, not a repeat).
4140
4592
  const gridCells = search.results.map((product) => {
4141
4593
  var _a;
4142
- return (jsx(ProductCard, { product: product, design: design, data: variantDataFor(product), selection: (_a = selectedOptions[product.id]) !== null && _a !== void 0 ? _a : {}, onSelect: (axis, value) => void selectProductOption(product, axis, value), onRequestMatrix: () => void requestVariantMatrix(product), onAddToCart: onAddToCart ? safelyAddToCart : undefined, quickAddState: quickAddStates[product.id], variantBusy: variantMatrixBusy.has(product.id), hrefFor: hrefFor, onClick: (selected, productUrl) => {
4594
+ return (jsx(ProductCard, { product: product, design: design, data: variantDataFor(product), selection: (_a = selectedOptions[product.id]) !== null && _a !== void 0 ? _a : {}, onSelect: (axis, value) => void selectProductOption(product, axis, value), onRequestMatrix: () => void requestVariantMatrix(product), onAddToCart: onAddToCart ? safelyAddToCart : undefined, quickAddState: quickAddStates[product.id], variantBusy: variantMatrixBusy.has(product.id), hrefFor: hrefFor, quickAddProbe: quickAddProbe, onClick: (selected, productUrl) => {
4143
4595
  var _a, _b;
4144
4596
  return search.trackClick(product.id, {
4145
4597
  productTitle: product.title,