@runwell/shopify-toolkit 0.18.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bin/runwell-shopify +10 -1
  2. package/lib/init.js +13 -2
  3. package/lib/list.js +22 -9
  4. package/lib/qa-bundles.js +117 -0
  5. package/lib/qa.js +147 -13
  6. package/modules/INDEX.md +14 -5
  7. package/modules/bundle-builder/README.md +6 -1
  8. package/modules/bundle-builder/module.json +5 -1
  9. package/modules/cart-cross-sell/snippets/runwell-cart-xsell.liquid +16 -0
  10. package/modules/runwell-bundle-system/README.md +35 -0
  11. package/modules/runwell-bundle-system/admin-metafields.json +55 -0
  12. package/modules/runwell-bundle-system/assets/runwell-bundle-system.css +1107 -0
  13. package/modules/runwell-bundle-system/assets/runwell-bundle-system.js +444 -0
  14. package/modules/runwell-bundle-system/module.json +137 -0
  15. package/modules/runwell-bundle-system/qa/mobile-checklist.md +105 -0
  16. package/modules/runwell-bundle-system/qa/v1.5-mobile-qa-checklist.md +103 -0
  17. package/modules/runwell-bundle-system/sections/runwell-bundle-cart-xsell.liquid +59 -0
  18. package/modules/runwell-bundle-system/sections/runwell-bundle-collection.liquid +121 -0
  19. package/modules/runwell-bundle-system/sections/runwell-bundle-home-stacks.liquid +77 -0
  20. package/modules/runwell-bundle-system/sections/runwell-bundle-pdp-banner.liquid +50 -0
  21. package/modules/runwell-bundle-system/sections/runwell-bundle-pdp-pairs-with.liquid +72 -0
  22. package/modules/runwell-bundle-system/sections/runwell-bundle-pdp.liquid +117 -0
  23. package/modules/runwell-bundle-system/settings.json +25 -0
  24. package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker-accordion.liquid +84 -0
  25. package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker-grid.liquid +72 -0
  26. package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker-radio.liquid +77 -0
  27. package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-picker.liquid +71 -0
  28. package/modules/runwell-bundle-system/snippets/runwell-bundle-byob-summary.liquid +39 -0
  29. package/modules/runwell-bundle-system/snippets/runwell-bundle-card.liquid +70 -0
  30. package/modules/runwell-bundle-system/snippets/runwell-bundle-cross-supplier.liquid +18 -0
  31. package/modules/runwell-bundle-system/snippets/runwell-bundle-data.liquid +75 -0
  32. package/modules/runwell-bundle-system/snippets/runwell-bundle-fomo.liquid +32 -0
  33. package/modules/runwell-bundle-system/snippets/runwell-bundle-free-gift.liquid +34 -0
  34. package/modules/runwell-bundle-system/snippets/runwell-bundle-multi-product.liquid +86 -0
  35. package/modules/runwell-bundle-system/snippets/runwell-bundle-pricing.liquid +30 -0
  36. package/modules/runwell-bundle-system/snippets/runwell-bundle-quantity-tiers.liquid +73 -0
  37. package/package.json +1 -1
