@runwell/shopify-toolkit 0.1.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 (76) hide show
  1. package/README.md +87 -0
  2. package/bin/runwell-shopify +98 -0
  3. package/lib/add.js +66 -0
  4. package/lib/config-loader.js +47 -0
  5. package/lib/doctor.js +66 -0
  6. package/lib/list.js +31 -0
  7. package/lib/remove.js +37 -0
  8. package/lib/sync.js +107 -0
  9. package/lib/template.js +57 -0
  10. package/lib/validate.js +23 -0
  11. package/modules/comparison-table/README.md +17 -0
  12. package/modules/comparison-table/module.json +50 -0
  13. package/modules/comparison-table/sections/runwell-comparison-table.liquid +157 -0
  14. package/modules/delivery-estimate/README.md +17 -0
  15. package/modules/delivery-estimate/module.json +14 -0
  16. package/modules/delivery-estimate/snippets/runwell-delivery-estimate.liquid +39 -0
  17. package/modules/editorial-block/README.md +17 -0
  18. package/modules/editorial-block/module.json +40 -0
  19. package/modules/editorial-block/sections/runwell-editorial-block.liquid +155 -0
  20. package/modules/editorial-hero/README.md +17 -0
  21. package/modules/editorial-hero/module.json +61 -0
  22. package/modules/editorial-hero/sections/runwell-video-hero.liquid +151 -0
  23. package/modules/exit-intent/README.md +18 -0
  24. package/modules/exit-intent/assets/runwell-exit-intent.js +54 -0
  25. package/modules/exit-intent/module.json +33 -0
  26. package/modules/exit-intent/sections/runwell-exit-intent.liquid +48 -0
  27. package/modules/faq/README.md +17 -0
  28. package/modules/faq/module.json +35 -0
  29. package/modules/faq/sections/runwell-faq.liquid +66 -0
  30. package/modules/how-it-works/README.md +17 -0
  31. package/modules/how-it-works/module.json +57 -0
  32. package/modules/how-it-works/sections/runwell-how-it-works.liquid +99 -0
  33. package/modules/inventory-urgency/README.md +17 -0
  34. package/modules/inventory-urgency/module.json +14 -0
  35. package/modules/inventory-urgency/snippets/runwell-inventory-urgency.liquid +19 -0
  36. package/modules/pdp-ingredients/README.md +17 -0
  37. package/modules/pdp-ingredients/module.json +40 -0
  38. package/modules/pdp-ingredients/sections/runwell-ingredients.liquid +139 -0
  39. package/modules/pdp-journal-link/README.md +17 -0
  40. package/modules/pdp-journal-link/module.json +53 -0
  41. package/modules/pdp-journal-link/sections/runwell-pdp-journal.liquid +124 -0
  42. package/modules/pdp-trust-checks/README.md +17 -0
  43. package/modules/pdp-trust-checks/module.json +49 -0
  44. package/modules/pdp-trust-checks/sections/runwell-pdp-trust.liquid +141 -0
  45. package/modules/post-purchase-upsell/README.md +52 -0
  46. package/modules/post-purchase-upsell/admin/discount-setup.md +25 -0
  47. package/modules/post-purchase-upsell/admin/order-status-paste.html +31 -0
  48. package/modules/post-purchase-upsell/assets/runwell-thank-you.css +119 -0
  49. package/modules/post-purchase-upsell/assets/runwell-thank-you.js +162 -0
  50. package/modules/post-purchase-upsell/module.json +44 -0
  51. package/modules/press-bar/README.md +17 -0
  52. package/modules/press-bar/module.json +30 -0
  53. package/modules/press-bar/sections/runwell-press-bar.liquid +119 -0
  54. package/modules/recently-viewed/README.md +18 -0
  55. package/modules/recently-viewed/assets/runwell-recently-viewed.js +57 -0
  56. package/modules/recently-viewed/module.json +33 -0
  57. package/modules/recently-viewed/sections/runwell-recently-viewed.liquid +38 -0
  58. package/modules/reviews/README.md +17 -0
  59. package/modules/reviews/module.json +20 -0
  60. package/modules/reviews/sections/runwell-pdp-reviews.liquid +93 -0
  61. package/modules/risk-reversal/README.md +17 -0
  62. package/modules/risk-reversal/module.json +49 -0
  63. package/modules/risk-reversal/sections/runwell-risk-reversal.liquid +94 -0
  64. package/modules/shipping-bar/README.md +17 -0
  65. package/modules/shipping-bar/module.json +45 -0
  66. package/modules/shipping-bar/sections/runwell-shipping-bar.liquid +95 -0
  67. package/modules/sticky-atc/README.md +17 -0
  68. package/modules/sticky-atc/module.json +14 -0
  69. package/modules/sticky-atc/sections/runwell-pdp-sticky.liquid +78 -0
  70. package/modules/testimonials/README.md +17 -0
  71. package/modules/testimonials/module.json +35 -0
  72. package/modules/testimonials/sections/runwell-testimonials.liquid +87 -0
  73. package/modules/trust-badges/README.md +17 -0
  74. package/modules/trust-badges/module.json +25 -0
  75. package/modules/trust-badges/sections/runwell-trust-badges.liquid +93 -0
  76. package/package.json +45 -0
