# Ürün paketi: ücretsiz Shopify bölümü

> Birlikte alınan iki ya da üç ürün: varyant menüleri, anlık toplam ve Cart AJAX API ile hepsini tek tıkla sepete ekleyen bir düğme.

Ürün paketi, seçtiğiniz iki ya da üç ürünü birlikte alınan bir set olarak sunan ücretsiz bir Shopify bölümüdür: her ürünün görseli, fiyatı ve varyant menüsü, güncel toplam ve tek bir "Hepsini sepete ekle" düğmesi. Müşteri bir ürünü onay kutusuyla setten çıkarabilir; düğme seçilen varyantları Shopify’ın Cart AJAX API’sine tek istekle ekler. Bölüm normal fiyatları gösterir; paket indirimi isterseniz bunu Shopify’da otomatik bir indirim olarak siz oluşturursunuz. Online Store 2.0 temalarında uygulama olmadan çalışır.

- **Kategori:** Ürün sayfası
- **Kullanıldığı yerler:** Ürün, Açılış sayfaları, Ana sayfa, Her sayfa
- **Okuduğu veri:** Seçtiğiniz en çok üç ürün
- **Boyut:** 1 dosyada 27 KB, küçük bir betikle
- **Lisans:** MIT, © 2026 Lintel
- **Sayfa:** https://lintelagency.com/tr/kaynaklar/shopify-bolumleri/urun-paketi
- **Canlı demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-product-bundle.html
- **Eklenme tarihi:** 2026-11-02

## Ne işe yarar

- Ürünün altında, bir sürahiyi bardaklar ve tepsiyle birlikte sunan "birlikte alın" seti
- Açılış sayfasında her parçayı tek tıkla sepete ekleyen başlangıç seti ya da hediye seti
- Paket uygulaması kullanmadan ana ürünün yanında bir aksesuar sunmak

## Tema düzenleyici ayarları

Bölüm tema düzenleyicide “Lib · Product bundle” adıyla görünür. Kimlikler, türler, etiketler ve varsayılan değerler bölümün şemasından gelir. Etiketler tema düzenleyicide göründüğü gibi İngilizcedir.

