[object Object]

← back to Designer Wallcoverings

Port MOQ enforcement to Steves Version 5.1 (Archetype): metafield min/step + sample exemption + clamp script; originals backed up

d8930599a0ae8a0d5125c3363acc3a1a976b0b67 · 2026-06-22 17:15:19 -0700 · Steve Abrams

Files touched

Diff

commit d8930599a0ae8a0d5125c3363acc3a1a976b0b67
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 22 17:15:19 2026 -0700

    Port MOQ enforcement to Steves Version 5.1 (Archetype): metafield min/step + sample exemption + clamp script; originals backed up
---
 .../v51/element-quantity-selector.FIXED.liquid     | 184 +++++++++++++++++++++
 .../v51/element-quantity-selector.ORIGINAL.liquid  | 181 ++++++++++++++++++++
 shopify/theme-moq/v51/quantity-picker.FIXED.liquid | 118 +++++++++++++
 .../theme-moq/v51/quantity-picker.ORIGINAL.liquid  |  70 ++++++++
 4 files changed, 553 insertions(+)

diff --git a/shopify/theme-moq/v51/element-quantity-selector.FIXED.liquid b/shopify/theme-moq/v51/element-quantity-selector.FIXED.liquid
new file mode 100644
index 00000000..5c2790f1
--- /dev/null
+++ b/shopify/theme-moq/v51/element-quantity-selector.FIXED.liquid
@@ -0,0 +1,184 @@
+{% doc %}
+  Copyright © 2025 Archetype Themes LP. All rights reserved.
+
+  Interactive quantity selector with plus/minus buttons and numeric input.
+
+  @param {string} [id] - ID of the quantity selector
+  @param {string} [name] - Name of the quantity selector
+  @param {number} [value] - Initial value of the quantity selector
+  @param {number} [min] - Minimum value of the quantity selector
+  @param {number} [max] - Maximum value of the quantity selector
+  @param {string} [form_id] - ID of the form the quantity selector is associated with
+  @param {string} [cart_item_key] - Key of the cart item the quantity selector is associated with
+  @param {string} [style] - Custom styles for the quantity selector
+  @param {boolean} [inverted] - Whether to invert the color scheme
+  @param {boolean} [disabled] - Whether to disable the quantity selector
+  @param {string} [label] - Label of the quantity selector
+
+  @example
+  {% render 'element.quantity-selector', value: 1, min: 1, max: 10 %}
+{% enddoc %}
+
+{%- liquid
+  capture default_id
+    render 'utility.id', name: product.id
+  endcapture
+
+  assign id = id | default: default_id
+  assign name = name | default: 'quantity'
+  assign value = value | default: 1
+  assign min = min | default: 1
+  assign step = step | default: 1
+  assign form_id = form_id | default: ''
+  assign cart_item_key = cart_item_key | default: ''
+  assign inverted = inverted | default: false
+  assign style = style | default: ''
+  assign disabled = disabled | default: false, allow_false: true
+  assign label = label | default: ''
+
+  assign inverted_input = false
+  assign inverted_buttons = true
+  if inverted
+    assign inverted_input = true
+    assign inverted_buttons = false
+  endif
+
+  assign value_string = value | append: ''
+  assign value_string_size = value_string.size
+
+  if label == blank
+    capture label
+     render 'utility.translate', key: 'labels.quantity', fallback: 'Quantity'
+    endcapture
+  endif
+-%}
+
+{%- capture custom_properties -%}
+  --digit-count: {{ value_string_size }}ch;
+{%- endcapture -%}
+
+{%- assign style = custom_properties | append: ' ' | append: style | strip -%}
+
+{%- capture input_attributes -%}
+  data-initial-value="{{ value }}"
+  min="{{ min }}"
+  step="{{ step }}"
+  data-step="{{ step }}"
+  max="{{ max }}"
+  pattern="[0-9]*"
+  form="{{ form_id }}"
+{%- endcapture -%}
+
+{%- capture button_minus_attributes -%}
+  aria-label="{% render 'utility.translate', key: 'actions.reduce_item_quantity', fallback: 'Reduce item quantity by one' -%}"
+{%- endcapture -%}
+
+{%- capture button_plus_attributes -%}
+  aria-label="{% render 'utility.translate', key: 'actions.increase_item_quantity', fallback: 'Increase item quantity by one' -%}"
+{%- endcapture -%}
+
+<quantity-selector
+  class="element-quantity-selector"
+  {% if cart_item_key != blank -%}
+    key="{{ cart_item_key }}"
+  {%- endif %}
+  style="{{ style }}"
+>
+  {%- render 'element.button',
+    classlist: 'element-quantity-selector__button element-quantity-selector__button--minus',
+    icon: 'minus',
+    attributes: button_minus_attributes,
+    inverted: inverted_buttons,
+    transparent: true,
+    type: 'button',
+    disabled: disabled,
+    style: '--element-button-color-primary: var(--color-primary); --element-button-color-secondary: var(--color-secondary);'
+  -%}
+
+  {%- render 'element.input',
+    type: 'text',
+    classlist: 'element-quantity-selector__input',
+    id: id,
+    name: name,
+    value: value,
+    attributes: input_attributes,
+    inverted: inverted_input,
+    disabled: disabled
+  -%}
+
+  {%- render 'element.button',
+    classlist: 'element-quantity-selector__button element-quantity-selector__button--plus',
+    icon: 'plus',
+    attributes: button_plus_attributes,
+    inverted: inverted_buttons,
+    transparent: true,
+    type: 'button',
+    disabled: disabled,
+    style: '--element-button-color-primary: var(--color-primary); --element-button-color-secondary: var(--color-secondary);'
+  -%}
+</quantity-selector>
+
+<script type="module">
+  import 'element.quantity-selector'
+</script>
+
+{% render 'style.primitive-tokens' %}
+{% render 'style.normalize' %}
+
+{% stylesheet %}
+  .element-quantity-selector {
+    --element-input-color-primary: var(--color-primary, #000);
+    --element-input-color-secondary: var(--color-secondary, #fff);
+
+    width: fit-content;
+    display: inline-block;
+    position: relative;
+    overflow: visible;
+    pointer-events: auto;
+
+    &.is-loading {
+      opacity: 0.5;
+      pointer-events: none;
+    }
+  }
+
+  .element-quantity-selector__input {
+    --element-input-radius: var(--element-button-radius);
+    --element-input-padding-inline: calc(
+      var(--element-input-line-height) * var(--element-input-font-size) + 2 * var(--element-input-padding-block)
+    );
+    --element-input-width: max(calc(var(--digit-count, 1ch) + 2 * var(--size-1)), 32px);
+    box-sizing: content-box;
+    text-align: center;
+  }
+
+  .element-quantity-selector__button {
+    --element-button-border-width: 0;
+    --element-button-color-primary: var(--element-input-color-primary, var(--root-color-primary, #000));
+    --element-button-color-secondary: var(--element-input-color-secondary, var(--root-color-secondary, #fff));
+
+    position: absolute;
+    top: 0;
+    height: 100%;
+    aspect-ratio: 1;
+    border: 1px solid transparent;
+    background-clip: padding-box;
+    z-index: 1;
+
+    &:has(+ .element-quantity-selector__input:focus),
+    .element-quantity-selector__input:focus + & {
+      border-width: var(--element-input-box-shadow-spread-radius--hover);
+    }
+
+    &:is(.element-quantity-selector__button--minus) {
+      border-radius: var(--element-button-radius) 0 0 var(--element-button-radius);
+      left: 0;
+    }
+
+    &:is(.element-quantity-selector__button--plus) {
+      border-radius: 0 var(--element-button-radius) var(--element-button-radius) 0;
+      right: 0;
+    }
+  }
+{% endstylesheet %}
+
diff --git a/shopify/theme-moq/v51/element-quantity-selector.ORIGINAL.liquid b/shopify/theme-moq/v51/element-quantity-selector.ORIGINAL.liquid
new file mode 100644
index 00000000..b8e00933
--- /dev/null
+++ b/shopify/theme-moq/v51/element-quantity-selector.ORIGINAL.liquid
@@ -0,0 +1,181 @@
+{% doc %}
+  Copyright © 2025 Archetype Themes LP. All rights reserved.
+
+  Interactive quantity selector with plus/minus buttons and numeric input.
+
+  @param {string} [id] - ID of the quantity selector
+  @param {string} [name] - Name of the quantity selector
+  @param {number} [value] - Initial value of the quantity selector
+  @param {number} [min] - Minimum value of the quantity selector
+  @param {number} [max] - Maximum value of the quantity selector
+  @param {string} [form_id] - ID of the form the quantity selector is associated with
+  @param {string} [cart_item_key] - Key of the cart item the quantity selector is associated with
+  @param {string} [style] - Custom styles for the quantity selector
+  @param {boolean} [inverted] - Whether to invert the color scheme
+  @param {boolean} [disabled] - Whether to disable the quantity selector
+  @param {string} [label] - Label of the quantity selector
+
+  @example
+  {% render 'element.quantity-selector', value: 1, min: 1, max: 10 %}
+{% enddoc %}
+
+{%- liquid
+  capture default_id
+    render 'utility.id', name: product.id
+  endcapture
+
+  assign id = id | default: default_id
+  assign name = name | default: 'quantity'
+  assign value = value | default: 1
+  assign min = min | default: 1
+  assign form_id = form_id | default: ''
+  assign cart_item_key = cart_item_key | default: ''
+  assign inverted = inverted | default: false
+  assign style = style | default: ''
+  assign disabled = disabled | default: false, allow_false: true
+  assign label = label | default: ''
+
+  assign inverted_input = false
+  assign inverted_buttons = true
+  if inverted
+    assign inverted_input = true
+    assign inverted_buttons = false
+  endif
+
+  assign value_string = value | append: ''
+  assign value_string_size = value_string.size
+
+  if label == blank
+    capture label
+     render 'utility.translate', key: 'labels.quantity', fallback: 'Quantity'
+    endcapture
+  endif
+-%}
+
+{%- capture custom_properties -%}
+  --digit-count: {{ value_string_size }}ch;
+{%- endcapture -%}
+
+{%- assign style = custom_properties | append: ' ' | append: style | strip -%}
+
+{%- capture input_attributes -%}
+  data-initial-value="{{ value }}"
+  min="{{ min }}"
+  max="{{ max }}"
+  pattern="[0-9]*"
+  form="{{ form_id }}"
+{%- endcapture -%}
+
+{%- capture button_minus_attributes -%}
+  aria-label="{% render 'utility.translate', key: 'actions.reduce_item_quantity', fallback: 'Reduce item quantity by one' -%}"
+{%- endcapture -%}
+
+{%- capture button_plus_attributes -%}
+  aria-label="{% render 'utility.translate', key: 'actions.increase_item_quantity', fallback: 'Increase item quantity by one' -%}"
+{%- endcapture -%}
+
+<quantity-selector
+  class="element-quantity-selector"
+  {% if cart_item_key != blank -%}
+    key="{{ cart_item_key }}"
+  {%- endif %}
+  style="{{ style }}"
+>
+  {%- render 'element.button',
+    classlist: 'element-quantity-selector__button element-quantity-selector__button--minus',
+    icon: 'minus',
+    attributes: button_minus_attributes,
+    inverted: inverted_buttons,
+    transparent: true,
+    type: 'button',
+    disabled: disabled,
+    style: '--element-button-color-primary: var(--color-primary); --element-button-color-secondary: var(--color-secondary);'
+  -%}
+
+  {%- render 'element.input',
+    type: 'text',
+    classlist: 'element-quantity-selector__input',
+    id: id,
+    name: name,
+    value: value,
+    attributes: input_attributes,
+    inverted: inverted_input,
+    disabled: disabled
+  -%}
+
+  {%- render 'element.button',
+    classlist: 'element-quantity-selector__button element-quantity-selector__button--plus',
+    icon: 'plus',
+    attributes: button_plus_attributes,
+    inverted: inverted_buttons,
+    transparent: true,
+    type: 'button',
+    disabled: disabled,
+    style: '--element-button-color-primary: var(--color-primary); --element-button-color-secondary: var(--color-secondary);'
+  -%}
+</quantity-selector>
+
+<script type="module">
+  import 'element.quantity-selector'
+</script>
+
+{% render 'style.primitive-tokens' %}
+{% render 'style.normalize' %}
+
+{% stylesheet %}
+  .element-quantity-selector {
+    --element-input-color-primary: var(--color-primary, #000);
+    --element-input-color-secondary: var(--color-secondary, #fff);
+
+    width: fit-content;
+    display: inline-block;
+    position: relative;
+    overflow: visible;
+    pointer-events: auto;
+
+    &.is-loading {
+      opacity: 0.5;
+      pointer-events: none;
+    }
+  }
+
+  .element-quantity-selector__input {
+    --element-input-radius: var(--element-button-radius);
+    --element-input-padding-inline: calc(
+      var(--element-input-line-height) * var(--element-input-font-size) + 2 * var(--element-input-padding-block)
+    );
+    --element-input-width: max(calc(var(--digit-count, 1ch) + 2 * var(--size-1)), 32px);
+    box-sizing: content-box;
+    text-align: center;
+  }
+
+  .element-quantity-selector__button {
+    --element-button-border-width: 0;
+    --element-button-color-primary: var(--element-input-color-primary, var(--root-color-primary, #000));
+    --element-button-color-secondary: var(--element-input-color-secondary, var(--root-color-secondary, #fff));
+
+    position: absolute;
+    top: 0;
+    height: 100%;
+    aspect-ratio: 1;
+    border: 1px solid transparent;
+    background-clip: padding-box;
+    z-index: 1;
+
+    &:has(+ .element-quantity-selector__input:focus),
+    .element-quantity-selector__input:focus + & {
+      border-width: var(--element-input-box-shadow-spread-radius--hover);
+    }
+
+    &:is(.element-quantity-selector__button--minus) {
+      border-radius: var(--element-button-radius) 0 0 var(--element-button-radius);
+      left: 0;
+    }
+
+    &:is(.element-quantity-selector__button--plus) {
+      border-radius: 0 var(--element-button-radius) var(--element-button-radius) 0;
+      right: 0;
+    }
+  }
+{% endstylesheet %}
+
diff --git a/shopify/theme-moq/v51/quantity-picker.FIXED.liquid b/shopify/theme-moq/v51/quantity-picker.FIXED.liquid
new file mode 100644
index 00000000..8d74fb6b
--- /dev/null
+++ b/shopify/theme-moq/v51/quantity-picker.FIXED.liquid
@@ -0,0 +1,118 @@
+{% doc %}
+  Copyright © 2025 Archetype Themes LP. All rights reserved.
+
+  Renders a quantity selector with optional inventory status display.
+
+  This component provides a quantity input field with increment/decrement buttons
+  and can optionally display inventory status information. It respects inventory
+  limits when inventory management is enabled and shows appropriate maximum values.
+
+  @param {boolean} [show_inventory_status] - Whether to show inventory status below quantity selector (default: true)
+  @param {number} [inventory_threshold] - The inventory level at which to show low stock warning (default: 10)
+
+  @example
+  {% render 'section.flex-pdp.quantity-picker', show_inventory_status: true, inventory_threshold: 5, product: closest.product %}
+{% enddoc %}
+
+{%- liquid
+  assign form_id = 'section.flex-pdp.form-' | append: section.id
+  assign show_inventory_status = show_inventory_status | default: block.settings.show_inventory_status, allow_false: true | default: true, allow_false: true
+  assign inventory_threshold = inventory_threshold | default: block.settings.inventory_threshold | default: 10
+  assign current_variant = product.selected_or_first_available_variant
+-%}
+
+{% capture product_inventory %}
+  {% render 'section.flex-pdp._inventory-text', product: product, inventory_threshold: inventory_threshold %}
+{% endcapture %}
+
+{% capture quantity_selector_label %}
+  {% capture quantity_selector_attributes %}
+    for="{% render 'utility.id', name: product.id %}"
+  {% endcapture -%}
+
+  {% if show_inventory_status %}
+    {%  capture quantity_selector_label_beginning %}
+      {%- render 'utility.translate', key: 'labels.quantity', fallback: 'Quantity' -%}
+    {% endcapture %}
+    {% assign quantity_selector_label_beginning = quantity_selector_label_beginning | strip | append: ':' %}
+    {%  capture quantity_selector_label_content %}
+      {% render 'element.text', as: 'span', text: quantity_selector_label_beginning, style: 'font-weight: 600;' %}
+      {% render 'element.text', as: 'span', text: product_inventory, style: 'opacity: 0.75;' %}
+    {% endcapture %}
+    {% render 'element.text', as: 'label', variant: 'body-sm', text: quantity_selector_label_content, attributes: quantity_selector_attributes %}
+  {% else %}
+    {% render 'element.text', as: 'label', variant: 'body-sm', locale: 'labels.quantity', text: 'Quantity', style: 'font-weight: 600;', attributes: quantity_selector_attributes %}
+  {% endif %}
+{% endcapture %}
+
+{% capture quantity_selector_stack %}
+  {%- liquid
+    # DW MOQ 2026-06-22: read order-min metafields (robust append coercion)
+    assign moq_min = product.metafields.global.v_prods_quantity_order_min | append: '' | plus: 0
+    assign moq_step = product.metafields.global.v_prods_quantity_order_units | append: '' | plus: 0
+    if moq_min < 1
+      assign moq_min = 1
+    endif
+    if moq_step < 1
+      assign moq_step = 1
+    endif
+    # Sample variant is exempt from the minimum (always orderable at qty 1)
+    assign sel_t = current_variant.title | downcase
+    assign eff_min = moq_min
+    assign eff_step = moq_step
+    if sel_t contains 'sample'
+      assign eff_min = 1
+      assign eff_step = 1
+    endif
+    assign value = eff_min
+    assign max = ''
+    assign min = eff_min
+    assign step = eff_step
+    assign disabled = false
+  -%}
+
+  {% if current_variant.inventory_management == 'shopify' and current_variant.inventory_policy != 'continue' and show_inventory_status %}
+    {% assign max = current_variant.inventory_quantity %}
+  {% endif %}
+
+  {% unless current_variant.available %}
+    {% assign max = 0 %}
+    {% assign min = 0 %}
+    {% assign value = 0 %}
+    {% assign disabled = true %}
+  {% endunless %}
+
+  {% render 'layout.stack', gap: 'xs', horizontal: true, slot: quantity_selector_label %}
+  {% render 'element.quantity-selector', product: product, form_id: form_id, max: max, value: value, min: min, step: step, disabled: disabled %}
+{% endcapture %}
+
+{% render 'layout.stack', gap: 'xs', slot: quantity_selector_stack %}
+
+
+{%- comment -%} DW MOQ 2026-06-22: enforce min + increment on the Archetype qty input (delegated, idempotent, defensive) {%- endcomment -%}
+<script>
+(function(){
+  if (window.__dwMoqBound) return; window.__dwMoqBound = true;
+  function snap(q){
+    try{
+      var min = parseInt(q.getAttribute('min'),10); if(isNaN(min)||min<1) min=1;
+      var step = parseInt(q.getAttribute('data-step')||q.getAttribute('step'),10); if(isNaN(step)||step<1) step=1;
+      var v = parseInt(q.value,10);
+      if(isNaN(v)||v<min) v=min;
+      var r=(v-min)%step; if(r!==0) v=v-r+step;
+      if(String(v)!==q.value) q.value=v;
+    }catch(e){}
+  }
+  document.addEventListener('change', function(e){
+    var q=e.target; if(q&&q.matches&&q.matches('input[name="quantity"], .element-quantity-selector__input')) snap(q);
+  }, true);
+  document.addEventListener('click', function(e){
+    var b=e.target.closest&&e.target.closest('.element-quantity-selector__button'); if(!b) return;
+    var sel=b.closest('quantity-selector, .element-quantity-selector'); var q=sel&&sel.querySelector('input');
+    if(q) setTimeout(function(){snap(q);},0);
+  }, true);
+  document.addEventListener('submit', function(e){
+    var f=e.target; var q=f&&f.querySelector&&f.querySelector('input[name="quantity"]'); if(q) snap(q);
+  }, true);
+})();
+</script>
diff --git a/shopify/theme-moq/v51/quantity-picker.ORIGINAL.liquid b/shopify/theme-moq/v51/quantity-picker.ORIGINAL.liquid
new file mode 100644
index 00000000..c4848b56
--- /dev/null
+++ b/shopify/theme-moq/v51/quantity-picker.ORIGINAL.liquid
@@ -0,0 +1,70 @@
+{% doc %}
+  Copyright © 2025 Archetype Themes LP. All rights reserved.
+
+  Renders a quantity selector with optional inventory status display.
+
+  This component provides a quantity input field with increment/decrement buttons
+  and can optionally display inventory status information. It respects inventory
+  limits when inventory management is enabled and shows appropriate maximum values.
+
+  @param {boolean} [show_inventory_status] - Whether to show inventory status below quantity selector (default: true)
+  @param {number} [inventory_threshold] - The inventory level at which to show low stock warning (default: 10)
+
+  @example
+  {% render 'section.flex-pdp.quantity-picker', show_inventory_status: true, inventory_threshold: 5, product: closest.product %}
+{% enddoc %}
+
+{%- liquid
+  assign form_id = 'section.flex-pdp.form-' | append: section.id
+  assign show_inventory_status = show_inventory_status | default: block.settings.show_inventory_status, allow_false: true | default: true, allow_false: true
+  assign inventory_threshold = inventory_threshold | default: block.settings.inventory_threshold | default: 10
+  assign current_variant = product.selected_or_first_available_variant
+-%}
+
+{% capture product_inventory %}
+  {% render 'section.flex-pdp._inventory-text', product: product, inventory_threshold: inventory_threshold %}
+{% endcapture %}
+
+{% capture quantity_selector_label %}
+  {% capture quantity_selector_attributes %}
+    for="{% render 'utility.id', name: product.id %}"
+  {% endcapture -%}
+
+  {% if show_inventory_status %}
+    {%  capture quantity_selector_label_beginning %}
+      {%- render 'utility.translate', key: 'labels.quantity', fallback: 'Quantity' -%}
+    {% endcapture %}
+    {% assign quantity_selector_label_beginning = quantity_selector_label_beginning | strip | append: ':' %}
+    {%  capture quantity_selector_label_content %}
+      {% render 'element.text', as: 'span', text: quantity_selector_label_beginning, style: 'font-weight: 600;' %}
+      {% render 'element.text', as: 'span', text: product_inventory, style: 'opacity: 0.75;' %}
+    {% endcapture %}
+    {% render 'element.text', as: 'label', variant: 'body-sm', text: quantity_selector_label_content, attributes: quantity_selector_attributes %}
+  {% else %}
+    {% render 'element.text', as: 'label', variant: 'body-sm', locale: 'labels.quantity', text: 'Quantity', style: 'font-weight: 600;', attributes: quantity_selector_attributes %}
+  {% endif %}
+{% endcapture %}
+
+{% capture quantity_selector_stack %}
+  {% assign value = 1 %}
+  {% assign max = '' %}
+  {% assign min = 1 %}
+  {% assign disabled = false %}
+
+  {% if current_variant.inventory_management == 'shopify' and current_variant.inventory_policy != 'continue' and show_inventory_status %}
+    {% assign max = current_variant.inventory_quantity %}
+  {% endif %}
+
+  {% unless current_variant.available %}
+    {% assign max = 0 %}
+    {% assign min = 0 %}
+    {% assign value = 0 %}
+    {% assign disabled = true %}
+  {% endunless %}
+
+  {% render 'layout.stack', gap: 'xs', horizontal: true, slot: quantity_selector_label %}
+  {% render 'element.quantity-selector', product: product, form_id: form_id, max: max, value: value, min: min, disabled: disabled %}
+{% endcapture %}
+
+{% render 'layout.stack', gap: 'xs', slot: quantity_selector_stack %}
+

← 8e45fc21 5.1 dev: horizontal collapsible Boost filter bar (dw-filter-  ·  back to Designer Wallcoverings  ·  5.1 PDP: hide theme's styled .select-wrapper shell when size 89778d14 →