> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://support.teeinblue.com/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Bulk Order for Personalized Products (only for Campaign by Product Base)

# Bulk Order for Personalized Products (Campaign by Product Base)

Selling personalized products often involves customers ordering multiple items with the **same design but different variants** (for example: different sizes, colors, or quantities).
The **Bulk Order setup** allows customers to customize a product **once**, then quickly add multiple variants of that same personalized design to the cart.
This approach works especially well for scenarios such as:
* Family shirts with the same design but different sizes
* Team merchandise with identical artwork
* Group gifts where each item shares the same personalization
This guide will show you how to implement a **Bulk Order workflow using Teeinblue**.

## Important Notes
Before setting up Bulk Order, please keep the following limitations in mind:
|| This setup only works with Campaign by Product Base.

|| The campaign must use one artwork applied to all variants.
* This ensures that the same personalized design can be reused when customers select multiple variants.
* These limitations are required because Teeinblue generates order designs based on the artworks applied to the campaign. If different artworks are linked to variants, the personalization form may change dynamically.

| Some knowledge of liquid code is required.

| Please backup your theme first to ensure that other features will not be affected if any editing errors occur.

| If you need assistance with this feature, please contact the support team, and we'll be happy to help.


# Overview of the Bulk Order Flow
With the Bulk Order setup:
1. Customers personalize the product **once**.
2. They can then select **multiple variants and quantities** in a bulk order form.
3. All selected variants are added to the cart **with the same personalized artwork**.
![](https://storage.crisp.chat/users/helpdesk/website/-/6/0/8/5/6085dc2a7293d800/image_12r9f0l.png)


# Step 1: Create a product template
To keep the implementation consistent across products, you should create a dedicated product template.


### How to create it
* Go to **Shopify Admin → Online Store → Themes**
* Create new Product template: `teeinblue-bulk-order`![](https://storage.crisp.chat/users/helpdesk/website/-/6/0/8/5/6085dc2a7293d800/image_hnsmm5.png)
* After creating the template, assign it to the products that will use the Bulk Order layout.   ![](https://storage.crisp.chat/users/helpdesk/website/-/6/0/8/5/6085dc2a7293d800/image_1ib5q23.png)



# Step 2: Prepare Your Campaign in Teeinblue
Create a campaign normally in Teeinblue, but ensure it follows these rules.
### Campaign requirements
* Campaign type: **Campaign by Product Base**
* Only **one artwork** used across the campaign
* Artwork must apply to **all variants**
This ensures the personalization data remains consistent when customers order multiple variants.

# Step 3: Add Bulk Order Layout to the Product Page
Next, we need to insert the Bulk Order interface into the product template.
* Create this file: `snippets/teeinblue-bundle.liquid`
```
{{ 'teeinblue-bundle.css' | asset_url | stylesheet_tag }}
<script>
  (() => {
    /* Delay `autofixer` to avoid 429 and ATC too many times.  */
    window.teeinblue = {
      ...(window.teeinblue || {}),
      autofixerDelay: 10000
    }
    
    /** Constants and shared state - START */


    /** Namespace prefix for bundle-specific classes and data attributes. */
    const TEE_BUNDLE_PREFIX = 'tee_bundle';


    /** Event emitted by Teeinblue after the campaign UI is injected. */
    const TEE_BUNDLE_INJECT_EVENT = 'teeinblue-event-component-injected';


    /** Event to dispatch for ajax cart updating */
    const TEE_EVENTS_AFTER_CART_ADDED = [
      'cart:refresh', // for Kalles theme
      {% comment %} 'teeinblue-event-after-cart-added' {% endcomment %}
    ]


    /** Timeout for a single native add-to-cart cycle. */
    const TEE_BUNDLE_ADD_TIMEOUT = 18000;


    /** Timeout while waiting for the live artwork form to be available. */
    const TEE_BUNDLE_FORM_TIMEOUT = 8000;


    /** Timeout while waiting for the native add-to-cart button to become ready. */
    const TEE_BUNDLE_BUTTON_TIMEOUT = 10000;


    /** Timeout while waiting for Teeinblue lifecycle hooks. */
    const TEE_BUNDLE_HOOK_TIMEOUT = 10000;


    /** Polling interval while waiting for the cart to update. */
    const TEE_BUNDLE_CART_POLL_INTERVAL = 500;


    /** Hard limit for dynamic controller reconciliation loops. */
    const TEE_BUNDLE_VARIANT_APPLY_MAX_STEPS = 24;


    /** Console prefix for bundle logs. */
    const TEE_BUNDLE_LOG_PREFIX = '[tee_bundle]';


    /** Bridge state used to capture the next Teeinblue lifecycle hook payload. */
    const tee_bundle_hookBridge = {
      activeCapture: null,
    };


    /** Constants and shared state - END */


    /** Debug logging - START */


    /** Write an informational debug log. */
    function tee_bundle_log(...args) {
      console.log(TEE_BUNDLE_LOG_PREFIX, ...args);
    }


    /** Write a warning debug log. */
    function tee_bundle_warn(...args) {
      console.warn(TEE_BUNDLE_LOG_PREFIX, ...args);
    }


    /** Write an error debug log. */
    function tee_bundle_error(...args) {
      console.error(TEE_BUNDLE_LOG_PREFIX, ...args);
    }


    /** Debug logging - END */


    /** Timing helpers - START */


    /** Sleep for a fixed amount of time. */
    function tee_bundle_wait(ms) {
      return new Promise((resolve) => setTimeout(resolve, ms));
    }


    /** Timing helpers - END */


    /** Text, formatting và money helpers - START */


    /** Escape HTML before rendering text inside injected markup. */
    function tee_bundle_escapeHtml(value) {
      const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
      };


      return String(value == null ? '' : value).replace(/[&<>"']/g, (char) => map[char]);
    }


    /** Escape CSS selectors that may contain special characters. */
    function tee_bundle_cssEscape(value) {
      if (window.CSS && typeof window.CSS.escape === 'function') {
        return window.CSS.escape(value);
      }


      return String(value).replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\$1');
    }


    /** Normalize text for stable matching across different Teeinblue controls. */
    function tee_bundle_normalizeText(value) {
      return String(value == null ? '' : value)
        .replace(/\s+/g, ' ')
        .replace(/[“”]/g, '"')
        .replace(/[‘’]/g, "'")
        .trim()
        .toLowerCase();
    }


    /** Compare two values after normalization. */
    function tee_bundle_isSameText(a, b) {
      return tee_bundle_normalizeText(a) === tee_bundle_normalizeText(b);
    }


    /** Deduplicate a list of display values while preserving order. */
    function tee_bundle_uniqueTexts(values) {
      const output = [];
      const seen = new Set();


      values.forEach((value) => {
        const normalized = tee_bundle_normalizeText(value);
        if (!normalized || seen.has(normalized)) return;


        seen.add(normalized);
        output.push(String(value).trim());
      });


      return output;
    }


    /** Resolve the active storefront currency. */
    function tee_bundle_getCurrencyCode() {
      return (
        window.teeinblueShop?.shopCurrency ||
        window.Shopify?.currency?.active ||
        window.Shopify?.currency?.default ||
        window.teeinblueCampaign?.currency ||
        'USD'
      );
    }


    /** Format a price amount expressed in the store's minor unit. */
    function tee_bundle_formatMoney(amount) {
      const safeAmount = Number(amount || 0) / 100;
      const currency = tee_bundle_getCurrencyCode();


      try {
        return new Intl.NumberFormat(undefined, {
          style: 'currency',
          currency,
        }).format(safeAmount);
      } catch (error) {
        return `${currency} ${safeAmount.toFixed(2)}`;
      }
    }


    /** Create a safe deep clone for logging or payload validation. */
    function tee_bundle_deepClone(value) {
      if (typeof structuredClone === 'function') {
        try {
          return structuredClone(value);
        } catch (error) {}
      }


      try {
        return JSON.parse(JSON.stringify(value));
      } catch (error) {
        return value;
      }
    }


    /** Text, formatting và money helpers - END */


    /** DOM helpers - START */


    /** Dispatch synthetic input and change events for a form control. */
    function tee_bundle_dispatchChange(el) {
      if (!el) return;
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
    }


    /** Emit a generic cart refresh event for themes that listen to it. */
    function tee_bundle_emitCartRefresh() {
      TEE_EVENTS_AFTER_CART_ADDED.forEach(eventName => document.dispatchEvent(new CustomEvent(eventName)));
    }


    /**
     * Auto-scroll blocking is intentionally disabled.
     * The bundle flow must never prevent user-driven scrolling.
     */
    function tee_bundle_holdScrollPosition() {


      const startX = window.scrollX || window.pageXOffset || 0;
      const startY = window.scrollY || window.pageYOffset || 0;
      const previousScrollRestoration = 'scrollRestoration' in history ? history.scrollRestoration : null;


      let active = true;
      let rafId = null;


      if (previousScrollRestoration !== null) {
        try {
          history.scrollRestoration = 'manual';
        } catch (error) {}
      }


      const restore = () => {
        window.scrollTo(startX, startY);
      };


      const tick = () => {
        if (!active) return;


        const currentX = window.scrollX || window.pageXOffset || 0;
        const currentY = window.scrollY || window.pageYOffset || 0;


        if (Math.abs(currentX - startX) > 1 || Math.abs(currentY - startY) > 1) {
          restore();
        }


        rafId = window.requestAnimationFrame(tick);
      };


      restore();
      rafId = window.requestAnimationFrame(tick);


      return () => {
        active = false;


        if (rafId) {
          window.cancelAnimationFrame(rafId);
        }


        restore();


        if (previousScrollRestoration !== null) {
          try {
            history.scrollRestoration = previousScrollRestoration;
          } catch (error) {}
        }
      };
    }


    /** Check whether an element is visible in the current document flow. */
    function tee_bundle_isVisible(el) {
      if (!el || !el.isConnected) return false;
      return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
    }


    /** DOM helpers - END */


    /** Campaign data - START */


    /** Return the current Teeinblue campaign object when available. */
    function tee_bundle_getCampaign() {
      const campaign = window.teeinblueCampaign;
      if (!campaign || typeof campaign !== 'object') return null;
      return campaign;
    }


    /** Check whether the current page is a Teeinblue product page. */
    function tee_bundle_isSupportedCampaign() {
      const campaign = tee_bundle_getCampaign();
      return !!(campaign && campaign.isTeeInBlueProduct === true);
    }


    /** Read all bundle-capable campaign variants from window.teeinblueCampaign.variantsById. */
    function tee_bundle_getCampaignVariants() {
      const campaign = tee_bundle_getCampaign();
      const variantsById = campaign?.variantsById;


      if (!variantsById || typeof variantsById !== 'object') {
        return [];
      }


      return Object.values(variantsById)
        .map((variant) => {
          const options = Array.isArray(variant.options)
            ? variant.options.map((item) => String(item || '').trim()).filter(Boolean)
            : [variant.option1, variant.option2, variant.option3]
                .map((item) => String(item || '').trim())
                .filter(Boolean);


          return {
            id: Number(variant.id),
            title: String(variant.public_title || variant.title || '').trim(),
            options,
            available: variant.available !== false,
            price: Number(variant.price || 0),
            raw: variant,
          };
        })
        .filter((variant) => Number.isFinite(variant.id) && variant.id > 0);
    }


    /** Resolve a campaign variant by id. */
    function tee_bundle_findCampaignVariantById(variantId) {
      return tee_bundle_getCampaignVariants().find((item) => String(item.id) === String(variantId)) || null;
    }


    /** Campaign data - END */


    /** Artwork root resolution - START */


    /** Resolve the Teeinblue artwork root. */
    function tee_bundle_getArtworkRoot(root = document) {
      if (root instanceof HTMLElement && root.id === 'tee-artwork-form') {
        return root;
      }


      return root.querySelector?.('#tee-artwork-form') || null;
    }


    /** Resolve the parent form associated with the Teeinblue artwork root. */
    function tee_bundle_getArtworkForm(artworkRoot) {
      return artworkRoot?.closest('form') || null;
    }


    /** Resolve the native Teeinblue add-to-cart button. */
    function tee_bundle_getTeeAtcButton(artworkRoot) {
      return tee_bundle_getArtworkForm(artworkRoot)?.querySelector('#teeAtcButton') || null;
    }


    /** Resolve all quantity inputs associated with the native Teeinblue form. */
    function tee_bundle_getQuantityInputs(artworkRoot) {
      const form = tee_bundle_getArtworkForm(artworkRoot);
      if (!form) return [];


      const nodes = Array.from(
        form.querySelectorAll('.tee-form-actions .tee-quantity-input, .tee-form-actions input[name="quantity"]')
      );


      return Array.from(new Set(nodes));
    }


    /** Build a stable identity for the current Teeinblue form instance. */
    function tee_bundle_getArtworkIdentity(artworkRoot) {
      const form = tee_bundle_getArtworkForm(artworkRoot);


      return {
        formId: form?.id || '',
        productId: form?.getAttribute('data-productid') || '',
        sectionId:
          form?.querySelector('input[name="section-id"]')?.value ||
          form?.querySelector('input[name="section_id"]')?.value ||
          '',
      };
    }


    /** Score a candidate artwork root during live re-resolution. */
    function tee_bundle_scoreArtworkRoot(artworkRoot, identity) {
      let score = 0;


      if (!(artworkRoot instanceof HTMLElement)) return score;
      if (artworkRoot.id === 'tee-artwork-form') score += 4;
      if (tee_bundle_isVisible(artworkRoot)) score += 4;


      const form = tee_bundle_getArtworkForm(artworkRoot);
      if (form) score += 2;


      if (identity.formId && form?.id === identity.formId) score += 6;
      if (identity.productId && (form?.getAttribute('data-productid') || '') === identity.productId) score += 4;


      const sectionId =
        form?.querySelector('input[name="section-id"]')?.value ||
        form?.querySelector('input[name="section_id"]')?.value ||
        '';


      if (identity.sectionId && sectionId === identity.sectionId) score += 4;
      if (tee_bundle_getTeeAtcButton(artworkRoot)) score += 2;


      return score;
    }


    /** Resolve the current live artwork root after any Teeinblue re-render. */
    function tee_bundle_resolveLiveArtworkRoot(artworkRootOrIdentity) {
      if (!artworkRootOrIdentity) return null;


      if (
        artworkRootOrIdentity instanceof HTMLElement &&
        artworkRootOrIdentity.isConnected &&
        artworkRootOrIdentity.id === 'tee-artwork-form'
      ) {
        return artworkRootOrIdentity;
      }


      const identity =
        artworkRootOrIdentity instanceof HTMLElement
          ? tee_bundle_getArtworkIdentity(artworkRootOrIdentity)
          : artworkRootOrIdentity;


      const candidates = Array.from(document.querySelectorAll('#tee-artwork-form'));
      candidates.sort((a, b) => tee_bundle_scoreArtworkRoot(b, identity) - tee_bundle_scoreArtworkRoot(a, identity));


      return candidates[0] || null;
    }


    /** Wait for the live artwork root to exist again after a re-render cycle. */
    async function tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity, timeout = TEE_BUNDLE_FORM_TIMEOUT) {
      const startedAt = Date.now();


      while (Date.now() - startedAt < timeout) {
        const liveArtworkRoot = tee_bundle_resolveLiveArtworkRoot(artworkRootOrIdentity);


        if (liveArtworkRoot) {
          return liveArtworkRoot;
        }


        await tee_bundle_wait(120);
      }


      throw new Error('The live Teeinblue artwork form could not be found.');
    }


    /** Wait for the native add-to-cart button to become ready again. */
    async function tee_bundle_waitForButtonReady(artworkRootOrIdentity, timeout = TEE_BUNDLE_BUTTON_TIMEOUT) {
      const startedAt = Date.now();


      while (Date.now() - startedAt < timeout) {
        const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
        const button = tee_bundle_getTeeAtcButton(liveArtworkRoot);


        if (
          button &&
          button.isConnected &&
          !button.disabled &&
          button.getAttribute('disabled') == null &&
          button.getAttribute('aria-disabled') !== 'true' &&
          !button.classList.contains('loading') &&
          !button.classList.contains('is-loading')
        ) {
          return { liveArtworkRoot, button };
        }


        await tee_bundle_wait(150);
      }


      throw new Error('The add-to-cart button did not become ready in time.');
    }


    /** Artwork root resolution - END */


    /** Teeinblue controller inspection - START */


    /** Build normalized option universes for each option index from campaign variants. */
    function tee_bundle_getCampaignOptionUniverses() {
      const variants = tee_bundle_getCampaignVariants();
      const universes = [];


      variants.forEach((variant) => {
        (variant.options || []).forEach((optionValue, optionIndex) => {
          if (!universes[optionIndex]) {
            universes[optionIndex] = new Set();
          }


          universes[optionIndex].add(tee_bundle_normalizeText(optionValue));
        });
      });


      return universes;
    }


    /** Resolve the visible Teeinblue option controllers currently rendered inside #tee-artwork-form. */
    function tee_bundle_getArtworkVariantControllers(artworkRoot) {
      if (!artworkRoot) return [];


      const controllers = [];


      const availableProductController =
        artworkRoot.querySelector('.tee-available-products .tee-field__input') ||
        artworkRoot.querySelector('.tee-available-products');


      if (availableProductController && tee_bundle_isVisible(availableProductController)) {
        controllers.push(availableProductController);
      }


      const variantControllers = Array.from(artworkRoot.querySelectorAll('.tee-block.tee-variants .tee-option')).filter(
        tee_bundle_isVisible
      );


      controllers.push(...variantControllers);


      return controllers;
    }


    /** Resolve the display text for a specific Teeinblue input. */
    function tee_bundle_getControllerInputDisplayText(artworkRoot, input) {
      if (!artworkRoot || !input) return '';


      const label = input.id ? artworkRoot.querySelector(`label[for="${tee_bundle_cssEscape(input.id)}"]`) : null;
      const labelText =
        label?.querySelector('span:not(.sr-only)')?.textContent ||
        label?.querySelector('.tee-radio-label__text')?.textContent ||
        label?.textContent ||
        '';


      const candidates = [
        label?.getAttribute('data-title'),
        label?.getAttribute('title'),
        input.getAttribute?.('data-title'),
        input.getAttribute?.('title'),
        labelText,
        input.value,
      ];


      return String(candidates.find((item) => tee_bundle_normalizeText(item)) || '').trim();
    }


    /** Resolve all selectable display values for a Teeinblue controller. */
    function tee_bundle_getControllerChoiceValues(artworkRoot, controller) {
      if (!artworkRoot || !controller) return [];


      const radioInputs = Array.from(controller.querySelectorAll('input[type="radio"]'));
      if (radioInputs.length) {
        return tee_bundle_uniqueTexts(
          radioInputs.map((input) => tee_bundle_getControllerInputDisplayText(artworkRoot, input))
        );
      }


      const select = controller.querySelector('select');
      if (select) {
        return tee_bundle_uniqueTexts(
          Array.from(select.options)
            .map((option) => String(option.textContent || option.value || '').trim())
            .filter(Boolean)
        );
      }


      return [];
    }


    /** Resolve the current selected value for a visible Teeinblue controller. */
    function tee_bundle_getControllerCurrentValue(artworkRoot, controller) {
      if (!artworkRoot || !controller) return '';


      const checkedInput = controller.querySelector('input[type="radio"]:checked');
      if (checkedInput) {
        return tee_bundle_getControllerInputDisplayText(artworkRoot, checkedInput);
      }


      const activeInput = controller.querySelector('.active input[type="radio"]');
      if (activeInput) {
        return tee_bundle_getControllerInputDisplayText(artworkRoot, activeInput);
      }


      const select = controller.querySelector('select');
      if (select) {
        const selectedOption = select.selectedOptions?.[0] || select.options?.[select.selectedIndex] || null;
        if (selectedOption) {
          return String(selectedOption.textContent || selectedOption.value || '').trim();
        }
      }


      return '';
    }


    /** Build descriptors for all currently visible Teeinblue controllers. */
    function tee_bundle_getArtworkControllerDescriptors(artworkRoot) {
      return tee_bundle_getArtworkVariantControllers(artworkRoot)
        .map((controller, index) => {
          const choiceValues = tee_bundle_getControllerChoiceValues(artworkRoot, controller);


          return {
            key: `controller-${index}`,
            index,
            controller,
            currentValue: tee_bundle_getControllerCurrentValue(artworkRoot, controller),
            choiceValues,
          };
        })
        .filter((descriptor) => descriptor.choiceValues.length > 0);
    }


    /**
     * Score how likely a controller belongs to a specific option index,
     * using campaign-wide option universes instead of UI labels.
     */
    function tee_bundle_scoreDescriptorForOptionIndex(descriptor, optionIndex, targetVariant = null) {
      const universes = tee_bundle_getCampaignOptionUniverses();
      const optionUniverse = universes[optionIndex] || new Set();


      let score = 0;


      descriptor.choiceValues.forEach((choiceValue) => {
        if (optionUniverse.has(tee_bundle_normalizeText(choiceValue))) {
          score += 2;
        }
      });


      if (descriptor.currentValue && optionUniverse.has(tee_bundle_normalizeText(descriptor.currentValue))) {
        score += 1;
      }


      if (targetVariant && targetVariant.options?.[optionIndex]) {
        const targetValue = targetVariant.options[optionIndex];


        if (descriptor.choiceValues.some((choiceValue) => tee_bundle_isSameText(choiceValue, targetValue))) {
          score += 6;
        }


        if (tee_bundle_isSameText(descriptor.currentValue, targetValue)) {
          score += 3;
        }
      }


      return score;
    }


    /**
     * Resolve candidate option indexes for a descriptor.
     * When targetVariant is provided, only indexes that can actually host the target value are returned.
     */
    function tee_bundle_getDescriptorCandidateIndexes(descriptor, targetVariant = null, mode = 'target') {
      const maxOptionCount = Math.max(
        ...tee_bundle_getCampaignVariants().map((variant) => variant.options?.length || 0),
        targetVariant?.options?.length || 0,
        0
      );


      const candidates = [];


      for (let optionIndex = 0; optionIndex < maxOptionCount; optionIndex += 1) {
        const score = tee_bundle_scoreDescriptorForOptionIndex(descriptor, optionIndex, targetVariant);
        if (score <= 0) continue;


        if (mode === 'target' && targetVariant?.options?.[optionIndex]) {
          const targetValue = targetVariant.options[optionIndex];
          const supportsTarget = descriptor.choiceValues.some((choiceValue) =>
            tee_bundle_isSameText(choiceValue, targetValue)
          );


          if (!supportsTarget) {
            continue;
          }
        }


        if (mode === 'current') {
          const universes = tee_bundle_getCampaignOptionUniverses();
          const optionUniverse = universes[optionIndex] || new Set();


          if (!descriptor.currentValue || !optionUniverse.has(tee_bundle_normalizeText(descriptor.currentValue))) {
            continue;
          }
        }


        candidates.push({
          optionIndex,
          score,
        });
      }


      candidates.sort((a, b) => b.score - a.score);


      return candidates;
    }


    /**
     * Assign visible controllers to unique option indexes.
     * This is label-independent and relies on campaign data plus the currently visible DOM values.
     */
    function tee_bundle_assignControllersToOptionIndexes(descriptors, targetVariant = null, mode = 'target') {
      if (!Array.isArray(descriptors) || !descriptors.length) {
        return null;
      }


      const items = descriptors.map((descriptor) => {
        return {
          descriptor,
          candidates: tee_bundle_getDescriptorCandidateIndexes(descriptor, targetVariant, mode),
        };
      });


      if (items.some((item) => !item.candidates.length)) {
        return null;
      }


      items.sort((a, b) => a.candidates.length - b.candidates.length);


      const assigned = new Map();
      const usedIndexes = new Set();


      function backtrack(pointer) {
        if (pointer >= items.length) {
          return true;
        }


        const item = items[pointer];


        for (const candidate of item.candidates) {
          if (usedIndexes.has(candidate.optionIndex)) continue;


          usedIndexes.add(candidate.optionIndex);
          assigned.set(item.descriptor.key, candidate.optionIndex);


          if (backtrack(pointer + 1)) {
            return true;
          }


          assigned.delete(item.descriptor.key);
          usedIndexes.delete(candidate.optionIndex);
        }


        return false;
      }


      return backtrack(0) ? assigned : null;
    }


    /** Infer the current campaign variant from the currently visible DOM state. */
    function tee_bundle_getCurrentArtworkVariant(artworkRoot) {
      const descriptors = tee_bundle_getArtworkControllerDescriptors(artworkRoot);
      if (!descriptors.length) return null;


      const variants = tee_bundle_getCampaignVariants();


      const matches = variants.filter((variant) => {
        const assignment = tee_bundle_assignControllersToOptionIndexes(descriptors, variant, 'target');
        if (!assignment) return false;


        return descriptors.every((descriptor) => {
          const optionIndex = assignment.get(descriptor.key);
          if (typeof optionIndex !== 'number') return false;


          const expectedValue = variant.options?.[optionIndex];
          if (!expectedValue) return false;


          return tee_bundle_isSameText(descriptor.currentValue, expectedValue);
        });
      });


      return matches[0] || null;
    }


    /** Build visible controller plans for a target variant. */
    function tee_bundle_getTargetControllerPlans(artworkRoot, targetVariant) {
      const descriptors = tee_bundle_getArtworkControllerDescriptors(artworkRoot);
      if (!descriptors.length) return [];


      const assignment = tee_bundle_assignControllersToOptionIndexes(descriptors, targetVariant, 'target');
      if (!assignment) return [];


      return descriptors
        .map((descriptor) => {
          const optionIndex = assignment.get(descriptor.key);
          if (typeof optionIndex !== 'number') return null;


          return {
            key: descriptor.key,
            controller: descriptor.controller,
            currentValue: descriptor.currentValue,
            choiceValues: descriptor.choiceValues,
            optionIndex,
            targetValue: targetVariant.options?.[optionIndex] || '',
          };
        })
        .filter(Boolean);
    }


    /**
     * Locate the best visible controller for a target option index without relying on labels.
     * This is especially important for Available Product because merchants can rename labels freely.
     */
    function tee_bundle_findBestControllerForOptionIndex(artworkRoot, targetVariant, optionIndex) {
      const descriptors = tee_bundle_getArtworkControllerDescriptors(artworkRoot);
      const targetValue = targetVariant?.options?.[optionIndex];


      if (!targetValue) return null;


      const candidates = descriptors
        .map((descriptor) => {
          const supportsTarget = descriptor.choiceValues.some((choiceValue) =>
            tee_bundle_isSameText(choiceValue, targetValue)
          );


          if (!supportsTarget) return null;


          return {
            descriptor,
            score: tee_bundle_scoreDescriptorForOptionIndex(descriptor, optionIndex, targetVariant),
          };
        })
        .filter(Boolean)
        .sort((a, b) => b.score - a.score);


      return candidates[0]?.descriptor || null;
    }


    /**
     * Wait until the DOM becomes compatible with the target product context after Available Product changes.
     * During Teeinblue re-render, old controllers may remain briefly; that state must not be treated as a hard failure.
     */
    async function tee_bundle_waitForTargetProductContext(artworkRootOrIdentity, targetVariant, timeout = 8000) {
      const startedAt = Date.now();


      while (Date.now() - startedAt < timeout) {
        const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
        const currentVariant = tee_bundle_getCurrentArtworkVariant(liveArtworkRoot);


        if (currentVariant && String(currentVariant.id) === String(targetVariant.id)) {
          return liveArtworkRoot;
        }


        const availableProductController = tee_bundle_findBestControllerForOptionIndex(
          liveArtworkRoot,
          targetVariant,
          0
        );
        const targetPlans = tee_bundle_getTargetControllerPlans(liveArtworkRoot, targetVariant);


        if (availableProductController || targetPlans.length) {
          return liveArtworkRoot;
        }


        await tee_bundle_wait(120);
      }


      throw new Error('The selected item is not available right now.');
    }


    /** Click a target value inside a visible controller. */
    async function tee_bundle_clickControllerValue(artworkRootOrIdentity, controller, targetValue) {
      const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
      if (!controller || !controller.isConnected) return false;


      const radioInputs = Array.from(controller.querySelectorAll('input[type="radio"]'));
      const matchingRadio = radioInputs.find((input) => {
        return tee_bundle_isSameText(tee_bundle_getControllerInputDisplayText(liveArtworkRoot, input), targetValue);
      });


      if (matchingRadio) {
        const label = matchingRadio.id
          ? liveArtworkRoot.querySelector(`label[for="${tee_bundle_cssEscape(matchingRadio.id)}"]`)
          : null;


        if (label) {
          label.click();
        } else {
          matchingRadio.checked = true;
          tee_bundle_dispatchChange(matchingRadio);
        }


        return true;
      }


      const select = controller.querySelector('select');
      if (select) {
        const option = Array.from(select.options).find((opt) => {
          return tee_bundle_isSameText(opt.textContent || opt.value || '', targetValue);
        });


        if (option) {
          select.value = option.value;
          tee_bundle_dispatchChange(select);
          return true;
        }
      }


      return false;
    }


    /**
     * Wait until a specific target value is reflected on the live DOM,
     * or until Teeinblue finishes a re-render and the DOM becomes compatible with the target variant.
     */
    async function tee_bundle_waitForControllerTransition(
      artworkRootOrIdentity,
      targetVariant,
      targetValue,
      timeout = 7000
    ) {
      const startedAt = Date.now();


      while (Date.now() - startedAt < timeout) {
        const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
        const currentVariant = tee_bundle_getCurrentArtworkVariant(liveArtworkRoot);


        if (currentVariant && String(currentVariant.id) === String(targetVariant.id)) {
          return liveArtworkRoot;
        }


        const plans = tee_bundle_getTargetControllerPlans(liveArtworkRoot, targetVariant);


        if (!plans.length) {
          await tee_bundle_wait(120);
          continue;
        }


        const explicitMatch = plans.find((plan) => tee_bundle_isSameText(plan.targetValue, targetValue));


        if (explicitMatch && tee_bundle_isSameText(explicitMatch.currentValue, explicitMatch.targetValue)) {
          return liveArtworkRoot;
        }


        const visiblePlansAligned = plans.every((plan) => tee_bundle_isSameText(plan.currentValue, plan.targetValue));
        if (visiblePlansAligned) {
          return liveArtworkRoot;
        }


        await tee_bundle_wait(120);
      }


      throw new Error(`The option "${targetValue}" could not be applied.`);
    }


    /**
     * Apply a target variant using only #tee-artwork-form and campaign data.
     * This implementation never relies on option labels and does not fail during transient DOM states.
     */
    async function tee_bundle_setArtworkVariant(artworkRootOrIdentity, targetVariant) {
      const startedAt = Date.now();
      let step = 0;


      tee_bundle_log('variant apply start', {
        variantId: targetVariant.id,
        title: targetVariant.title,
        options: targetVariant.options,
      });


      while (Date.now() - startedAt < 15000 && step < TEE_BUNDLE_VARIANT_APPLY_MAX_STEPS) {
        step += 1;


        const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
        const currentVariant = tee_bundle_getCurrentArtworkVariant(liveArtworkRoot);


        if (currentVariant && String(currentVariant.id) === String(targetVariant.id)) {
          tee_bundle_log('variant apply complete by exact match', {
            variantId: targetVariant.id,
            step,
          });
          return liveArtworkRoot;
        }


        /**
         * Step 1: force Available Product first when needed.
         * This is handled independently because changing it can invalidate or replace all remaining controllers.
         */
        const productController = tee_bundle_findBestControllerForOptionIndex(liveArtworkRoot, targetVariant, 0);


        if (productController && !tee_bundle_isSameText(productController.currentValue, targetVariant.options[0])) {
          tee_bundle_log('applying available product', {
            step,
            currentValue: productController.currentValue,
            targetValue: targetVariant.options[0],
          });


          const clicked = await tee_bundle_clickControllerValue(
            artworkRootOrIdentity,
            productController.controller,
            targetVariant.options[0]
          ).catch((error) => {
            tee_bundle_error('available product click failed', error);
            return false;
          });


          if (!clicked) {
            throw new Error(`The option "${targetVariant.options[0]}" is not available right now.`);
          }


          await tee_bundle_waitForTargetProductContext(artworkRootOrIdentity, targetVariant, 8000);
          await tee_bundle_waitForButtonReady(artworkRootOrIdentity, 8000);
          await tee_bundle_wait(250);
          continue;
        }


        /**
         * Step 2: after product context is correct, reconcile the remaining visible controllers.
         * If the DOM is still in transition and cannot be mapped yet, keep waiting instead of failing.
         */
        const plans = tee_bundle_getTargetControllerPlans(liveArtworkRoot, targetVariant);


        tee_bundle_log('variant reconciliation', {
          step,
          plans: plans.map((plan) => ({
            optionIndex: plan.optionIndex,
            currentValue: plan.currentValue,
            targetValue: plan.targetValue,
            choiceValues: plan.choiceValues,
          })),
        });


        if (!plans.length) {
          await tee_bundle_wait(120);
          continue;
        }


        const mismatch = plans.find((plan) => !tee_bundle_isSameText(plan.currentValue, plan.targetValue));


        if (!mismatch) {
          await tee_bundle_waitForButtonReady(artworkRootOrIdentity, 8000);
          await tee_bundle_wait(350);


          const reResolvedRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity);
          const maybeVariant = tee_bundle_getCurrentArtworkVariant(reResolvedRoot);


          if (maybeVariant && String(maybeVariant.id) === String(targetVariant.id)) {
            tee_bundle_log('variant apply complete after stabilization', {
              variantId: targetVariant.id,
              step,
            });
            return reResolvedRoot;
          }


          tee_bundle_warn('visible controllers are aligned; exact variant could not be confirmed from DOM only', {
            targetVariantId: targetVariant.id,
            targetTitle: targetVariant.title,
          });


          return reResolvedRoot;
        }


        tee_bundle_log('applying controller mismatch', {
          step,
          optionIndex: mismatch.optionIndex,
          currentValue: mismatch.currentValue,
          targetValue: mismatch.targetValue,
        });


        const clicked = await tee_bundle_clickControllerValue(
          artworkRootOrIdentity,
          mismatch.controller,
          mismatch.targetValue
        ).catch((error) => {
          tee_bundle_error('controller click failed', {
            step,
            optionIndex: mismatch.optionIndex,
            targetValue: mismatch.targetValue,
            error,
          });


          return false;
        });


        if (!clicked) {
          throw new Error(`The option "${mismatch.targetValue}" is not available right now.`);
        }


        await tee_bundle_waitForControllerTransition(artworkRootOrIdentity, targetVariant, mismatch.targetValue, 7000);


        await tee_bundle_waitForLiveArtworkRoot(artworkRootOrIdentity, 7000);
        await tee_bundle_waitForButtonReady(artworkRootOrIdentity, 8000);
        await tee_bundle_wait(250);
      }


      throw new Error(`The variant "${targetVariant.title}" could not be applied.`);
    }


    /** Teeinblue controller inspection - END */


    /** Quantity và snapshot helpers - START */


    /** Update all native Teeinblue quantity inputs for the current form. */
    function tee_bundle_setArtworkQuantity(artworkRoot, quantity) {
      const safeQty = Math.max(1, Number(quantity) || 1);


      tee_bundle_getQuantityInputs(artworkRoot).forEach((input) => {
        input.value = safeQty;
        tee_bundle_dispatchChange(input);
      });
    }


    /** Snapshot the current artwork state before the bundle flow mutates it. */
    function tee_bundle_snapshotArtworkState(artworkRoot) {
      const currentVariant = tee_bundle_getCurrentArtworkVariant(artworkRoot);


      return {
        artworkIdentity: tee_bundle_getArtworkIdentity(artworkRoot),
        variantId: currentVariant?.id || null,
        quantityValues: tee_bundle_getQuantityInputs(artworkRoot).map((input) => String(input.value || '1')),
      };
    }


    /** Restore all quantity inputs to their original values. */
    function tee_bundle_restoreArtworkQuantityState(artworkRoot, quantityValues) {
      const inputs = tee_bundle_getQuantityInputs(artworkRoot);


      if (!Array.isArray(quantityValues) || !inputs.length) return;


      inputs.forEach((input, index) => {
        input.value = quantityValues[index] || quantityValues[0] || '1';
        tee_bundle_dispatchChange(input);
      });
    }


    /** Restore the original artwork state after the bundle flow completes. */
    async function tee_bundle_restoreArtworkState(snapshot) {
      if (!snapshot) return;


      tee_bundle_log('restoring artwork state', snapshot);


      const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(snapshot.artworkIdentity);


      if (snapshot.variantId) {
        const targetVariant = tee_bundle_findCampaignVariantById(snapshot.variantId);


        if (targetVariant) {
          await tee_bundle_setArtworkVariant(snapshot.artworkIdentity, targetVariant);
        }
      }


      const restoredArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(snapshot.artworkIdentity);
      tee_bundle_restoreArtworkQuantityState(restoredArtworkRoot, snapshot.quantityValues);


      await tee_bundle_wait(220);
    }


    /** Quantity và snapshot helpers - END */


    /** Cart API và rollback - START */


    /** Read the current cart state from /cart.js. */
    async function tee_bundle_fetchCart() {
      const response = await fetch('/cart.js', {
        headers: {
          Accept: 'application/json',
        },
      });


      if (!response.ok) {
        throw new Error('Unable to read the current cart.');
      }


      return response.json();
    }


    /** Resolve the total cart item count from a cart payload. */
    function tee_bundle_getCartItemCount(cart) {
      if (typeof cart?.item_count === 'number') {
        return cart.item_count;
      }


      return (cart?.items || []).reduce((sum, item) => {
        return sum + (Number(item.quantity) || 0);
      }, 0);
    }


    /** Wait until the cart item count increases after a native Teeinblue add-to-cart action. */
    function tee_bundle_waitForCartIncrease(beforeCount, expectedIncrease) {
      return (async () => {
        const startedAt = Date.now();


        while (Date.now() - startedAt < TEE_BUNDLE_ADD_TIMEOUT) {
          const afterCart = await tee_bundle_fetchCart();
          const afterCount = tee_bundle_getCartItemCount(afterCart);


          if (afterCount >= beforeCount + expectedIncrease) {
            return afterCart;
          }


          await tee_bundle_wait(TEE_BUNDLE_CART_POLL_INTERVAL);
        }


        throw new Error('The cart did not update after adding the selected item.');
      })();
    }


    /** Update cart line quantities using cart/update.js and line keys. */
    async function tee_bundle_updateCartByKeyQuantities(updates) {
      const params = new URLSearchParams();


      updates.forEach((quantity, key) => {
        params.append(`updates[${key}]`, String(quantity));
      });


      const response = await fetch('/cart/update.js', {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        },
        body: params.toString(),
      });


      if (!response.ok) {
        throw new Error('Unable to restore the previous cart state.');
      }


      return response.json();
    }


    /** Restore the cart back to its original snapshot if the bundle flow fails. */
    async function tee_bundle_rollbackCartToSnapshot(originalCart) {
      const currentCart = await tee_bundle_fetchCart();


      const originalQtyByKey = new Map(
        (originalCart?.items || []).map((item) => [item.key, Number(item.quantity) || 0])
      );


      const updates = new Map();


      for (const item of currentCart?.items || []) {
        const originalQty = originalQtyByKey.get(item.key) || 0;
        const currentQty = Number(item.quantity) || 0;


        if (currentQty !== originalQty) {
          updates.set(item.key, originalQty);
        }
      }


      if (!updates.size) {
        return currentCart;
      }


      tee_bundle_log('rolling back cart', {
        updates: Array.from(updates.entries()),
      });


      const restoredCart = await tee_bundle_updateCartByKeyQuantities(updates);
      await tee_bundle_wait(250);
      tee_bundle_emitCartRefresh();


      return restoredCart;
    }


    /** Cart API và rollback - END */


    /** Teeinblue hooks bridge - START */


    /** Resolve the Teeinblue API object on window. */
    function tee_bundle_getTeeinblueApi() {
      if (!window.teeinblue || typeof window.teeinblue !== 'object') {
        return null;
      }


      return window.teeinblue;
    }


    /** Clear the active hook capture session. */
    function tee_bundle_clearHookCapture() {
      const capture = tee_bundle_hookBridge.activeCapture;


      if (capture?.timer) {
        clearTimeout(capture.timer);
      }


      tee_bundle_hookBridge.activeCapture = null;
    }


    /** Resolve the active hook capture promise. */
    function tee_bundle_resolveHookCapture(payload) {
      const capture = tee_bundle_hookBridge.activeCapture;
      if (!capture || capture.done) return;


      capture.done = true;
      const resolve = capture.resolve;
      tee_bundle_clearHookCapture();
      resolve(payload);
    }


    /** Reject the active hook capture promise. */
    function tee_bundle_rejectHookCapture(error) {
      const capture = tee_bundle_hookBridge.activeCapture;
      if (!capture || capture.done) return;


      capture.done = true;
      const reject = capture.reject;
      tee_bundle_clearHookCapture();
      reject(error);
    }


    /**
     * Install wrappers around the Teeinblue lifecycle hooks while preserving any existing implementation.
     * The captured payload is used to validate the exact variant and quantity before the cart update is accepted.
     */
    function tee_bundle_installTeeinblueHooks() {
      const api = tee_bundle_getTeeinblueApi();
      if (!api) return false;


      if (!Object.prototype.hasOwnProperty.call(api, '__tee_bundle_originalTransformBeforeAddToCart')) {
        api.__tee_bundle_originalTransformBeforeAddToCart = api.transformBeforeAddToCart;
      }


      if (!Object.prototype.hasOwnProperty.call(api, '__tee_bundle_originalBeforeAddToCart')) {
        api.__tee_bundle_originalBeforeAddToCart = api.beforeAddToCart;
      }


      if (!api.__tee_bundle_transformWrapped) {
        api.transformBeforeAddToCart = async function (input) {
          tee_bundle_log('transformBeforeAddToCart', tee_bundle_deepClone(input));


          tee_bundle_resolveHookCapture({
            hook: 'transformBeforeAddToCart',
            input: tee_bundle_deepClone(input),
          });


          const original = api.__tee_bundle_originalTransformBeforeAddToCart;
          if (typeof original === 'function') {
            return await original(input);
          }


          return input;
        };


        api.__tee_bundle_transformWrapped = true;
      }


      if (!api.__tee_bundle_beforeWrapped) {
        api.beforeAddToCart = async function (customizationId, customizationData, personalizedData) {
          tee_bundle_log('beforeAddToCart', {
            customizationId,
            customizationData: tee_bundle_deepClone(customizationData),
            personalizedData: tee_bundle_deepClone(personalizedData),
          });


          tee_bundle_resolveHookCapture({
            hook: 'beforeAddToCart',
            customizationId,
            customizationData: tee_bundle_deepClone(customizationData),
            personalizedData: tee_bundle_deepClone(personalizedData),
          });


          const original = api.__tee_bundle_originalBeforeAddToCart;
          if (typeof original === 'function') {
            return await original(customizationId, customizationData, personalizedData);
          }
        };


        api.__tee_bundle_beforeWrapped = true;
      }


      return true;
    }


    /** Open a single-use capture session for the next native Teeinblue add-to-cart call. */
    function tee_bundle_openHookCapture() {
      if (!tee_bundle_installTeeinblueHooks()) {
        throw new Error('Teeinblue hooks are not available on this page.');
      }


      tee_bundle_clearHookCapture();


      return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          tee_bundle_rejectHookCapture(new Error('Teeinblue did not complete the add-to-cart lifecycle in time.'));
        }, TEE_BUNDLE_HOOK_TIMEOUT);


        tee_bundle_hookBridge.activeCapture = {
          resolve,
          reject,
          timer,
          done: false,
        };
      });
    }


    /** Validate the captured Teeinblue hook payload against the expected selection. */
    function tee_bundle_validateHookPayload(captured, selected) {
      if (!captured) return;


      if (captured.hook === 'transformBeforeAddToCart' && captured.input) {
        const inputId = Number(captured.input.id);
        const inputQuantity = Number(captured.input.quantity || 1);


        if (Number.isFinite(inputId) && inputId !== Number(selected.variant.id)) {
          throw new Error(`Teeinblue generated payload for variant ${inputId} instead of ${selected.variant.id}.`);
        }


        if (inputQuantity !== Number(selected.quantity)) {
          throw new Error(
            `Teeinblue generated quantity ${inputQuantity} instead of ${selected.quantity} for variant ${selected.variant.id}.`
          );
        }
      }
    }


    /** Teeinblue hooks bridge - END */


    /** Bundle UI rendering và state - START */


    /** Render the bundle UI using campaign variant data. */
    function tee_bundle_renderBundle(variants) {
      const wrapper = document.createElement('section');
      wrapper.className = `${TEE_BUNDLE_PREFIX}-bundle`;
      wrapper.setAttribute('data-tee-bundle-bundle', 'true');


      const rowsHtml = variants
        .map((variant) => {
          return `
            <div class="${TEE_BUNDLE_PREFIX}-row" data-tee-bundle-variant-id="${variant.id}">
              <label class="${TEE_BUNDLE_PREFIX}-variant">
                <input
                  type="checkbox"
                  class="${TEE_BUNDLE_PREFIX}-checkbox"
                  data-tee-bundle-checkbox
                  value="${variant.id}"
                >
                <span class="${TEE_BUNDLE_PREFIX}-variant-meta">
                  <span class="${TEE_BUNDLE_PREFIX}-variant-title">${tee_bundle_escapeHtml(variant.title)}</span>
                  <span class="${TEE_BUNDLE_PREFIX}-variant-price">
                    ${tee_bundle_escapeHtml(tee_bundle_formatMoney(variant.price))}
                  </span>
                </span>
              </label>


              <div class="${TEE_BUNDLE_PREFIX}-qty">
                <button type="button" class="${TEE_BUNDLE_PREFIX}-qty-btn" data-tee-bundle-qty="minus" disabled>-</button>
                <input
                  type="number"
                  min="1"
                  step="1"
                  value="1"
                  class="${TEE_BUNDLE_PREFIX}-qty-input"
                  data-tee-bundle-qty-input
                  disabled
                >
                <button type="button" class="${TEE_BUNDLE_PREFIX}-qty-btn" data-tee-bundle-qty="plus" disabled>+</button>
              </div>
            </div>
          `;
        })
        .join('');


      wrapper.innerHTML = `
        <div class="${TEE_BUNDLE_PREFIX}-header">
          <div>
            <h3 class="${TEE_BUNDLE_PREFIX}-title">Bundle Add to Cart</h3>
            <p class="${TEE_BUNDLE_PREFIX}-desc">Choose multiple items and add them together.</p>
          </div>
        </div>


        <div class="${TEE_BUNDLE_PREFIX}-list">
          ${rowsHtml}
        </div>


        <div class="${TEE_BUNDLE_PREFIX}-summary">
          <span class="${TEE_BUNDLE_PREFIX}-summary-label">Bundle total</span>
          <strong class="${TEE_BUNDLE_PREFIX}-summary-value" data-tee-bundle-total>${tee_bundle_formatMoney(0)}</strong>
        </div>


        <div class="${TEE_BUNDLE_PREFIX}-actions">
          <button type="button" class="${TEE_BUNDLE_PREFIX}-atc" data-tee-bundle-bundle-atc>
            <span class="${TEE_BUNDLE_PREFIX}-atc-label">Add Selected Items to Cart</span>
          </button>
        </div>


        <div class="${TEE_BUNDLE_PREFIX}-status" data-tee-bundle-status aria-live="polite"></div>
      `;


      return wrapper;
    }


    /** Update the bundle status message using user-facing language. */
    function tee_bundle_setStatus(root, message, type = 'info') {
      const status = root.querySelector('[data-tee-bundle-status]');
      if (!status) return;


      status.className = `${TEE_BUNDLE_PREFIX}-status is-${type}`;
      status.textContent = message || '';
    }


    /** Toggle the enabled state of an individual bundle row. */
    function tee_bundle_toggleRow(row, checked) {
      row.classList.toggle('is-selected', checked);


      row.querySelectorAll('[data-tee-bundle-qty-input], [data-tee-bundle-qty]').forEach((el) => {
        el.disabled = !checked;
      });


      const input = row.querySelector('[data-tee-bundle-qty-input]');
      if (input && (!input.value || Number(input.value) < 1)) {
        input.value = 1;
      }
    }


    /** Read all selected bundle items from the bundle UI. */
    function tee_bundle_getSelectedBundleItems(root, variants) {
      return Array.from(root.querySelectorAll(`.${TEE_BUNDLE_PREFIX}-row`))
        .map((row) => {
          const checkbox = row.querySelector('[data-tee-bundle-checkbox]');
          const qtyInput = row.querySelector('[data-tee-bundle-qty-input]');
          const variantId = Number(row.getAttribute('data-tee-bundle-variant-id'));
          const variant = variants.find((item) => item.id === variantId);


          if (!checkbox || !qtyInput || !variant || !checkbox.checked) return null;


          return {
            variant,
            quantity: Math.max(1, Number(qtyInput.value) || 1),
          };
        })
        .filter(Boolean);
    }


    /** Recalculate and render the bundle total. */
    function tee_bundle_updateTotal(root, variants) {
      const totalNode = root.querySelector('[data-tee-bundle-total]');
      if (!totalNode) return;


      const selectedItems = tee_bundle_getSelectedBundleItems(root, variants);
      const totalAmount = selectedItems.reduce((sum, item) => {
        return sum + item.variant.price * item.quantity;
      }, 0);


      totalNode.textContent = tee_bundle_formatMoney(totalAmount);
    }


    /** Toggle the loading state of the bundle add-to-cart button. */
    function tee_bundle_setButtonLoading(root, isLoading) {
      const button = root.querySelector('[data-tee-bundle-bundle-atc]');
      if (!button) return;


      if (!button.dataset.defaultHtml) {
        button.dataset.defaultHtml = button.innerHTML;
      }


      if (isLoading) {
        button.classList.add('is-loading');
        button.innerHTML = `
          <span class="${TEE_BUNDLE_PREFIX}-spinner" aria-hidden="true"></span>
          <span class="${TEE_BUNDLE_PREFIX}-atc-label">Adding...</span>
        `;
      } else {
        button.classList.remove('is-loading');
        button.innerHTML = button.dataset.defaultHtml;
      }
    }


    /** Lock or unlock the bundle UI during processing. */
    function tee_bundle_setLoading(root, isLoading) {
      root.classList.toggle('is-loading', isLoading);
      tee_bundle_setButtonLoading(root, isLoading);


      root.querySelectorAll('button, input[type="checkbox"], input[type="number"]').forEach((el) => {
        if (el.matches('[data-tee-bundle-checkbox]')) {
          el.disabled = isLoading;
          return;
        }


        if (el.matches('[data-tee-bundle-qty-input], [data-tee-bundle-qty]')) {
          const row = el.closest(`.${TEE_BUNDLE_PREFIX}-row`);
          const checkbox = row ? row.querySelector('[data-tee-bundle-checkbox]') : null;
          el.disabled = isLoading || !(checkbox && checkbox.checked);
          return;
        }


        el.disabled = isLoading;
      });
    }


    /** Bundle UI rendering và state - END */


    /** Native bundle flow - START */


    /** Add one selected bundle item through the native Teeinblue add-to-cart flow. */
    async function tee_bundle_nativeAddSelectedItem(artworkIdentity, selected, beforeCart) {
      const beforeCount = tee_bundle_getCartItemCount(beforeCart);


      tee_bundle_log('native add start', {
        variantId: selected.variant.id,
        title: selected.variant.title,
        quantity: selected.quantity,
      });


      await tee_bundle_waitForButtonReady(artworkIdentity, 8000);
      await tee_bundle_setArtworkVariant(artworkIdentity, selected.variant);


      let readyState = await tee_bundle_waitForButtonReady(artworkIdentity, 8000);
      let liveArtworkRoot = readyState.liveArtworkRoot;
      let teeAtcButton = readyState.button;


      tee_bundle_setArtworkQuantity(liveArtworkRoot, selected.quantity);
      await tee_bundle_wait(220);


      readyState = await tee_bundle_waitForButtonReady(artworkIdentity, 8000);
      liveArtworkRoot = readyState.liveArtworkRoot;
      teeAtcButton = readyState.button;


      const hookPromise = tee_bundle_openHookCapture();
      teeAtcButton.click();


      const captured = await hookPromise;
      tee_bundle_validateHookPayload(captured, selected);


      const afterCart = await tee_bundle_waitForCartIncrease(beforeCount, selected.quantity);


      await tee_bundle_waitForButtonReady(artworkIdentity, 10000);
      await tee_bundle_waitForLiveArtworkRoot(artworkIdentity, 7000);
      await tee_bundle_wait(350);


      tee_bundle_log('native add complete', {
        variantId: selected.variant.id,
        quantity: selected.quantity,
      });


      return afterCart;
    }


    /** Execute the full bundle flow for all selected rows. */
    async function tee_bundle_handleBundleAtc(artworkRoot, root, variants) {
      const selectedItems = tee_bundle_getSelectedBundleItems(root, variants);


      if (!selectedItems.length) {
        tee_bundle_setStatus(root, 'Please choose at least one item.', 'error');
        return;
      }


      const liveArtworkRoot = await tee_bundle_waitForLiveArtworkRoot(artworkRoot);
      const snapshot = tee_bundle_snapshotArtworkState(liveArtworkRoot);
      const artworkIdentity = snapshot.artworkIdentity;
      const releaseScroll = tee_bundle_holdScrollPosition();


      tee_bundle_log('bundle flow started', {
        items: selectedItems.map((item) => ({
          variantId: item.variant.id,
          title: item.variant.title,
          quantity: item.quantity,
        })),
      });


      tee_bundle_setLoading(root, true);
      tee_bundle_setStatus(root, 'Preparing your selected items...', 'info');


      let originalCart = null;
      let workingCart = null;


      try {
        originalCart = await tee_bundle_fetchCart();
        workingCart = originalCart;


        for (let index = 0; index < selectedItems.length; index += 1) {
          const selected = selectedItems[index];


          tee_bundle_setStatus(root, `Adding item ${index + 1} of ${selectedItems.length}...`, 'info');
          workingCart = await tee_bundle_nativeAddSelectedItem(artworkIdentity, selected, workingCart);
          await tee_bundle_wait(400);
        }


        await tee_bundle_restoreArtworkState(snapshot);
        tee_bundle_emitCartRefresh();


        tee_bundle_setStatus(root, 'Your selected items have been added to the cart.', 'success');


        document.dispatchEvent(
          new CustomEvent('tee_bundle:bundle:added', {
            detail: {
              items: selectedItems,
            },
          })
        );


        tee_bundle_log('bundle flow completed successfully');
      } catch (error) {
        tee_bundle_error('bundle flow failed', error);


        try {
          if (originalCart) {
            tee_bundle_setStatus(root, 'Something went wrong. Restoring your cart...', 'info');
            await tee_bundle_rollbackCartToSnapshot(originalCart);
          }
        } catch (rollbackError) {
          tee_bundle_error('cart rollback failed', rollbackError);


          tee_bundle_setStatus(
            root,
            'We could not finish adding your items, and part of the cart could not be restored automatically.',
            'error'
          );


          try {
            await tee_bundle_restoreArtworkState(snapshot);
          } catch (restoreError) {
            tee_bundle_error('artwork restore failed after rollback failure', restoreError);
          }


          tee_bundle_emitCartRefresh();
          tee_bundle_setLoading(root, false);
          releaseScroll();
          return;
        }


        try {
          await tee_bundle_restoreArtworkState(snapshot);
        } catch (restoreError) {
          tee_bundle_error('final artwork restore failed', restoreError);
        }


        tee_bundle_emitCartRefresh();


        tee_bundle_setStatus(root, 'We could not add all selected items. Your cart has been restored.', 'error');
      } finally {
        tee_bundle_clearHookCapture();


        try {
          await tee_bundle_restoreArtworkState(snapshot);
        } catch (restoreError) {
          tee_bundle_error('final artwork restore failed', restoreError);
        }


        tee_bundle_setLoading(root, false);
        tee_bundle_updateTotal(root, variants);
        releaseScroll();
      }
    }


    /** Native bundle flow - END */


    /** Bundle event binding - START */


    /** Bind bundle UI interactions for the current artwork instance. */
    function tee_bundle_bindBundle(artworkRoot, root, variants) {
      root.addEventListener('change', (event) => {
        const target = event.target;


        if (target.matches('[data-tee-bundle-checkbox]')) {
          const row = target.closest(`.${TEE_BUNDLE_PREFIX}-row`);
          if (row) {
            tee_bundle_toggleRow(row, target.checked);
          }
          tee_bundle_updateTotal(root, variants);
        }


        if (target.matches('[data-tee-bundle-qty-input]')) {
          const nextValue = Math.max(1, Number(target.value) || 1);
          target.value = nextValue;
          tee_bundle_updateTotal(root, variants);
        }
      });


      root.addEventListener('click', (event) => {
        const qtyButton = event.target.closest('[data-tee-bundle-qty]');
        if (qtyButton) {
          const row = qtyButton.closest(`.${TEE_BUNDLE_PREFIX}-row`);
          const input = row ? row.querySelector('[data-tee-bundle-qty-input]') : null;
          if (!input) return;


          const currentValue = Math.max(1, Number(input.value) || 1);
          const action = qtyButton.getAttribute('data-tee-bundle-qty');
          input.value = action === 'minus' ? Math.max(1, currentValue - 1) : currentValue + 1;
          tee_bundle_updateTotal(root, variants);
          return;
        }


        const bundleButton = event.target.closest('[data-tee-bundle-bundle-atc]');
        if (bundleButton) {
          tee_bundle_handleBundleAtc(artworkRoot, root, variants);
        }
      });
    }


    /** Bundle event binding - END */


    /** Mount logic - START */


    /** Mount the bundle UI into #tee-artwork-form. */
    function tee_bundle_mountArtwork(artworkRoot) {
      if (!artworkRoot || artworkRoot.dataset.teeBundleMounted === 'true') return;
      if (!tee_bundle_isSupportedCampaign()) return;


      const variants = tee_bundle_getCampaignVariants();
      const teeAtcButton = tee_bundle_getTeeAtcButton(artworkRoot);


      if (!variants.length || !teeAtcButton) {
        tee_bundle_warn('bundle mount skipped', {
          hasVariants: !!variants.length,
          hasButton: !!teeAtcButton,
        });
        return;
      }


      if (artworkRoot.querySelector(`.${TEE_BUNDLE_PREFIX}-bundle`)) {
        artworkRoot.dataset.teeBundleMounted = 'true';
        return;
      }


      tee_bundle_log('mounting bundle UI', {
        variantCount: variants.length,
      });


      const bundle = tee_bundle_renderBundle(variants);


      const anchor =
        artworkRoot.querySelector('#teeFormActions') ||
        artworkRoot.querySelector('.tee-customize-main-form') ||
        artworkRoot.querySelector('.tee-customization-warning') ||
        artworkRoot.firstElementChild ||
        null;


      if (anchor) {
        anchor.insertAdjacentElement('beforebegin', bundle);
      } else {
        artworkRoot.appendChild(bundle);
      }


      tee_bundle_bindBundle(artworkRoot, bundle, variants);
      tee_bundle_updateTotal(bundle, variants);
      artworkRoot.dataset.teeBundleMounted = 'true';
    }


    /** Mount the bundle UI for all current #tee-artwork-form instances. */
    function tee_bundle_mountAll() {
      if (!tee_bundle_isSupportedCampaign()) {
        tee_bundle_log('mount skipped: current page is not a Teeinblue product');
        return;
      }


      document.querySelectorAll('#tee-artwork-form').forEach(tee_bundle_mountArtwork);
    }


    /** Mount logic - END */


    /** Bootstrap - START */


    /** Initialize the bundle runtime and remount whenever Teeinblue re-injects its UI. */
    function tee_bundle_boot() {
      tee_bundle_log('booting bundle system');


      tee_bundle_installTeeinblueHooks();
      tee_bundle_mountAll();


      document.addEventListener('DOMContentLoaded', () => {
        tee_bundle_log('DOMContentLoaded');
        tee_bundle_installTeeinblueHooks();
        tee_bundle_mountAll();
      });


      document.addEventListener(TEE_BUNDLE_INJECT_EVENT, () => {
        tee_bundle_log('received event', TEE_BUNDLE_INJECT_EVENT, {
          isTeeInBlueProduct: window.teeinblueCampaign?.isTeeInBlueProduct,
        });


        setTimeout(() => {
          tee_bundle_installTeeinblueHooks();
          tee_bundle_mountAll();
        }, 80);
      });


      const observer = new MutationObserver((mutations) => {
        let shouldMount = false;


        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (!(node instanceof HTMLElement)) continue;


            if (node.matches?.('#tee-artwork-form') || node.querySelector?.('#tee-artwork-form')) {
              shouldMount = true;
              break;
            }
          }


          if (shouldMount) break;
        }


        if (shouldMount) {
          tee_bundle_log('artwork mutation detected, remounting');
          tee_bundle_installTeeinblueHooks();
          tee_bundle_mountAll();
        }
      });


      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
      });
    }


    /** Bootstrap - END */


    /** Run the bundle bootstrap routine. */
    tee_bundle_boot();
  })();
</script>
```


* Create this file: `assets/teeinblue-bundle.css`
```
body.teeinblue-enabled:not(.teeinblue-platform-product-enabled) .tee-customization-wrapper~.tee_bundle-bundle {
  display: block !important;
}


.tee_bundle-bundle {
  margin: 18px 0 22px;
  padding: 18px;
  border: 1px solid rgba(0, 0, 0, 0.12);
  border-radius: 14px;
  background: #fff;
}


.tee_bundle-header {
  margin-bottom: 14px;
}


.tee_bundle-title {
  margin: 0 0 6px;
  font-size: 18px;
  line-height: 1.3;
  font-weight: 600;
  color: #111;
}


.tee_bundle-desc {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  color: #555;
}


.tee_bundle-list {
  display: grid;
  gap: 10px;
}


.tee_bundle-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 14px;
  align-items: center;
  padding: 12px 14px;
  border: 1px solid rgba(0, 0, 0, 0.10);
  border-radius: 12px;
  background: #fafafa;
  transition: border-color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
}


.tee_bundle-row.is-selected {
  border-color: #111;
  background: #fff;
}


.tee_bundle-variant {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  margin: 0;
  cursor: pointer;
}


.tee_bundle-checkbox {
  flex: 0 0 auto;
  margin: 0;
}


.tee_bundle-variant-title {
  display: block;
  min-width: 0;
  font-size: 14px;
  line-height: 1.5;
  color: #111;
  word-break: break-word;
}


.tee_bundle-qty {
  display: inline-grid;
  grid-template-columns: 34px 58px 34px;
  gap: 6px;
  align-items: center;
}


.tee_bundle-qty-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid rgba(0, 0, 0, 0.16);
  border-radius: 8px;
  background: #fff;
  color: #111;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}


.tee_bundle-qty-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}


.tee_bundle-qty-input {
  width: 58px;
  height: 34px;
  margin: 0;
  padding: 0 8px;
  border: 1px solid rgba(0, 0, 0, 0.16);
  border-radius: 8px;
  background: #fff;
  text-align: center;
  font-size: 14px;
}


.tee_bundle-actions {
  margin-top: 16px;
}


.tee_bundle-atc {
  width: 100%;
  min-height: 46px;
  padding: 12px 16px;
  border: 0;
  border-radius: 999px;
  background: #111;
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  transition: opacity 0.2s ease, transform 0.2s ease;
}


.tee_bundle-atc:hover {
  opacity: 0.92;
}


.tee_bundle-atc:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}


.tee_bundle-status {
  margin-top: 12px;
  font-size: 13px;
  line-height: 1.5;
  min-height: 20px;
}


.tee_bundle-status.is-info {
  color: #555;
}


.tee_bundle-status.is-success {
  color: #0f7a32;
}


.tee_bundle-status.is-error {
  color: #b42318;
}


.tee_bundle-bundle.is-loading {
  opacity: 0.8;
}


@media (max-width: 767px) {
  .tee_bundle-row {
    grid-template-columns: 1fr;
  }


  .tee_bundle-qty {
    justify-self: start;
  }
}


.tee_bundle-atc {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}


.tee_bundle-atc.is-loading {
  pointer-events: none;
}


.tee_bundle-spinner {
  width: 16px;
  height: 16px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  display: inline-block;
  animation: tee_bundle-spin 0.7s linear infinite;
  flex: 0 0 auto;
}


@keyframes tee_bundle-spin {
  to {
    transform: rotate(360deg);
  }
}


#tee-artwork-form {
  display: grid !important;


  .tee-option {
    order: 1000;
  }
  .tee-variants {
    order: 1001;
  }
  .tee-form-actions {
    order: 1002;
  }
  .tee-customization-warning {
    order: 1003;
  }
  .tee-description {
    order: 1004;
  }
}
```


* Go to `layout/theme.liquid` and add this script right above the close `</head>` tag:
```
{% if request.page_type == 'product' and product.template_suffix == 'teeinblue-bulk-order' %}
  {% render 'teeinblue-bundle' %}
{% endif %}
```


![](https://storage.crisp.chat/users/helpdesk/website/-/6/0/8/5/6085dc2a7293d800/image_uibo7i.png)



# Step 4: Test the Bulk Order Flow
Finally, test the entire process on your storefront.
Things to verify:
* Personalization form loads correctly
* Variant quantities can be selected
* Multiple variants are added to the cart
* All items contain the same personalized design