@@ -0,0 +1,105 @@
1
+ # Mobile QA checklist: runwell-bundle-system v1
2
+
3
+ Real-device QA at 375px viewport for every enabled bundle surface. Runs on Lushi staging after BS-15 wires the module. Must pass before any production deploy.
4
+
5
+ ## Environment
6
+
7
+ - Real iPhone (iPhone 12 or newer; iOS 17+).
8
+ - Safari (not Chrome iOS; Safari is the rendering engine on iOS).
9
+ - Lushi staging dev store URL.
10
+ - 3 sample bundles configured per BS-15.
11
+
12
+ ## Per-surface checks
13
+
14
+ ### Surface 1: /bundles collection page
15
+
16
+ - [ ] Page loads without console errors.
17
+ - [ ] Single-column grid at 375px.
18
+ - [ ] Hero card variant renders for the first bundle when `feature_first_bundle` is on.
19
+ - [ ] Card spacing 24px on mobile.
20
+ - [ ] Sticky page heading visible while scrolling.
21
+ - [ ] All cards tappable; tap routes to bundle PDP.
22
+ - [ ] Strikethrough subtotal + savings badge legible.
23
+
24
+ ### Surface 2: PDP "Pairs well with"
25
+
26
+ - [ ] Renders below buy box on a non-bundle PDP with matching bundles.
27
+ - [ ] Self-suppresses on a non-bundle PDP without matching bundles.
28
+ - [ ] Card width 96% of viewport.
29
+ - [ ] Eyebrow and heading legible.
30
+ - [ ] Tap routes to bundle PDP.
31
+
32
+ ### Surface 3: home page curated stacks
33
+
34
+ - [ ] Vertical stack (no horizontal carousel) at 375px.
35
+ - [ ] All 3 to 4 cards visible without horizontal swipe.
36
+ - [ ] Section heading and eyebrow at top.
37
+ - [ ] Background band setting respected (white / oat / celadon-tint).
38
+ - [ ] Each card tappable; tap routes to bundle PDP.
39
+
40
+ ### Surface 4: cart drawer bundle xsell
41
+
42
+ - [ ] Drawer opens, bundle xsell card visible at top of xsell area.
43
+ - [ ] cart-cross-sell does NOT also render (coordination contract).
44
+ - [ ] CTA "Add bundle and save" visible and tappable.
45
+ - [ ] Tap CTA: in-cart components removed, bundle product added, drawer refreshes.
46
+ - [ ] Performance: match logic runs in under 100ms.
47
+
48
+ ### Surface 5: PDP bundle banner
49
+
50
+ - [ ] Renders near buy box on non-bundle PDPs with matches.
51
+ - [ ] 44px height enforced.
52
+ - [ ] Tap routes to bundle PDP.
53
+ - [ ] Copy template `{savings_pct}` resolved to actual value.
54
+ - [ ] Background brand-tinted (celadon by default), contrast text legible.
55
+
56
+ ### Surface 6: collection page bundles filter
57
+
58
+ Skipped for Lushi v1 (no collection pages enabled). Re-enable once Lushi has a /collections/skincare or similar.
59
+
60
+ - [ ] Filter chip + 2 pinned cards render above the collection grid.
61
+ - [ ] Chip tap filters grid to bundles only.
62
+ - [ ] Chip tap again clears the filter.
63
+ - [ ] URL syncs `?filter=bundle`.
64
+
65
+ ### Bundle PDP, Mode A (Lusha pattern, when v1.1 enables)
66
+
67
+ - [ ] Radio picker tappable.
68
+ - [ ] Selected tier visually distinct (brand-color border).
69
+ - [ ] Per-tier price + savings badge legible.
70
+ - [ ] FOMO countdown updates (every second when sub-day; every minute when day+).
71
+ - [ ] Sticky ATC at bottom of viewport when scrolled past buy box.
72
+
73
+ ### Bundle PDP, Mode B (Lushi pattern)
74
+
75
+ - [ ] Component breakdown list legible.
76
+ - [ ] Component thumbnails 60px square (80px at 1024px+).
77
+ - [ ] Cross-supplier disclosure renders when `bundle_cross_supplier` is on and `bundle_supplier_count > 1`.
78
+ - [ ] Strikethrough subtotal + savings badge correct per pricing model.
79
+ - [ ] ATC adds bundle to cart.
80
+
81
+ ## Cross-cutting checks
82
+
83
+ - [ ] No horizontal scroll on any surface.
84
+ - [ ] All tap targets >= 44px.
85
+ - [ ] Brand fonts loaded; no FOUT.
86
+ - [ ] Lighthouse mobile score >= 80 on bundle PDP and /bundles page.
87
+ - [ ] Total module byte footprint under 80KB combined (Liquid + CSS + JS, pre-minification).
88
+ - [ ] No console errors on any surface.
89
+
90
+ ## Screenshot capture
91
+
92
+ Per surface, capture 3 states:
93
+ 1. Default render.
94
+ 2. Interaction state (tap-active, modal open, etc.).
95
+ 3. Edge case (no matches, out of stock, cross-supplier disclosure visible).
96
+
97
+ File path: `_clients/capital-v/lushi/qa/bundle-system-mobile-YYYY-MM-DD/surface-N-state.png`.
98
+
99
+ ## Sign-off
100
+
101
+ - [ ] All applicable checks above passed.
102
+ - [ ] Screenshots captured and committed.
103
+ - [ ] Any failures logged as follow-up issues and resolved before BS-15 sign-off.
104
+ - [ ] Lighthouse mobile scores documented in the QA handoff doc.
105
+ - [ ] QA owner + date: ___________________
@@ -0,0 +1,103 @@
1
+ # v1.5 mobile QA checklist: Mode C BYOB + admin block
2
+
3
+ Re-validates the storefront at 375 px after Mode C BYOB lands, plus admin block usability on Shopify's mobile admin app. Mirrors BS-14's v1 pass; adds the new v1.5 surfaces.
4
+
5
+ ## Environment
6
+
7
+ - Real iPhone (iPhone 12 or newer; iOS 17+).
8
+ - Safari for storefront; Shopify mobile admin app for admin block.
9
+ - Staging tenant with at least:
10
+ - 1 Mode A bundle (Lusha pattern, for regression).
11
+ - 1 Mode B bundle (Lushi pattern, for regression).
12
+ - 1 Mode C bundle per layout: grid, accordion, radio.
13
+
14
+ ## Storefront checks (per Mode C layout)
15
+
16
+ ### Grid layout
17
+
18
+ - [ ] Picker renders at 2 columns at 375 px.
19
+ - [ ] Each candidate card has a 44 px tap target.
20
+ - [ ] Tap toggles selection; checkmark / border state matches the rest of the bundle module.
21
+ - [ ] Live counter updates as you tap.
22
+ - [ ] Subtotal + savings refresh client-side.
23
+ - [ ] ATC stays disabled until min picks reached; enables when in range.
24
+ - [ ] Max-pick cap drops the oldest selection (or refuses the new one, depending on convention).
25
+ - [ ] Required candidates are pre-checked and not deselectable.
26
+ - [ ] Unavailable candidates render disabled, not removed.
27
+ - [ ] Submit posts a multi-line cart add; cart drawer reflects the picks.
28
+
29
+ ### Accordion layout
30
+
31
+ - [ ] Categories render as `<details>` blocks; first one open by default.
32
+ - [ ] Per-category count badge updates on each pick.
33
+ - [ ] Total picks across categories enforced against bundle_byob_max_picks.
34
+ - [ ] Required candidates ship pre-checked in their parent category.
35
+
36
+ ### Radio layout
37
+
38
+ - [ ] Each category renders as a radio group; first option pre-selected.
39
+ - [ ] One pick per category enforced (radios).
40
+ - [ ] Total picks equals category count; ATC enables once every category has a pick.
41
+ - [ ] Switching pick within a category updates subtotal + total.
42
+
43
+ ## Storefront regression (v1 surfaces with a Mode C bundle in catalog)
44
+
45
+ - [ ] /bundles page lists the Mode C bundle alongside Mode A and Mode B.
46
+ - [ ] Bundle card hero variant renders correctly with the BYOB savings badge.
47
+ - [ ] PDP pairs-with widget shows the Mode C bundle when a component is the current PDP.
48
+ - [ ] Home page curated stacks strip includes the Mode C bundle when picked.
49
+ - [ ] Cart drawer cross-sell offers the Mode C bundle when a partial set is in cart.
50
+ - [ ] PDP banner with a Mode C bundle reads "Save N% when bundled".
51
+
52
+ ## Admin block checks (Shopify mobile admin app)
53
+
54
+ Best-effort: mobile admin is rare; document any limitations.
55
+
56
+ ### Composition panel (BS-205)
57
+
58
+ - [ ] Mode selector toggles between tier / component / candidate sub-UIs.
59
+ - [ ] Add / remove / reorder works in each mode.
60
+ - [ ] Validation banner surfaces when constraints are violated.
61
+
62
+ ### Pricing panel (BS-206)
63
+
64
+ - [ ] Pricing model dropdown limits to allowed modes per bundle_mode.
65
+ - [ ] Value editor changes shape with model.
66
+ - [ ] Save disabled when value is empty.
67
+
68
+ ### Surfaces panel (BS-207)
69
+
70
+ - [ ] All 6 surface toggles render.
71
+ - [ ] Toggle state persists after save.
72
+
73
+ ### Cross-supplier banner (BS-208)
74
+
75
+ - [ ] Banner appears when components span multiple suppliers.
76
+ - [ ] Mentions the actual supplier count.
77
+ - [ ] Suppresses when count is 1.
78
+
79
+ ### Bundle preview (BS-209)
80
+
81
+ - [ ] Iframe renders at 375 px.
82
+ - [ ] Preview reflects the saved state (not buffered edits).
83
+
84
+ ## Cross-cutting
85
+
86
+ - [ ] No horizontal scroll on any storefront surface.
87
+ - [ ] All tap targets >= 44 px.
88
+ - [ ] Total module bytes still under 80 KB (CSS + JS, pre-minification).
89
+ - [ ] No console errors on storefront or admin.
90
+ - [ ] Lighthouse mobile score >= 80 on the BYOB PDP.
91
+
92
+ ## Screenshot capture
93
+
94
+ Per surface, capture 3 states (default, mid-interaction, validation error).
95
+
96
+ Path: `runwell-shopify-toolkit/docs/bundle-system/qa/v1.5-screenshots-YYYY-MM-DD/`.
97
+
98
+ ## Sign-off
99
+
100
+ - [ ] All applicable checks passed.
101
+ - [ ] Screenshots captured and committed.
102
+ - [ ] P1 failures fixed; P2 logged for v1.5 patch.
103
+ - [ ] QA owner + date: ___________________
@@ -0,0 +1,59 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-cart-xsell.liquid.
3
+ Surface 4: cart drawer bundle cross-sell. Suggests "complete the
4
+ stack" when 1 to N-1 components of a defined bundle sit in cart.
5
+
6
+ How it wires:
7
+ 1. This section emits a JSON island with the bundle_index forward and
8
+ reverse maps so the JS does not need a network round trip.
9
+ 2. The section also emits an empty <div> slot that JS fills with a
10
+ bundle card (rendered via Section Rendering API call when a match
11
+ is detected).
12
+ 3. JS dispatches `runwell:bundle-xsell-active` when it injects content
13
+ so the cart-cross-sell snippet can short-circuit.
14
+
15
+ Coordination contract: see modules/cart-cross-sell/snippets/runwell-cart-xsell.liquid
16
+ for the receiving guard. See _clients/capital-v/lushi/specs/bundle-system/spec.md
17
+ section 6.1.
18
+ {%- endcomment -%}
19
+
20
+ {%- if settings.bundle_system__surface_4_cart_drawer_xsell_enabled == false -%}
21
+ {%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
22
+ {%- else -%}
23
+ {%- assign bundle_index = shop.metaobjects.bundle_index.entries.value -%}
24
+ {%- assign render_slot = false -%}
25
+ {%- if bundle_index and bundle_index.products and bundle_index.bundles -%}
26
+ {%- assign render_slot = true -%}
27
+ {%- endif -%}
28
+
29
+ {%- if render_slot -%}
30
+ <aside
31
+ class="runwell-bundle-cart-xsell"
32
+ data-runwell-bundle-xsell-slot
33
+ data-eyebrow="{{ section.settings.eyebrow | escape }}"
34
+ data-cta="{{ section.settings.cta | escape }}"
35
+ ></aside>
36
+
37
+ <script type="application/json" data-runwell-bundle-index>
38
+ {{ bundle_index | json }}
39
+ </script>
40
+
41
+ {{ 'runwell-bundle-system.css' | asset_url | stylesheet_tag }}
42
+ <script src="{{ 'runwell-bundle-system.js' | asset_url }}" defer="defer"></script>
43
+ {%- endif -%}
44
+ {%- endif -%}
45
+
46
+ {% schema %}
47
+ {
48
+ "name": "Bundle cart cross-sell",
49
+ "tag": "section",
50
+ "class": "section-runwell-bundle-cart-xsell",
51
+ "settings": [
52
+ { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Complete the stack" },
53
+ { "type": "text", "id": "cta", "label": "CTA label", "default": "Add bundle and save" }
54
+ ],
55
+ "presets": [
56
+ { "name": "Bundle: cart drawer cross-sell", "category": "Runwell" }
57
+ ]
58
+ }
59
+ {% endschema %}
@@ -0,0 +1,121 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-collection.liquid.
3
+ Shared by Surface 1 (/bundles dedicated page, mode 'grid') and
4
+ Surface 6 (collection-page bundles filter chip, mode 'filter_chip').
5
+
6
+ Bundle detection: prefers products with the runwell.bundle_mode
7
+ metafield set; falls back to products tagged 'bundle'.
8
+
9
+ See _clients/capital-v/lushi/specs/bundle-system/spec.md sections 5, 8.1, 8.6.
10
+ {%- endcomment -%}
11
+
12
+ {%- assign mode = section.settings.mode | default: 'grid' -%}
13
+
14
+ {%- comment -%} Build the bundles list from collection.products if present, otherwise scan all_products. {%- endcomment -%}
15
+ {%- assign source_products = collection.products -%}
16
+ {%- if source_products == blank or source_products.size == 0 -%}
17
+ {%- assign source_products = all_products -%}
18
+ {%- endif -%}
19
+
20
+ {%- assign bundles_meta = source_products | where: 'metafields.runwell.bundle_mode' -%}
21
+ {%- assign bundles = bundles_meta -%}
22
+ {%- if bundles == empty or bundles.size == 0 -%}
23
+ {%- assign bundles = source_products | where: 'tags', 'bundle' -%}
24
+ {%- endif -%}
25
+
26
+ {%- if mode == 'grid' -%}
27
+ {%- if settings.bundle_system__surface_1_collection_page_enabled == false -%}
28
+ {%- comment -%} Tenant has disabled the /bundles page surface. {%- endcomment -%}
29
+ {%- else -%}
30
+ <section
31
+ class="runwell-bundle-collection runwell-bundle-collection--grid color-{{ section.settings.color_scheme }}"
32
+ style="padding-top: {{ section.settings.padding_top }}px; padding-bottom: {{ section.settings.padding_bottom }}px;"
33
+ >
34
+ <div class="runwell-bundle-collection__inner page-width">
35
+ {%- if section.settings.show_heading -%}
36
+ <header class="runwell-bundle-collection__heading">
37
+ <h1 class="runwell-bundle-collection__title">{{ section.settings.title }}</h1>
38
+ {%- if section.settings.subline != blank -%}
39
+ <p class="runwell-bundle-collection__subline">{{ section.settings.subline }}</p>
40
+ {%- endif -%}
41
+ </header>
42
+ {%- endif -%}
43
+
44
+ <div class="runwell-bundle-collection__grid">
45
+ {%- for bundle_product in bundles -%}
46
+ {%- assign card_variant = 'default' -%}
47
+ {%- if forloop.first and section.settings.feature_first_bundle -%}
48
+ {%- assign card_variant = 'hero' -%}
49
+ {%- endif -%}
50
+ {% render 'runwell-bundle-card', bundle_product: bundle_product, variant: card_variant %}
51
+ {%- else -%}
52
+ <p class="runwell-bundle-collection__empty">
53
+ No bundles configured yet. Add the <code>bundle</code> tag to a product or set the <code>runwell.bundle_mode</code> metafield to surface it here.
54
+ </p>
55
+ {%- endfor -%}
56
+ </div>
57
+ </div>
58
+ </section>
59
+ {%- endif -%}
60
+
61
+ {%- elsif mode == 'filter_chip' -%}
62
+ {%- if settings.bundle_system__surface_6_collection_filter_enabled == false -%}
63
+ {%- comment -%} Tenant has not enabled the collection-page filter surface. {%- endcomment -%}
64
+ {%- else -%}
65
+ {%- if bundles and bundles.size > 0 -%}
66
+ <section
67
+ class="runwell-bundle-collection runwell-bundle-collection--filter-chip"
68
+ data-bundle-handles="{%- for b in bundles -%}{{ b.handle }}{%- unless forloop.last -%},{%- endunless -%}{%- endfor -%}"
69
+ >
70
+ <button class="runwell-bundle-collection__chip" data-runwell-bundle-filter-chip aria-pressed="false">
71
+ {{ section.settings.chip_label | default: 'Bundles' }}
72
+ <span class="runwell-bundle-collection__chip-count">({{ bundles.size }})</span>
73
+ </button>
74
+ <div class="runwell-bundle-collection__pinned">
75
+ {%- for bundle_product in bundles limit: 2 -%}
76
+ {% render 'runwell-bundle-card', bundle_product: bundle_product, variant: 'compact' %}
77
+ {%- endfor -%}
78
+ </div>
79
+ </section>
80
+ {%- endif -%}
81
+ {%- endif -%}
82
+ {%- endif -%}
83
+
84
+ {{ 'runwell-bundle-system.css' | asset_url | stylesheet_tag }}
85
+ <script src="{{ 'runwell-bundle-system.js' | asset_url }}" defer="defer"></script>
86
+
87
+ {% schema %}
88
+ {
89
+ "name": "Bundle collection",
90
+ "tag": "section",
91
+ "class": "section-runwell-bundle-collection",
92
+ "settings": [
93
+ {
94
+ "type": "select",
95
+ "id": "mode",
96
+ "label": "Layout mode",
97
+ "options": [
98
+ { "value": "grid", "label": "Grid (/bundles page)" },
99
+ { "value": "filter_chip", "label": "Filter chip (collection page)" }
100
+ ],
101
+ "default": "grid",
102
+ "info": "Grid: standalone /bundles page (Surface 1). Filter chip: pinned cards + filter on a collection page (Surface 6)."
103
+ },
104
+ { "type": "header", "content": "Grid mode" },
105
+ { "type": "checkbox", "id": "show_heading", "label": "Show page heading", "default": true },
106
+ { "type": "text", "id": "title", "label": "Heading", "default": "Bundles" },
107
+ { "type": "text", "id": "subline", "label": "Subline", "default": "Curated combinations, save more" },
108
+ { "type": "checkbox", "id": "feature_first_bundle", "label": "Feature first bundle as hero card", "default": true },
109
+ { "type": "header", "content": "Filter chip mode" },
110
+ { "type": "text", "id": "chip_label", "label": "Chip label", "default": "Bundles" },
111
+ { "type": "header", "content": "Layout" },
112
+ { "type": "color_scheme", "id": "color_scheme", "label": "Color scheme", "default": "scheme-1" },
113
+ { "type": "range", "id": "padding_top", "label": "Top padding", "min": 0, "max": 160, "step": 4, "default": 64, "unit": "px" },
114
+ { "type": "range", "id": "padding_bottom", "label": "Bottom padding", "min": 0, "max": 160, "step": 4, "default": 64, "unit": "px" }
115
+ ],
116
+ "presets": [
117
+ { "name": "Bundle: collection grid", "category": "Runwell", "settings": { "mode": "grid" } },
118
+ { "name": "Bundle: collection filter chip", "category": "Runwell", "settings": { "mode": "filter_chip" } }
119
+ ]
120
+ }
121
+ {% endschema %}
@@ -0,0 +1,77 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-home-stacks.liquid.
3
+ Surface 3: home page strip of 3 to 4 featured bundle cards. Editorial
4
+ pacing for new visitors. Merchant picks the bundles via a comma-
5
+ separated handle list; v1.5 can upgrade to a multi-product selector.
6
+
7
+ See _clients/capital-v/lushi/specs/bundle-system/spec.md sections 5, 8.3.
8
+ {%- endcomment -%}
9
+
10
+ {%- if settings.bundle_system__surface_3_home_stacks_enabled == false -%}
11
+ {%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
12
+ {%- else -%}
13
+ {%- assign picked_handles = section.settings.bundle_handles | split: ',' -%}
14
+ {%- assign rendered_count = 0 -%}
15
+
16
+ <section
17
+ class="runwell-bundle-home-stacks runwell-bundle-home-stacks--bg-{{ section.settings.bg_band }} color-{{ section.settings.color_scheme }}"
18
+ style="padding-top: {{ section.settings.padding_top }}px; padding-bottom: {{ section.settings.padding_bottom }}px;"
19
+ >
20
+ <div class="runwell-bundle-home-stacks__inner page-width">
21
+ <p class="runwell-bundle-home-stacks__eyebrow">{{ section.settings.eyebrow }}</p>
22
+ <h2 class="runwell-bundle-home-stacks__heading">{{ section.settings.heading }}</h2>
23
+ <div class="runwell-bundle-home-stacks__cards">
24
+ {%- for raw_handle in picked_handles limit: 4 -%}
25
+ {%- assign handle = raw_handle | strip -%}
26
+ {%- assign bundle_product = all_products[handle] -%}
27
+ {%- if bundle_product != blank -%}
28
+ {% render 'runwell-bundle-card', bundle_product: bundle_product, variant: 'default' %}
29
+ {%- assign rendered_count = rendered_count | plus: 1 -%}
30
+ {%- endif -%}
31
+ {%- endfor -%}
32
+ {%- if rendered_count == 0 -%}
33
+ <p class="runwell-bundle-home-stacks__empty">
34
+ Add bundle product handles in section settings to populate this strip.
35
+ </p>
36
+ {%- endif -%}
37
+ </div>
38
+ </div>
39
+ </section>
40
+
41
+ {{ 'runwell-bundle-system.css' | asset_url | stylesheet_tag }}
42
+ {%- endif -%}
43
+
44
+ {% schema %}
45
+ {
46
+ "name": "Bundle stacks",
47
+ "tag": "section",
48
+ "class": "section-runwell-bundle-home-stacks",
49
+ "settings": [
50
+ { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Editor's picks" },
51
+ { "type": "text", "id": "heading", "label": "Heading", "default": "Curated stacks" },
52
+ {
53
+ "type": "textarea",
54
+ "id": "bundle_handles",
55
+ "label": "Bundle product handles (comma-separated, max 4)",
56
+ "info": "Example: morning-stack, recovery-stack, glow-stack"
57
+ },
58
+ {
59
+ "type": "select",
60
+ "id": "bg_band",
61
+ "label": "Background",
62
+ "options": [
63
+ { "value": "white", "label": "White" },
64
+ { "value": "oat", "label": "Oat" },
65
+ { "value": "celadon-tint", "label": "Celadon tint" }
66
+ ],
67
+ "default": "white"
68
+ },
69
+ { "type": "color_scheme", "id": "color_scheme", "label": "Color scheme", "default": "scheme-1" },
70
+ { "type": "range", "id": "padding_top", "label": "Top padding", "min": 0, "max": 160, "step": 4, "default": 64, "unit": "px" },
71
+ { "type": "range", "id": "padding_bottom", "label": "Bottom padding", "min": 0, "max": 160, "step": 4, "default": 64, "unit": "px" }
72
+ ],
73
+ "presets": [
74
+ { "name": "Bundle: curated stacks", "category": "Runwell" }
75
+ ]
76
+ }
77
+ {% endschema %}
@@ -0,0 +1,50 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-pdp-banner.liquid.
3
+ Surface 5: subtle single-line banner near the buy box on non-bundle
4
+ PDPs. "Save 17% when bundled. View bundle." Tap routes to the bundle
5
+ PDP. Picks the first matching bundle (highest savings_pct if multiple).
6
+
7
+ See _clients/capital-v/lushi/specs/bundle-system/spec.md sections 5, 8.5.
8
+ {%- endcomment -%}
9
+
10
+ {%- if settings.bundle_system__surface_5_pdp_banner_enabled == false -%}
11
+ {%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
12
+ {%- else -%}
13
+ {%- assign bundle_index = shop.metaobjects.bundle_index.entries.value -%}
14
+ {%- assign matching_handles = '' -%}
15
+ {%- if bundle_index and bundle_index.products -%}
16
+ {%- assign matching_handles = bundle_index.products[product.handle] -%}
17
+ {%- endif -%}
18
+
19
+ {%- if matching_handles and matching_handles.size > 0 -%}
20
+ {%- assign bundle_handle = matching_handles | first -%}
21
+ {%- assign bundle_product = all_products[bundle_handle] -%}
22
+ {%- if bundle_product != blank -%}
23
+ {%- assign savings_pct = bundle_product.metafields.runwell.bundle_savings_pct.value | default: 15 -%}
24
+ {%- assign savings_amount = 0 -%}
25
+ {%- assign template = section.settings.copy_template | default: settings.bundle_system__surface_5_copy_template | default: 'Save {savings_pct}% when bundled' -%}
26
+ {%- assign copy = template | replace: '{savings_pct}', savings_pct | replace: '{savings_amount}', savings_amount -%}
27
+
28
+ <a class="runwell-bundle-pdp-banner" href="{{ bundle_product.url }}" data-bundle-handle="{{ bundle_handle }}">
29
+ <span class="runwell-bundle-pdp-banner__copy">{{ copy }}</span>
30
+ <span class="runwell-bundle-pdp-banner__chevron" aria-hidden="true">&rsaquo;</span>
31
+ </a>
32
+
33
+ {{ 'runwell-bundle-system.css' | asset_url | stylesheet_tag }}
34
+ {%- endif -%}
35
+ {%- endif -%}
36
+ {%- endif -%}
37
+
38
+ {% schema %}
39
+ {
40
+ "name": "Bundle banner",
41
+ "tag": "section",
42
+ "class": "section-runwell-bundle-pdp-banner",
43
+ "settings": [
44
+ { "type": "text", "id": "copy_template", "label": "Copy template", "default": "Save {savings_pct}% when bundled", "info": "Use {savings_pct} or {savings_amount} placeholders. Leave blank to use the site-wide default." }
45
+ ],
46
+ "presets": [
47
+ { "name": "Bundle: PDP banner", "category": "Runwell" }
48
+ ]
49
+ }
50
+ {% endschema %}
@@ -0,0 +1,72 @@
1
+ {%- comment -%}
2
+ runwell-bundle-system: runwell-bundle-pdp-pairs-with.liquid.
3
+ Surface 2: PDP "Pairs well with" widget. Renders 1 to N bundle cards
4
+ on non-bundle PDPs when the current product appears in at least one
5
+ bundle. Uses the bundle_index shop metaobject for fast reverse lookup
6
+ (one read).
7
+
8
+ Self-suppresses (renders nothing) if no bundles match.
9
+ See _clients/capital-v/lushi/specs/bundle-system/spec.md sections 5, 7.
10
+ {%- endcomment -%}
11
+
12
+ {%- if settings.bundle_system__surface_2_pdp_pairs_with_enabled == false -%}
13
+ {%- comment -%} Tenant has disabled this surface. {%- endcomment -%}
14
+ {%- else -%}
15
+ {%- assign bundle_index = shop.metaobjects.bundle_index.entries.value -%}
16
+ {%- assign matching_handles = '' -%}
17
+ {%- if bundle_index and bundle_index.products -%}
18
+ {%- assign matching_handles = bundle_index.products[product.handle] -%}
19
+ {%- endif -%}
20
+
21
+ {%- if matching_handles and matching_handles.size > 0 -%}
22
+ <section
23
+ class="runwell-bundle-pdp-pairs-with color-{{ section.settings.color_scheme }}"
24
+ data-product-handle="{{ product.handle }}"
25
+ style="padding-top: {{ section.settings.padding_top }}px; padding-bottom: {{ section.settings.padding_bottom }}px;"
26
+ >
27
+ <div class="runwell-bundle-pdp-pairs-with__inner page-width">
28
+ <p class="runwell-bundle-pdp-pairs-with__eyebrow">{{ section.settings.eyebrow }}</p>
29
+ <h3 class="runwell-bundle-pdp-pairs-with__heading">{{ section.settings.heading }}</h3>
30
+ <div class="runwell-bundle-pdp-pairs-with__cards" data-runwell-card-scroll>
31
+ {%- assign max_cards = section.settings.max_cards | plus: 0 | default: 2 -%}
32
+ {%- for handle in matching_handles limit: max_cards -%}
33
+ {%- assign bundle_product = all_products[handle] -%}
34
+ {%- if bundle_product != blank -%}
35
+ {% render 'runwell-bundle-card', bundle_product: bundle_product, variant: 'compact' %}
36
+ {%- endif -%}
37
+ {%- endfor -%}
38
+ </div>
39
+ </div>
40
+ </section>
41
+
42
+ {{ 'runwell-bundle-system.css' | asset_url | stylesheet_tag }}
43
+ {%- endif -%}
44
+ {%- endif -%}
45
+
46
+ {% schema %}
47
+ {
48
+ "name": "Bundle pairs-with",
49
+ "tag": "section",
50
+ "class": "section-runwell-bundle-pdp-pairs-with",
51
+ "settings": [
52
+ { "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Pairs well with" },
53
+ { "type": "text", "id": "heading", "label": "Heading", "default": "Complete the routine" },
54
+ {
55
+ "type": "select",
56
+ "id": "max_cards",
57
+ "label": "Max bundle cards",
58
+ "options": [
59
+ { "value": "1", "label": "1" },
60
+ { "value": "2", "label": "2" }
61
+ ],
62
+ "default": "2"
63
+ },
64
+ { "type": "color_scheme", "id": "color_scheme", "label": "Color scheme", "default": "scheme-1" },
65
+ { "type": "range", "id": "padding_top", "label": "Top padding", "min": 0, "max": 100, "step": 4, "default": 32, "unit": "px" },
66
+ { "type": "range", "id": "padding_bottom", "label": "Bottom padding", "min": 0, "max": 100, "step": 4, "default": 32, "unit": "px" }
67
+ ],
68
+ "presets": [
69
+ { "name": "Bundle: pairs well with", "category": "Runwell" }
70
+ ]
71
+ }
72
+ {% endschema %}