### Content

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | `Better <em>together</em>` |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large` |
| `text` | `richtext` | Text | `<p>Pieces that are made to be used together.</p>` |  |
| `discount_note` | `text` | Discount note | – |  |

### Words

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `button_label` | `text` | Button | Add all to cart |  |
| `total_label` | `text` | Total | Total |  |
| `include_label` | `text` | Screen reader: before each product's checkbox | Include |  |
| `variant_label` | `text` | Screen reader: variant menu | Options |  |
| `sold_out_label` | `text` | Sold out | Sold out |  |
| `sale_label` | `text` | Screen reader: sale price | Sale price |  |
| `regular_label` | `text` | Screen reader: regular price | Regular price |  |
| `added_message` | `text` | Message when added | Added to your cart. |  |
| `error_message` | `text` | Message when adding fails | These products could not be added. Please try again. |  |
| `empty_message` | `text` | Message when nothing is chosen | Choose at least one product. |  |
| `after_add` | `select` | After adding | cart | `cart`, `stay` |

### Layout

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `image_ratio` | `select` | Image shape | square | `square`, `portrait` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `text_alignment` | Heading alignment on desktop | left |  |
| `text_alignment_mobile` | `select` | Heading alignment on mobile | left | `left`, `center`, `right` |

### Colors

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FFFDF9 |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #1C1917 |  |
| `color_accent` | `color` | Button and checkboxes | #1C1917 |  |
| `color_accent_text` | `color` | Button text | #FFFDF9 |  |
| `color_card` | `color` | Card and total background | #F5F2EC |  |
| `color_sale` | `color` | Sale price and discount note | #B4432F |  |

### Spacing and visibility

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `72` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `72` | 0–200 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `44` | 0–160 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `44` | 0–160 px, step 4 |
| `show_on` | `select` | Show on | all | `all`, `desktop`, `mobile` |
| `anchor_id` | `text` | Anchor ID | – |  |

### Motion

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `animate` | `checkbox` | Fade in the products on load | `false` |  |

## Bloklar

Toplamda en fazla 3 blok.

### Product (`product`, en fazla 3)

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `product` | `product` | Product | – |  |
| `quantity` | `range` | Quantity | `1` | 1–5, step 1 |
| `checked` | `checkbox` | Included by default | `true` |  |

## Kurulum

### Shopify yönetim panelinde

1. Canlı temanızda değil, bir kopyasında çalışın: Shopify yönetim panelinde **Online Mağaza > Temalar** sayfasına gidin, mevcut temanızın **…** menüsünü açın ve **Kopya oluştur**’u seçin.
2. Kopyada **…** menüsünü açın ve **Kodu düzenle**’yi seçin.
3. `sections` klasörüne `lib-product-bundle.liquid` adında yeni bir dosya ekleyin, aşağıdaki kaynak koddan `sections/lib-product-bundle.liquid` içeriğini yapıştırıp kaydedin.
4. Kopyada **Özelleştir**’i açın, **Bölüm ekle**’yi seçin ve **Lib · Product bundle** bölümünü ekleyin. Örnek içeriği kendi içeriğinizle değiştirin, renkleri ayarlayın.
5. Kopyayı önizleyin, her şey doğru göründüğünde yayınlayın.

### Shopify CLI ile

Yerel tema klasörünüzde dosyaları indirin ve yalnızca bunları yayında olmayan bir temaya gönderin (temanın kimliğini `shopify theme list` gösterir):

```sh
curl -fsSL -o sections/lib-product-bundle.liquid https://lintelagency.com/resources/shopify-sections/files/lib-product-bundle.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-product-bundle.liquid
```

Canlı temaya asla göndermeyin: her zaman yayında olmayan bir temanın kimliğini verin, `--live` ya da `--allow-live` kullanmayın. Ardından bölümü yukarıdaki gibi tema düzenleyicide ekleyin.

## Dosyalar

- [sections/lib-product-bundle.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-product-bundle.liquid): `sections/` klasörüne

## Sık sorulan sorular

### Bölüm paket indirimi uyguluyor mu?

Hayır. Ürünleri normal fiyatlarıyla ekler. Seti indirimli satmak için Shopify’da (İndirimler) otomatik bir indirim oluşturun, örneğin bu ürünler sepetteyken geçerli bir yüzde indirimi; ardından bunu bölümün indirim notunda belirtin. Not yalnızca bilgilendirme metnidir.

### Ürünler sepete nasıl ekleniyor?

JavaScript ile: seçilen tüm varyantlar ve adetler tek bir /cart/add.js isteğiyle gönderilir. Ardından, sizin seçiminize göre müşteri sepete yönlendirilir ya da sayfada kalıp bir mesaj görür. cart:refresh olayı da bu olayı dinleyen temaların sepet sayacını güncellemesini sağlar. JavaScript yoksa form, stokta olan tüm ürünleri /cart/add adresine gönderir.

### Tükenen ürünlere ne oluyor?

Tükenen ürün soluk ve "Tükendi" etiketiyle gösterilir ve sepete eklenmez. Tükenen varyantlar menüde görünür ama seçilemez.

### Müşteri varyantı ve adedi değiştirebilir mi?

Seçenekleri olan her ürünün bir varyant menüsü vardır; fiyat ve toplam seçilen varyanta göre değişir. Ürün başına adedi blokta siz belirlersiniz (örneğin iki bardak); müşteri ise onay kutularıyla ürünleri sete ekler ya da setten çıkarır.

### Toplam mağazanın para birimi biçiminde mi?

Evet. Biçim, Shopify’ın money filtresiyle oluşturulan bir fiyattan alınır; para birimi simgesi, ayraçlar ve ondalıklar mağazanızın geri kalanıyla aynıdır.

## Kaynak kod

### sections/lib-product-bundle.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Product bundle ("buy it together"): two or three products you pick, each with its image, price
  and variant menu, a running total and one button that adds them all to the cart through
  Shopify's Cart AJAX API (/cart/add.js). Shoppers can leave a product out with its checkbox.
  The section only displays prices: a bundle discount has to be set up in Shopify (Discounts),
  and the optional discount note is text you type. Without JavaScript the form posts every
  available product to /cart/add. Sold-out products are shown but never added.
  Depends on: nothing.
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign uid = 'LibBundle-' | append: section.id
-%}

<div
  class="lib-bundle lib-bundle--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-ratio="{{ s.image_ratio }}"
  {% if s.animate %}
    data-animate
  {% endif %}
  style="
    --lib-bg: {{ s.color_bg }};
    {% if s.color_bg_gradient != blank %}--lib-bg-image: {{ s.color_bg_gradient }};{% endif %}
    --lib-text: {{ s.color_text }};
    --lib-accent: {{ s.color_accent }};
    --lib-accent-text: {{ s.color_accent_text }};
    --lib-card: {{ s.color_card }};
    --lib-sale: {{ s.color_sale }};
    --lib-pt: {{ s.padding_top }}px;
    --lib-pb: {{ s.padding_bottom }}px;
    --lib-pt-m: {{ s.padding_top_mobile }}px;
    --lib-pb-m: {{ s.padding_bottom_mobile }}px;
  "
>
  <div class="lib-bundle__inner">
    {%- if s.heading != blank or s.text != blank -%}
      <header class="lib-bundle__header" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
        {%- if s.heading != blank -%}
          <{{ heading_tag }} class="lib-bundle__heading lib-bundle__heading--{{ s.heading_size }}" id="{{ uid }}-title">
            {{- s.heading -}}
          </{{ heading_tag }}>
        {%- endif -%}
        {%- if s.text != blank -%}
          <div class="lib-bundle__text">{{ s.text }}</div>
        {%- endif -%}
      </header>
    {%- endif -%}

    <lib-product-bundle
      data-after-add="{{ s.after_add }}"
      data-cart-url="{{ routes.cart_url }}"
      data-add-url="{{ routes.cart_add_url }}.js"
      data-money-sample="{{ 123456 | money | escape }}"
      data-msg-added="{{ s.added_message | escape }}"
      data-msg-error="{{ s.error_message | escape }}"
      data-msg-empty="{{ s.empty_message | escape }}"
    >
      <form action="{{ routes.cart_add_url }}" method="post" class="lib-bundle__form" novalidate>
        <ul class="lib-bundle__items" role="list">
          {%- for block in section.blocks -%}
            {%- liquid
              assign b = block.settings
              assign p = b.product
              if p == blank and request.design_mode == false
                continue
              endif
              assign item_id = uid | append: '-' | append: block.id
              assign v = p.selected_or_first_available_variant
              assign qty = b.quantity | default: 1
            -%}
            <li
              class="lib-bundle__item{% if p != blank and p.available == false %} is-sold-out{% endif %}"
              {% if p != blank and p.available %}
                data-item data-qty="{{ qty }}"
              {% endif %}
              {{ block.shopify_attributes }}
            >
              <div class="lib-bundle__media">
                {%- if p.featured_image != blank -%}
                  {{
                    p.featured_image
                    | image_url: width: 600
                    | image_tag: class: 'lib-bundle__img', loading: 'lazy', alt: p.featured_image.alt, widths: '200, 400, 600', sizes: '(min-width: 990px) 20vw, 30vw'
                  }}
                {%- else -%}
                  {{ 'product-1' | placeholder_svg_tag: 'lib-bundle__img lib-bundle__img--ph' }}
                {%- endif -%}
              </div>

              <div class="lib-bundle__body">
                {%- if p != blank and p.available -%}
                  <label class="lib-bundle__pick" hidden data-pick>
                    <input type="checkbox" class="lib-bundle__check" {% if b.checked %}checked{% endif %} data-include>
                    <span class="lib-bundle__sr">{{ s.include_label | escape }}</span>
                    <span class="lib-bundle__title">{{ p.title | escape }}</span>
                  </label>
                  <p class="lib-bundle__title lib-bundle__title--nojs" data-nojs-title>
                    {%- if qty > 1 %}{{ qty }} × {% endif %}{{ p.title | escape -}}
                  </p>
                {%- else -%}
                  <p class="lib-bundle__title">{{ p.title | default: 'Example product' | escape }}</p>
                {%- endif -%}
                {%- if qty > 1 and p.available -%}
                  <p class="lib-bundle__qty" hidden data-qty-label>× {{ qty }}</p>
                {%- endif -%}

                <p class="lib-bundle__price" data-price-wrap>
                  {%- if p == blank -%}
                    <span class="lib-bundle__now">{{ 4800 | money }}</span>
                  {%- elsif p.available == false -%}
                    <span class="lib-bundle__badge">{{ s.sold_out_label | escape }}</span>
                  {%- else -%}
                    {%- assign on_sale = false -%}
                    {%- if v.compare_at_price > v.price -%}{%- assign on_sale = true -%}{%- endif -%}
                    <span class="lib-bundle__sr" data-sale-sr {% unless on_sale %}hidden{% endunless %}>{{ s.sale_label | escape }}</span>
                    <span class="lib-bundle__now{% if on_sale %} is-sale{% endif %}" data-price>{{ v.price | money }}</span>
                    <span class="lib-bundle__sr" data-was-sr {% unless on_sale %}hidden{% endunless %}>{{ s.regular_label | escape }}</span>
                    <s class="lib-bundle__was" data-was>{% if on_sale %}{{ v.compare_at_price | money }}{% endif %}</s>
                  {%- endif -%}
                </p>

                {%- if p != blank and p.available -%}
                  {%- if p.has_only_default_variant -%}
                    <input type="hidden" name="items[][id]" value="{{ v.id }}" data-variant data-cents="{{ v.price }}">
                  {%- else -%}
                    <label class="lib-bundle__sr" for="{{ item_id }}-variant">{{ s.variant_label | escape }}: {{ p.title | escape }}</label>
                    <select class="lib-bundle__select" id="{{ item_id }}-variant" name="items[][id]" data-variant>
                      {%- for variant in p.variants -%}
                        <option
                          value="{{ variant.id }}"
                          data-cents="{{ variant.price }}"
                          data-price="{{ variant.price | money | escape }}"
                          data-compare="{% if variant.compare_at_price > variant.price %}{{ variant.compare_at_price | money | escape }}{% endif %}"
                          {% if variant.id == v.id %}selected{% endif %}
                          {% unless variant.available %}disabled{% endunless %}
                        >
                          {{- variant.title | escape }}{% unless variant.available %} ({{ s.sold_out_label | escape }}){% endunless -%}
                        </option>
                      {%- endfor -%}
                    </select>
                  {%- endif -%}
                  <input type="hidden" name="items[][quantity]" value="{{ qty }}">
                {%- endif -%}
              </div>
            </li>
          {%- endfor -%}
        </ul>

        <div class="lib-bundle__summary">
          {%- if s.discount_note != blank -%}
            <p class="lib-bundle__note">{{ s.discount_note | escape }}</p>
          {%- endif -%}
          <p class="lib-bundle__total" hidden data-total-wrap>
            <span>{{ s.total_label | escape }}</span>
            <strong data-total aria-live="polite"></strong>
          </p>
          <button type="submit" class="lib-bundle__button" data-submit>
            <span>{{ s.button_label | escape }}</span>
          </button>
          <p class="lib-bundle__status" role="status" data-status></p>
        </div>
      </form>
    </lib-product-bundle>
  </div>
</div>

{% stylesheet %}
  .lib-bundle {
    background-color: var(--lib-bg);
    background-image: var(--lib-bg-image, none);
    color: var(--lib-text);
    padding: var(--lib-pt) 1rem var(--lib-pb);
  }
  .lib-bundle__inner { max-width: 1200px; margin: 0 auto; display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); }
  .lib-bundle--full .lib-bundle__inner { max-width: none; }
  @media (max-width: 749px) {
    .lib-bundle { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-bundle[data-show='desktop'] { display: none; }
  }
  @media (min-width: 750px) {
    .lib-bundle[data-show='mobile'] { display: none; }
  }

  .lib-bundle__header { text-align: left; }
  .lib-bundle__header[data-align='center'] { text-align: center; }
  .lib-bundle__header[data-align='right'] { text-align: right; }
  @media (max-width: 749px) {
    .lib-bundle__header[data-align-m='left'] { text-align: left; }
    .lib-bundle__header[data-align-m='center'] { text-align: center; }
    .lib-bundle__header[data-align-m='right'] { text-align: right; }
  }
  .lib-bundle__heading { margin: 0; font-size: clamp(1.75rem, 3.2vw, 2.5rem); line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; }
  .lib-bundle__heading--small { font-size: clamp(1.4rem, 2.2vw, 1.75rem); }
  .lib-bundle__heading--large { font-size: clamp(2rem, 4.2vw, 3.25rem); }
  .lib-bundle__text { max-width: 40rem; margin-top: 0.75rem; font-size: 1.0625rem; line-height: 1.6; opacity: 0.8; }
  .lib-bundle__header[data-align='center'] .lib-bundle__text { margin-inline: auto; }
  .lib-bundle__header[data-align='right'] .lib-bundle__text { margin-inline-start: auto; }
  .lib-bundle__text > :first-child { margin-top: 0; }
  .lib-bundle__text > :last-child { margin-bottom: 0; }

  .lib-bundle__form { display: grid; gap: 1.5rem; }
  @media (min-width: 990px) {
    .lib-bundle__form { grid-template-columns: minmax(0, 1fr) 18rem; align-items: center; gap: 2.5rem; }
  }
  .lib-bundle__items { display: grid; gap: 0.75rem; margin: 0; padding: 0; list-style: none; }
  @media (min-width: 750px) {
    .lib-bundle__items { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2.75rem; }
    /* The plus sign between products. */
    .lib-bundle__item + .lib-bundle__item::before {
      content: '+';
      position: absolute;
      left: -1.375rem;
      top: 35%;
      translate: -50% -50%;
      font-size: 1.5rem;
      font-weight: 300;
      opacity: 0.55;
    }
  }
  .lib-bundle__item {
    position: relative;
    display: grid;
    grid-template-columns: 6rem minmax(0, 1fr);
    gap: 1rem;
    align-items: center;
    padding: 0.75rem;
    border-radius: 14px;
    background: var(--lib-card);
    transition: opacity 0.2s ease;
  }
  @media (min-width: 750px) {
    .lib-bundle__item { grid-template-columns: 1fr; align-items: start; align-content: start; padding: 0; background: none; }
  }
  .lib-bundle__item.is-out,
  .lib-bundle__item.is-sold-out { opacity: 0.5; }
  .lib-bundle__media { overflow: hidden; border-radius: 10px; aspect-ratio: 1; background: color-mix(in srgb, var(--lib-text) 6%, transparent); }
  .lib-bundle[data-ratio='portrait'] .lib-bundle__media { aspect-ratio: 4 / 5; }
  .lib-bundle__img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .lib-bundle__img--ph { fill: color-mix(in srgb, var(--lib-text) 22%, transparent); }
  .lib-bundle__body { display: grid; gap: 0.35rem; min-width: 0; }
  .lib-bundle__pick { display: flex; align-items: flex-start; gap: 0.55rem; cursor: pointer; }
  .lib-bundle__pick[hidden], .lib-bundle__qty[hidden] { display: none; }
  .lib-bundle__check {
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    margin: 0.1rem 0 0;
    accent-color: var(--lib-accent);
    cursor: pointer;
  }
  .lib-bundle__check:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 2px; }
  .lib-bundle__title { margin: 0; font-weight: 600; line-height: 1.35; }
  .lib-bundle__qty { margin: 0; font-size: 0.875rem; opacity: 0.75; }
  .lib-bundle__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.5rem; margin: 0; }
  .lib-bundle__now.is-sale { color: var(--lib-sale); font-weight: 600; }
  .lib-bundle__was { opacity: 0.6; font-size: 0.875em; }
  .lib-bundle__badge { font-size: 0.8125rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
  .lib-bundle__select {
    width: 100%;
    min-height: 2.5rem;
    margin-top: 0.25rem;
    padding: 0.4rem 0.6rem;
    border: 1px solid color-mix(in srgb, var(--lib-text) 35%, transparent);
    border-radius: 8px;
    background: var(--lib-bg);
    color: inherit;
    font: inherit;
    font-size: 0.9375rem;
  }
  .lib-bundle__select:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 1px; }

  .lib-bundle__summary {
    display: grid;
    gap: 0.85rem;
    padding: 1.5rem;
    border-radius: 16px;
    background: var(--lib-card);
  }
  .lib-bundle__note { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--lib-sale); }
  .lib-bundle__total { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin: 0; font-size: 1.0625rem; }
  .lib-bundle__total[hidden] { display: none; }
  .lib-bundle__total strong { font-size: 1.375rem; font-variant-numeric: tabular-nums; }
  .lib-bundle__button {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    min-height: 3rem;
    padding: 0.75rem 1.5rem;
    border: 0;
    border-radius: 999px;
    background: var(--lib-accent);
    color: var(--lib-accent-text);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }
  .lib-bundle__button:hover { opacity: 0.9; }
  .lib-bundle__button:focus-visible { outline: 2px solid var(--lib-text); outline-offset: 3px; }
  .lib-bundle__button[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; }
  .lib-bundle__status { margin: 0; font-size: 0.875rem; }
  .lib-bundle__status:empty { display: none; }
  .lib-bundle__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .lib-bundle__sr[hidden] { display: none; }

  @media (prefers-reduced-motion: no-preference) {
    .lib-bundle[data-animate] .lib-bundle__item { animation: lib-bundle-in 0.6s ease-out both; }
    .lib-bundle[data-animate] .lib-bundle__item:nth-child(2) { animation-delay: 0.1s; }
    .lib-bundle[data-animate] .lib-bundle__item:nth-child(3) { animation-delay: 0.2s; }
  }
  @keyframes lib-bundle-in { from { opacity: 0; transform: translateY(10px); } }
{% endstylesheet %}

{% javascript %}
  if (!customElements.get('lib-product-bundle')) {
    customElements.define(
      'lib-product-bundle',
      class extends HTMLElement {
        connectedCallback() {
          if (this.form) return;
          this.form = this.querySelector('form');
          this.items = [...this.querySelectorAll('[data-item]')];
          this.status = this.querySelector('[data-status]');
          this.button = this.querySelector('[data-submit]');
          this.totalEl = this.querySelector('[data-total]');
          if (!this.form || !this.items.length) return;

          // With JavaScript, products can be left out: show the checkboxes, hide the plain titles.
          this.items.forEach((item) => {
            item.querySelector('[data-pick]')?.removeAttribute('hidden');
            item.querySelector('[data-qty-label]')?.removeAttribute('hidden');
            item.querySelector('[data-nojs-title]')?.remove();
            item.querySelector('[data-include]')?.addEventListener('change', () => this.update());
            const select = item.querySelector('select[data-variant]');
            if (select) select.addEventListener('change', () => this.showVariant(item, select));
          });
          this.querySelector('[data-total-wrap]')?.removeAttribute('hidden');
          this.form.addEventListener('submit', (event) => this.submit(event));
          this.update();
        }

        chosen() {
          return this.items.filter((item) => item.querySelector('[data-include]')?.checked);
        }

        cents(item) {
          const input = item.querySelector('[data-variant]');
          const source = input?.tagName === 'SELECT' ? input.selectedOptions[0] : input;
          return Number(source?.dataset.cents || 0) * Number(item.dataset.qty || 1);
        }

        // Formats cents like the store does. The section renders 123456 cents with Shopify's money
        // filter (data-money-sample, e.g. "$1,234.56" or "1.234,56 €"); the separators, decimals and
        // currency sign are read from that sample, so any money format works.
        money(cents) {
          const sample = this.dataset.moneySample || '';
          const match = sample.match(/1(\D?)234(\D)56|1(\D?)235/);
          if (!match) return (cents / 100).toFixed(2);
          const point = match[2] || '';
          const thousands = (point ? match[1] : match[3]) || '';
          const [whole, frac] = (cents / 100).toFixed(point ? 2 : 0).split('.');
          const number = whole.replace(/\B(?=(\d{3})+(?!\d))/g, thousands) + (frac ? point + frac : '');
          return sample.replace(match[0], number);
        }

        showVariant(item, select) {
          const option = select.selectedOptions[0];
          const price = item.querySelector('[data-price]');
          const was = item.querySelector('[data-was]');
          const onSale = Boolean(option.dataset.compare);
          if (price) {
            price.textContent = option.dataset.price;
            price.classList.toggle('is-sale', onSale);
          }
          if (was) was.textContent = option.dataset.compare || '';
          item.querySelector('[data-sale-sr]')?.toggleAttribute('hidden', !onSale);
          item.querySelector('[data-was-sr]')?.toggleAttribute('hidden', !onSale);
          this.update();
        }

        update() {
          const chosen = this.chosen();
          this.items.forEach((item) => item.classList.toggle('is-out', !chosen.includes(item)));
          if (this.totalEl) this.totalEl.textContent = this.money(chosen.reduce((sum, item) => sum + this.cents(item), 0));
          // aria-disabled keeps the button focusable, so the "choose a product" message can be heard.
          this.button?.setAttribute('aria-disabled', String(chosen.length === 0));
        }

        say(message) {
          if (!this.status) return;
          this.status.textContent = '';
          requestAnimationFrame(() => {
            this.status.textContent = message;
          });
        }

        async submit(event) {
          event.preventDefault();
          const chosen = this.chosen();
          if (!chosen.length) {
            this.say(this.dataset.msgEmpty);
            return;
          }
          const items = chosen.map((item) => ({
            id: Number(item.querySelector('[data-variant]').value),
            quantity: Number(item.dataset.qty || 1),
          }));
          this.button.setAttribute('aria-busy', 'true');
          try {
            const response = await fetch(this.dataset.addUrl, {
              method: 'POST',
              headers: {'Content-Type': 'application/json', Accept: 'application/json'},
              body: JSON.stringify({items}),
            });
            const body = await response.json().catch(() => ({}));
            if (!response.ok) throw new Error(body.description || '');
            document.dispatchEvent(new CustomEvent('cart:refresh', {bubbles: true, detail: {items: body.items || items}}));
            if (this.dataset.afterAdd === 'cart') {
              window.location.href = this.dataset.cartUrl;
              return;
            }
            this.say(this.dataset.msgAdded);
          } catch (error) {
            // Shopify's own message (e.g. a quantity limit) is more useful than the generic one.
            this.say(error.message || this.dataset.msgError);
          } finally {
            this.button.removeAttribute('aria-busy');
          }
        }
      }
    );
  }
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Product bundle",
  "tag": "section",
  "class": "lib-section",
  "max_blocks": 3,
  "settings": [
    {
      "type": "paragraph",
      "content": "The section adds the products at their normal prices. For a bundle discount, create an automatic discount in Shopify (Discounts) for these products; the note below only describes it."
    },
    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Better <em>together</em>" },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Heading tag",
      "info": "Use H1 only when this is the page's main heading.",
      "options": [
        { "value": "h1", "label": "H1" },
        { "value": "h2", "label": "H2" },
        { "value": "h3", "label": "H3" },
        { "value": "p", "label": "Paragraph" }
      ],
      "default": "h2"
    },
    {
      "type": "select",
      "id": "heading_size",
      "label": "Heading size",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" }
      ],
      "default": "medium"
    },
    { "type": "richtext", "id": "text", "label": "Text", "default": "<p>Pieces that are made to be used together.</p>" },
    { "type": "text", "id": "discount_note", "label": "Discount note", "info": "Optional, e.g. 10% off when bought together. Only text: set up the discount itself in Shopify." },

    { "type": "header", "content": "Words" },
    { "type": "text", "id": "button_label", "label": "Button", "default": "Add all to cart" },
    { "type": "text", "id": "total_label", "label": "Total", "default": "Total" },
    { "type": "text", "id": "include_label", "label": "Screen reader: before each product's checkbox", "default": "Include" },
    { "type": "text", "id": "variant_label", "label": "Screen reader: variant menu", "default": "Options" },
    { "type": "text", "id": "sold_out_label", "label": "Sold out", "default": "Sold out" },
    { "type": "text", "id": "sale_label", "label": "Screen reader: sale price", "default": "Sale price" },
    { "type": "text", "id": "regular_label", "label": "Screen reader: regular price", "default": "Regular price" },
    { "type": "text", "id": "added_message", "label": "Message when added", "default": "Added to your cart." },
    { "type": "text", "id": "error_message", "label": "Message when adding fails", "default": "These products could not be added. Please try again." },
    { "type": "text", "id": "empty_message", "label": "Message when nothing is chosen", "default": "Choose at least one product." },
    {
      "type": "select",
      "id": "after_add",
      "label": "After adding",
      "info": "Staying on the page sends a cart:refresh event that many themes use to update the cart count.",
      "options": [
        { "value": "cart", "label": "Go to the cart" },
        { "value": "stay", "label": "Stay and show the message" }
      ],
      "default": "cart"
    },

    { "type": "header", "content": "Layout" },
    {
      "type": "select",
      "id": "image_ratio",
      "label": "Image shape",
      "options": [
        { "value": "square", "label": "Square" },
        { "value": "portrait", "label": "Portrait (4:5)" }
      ],
      "default": "square"
    },
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page", "label": "Page width" },
        { "value": "full", "label": "Full width" }
      ],
      "default": "page"
    },
    { "type": "text_alignment", "id": "text_alignment", "label": "Heading alignment on desktop", "default": "left" },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Heading alignment on mobile",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" },
        { "value": "right", "label": "Right" }
      ],
      "default": "left"
    },

    { "type": "header", "content": "Colors" },
    { "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFDF9" },
    { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#1C1917" },
    { "type": "color", "id": "color_accent", "label": "Button and checkboxes", "default": "#1C1917" },
    { "type": "color", "id": "color_accent_text", "label": "Button text", "default": "#FFFDF9" },
    { "type": "color", "id": "color_card", "label": "Card and total background", "default": "#F5F2EC" },
    { "type": "color", "id": "color_sale", "label": "Sale price and discount note", "default": "#B4432F" },

    { "type": "header", "content": "Spacing and visibility" },
    { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 72 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 72 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 44 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 44 },
    {
      "type": "select",
      "id": "show_on",
      "label": "Show on",
      "options": [
        { "value": "all", "label": "All devices" },
        { "value": "desktop", "label": "Desktop only" },
        { "value": "mobile", "label": "Mobile only" }
      ],
      "default": "all"
    },
    { "type": "text", "id": "anchor_id", "label": "Anchor ID", "info": "Optional. Link to this section with #your-id." },

    { "type": "header", "content": "Motion" },
    { "type": "checkbox", "id": "animate", "label": "Fade in the products on load", "info": "Always off for visitors who prefer reduced motion.", "default": false }
  ],
  "blocks": [
    {
      "type": "product",
      "name": "Product",
      "limit": 3,
      "settings": [
        { "type": "product", "id": "product", "label": "Product" },
        { "type": "range", "id": "quantity", "label": "Quantity", "min": 1, "max": 5, "step": 1, "default": 1 },
        { "type": "checkbox", "id": "checked", "label": "Included by default", "default": true }
      ]
    }
  ],
  "presets": [
    {
      "name": "Lib · Product bundle",
      "blocks": [
        { "type": "product" },
        { "type": "product" },
        { "type": "product" }
      ]
    }
  ]
}
{% endschema %}
```

## Lisans

MIT Lisansı, © 2026 Lintel. Kendi mağazanızda ya da müşteri işlerinde ücretsiz kullanabilir, değiştirebilir ve yayınlayabilirsiniz; her dosyanın başındaki lisans satırını koruyun.