@@ -0,0 +1,25 @@
1
+ # Discount setup for post-purchase-upsell
2
+
3
+ This module surfaces a discount-applied deep link to complementary products on the Shopify order status page. Create the matching discount code in Shopify before the upsell goes live.
4
+
5
+ ## Steps
6
+
7
+ 1. Go to **Discounts > Create discount > Amount off order**
8
+ 2. Method: **Discount code**
9
+ 3. Code: **{{config.discount_code}}**
10
+ 4. Discount value: **15% off** (or whatever rate matches your `discount_label` config)
11
+ 5. Applies to: All products
12
+ 6. Customer eligibility: All customers
13
+ 7. Maximum discount uses: 1 use per customer
14
+ 8. Active dates: Starts today, no end date
15
+ 9. Save
16
+
17
+ ## URL params auto-apply
18
+
19
+ When customers click an upsell card, the URL includes `?discount={{config.discount_code}}`. Shopify auto-applies the code at checkout if the discount is valid for the customer.
20
+
21
+ ## Test
22
+
23
+ 1. Place a test order on the dev store
24
+ 2. On the order status page, scroll past the order summary; the upsell strip should render with 3 complementary products
25
+ 3. Click any card; confirm the discount is auto-applied at checkout
@@ -0,0 +1,31 @@
1
+ <!-- Runwell post-purchase upsell. Paste this entire block into:
2
+ Shopify admin > Settings > Checkout > Order status page > Additional scripts.
3
+ Generated from @runwell/shopify-toolkit. Module: post-purchase-upsell.
4
+ Discount code in use: {{config.discount_code}}
5
+ -->
6
+ <div data-runwell-upsell></div>
7
+ <style>
8
+ /* Runwell post-purchase upsell inline styles for the order status page. */
9
+ .runwell-ty-upsell{margin:2.5rem 0;padding:2.5rem 1.5rem;background:{{brand.cream}};border-radius:8px;color:{{brand.primary}};font-family:system-ui,-apple-system,sans-serif}
10
+ .runwell-ty-upsell__inner{max-width:880px;margin:0 auto}
11
+ .runwell-ty-upsell__header{text-align:center;margin-bottom:2rem}
12
+ .runwell-ty-upsell__eyebrow{font-size:.78rem;letter-spacing:.2em;text-transform:uppercase;font-weight:700;margin:0 0 .6rem;opacity:.65}
13
+ .runwell-ty-upsell__heading{font-family:Georgia,serif;font-weight:400;font-size:clamp(1.7rem,3vw,2.4rem);line-height:1.1;margin:0 0 .8rem}
14
+ .runwell-ty-upsell__lede{font-size:.95rem;line-height:1.5;margin:0;opacity:.85;max-width:50ch;margin-inline:auto}
15
+ .runwell-ty-upsell__lede strong{background:rgba(173,221,189,.5);padding:.05em .35em;border-radius:2px;font-weight:700;letter-spacing:.04em}
16
+ .runwell-ty-upsell__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.2rem}
17
+ .runwell-ty-card{display:flex;flex-direction:column;background:#fff;border-radius:6px;padding:1.2rem;text-decoration:none;color:inherit;transition:transform .18s,box-shadow .18s;border:1px solid rgba(0,0,0,.06)}
18
+ .runwell-ty-card:hover{transform:translateY(-2px);box-shadow:0 12px 28px rgba(0,0,0,.08)}
19
+ .runwell-ty-card__media{aspect-ratio:4/5;background:{{brand.oat}};border-radius:4px;overflow:hidden;margin-bottom:.9rem}
20
+ .runwell-ty-card__media img{width:100%;height:100%;object-fit:cover;display:block}
21
+ .runwell-ty-card__body{flex:1;display:flex;flex-direction:column;gap:.2rem;margin-bottom:1rem}
22
+ .runwell-ty-card__title{font-family:Georgia,serif;font-size:1.1rem;line-height:1.2}
23
+ .runwell-ty-card__price{font-size:.9rem;opacity:.7}
24
+ .runwell-ty-card__cta{font-size:.85rem;font-weight:700;text-decoration:underline;text-underline-offset:4px;text-decoration-color:{{brand.accent}};margin-top:auto}
25
+ @media (max-width:749px){.runwell-ty-upsell{padding:2rem 1rem}.runwell-ty-upsell__grid{grid-template-columns:1fr}}
26
+ </style>
27
+ <script>
28
+ window.RUNWELL_THANKYOU_DISCOUNT = '{{config.discount_code}}';
29
+ window.RUNWELL_THANKYOU_DISCOUNT_LABEL = '{{config.discount_label}}';
30
+ </script>
31
+ <script src="https://{{client.store}}/cdn/shop/files/runwell-thank-you.js" defer></script>
@@ -0,0 +1,119 @@
1
+ /* Runwell post-purchase upsell styles. Uses CSS custom properties from
2
+ the client brand tokens so the upsell matches the rest of the theme.
3
+ Generated from @runwell/shopify-toolkit; do not hand-edit in client themes.
4
+ */
5
+ .runwell-ty-upsell {
6
+ margin: 2.5rem 0;
7
+ padding: 2.5rem 1.5rem;
8
+ background: var(--runwell-cream, {{brand.cream}});
9
+ border-radius: 8px;
10
+ font-family: var(--font-body-family, system-ui, sans-serif);
11
+ color: var(--runwell-primary, {{brand.primary}});
12
+ }
13
+ .runwell-ty-upsell__inner {
14
+ max-width: 880px;
15
+ margin: 0 auto;
16
+ }
17
+ .runwell-ty-upsell__header {
18
+ text-align: center;
19
+ margin-bottom: 2rem;
20
+ }
21
+ .runwell-ty-upsell__eyebrow {
22
+ font-size: 0.78rem;
23
+ letter-spacing: 0.2em;
24
+ text-transform: uppercase;
25
+ font-weight: 700;
26
+ margin: 0 0 0.6rem 0;
27
+ opacity: 0.65;
28
+ }
29
+ .runwell-ty-upsell__heading {
30
+ font-family: var(--font-heading-family, Georgia, serif);
31
+ font-style: normal;
32
+ font-weight: 400;
33
+ font-size: clamp(1.7rem, 3vw, 2.4rem);
34
+ line-height: 1.1;
35
+ margin: 0 0 0.8rem 0;
36
+ }
37
+ .runwell-ty-upsell__lede {
38
+ font-size: 0.95rem;
39
+ line-height: 1.5;
40
+ margin: 0;
41
+ opacity: 0.85;
42
+ max-width: 50ch;
43
+ margin-inline: auto;
44
+ }
45
+ .runwell-ty-upsell__lede strong {
46
+ background: var(--runwell-accent-soft, rgba(173, 221, 189, 0.5));
47
+ padding: 0.05em 0.35em;
48
+ border-radius: 2px;
49
+ font-weight: 700;
50
+ letter-spacing: 0.04em;
51
+ }
52
+ .runwell-ty-upsell__grid {
53
+ display: grid;
54
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
55
+ gap: 1.2rem;
56
+ }
57
+ .runwell-ty-card {
58
+ display: flex;
59
+ flex-direction: column;
60
+ background: #FFFFFF;
61
+ border-radius: 6px;
62
+ padding: 1.2rem;
63
+ text-decoration: none;
64
+ color: inherit;
65
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
66
+ border: 1px solid rgba(0, 0, 0, 0.06);
67
+ }
68
+ .runwell-ty-card:hover {
69
+ transform: translateY(-2px);
70
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08);
71
+ }
72
+ .runwell-ty-card__media {
73
+ aspect-ratio: 4/5;
74
+ background: var(--runwell-oat, {{brand.oat}});
75
+ border-radius: 4px;
76
+ overflow: hidden;
77
+ margin-bottom: 0.9rem;
78
+ }
79
+ .runwell-ty-card__media img {
80
+ width: 100%;
81
+ height: 100%;
82
+ object-fit: cover;
83
+ display: block;
84
+ }
85
+ .runwell-ty-card__media--noimg {
86
+ background: linear-gradient(135deg, var(--runwell-oat, #F5F0EE), var(--runwell-cream, #EDE6D8));
87
+ }
88
+ .runwell-ty-card__body {
89
+ flex: 1;
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 0.2rem;
93
+ margin-bottom: 1rem;
94
+ }
95
+ .runwell-ty-card__title {
96
+ font-family: var(--font-heading-family, Georgia, serif);
97
+ font-size: 1.1rem;
98
+ line-height: 1.2;
99
+ }
100
+ .runwell-ty-card__price {
101
+ font-size: 0.9rem;
102
+ opacity: 0.7;
103
+ }
104
+ .runwell-ty-card__cta {
105
+ font-size: 0.85rem;
106
+ font-weight: 700;
107
+ text-decoration: underline;
108
+ text-underline-offset: 4px;
109
+ text-decoration-color: var(--runwell-accent, {{brand.accent}});
110
+ margin-top: auto;
111
+ }
112
+ @media (max-width: 749px) {
113
+ .runwell-ty-upsell {
114
+ padding: 2rem 1rem;
115
+ }
116
+ .runwell-ty-upsell__grid {
117
+ grid-template-columns: 1fr;
118
+ }
119
+ }
@@ -0,0 +1,162 @@
1
+ /* Runwell post-purchase upsell module.
2
+ Renders on Shopify order status page using the Shopify.checkout
3
+ global. Pulls complementary products based on what was ordered and
4
+ surfaces a deep link with the configured discount code applied.
5
+ Replaces Reconvert / OneClickUpsell (display layer only).
6
+ Generated from @runwell/shopify-toolkit; do not hand-edit in client themes.
7
+ */
8
+ (function () {
9
+ if (typeof window === 'undefined') return;
10
+ var SHOP = window.Shopify && window.Shopify.shop ? window.Shopify.shop : null;
11
+ if (!SHOP) return;
12
+
13
+ var DISCOUNT_CODE = (window.RUNWELL_THANKYOU_DISCOUNT || '{{config.discount_code}}');
14
+ var DISCOUNT_LABEL = (window.RUNWELL_THANKYOU_DISCOUNT_LABEL || '{{config.discount_label}}');
15
+ var MAX_ITEMS = {{config.max_items}};
16
+ var HEADING = '{{config.heading}}';
17
+ var EYEBROW = '{{config.eyebrow}}';
18
+ var LEDE = '{{config.lede}}';
19
+
20
+ function endpoint() {
21
+ return 'https://' + SHOP;
22
+ }
23
+
24
+ function detectCategory(title) {
25
+ if (!title) return null;
26
+ var t = title.toLowerCase();
27
+ var skincare = ['serum', 'oil', 'moisturizer', 'cleanser', 'cream', 'spf', 'sunscreen', 'face', 'skin'];
28
+ var supplement = ['magnesium', 'collagen', 'tonic', 'vitamin', 'tincture', 'capsule', 'protein', 'mineral'];
29
+ for (var i = 0; i < skincare.length; i++) if (t.indexOf(skincare[i]) !== -1) return 'skincare';
30
+ for (var j = 0; j < supplement.length; j++) if (t.indexOf(supplement[j]) !== -1) return 'supplement';
31
+ return null;
32
+ }
33
+
34
+ function getOrderedItems() {
35
+ if (window.Shopify && window.Shopify.checkout && window.Shopify.checkout.line_items) {
36
+ return window.Shopify.checkout.line_items.map(function (li) {
37
+ return { product_id: li.product_id, handle: li.handle || null, title: li.title || '' };
38
+ });
39
+ }
40
+ return [];
41
+ }
42
+
43
+ function fetchAllProducts() {
44
+ return fetch(endpoint() + '/products.json?limit=20')
45
+ .then(function (r) { return r.ok ? r.json() : { products: [] }; })
46
+ .then(function (j) { return j.products || []; })
47
+ .catch(function () { return []; });
48
+ }
49
+
50
+ function fetchRecommendations(productId) {
51
+ var intents = ['complementary', 'related'];
52
+ var attempt = function (i) {
53
+ if (i >= intents.length) return Promise.resolve([]);
54
+ return fetch(endpoint() + '/recommendations/products.json?product_id=' + productId + '&limit=' + MAX_ITEMS + '&intent=' + intents[i])
55
+ .then(function (r) { return r.ok ? r.json() : { products: [] }; })
56
+ .then(function (j) {
57
+ if (j.products && j.products.length) return j.products;
58
+ return attempt(i + 1);
59
+ })
60
+ .catch(function () { return attempt(i + 1); });
61
+ };
62
+ return attempt(0);
63
+ }
64
+
65
+ function pickFallback(allProducts, ordered) {
66
+ var orderedHandles = {};
67
+ var orderedCategories = {};
68
+ ordered.forEach(function (li) {
69
+ if (li.handle) orderedHandles[li.handle] = true;
70
+ var cat = detectCategory(li.title);
71
+ if (cat) orderedCategories[cat] = true;
72
+ });
73
+ var oppositeCategory = null;
74
+ if (orderedCategories.skincare && !orderedCategories.supplement) oppositeCategory = 'supplement';
75
+ else if (orderedCategories.supplement && !orderedCategories.skincare) oppositeCategory = 'skincare';
76
+
77
+ var ranked = allProducts.filter(function (p) { return !orderedHandles[p.handle]; });
78
+ if (oppositeCategory) {
79
+ ranked.sort(function (a, b) {
80
+ var aMatch = detectCategory(a.title) === oppositeCategory ? 0 : 1;
81
+ var bMatch = detectCategory(b.title) === oppositeCategory ? 0 : 1;
82
+ return aMatch - bMatch;
83
+ });
84
+ }
85
+ return ranked.slice(0, MAX_ITEMS);
86
+ }
87
+
88
+ function moneyFormat(cents) {
89
+ var dollars = (cents / 100).toFixed(2);
90
+ return '$' + dollars;
91
+ }
92
+
93
+ function render(container, products) {
94
+ if (!products.length) { container.style.display = 'none'; return; }
95
+ var html =
96
+ '<aside class="runwell-ty-upsell" role="region" aria-labelledby="runwell-ty-title">' +
97
+ '<div class="runwell-ty-upsell__inner">' +
98
+ '<header class="runwell-ty-upsell__header">' +
99
+ '<p class="runwell-ty-upsell__eyebrow">' + EYEBROW + '</p>' +
100
+ '<h2 id="runwell-ty-title" class="runwell-ty-upsell__heading">' + HEADING + '</h2>' +
101
+ '<p class="runwell-ty-upsell__lede">' + LEDE + ' Use code <strong>' + DISCOUNT_CODE + '</strong> for ' + DISCOUNT_LABEL + ' (auto-applied below).</p>' +
102
+ '</header>' +
103
+ '<div class="runwell-ty-upsell__grid">' +
104
+ products.map(function (p) {
105
+ var price = (p.variants && p.variants[0] && p.variants[0].price) ? p.variants[0].price : null;
106
+ var img = p.images && p.images[0] ? (typeof p.images[0] === 'string' ? p.images[0] : p.images[0].src) : null;
107
+ var url = '/products/' + p.handle + '?discount=' + encodeURIComponent(DISCOUNT_CODE);
108
+ return (
109
+ '<a class="runwell-ty-card" href="' + url + '">' +
110
+ (img ? '<div class="runwell-ty-card__media"><img src="' + img + '" alt="' + (p.title || '').replace(/"/g, '&quot;') + '" loading="lazy"></div>' : '<div class="runwell-ty-card__media runwell-ty-card__media--noimg"></div>') +
111
+ '<div class="runwell-ty-card__body">' +
112
+ '<span class="runwell-ty-card__title">' + (p.title || '') + '</span>' +
113
+ (price ? '<span class="runwell-ty-card__price">' + (typeof price === 'number' ? moneyFormat(price) : '$' + price) + '</span>' : '') +
114
+ '</div>' +
115
+ '<span class="runwell-ty-card__cta">Add with ' + DISCOUNT_LABEL.replace(' your next order', '') + ' &rarr;</span>' +
116
+ '</a>'
117
+ );
118
+ }).join('') +
119
+ '</div>' +
120
+ '</div>' +
121
+ '</aside>';
122
+ container.innerHTML = html;
123
+ container.style.display = '';
124
+ }
125
+
126
+ function inject() {
127
+ var host = document.querySelector('[data-lushi-upsell]');
128
+ if (host) return host;
129
+ var anchor = document.querySelector('.os-step__info, .os-step, .step__sections, .main__content, main') || document.body;
130
+ var div = document.createElement('div');
131
+ div.setAttribute('data-lushi-upsell', '');
132
+ anchor.appendChild(div);
133
+ return div;
134
+ }
135
+
136
+ function run() {
137
+ var ordered = getOrderedItems();
138
+ if (!ordered.length) return;
139
+ var firstId = ordered[0].product_id;
140
+
141
+ fetchRecommendations(firstId)
142
+ .then(function (recs) {
143
+ var dedup = recs.filter(function (p) {
144
+ return !ordered.some(function (o) { return o.product_id === p.id || o.handle === p.handle; });
145
+ }).slice(0, MAX_ITEMS);
146
+ if (dedup.length >= MAX_ITEMS) return dedup;
147
+ return fetchAllProducts().then(function (all) {
148
+ return pickFallback(all, ordered);
149
+ });
150
+ })
151
+ .then(function (products) {
152
+ var host = inject();
153
+ render(host, products);
154
+ });
155
+ }
156
+
157
+ if (document.readyState === 'loading') {
158
+ document.addEventListener('DOMContentLoaded', run);
159
+ } else {
160
+ run();
161
+ }
162
+ })();
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "post-purchase-upsell",
3
+ "version": "0.1.0",
4
+ "category": "conversion",
5
+ "description": "Order status page upsell with auto-applied discount on click-through. Replaces Reconvert / OneClickUpsell / AfterSell display layer. Native, app-free.",
6
+ "replaces": ["reconvert", "one-click-upsell", "aftersell"],
7
+ "shopify_plan_required": "basic",
8
+ "shopify_features_used": ["order_status_page_additional_scripts"],
9
+ "files": {
10
+ "assets": [
11
+ "assets/runwell-thank-you.js",
12
+ "assets/runwell-thank-you.css"
13
+ ],
14
+ "admin_blocks": [
15
+ "admin/order-status-paste.html",
16
+ "admin/discount-setup.md"
17
+ ]
18
+ },
19
+ "config": {
20
+ "schema": {
21
+ "discount_code": { "type": "string", "required": true, "default": "WELCOME15" },
22
+ "discount_label": { "type": "string", "required": true, "default": "15% off your next order" },
23
+ "max_items": { "type": "integer", "default": 3, "min": 1, "max": 6 },
24
+ "heading": { "type": "string", "default": "Complete the ritual." },
25
+ "eyebrow": { "type": "string", "default": "While you wait" },
26
+ "lede": { "type": "string", "default": "A few of our customers tend to add these next." },
27
+ "cross_category_logic": { "type": "boolean", "default": true }
28
+ }
29
+ },
30
+ "admin_steps": [
31
+ {
32
+ "id": "create-discount",
33
+ "label": "Create the discount code",
34
+ "url": "https://admin.shopify.com/store/{store_handle}/discounts/new",
35
+ "summary": "Create a percentage discount code matching the discount_code config (e.g. WELCOME15). Type: Amount off order. Value: 15%. Customer eligibility: All. Maximum uses per customer: 1."
36
+ },
37
+ {
38
+ "id": "paste-bootstrap",
39
+ "label": "Paste the upsell bootstrap into Order status page additional scripts",
40
+ "url": "https://admin.shopify.com/store/{store_handle}/settings/checkout",
41
+ "summary": "Settings, Checkout, Order status page, Additional scripts, paste contents of admin/order-status-paste.html (after running runwell-shopify sync, the rendered version with the configured discount code lives in the client theme runwell-admin/post-purchase-upsell/order-status-paste.html)."
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,17 @@
1
+ # press-bar
2
+
3
+ Lushi press bar. Migrated from Lushi (`sections/lushi-press-bar.liquid`) into the Runwell Shopify Toolkit.
4
+
5
+ Category: `social-proof`
6
+
7
+ ## Files
8
+
9
+ - `sections/runwell-press-bar.liquid`
10
+
11
+ ## Config
12
+
13
+ See `module.json` config schema. Defaults match the original Lushi defaults; override per client in `runwell.config.json`.
14
+
15
+ ## Lineage
16
+
17
+ This module was extracted from the Lushi build (Capital V) and generalised. The original lives at `lushi-shopify/sections/lushi-press-bar.liquid`. The toolkit version uses `runwell-` class prefixes and pulls brand vars from the client config.
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "press-bar",
3
+ "version": "0.1.0",
4
+ "category": "social-proof",
5
+ "description": "Lushi press bar module migrated from Lushi.",
6
+ "files": {
7
+ "sections": [
8
+ "sections/runwell-press-bar.liquid"
9
+ ]
10
+ },
11
+ "config": {
12
+ "schema": {
13
+ "eyebrow": {
14
+ "type": "string",
15
+ "default": "As seen in",
16
+ "label": "Eyebrow"
17
+ },
18
+ "background_color": {
19
+ "type": "string",
20
+ "default": "#F5F0EE",
21
+ "label": "Background color"
22
+ },
23
+ "text_color": {
24
+ "type": "string",
25
+ "default": "#0B3D38",
26
+ "label": "Text color"
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,119 @@
1
+ {%- comment -%}
2
+ Lushi press bar. Logos in a row with optional eyebrow text and link.
3
+ Used on homepage and about page. Keep tight; the wins do the talking.
4
+
5
+ Block options (in priority order):
6
+ 1. block.settings.image (Shopify-uploaded image, image_picker)
7
+ 2. block.settings.asset_filename (bundled theme asset, e.g. runwell-press-vogue.png)
8
+ 3. block.settings.label (text fallback in italic Baskervville)
9
+
10
+ Default homepage preset uses bundled asset filenames so the bar
11
+ renders on day one without any admin asset uploads.
12
+ {%- endcomment -%}
13
+
14
+ <section
15
+ class="runwell-press-bar"
16
+ style="background: {{ section.settings.background_color }}; color: {{ section.settings.text_color }}; padding: clamp(2rem, 4vw, 3rem) 6vw;"
17
+ >
18
+ <div style="max-width: 1400px; margin: 0 auto;">
19
+ {%- if section.settings.eyebrow != blank -%}
20
+ <div style="text-align: center; font-family: var(--font-body-family); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; opacity: 0.6; margin-bottom: 1.6rem;">
21
+ {{ section.settings.eyebrow }}
22
+ </div>
23
+ {%- endif -%}
24
+
25
+ <div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: clamp(2rem, 5vw, 4rem);">
26
+ {%- for block in section.blocks -%}
27
+ <div {{ block.shopify_attributes }} style="text-align: center;">
28
+ {%- if block.settings.image != blank -%}
29
+ <img
30
+ src="{{ block.settings.image | image_url: height: 80 }}"
31
+ alt="{{ block.settings.label | escape }}"
32
+ width="180"
33
+ height="40"
34
+ loading="lazy">
35
+ {%- elsif block.settings.asset_filename != blank -%}
36
+ <img
37
+ src="{{ block.settings.asset_filename | asset_url }}"
38
+ alt="{{ block.settings.label | escape }}"
39
+ width="180"
40
+ height="40"
41
+ loading="lazy">
42
+ {%- elsif block.settings.label != blank -%}
43
+ <span style="font-family: var(--font-heading-family); font-style: italic; font-weight: 400; font-size: clamp(1.1rem, 2vw, 1.5rem);">
44
+ {{ block.settings.label }}
45
+ </span>
46
+ {%- endif -%}
47
+ </div>
48
+ {%- endfor -%}
49
+ </div>
50
+ </div>
51
+ </section>
52
+
53
+ {% schema %}
54
+ {
55
+ "name": "Lushi press bar",
56
+ "tag": "section",
57
+ "class": "section-runwell-press-bar",
58
+ "settings": [
59
+ {
60
+ "type": "text",
61
+ "id": "eyebrow",
62
+ "label": "Eyebrow",
63
+ "default": "As seen in"
64
+ },
65
+ {
66
+ "type": "color",
67
+ "id": "background_color",
68
+ "label": "Background color",
69
+ "default": "#F5F0EE"
70
+ },
71
+ {
72
+ "type": "color",
73
+ "id": "text_color",
74
+ "label": "Text color",
75
+ "default": "#0B3D38"
76
+ }
77
+ ],
78
+ "blocks": [
79
+ {
80
+ "type": "logo",
81
+ "name": "Logo",
82
+ "settings": [
83
+ {
84
+ "type": "image_picker",
85
+ "id": "image",
86
+ "label": "Logo image (uploaded)",
87
+ "info": "Optional. Falls through to bundled asset, then text label."
88
+ },
89
+ {
90
+ "type": "text",
91
+ "id": "asset_filename",
92
+ "label": "Bundled asset filename",
93
+ "info": "Use a theme asset filename (e.g. runwell-press-bloomberg.png)."
94
+ },
95
+ {
96
+ "type": "text",
97
+ "id": "label",
98
+ "label": "Label / alt text",
99
+ "default": "Publication"
100
+ }
101
+ ]
102
+ }
103
+ ],
104
+ "max_blocks": 9,
105
+ "presets": [
106
+ {
107
+ "name": "Lushi press bar",
108
+ "blocks": [
109
+ { "type": "logo", "settings": { "label": "Bloomberg", "asset_filename": "runwell-press-bloomberg.png" } },
110
+ { "type": "logo", "settings": { "label": "Yahoo Finance", "asset_filename": "runwell-press-yahoo-finance.png" } },
111
+ { "type": "logo", "settings": { "label": "Fortune", "asset_filename": "runwell-press-fortune.png" } },
112
+ { "type": "logo", "settings": { "label": "Inc.", "asset_filename": "runwell-press-inc.png" } },
113
+ { "type": "logo", "settings": { "label": "MSN", "asset_filename": "runwell-press-msn.png" } },
114
+ { "type": "logo", "settings": { "label": "Fierce Healthcare","asset_filename": "runwell-press-fierce-healthcare.png" } }
115
+ ]
116
+ }
117
+ ]
118
+ }
119
+ {% endschema %}
@@ -0,0 +1,18 @@
1
+ # recently-viewed
2
+
3
+ Lushi recently viewed. Migrated from Lushi (`sections/lushi-recently-viewed.liquid`) into the Runwell Shopify Toolkit.
4
+
5
+ Category: `pdp`
6
+
7
+ ## Files
8
+
9
+ - `sections/runwell-recently-viewed.liquid`
10
+ - `assets/runwell-recently-viewed.js`
11
+
12
+ ## Config
13
+
14
+ See `module.json` config schema. Defaults match the original Lushi defaults; override per client in `runwell.config.json`.
15
+
16
+ ## Lineage
17
+
18
+ This module was extracted from the Lushi build (Capital V) and generalised. The original lives at `lushi-shopify/sections/lushi-recently-viewed.liquid`. The toolkit version uses `runwell-` class prefixes and pulls brand vars from the client config.
@@ -0,0 +1,57 @@
1
+ /* Lushi: track recently viewed products in localStorage and render a
2
+ "Recently viewed" section on PDP and home. Replaces the "Recently
3
+ Viewed" feature from Vitals/Pagefly without the app overhead. */
4
+ (function () {
5
+ if (typeof window === 'undefined') return;
6
+ var KEY = 'lushi_recently_viewed';
7
+ var MAX = 8;
8
+
9
+ function read() {
10
+ try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; }
11
+ }
12
+ function write(list) {
13
+ try { localStorage.setItem(KEY, JSON.stringify(list)); } catch (e) {}
14
+ }
15
+ function track() {
16
+ var meta = document.querySelector('meta[name="runwell-product-handle"]');
17
+ if (!meta) return;
18
+ var handle = meta.getAttribute('content');
19
+ if (!handle) return;
20
+ var list = read().filter(function (h) { return h !== handle; });
21
+ list.unshift(handle);
22
+ if (list.length > MAX) list = list.slice(0, MAX);
23
+ write(list);
24
+ }
25
+ function render() {
26
+ var host = document.querySelector('[data-runwell-recently-viewed]');
27
+ if (!host) return;
28
+ var list = read();
29
+ var currentHandle = (document.querySelector('meta[name="runwell-product-handle"]') || {}).getAttribute && document.querySelector('meta[name="runwell-product-handle"]').getAttribute('content');
30
+ var handles = list.filter(function (h) { return h && h !== currentHandle; }).slice(0, 4);
31
+ if (!handles.length) { host.style.display = 'none'; return; }
32
+ Promise.all(handles.map(function (h) {
33
+ return fetch('/products/' + h + '.js').then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
34
+ })).then(function (products) {
35
+ products = products.filter(Boolean);
36
+ if (!products.length) { host.style.display = 'none'; return; }
37
+ var grid = host.querySelector('[data-runwell-recently-viewed-grid]');
38
+ if (!grid) return;
39
+ grid.innerHTML = products.map(function (p) {
40
+ var img = p.featured_image ? '<img src="' + p.featured_image + '" alt="' + p.title + '" loading="lazy">' : '<div class="runwell-rv__noimg"></div>';
41
+ var price = (p.price / 100).toFixed(2);
42
+ return '<a class="runwell-rv__card" href="/products/' + p.handle + '">' +
43
+ '<div class="runwell-rv__media">' + img + '</div>' +
44
+ '<div class="runwell-rv__title">' + p.title + '</div>' +
45
+ '<div class="runwell-rv__price">$' + price + '</div>' +
46
+ '</a>';
47
+ }).join('');
48
+ host.style.display = '';
49
+ });
50
+ }
51
+ track();
52
+ if (document.readyState === 'loading') {
53
+ document.addEventListener('DOMContentLoaded', render);
54
+ } else {
55
+ render();
56
+ }
57
+ })();
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "recently-viewed",
3
+ "version": "0.1.0",
4
+ "category": "pdp",
5
+ "description": "Lushi recently viewed module migrated from Lushi.",
6
+ "files": {
7
+ "sections": [
8
+ "sections/runwell-recently-viewed.liquid"
9
+ ],
10
+ "assets": [
11
+ "assets/runwell-recently-viewed.js"
12
+ ]
13
+ },
14
+ "config": {
15
+ "schema": {
16
+ "eyebrow": {
17
+ "type": "string",
18
+ "default": "Picking up where you left off",
19
+ "label": "Eyebrow"
20
+ },
21
+ "heading": {
22
+ "type": "string",
23
+ "default": "Recently viewed.",
24
+ "label": "Heading"
25
+ },
26
+ "background_color": {
27
+ "type": "string",
28
+ "default": "#FFFFFF",
29
+ "label": "Background"
30
+ }
31
+ }
32
+ }
33
+ }