@runwell/shopify-toolkit 0.7.0 → 0.9.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.
- package/bin/runwell-shopify +13 -0
- package/lib/baseline.js +203 -0
- package/lib/init.js +122 -0
- package/lib/sync.js +16 -1
- package/lib/upgrade-baseline.js +25 -0
- package/modules/comparison-table/variants/feature-columns/sections/runwell-comparison-table.liquid +3 -3
- package/modules/delivery-estimate/snippets/runwell-delivery-estimate.liquid +1 -1
- package/modules/editorial-block/sections/runwell-editorial-block.liquid +3 -3
- package/modules/editorial-hero/variants/video-bg/sections/runwell-video-hero.liquid +3 -3
- package/modules/exit-intent/sections/runwell-exit-intent.liquid +3 -3
- package/modules/faq/sections/runwell-faq.liquid +5 -5
- package/modules/how-it-works/sections/runwell-how-it-works.liquid +5 -5
- package/modules/inventory-urgency/snippets/runwell-inventory-urgency.liquid +1 -1
- package/modules/pdp-ingredients/sections/runwell-ingredients.liquid +14 -6
- package/modules/pdp-journal-link/sections/runwell-pdp-journal.liquid +18 -9
- package/modules/pdp-trust-checks/sections/runwell-pdp-trust.liquid +4 -4
- package/modules/press-bar/sections/runwell-press-bar.liquid +3 -3
- package/modules/recently-viewed/sections/runwell-recently-viewed.liquid +3 -3
- package/modules/reviews/sections/runwell-pdp-reviews.liquid +15 -9
- package/modules/risk-reversal/sections/runwell-risk-reversal.liquid +3 -3
- package/modules/shipping-bar/sections/runwell-shipping-bar.liquid +3 -3
- package/modules/sticky-atc/sections/runwell-pdp-sticky.liquid +3 -3
- package/modules/testimonials/variants/grid-quotes/sections/runwell-testimonials.liquid +3 -3
- package/modules/trust-badges/sections/runwell-trust-badges.liquid +4 -4
- package/package.json +1 -1
package/bin/runwell-shopify
CHANGED
|
@@ -8,6 +8,8 @@ import { remove } from '../lib/remove.js';
|
|
|
8
8
|
import { doctor } from '../lib/doctor.js';
|
|
9
9
|
import { validate } from '../lib/validate.js';
|
|
10
10
|
import { qa } from '../lib/qa.js';
|
|
11
|
+
import { init } from '../lib/init.js';
|
|
12
|
+
import { upgradeBaseline } from '../lib/upgrade-baseline.js';
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
15
|
const __dirname = path.dirname(__filename);
|
|
@@ -49,6 +51,11 @@ Commands:
|
|
|
49
51
|
qa Code-first QA pipeline (validate-config, doctor,
|
|
50
52
|
template-integrity, orphan-assets, theme-check).
|
|
51
53
|
Run before any visual QA or "done" declaration.
|
|
54
|
+
init <client> Scaffold a new tenant theme directory pinned to
|
|
55
|
+
the dawn-runwell baseline + toolkit. Use --baseline
|
|
56
|
+
to override the default pin.
|
|
57
|
+
upgrade-baseline <pkg@version> Bump the baseline pin in runwell.config.json.
|
|
58
|
+
Run "runwell-shopify sync" after to apply.
|
|
52
59
|
help Show this help
|
|
53
60
|
|
|
54
61
|
Common options:
|
|
@@ -86,6 +93,12 @@ flags.toolkitRoot = TOOLKIT_ROOT;
|
|
|
86
93
|
case 'qa':
|
|
87
94
|
await qa(flags);
|
|
88
95
|
break;
|
|
96
|
+
case 'init':
|
|
97
|
+
await init(flags);
|
|
98
|
+
break;
|
|
99
|
+
case 'upgrade-baseline':
|
|
100
|
+
await upgradeBaseline(flags);
|
|
101
|
+
break;
|
|
89
102
|
case undefined:
|
|
90
103
|
case 'help':
|
|
91
104
|
case '--help':
|
package/lib/baseline.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { interpolate } from './template.js';
|
|
6
|
+
|
|
7
|
+
/* baseline.js: resolve, fetch, and apply the @runwell/dawn-runwell
|
|
8
|
+
baseline (or another baseline package) into a tenant theme dir.
|
|
9
|
+
|
|
10
|
+
Resolution order:
|
|
11
|
+
1. flags.baselinePath (CLI override)
|
|
12
|
+
2. config.baseline_path (local dev override in runwell.config.json)
|
|
13
|
+
3. config.baseline (e.g. "@runwell/dawn-runwell@^1.0.0") via the
|
|
14
|
+
cache at ~/.runwell/baseline-cache/<pkg>/<version>/
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const CACHE_DIR = path.join(os.homedir(), '.runwell', 'baseline-cache');
|
|
18
|
+
|
|
19
|
+
export function parseBaselinePin(pin) {
|
|
20
|
+
if (!pin) return null;
|
|
21
|
+
// accept "@runwell/dawn-runwell@^1.0.0" or "@runwell/dawn-runwell@latest"
|
|
22
|
+
const m = pin.match(/^(@?[^@]+)@(.+)$/);
|
|
23
|
+
if (!m) return { name: pin, version: 'latest' };
|
|
24
|
+
return { name: m[1], version: m[2] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveBaseline(flags, config) {
|
|
28
|
+
if (flags && flags.baselinePath) return flags.baselinePath;
|
|
29
|
+
if (config && config.baseline_path) return path.resolve(config.baseline_path);
|
|
30
|
+
if (!config || !config.baseline) return null;
|
|
31
|
+
|
|
32
|
+
const pin = parseBaselinePin(config.baseline);
|
|
33
|
+
if (!pin) return null;
|
|
34
|
+
const safeName = pin.name.replace('/', '__');
|
|
35
|
+
const versionDir = path.join(CACHE_DIR, safeName, pin.version);
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(path.join(versionDir, 'baseline-manifest.json'))) {
|
|
38
|
+
return versionDir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Cache miss: fetch via npm pack
|
|
42
|
+
console.log(`Fetching baseline: ${pin.name}@${pin.version} ...`);
|
|
43
|
+
fs.mkdirSync(versionDir, { recursive: true });
|
|
44
|
+
const packResult = spawnSync('npm', ['pack', `${pin.name}@${pin.version}`, '--pack-destination', versionDir], {
|
|
45
|
+
stdio: 'pipe',
|
|
46
|
+
encoding: 'utf8'
|
|
47
|
+
});
|
|
48
|
+
if (packResult.status !== 0) {
|
|
49
|
+
throw new Error(`npm pack failed: ${packResult.stderr || packResult.stdout}`);
|
|
50
|
+
}
|
|
51
|
+
const tarball = packResult.stdout.trim().split('\n').pop();
|
|
52
|
+
const tarPath = path.join(versionDir, tarball);
|
|
53
|
+
// npm pack delivers a tarball that extracts to ./package/
|
|
54
|
+
const tarResult = spawnSync('tar', ['-xzf', tarPath, '-C', versionDir, '--strip-components=1'], { stdio: 'inherit' });
|
|
55
|
+
if (tarResult.status !== 0) throw new Error('tar extract failed');
|
|
56
|
+
fs.unlinkSync(tarPath);
|
|
57
|
+
return versionDir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadBaselineManifest(baselineRoot) {
|
|
61
|
+
const p = path.join(baselineRoot, 'baseline-manifest.json');
|
|
62
|
+
if (!fs.existsSync(p)) {
|
|
63
|
+
throw new Error(`baseline-manifest.json not found at ${p}`);
|
|
64
|
+
}
|
|
65
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function syncBaseline({ baselineRoot, targetDir, config, dryRun, written }) {
|
|
69
|
+
const manifest = loadBaselineManifest(baselineRoot);
|
|
70
|
+
console.log(`\n[baseline: ${manifest.name}@${manifest.version}]`);
|
|
71
|
+
|
|
72
|
+
const interpolationVars = {
|
|
73
|
+
config: {},
|
|
74
|
+
brand: config.brand || {},
|
|
75
|
+
client: { name: config.client, store: config.store }
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 1. Owned files (verbatim or interpolated)
|
|
79
|
+
for (const [bucket, files] of Object.entries(manifest.files || {})) {
|
|
80
|
+
if (!Array.isArray(files)) continue;
|
|
81
|
+
const targetSubdir = bucketToTargetDir(bucket, targetDir);
|
|
82
|
+
if (!targetSubdir) continue;
|
|
83
|
+
for (const rel of files) {
|
|
84
|
+
const sourcePath = path.join(baselineRoot, bucket, rel);
|
|
85
|
+
if (!fs.existsSync(sourcePath)) {
|
|
86
|
+
console.warn(` [warn] missing baseline source: ${bucket}/${rel}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const filename = path.basename(rel);
|
|
90
|
+
const targetPath = path.join(targetSubdir, filename);
|
|
91
|
+
const isText = isTextFile(filename);
|
|
92
|
+
let content = fs.readFileSync(sourcePath, isText ? 'utf8' : null);
|
|
93
|
+
if (isText) content = interpolate(content, interpolationVars);
|
|
94
|
+
if (!dryRun) {
|
|
95
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
96
|
+
fs.writeFileSync(targetPath, content);
|
|
97
|
+
}
|
|
98
|
+
console.log(` + ${path.relative(targetDir, targetPath)}`);
|
|
99
|
+
written.push({ source: 'baseline', target: targetPath, bucket, baseline_version: manifest.version });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Patches (deep-merge into existing tenant files)
|
|
104
|
+
for (const [tenantRel, patchRel] of Object.entries(manifest.patches || {})) {
|
|
105
|
+
const patchPath = path.join(baselineRoot, patchRel);
|
|
106
|
+
const tenantPath = path.join(targetDir, tenantRel);
|
|
107
|
+
if (!fs.existsSync(patchPath)) {
|
|
108
|
+
console.warn(` [warn] missing baseline patch: ${patchRel}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!fs.existsSync(tenantPath)) {
|
|
112
|
+
console.warn(` [warn] tenant file ${tenantRel} not present; skipping patch (run shopify theme pull first)`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const patchData = JSON.parse(fs.readFileSync(patchPath, 'utf8'));
|
|
116
|
+
const tenantData = JSON.parse(fs.readFileSync(tenantPath, 'utf8'));
|
|
117
|
+
if (tenantRel === 'config/settings_schema.json') {
|
|
118
|
+
applySettingsPatches(tenantData, patchData);
|
|
119
|
+
} else if (tenantRel.startsWith('locales/')) {
|
|
120
|
+
deepMerge(tenantData, patchData.patches || {});
|
|
121
|
+
} else {
|
|
122
|
+
deepMerge(tenantData, patchData);
|
|
123
|
+
}
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
fs.writeFileSync(tenantPath, JSON.stringify(tenantData, null, 2) + '\n');
|
|
126
|
+
}
|
|
127
|
+
console.log(` ~ patched ${tenantRel}`);
|
|
128
|
+
written.push({ source: 'baseline-patch', target: tenantPath, bucket: 'patch', baseline_version: manifest.version });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function applySettingsPatches(tenantSchema, patchDoc) {
|
|
133
|
+
// tenantSchema is an array of setting groups. patchDoc.patches is an array.
|
|
134
|
+
const patches = patchDoc.patches || [];
|
|
135
|
+
for (const patchGroup of patches) {
|
|
136
|
+
const existing = tenantSchema.find(g => g.name === patchGroup.name);
|
|
137
|
+
if (!existing) {
|
|
138
|
+
tenantSchema.push(patchGroup);
|
|
139
|
+
} else {
|
|
140
|
+
// Merge settings, skipping duplicates by id (or by content for header/paragraph)
|
|
141
|
+
const existingIds = new Set((existing.settings || []).map(s => s.id).filter(Boolean));
|
|
142
|
+
for (const s of patchGroup.settings || []) {
|
|
143
|
+
if (s.id && existingIds.has(s.id)) continue;
|
|
144
|
+
existing.settings = existing.settings || [];
|
|
145
|
+
existing.settings.push(s);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function deepMerge(target, source) {
|
|
152
|
+
for (const key of Object.keys(source)) {
|
|
153
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
154
|
+
target[key] = target[key] || {};
|
|
155
|
+
deepMerge(target[key], source[key]);
|
|
156
|
+
} else {
|
|
157
|
+
target[key] = source[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function bucketToTargetDir(bucket, targetDir) {
|
|
163
|
+
switch (bucket) {
|
|
164
|
+
case 'assets': return path.join(targetDir, 'assets');
|
|
165
|
+
case 'snippets': return path.join(targetDir, 'snippets');
|
|
166
|
+
case 'sections': return path.join(targetDir, 'sections');
|
|
167
|
+
case 'templates': return path.join(targetDir, 'templates');
|
|
168
|
+
case 'layout': return path.join(targetDir, 'layout');
|
|
169
|
+
case 'locales': return path.join(targetDir, 'locales');
|
|
170
|
+
case 'config': return path.join(targetDir, 'config');
|
|
171
|
+
default: return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isTextFile(filename) {
|
|
176
|
+
const ext = path.extname(filename).toLowerCase();
|
|
177
|
+
return ['.liquid', '.js', '.css', '.json', '.html', '.svg', '.md', '.txt'].includes(ext);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function applyTenantOverrides({ targetDir, dryRun, written }) {
|
|
181
|
+
const overridesDir = path.join(targetDir, 'tenant-overrides');
|
|
182
|
+
if (!fs.existsSync(overridesDir)) return;
|
|
183
|
+
console.log(`\n[tenant-overrides]`);
|
|
184
|
+
walkAndCopy(overridesDir, targetDir, overridesDir, dryRun, written);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function walkAndCopy(srcRoot, dstRoot, currentDir, dryRun, written) {
|
|
188
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
189
|
+
const srcPath = path.join(currentDir, entry.name);
|
|
190
|
+
const rel = path.relative(srcRoot, srcPath);
|
|
191
|
+
const dstPath = path.join(dstRoot, rel);
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
walkAndCopy(srcRoot, dstRoot, srcPath, dryRun, written);
|
|
194
|
+
} else {
|
|
195
|
+
if (!dryRun) {
|
|
196
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
197
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
198
|
+
}
|
|
199
|
+
console.log(` + ${rel}`);
|
|
200
|
+
written.push({ source: 'tenant-override', target: dstPath, bucket: 'override' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/* runwell-shopify init <client> [--baseline <pin>]: scaffold a new
|
|
5
|
+
tenant theme directory with the minimum required files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export async function init(flags) {
|
|
9
|
+
const client = flags.positional && flags.positional[0];
|
|
10
|
+
if (!client) {
|
|
11
|
+
console.error('Usage: runwell-shopify init <client-name> [--baseline @runwell/dawn-runwell@^1.0.0] [--target ./<dir>]');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const targetDir = path.resolve(flags.target || `./${client}-shopify`);
|
|
16
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
17
|
+
console.error(`Target directory ${targetDir} is not empty. Choose another path or remove existing contents.`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
22
|
+
for (const d of ['layout', 'sections', 'snippets', 'assets', 'config', 'locales', 'templates', 'tenant-overrides']) {
|
|
23
|
+
fs.mkdirSync(path.join(targetDir, d), { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const baseline = flags.baseline || '@runwell/dawn-runwell@^1.0.0';
|
|
27
|
+
|
|
28
|
+
const config = {
|
|
29
|
+
client,
|
|
30
|
+
store: 'TODO.myshopify.com',
|
|
31
|
+
theme_path: '.',
|
|
32
|
+
baseline,
|
|
33
|
+
toolkit_version: '^0.9.0',
|
|
34
|
+
brand: {
|
|
35
|
+
name: client.charAt(0).toUpperCase() + client.slice(1),
|
|
36
|
+
primary: '#1A1A1A',
|
|
37
|
+
accent: '#D4AF37',
|
|
38
|
+
cream: '#F5EFE6',
|
|
39
|
+
currency: 'USD',
|
|
40
|
+
support_email: `support@${client}.com`,
|
|
41
|
+
tagline: ''
|
|
42
|
+
},
|
|
43
|
+
modules: {
|
|
44
|
+
'_shared/css-tokens': { enabled: true, config: {} }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(path.join(targetDir, 'runwell.config.json'), JSON.stringify(config, null, 2) + '\n');
|
|
49
|
+
|
|
50
|
+
// Scaffold templates
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(targetDir, 'templates', 'index.json'),
|
|
53
|
+
JSON.stringify({ sections: {}, order: [] }, null, 2) + '\n'
|
|
54
|
+
);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(targetDir, 'templates', 'product.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
sections: {
|
|
59
|
+
main: {
|
|
60
|
+
type: 'main-product',
|
|
61
|
+
blocks: {
|
|
62
|
+
title: { type: 'title' },
|
|
63
|
+
price: { type: 'price' },
|
|
64
|
+
buy_buttons: { type: 'buy_buttons', settings: { show_dynamic_checkout: true } }
|
|
65
|
+
},
|
|
66
|
+
block_order: ['title', 'price', 'buy_buttons'],
|
|
67
|
+
settings: {}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
order: ['main']
|
|
71
|
+
}, null, 2) + '\n'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// README
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(targetDir, 'README.md'),
|
|
77
|
+
`# ${client} Shopify theme
|
|
78
|
+
|
|
79
|
+
Tenant theme for ${client}, built on @runwell/dawn-runwell + @runwell/shopify-toolkit.
|
|
80
|
+
|
|
81
|
+
## Lock context (always state these before any work)
|
|
82
|
+
|
|
83
|
+
- Client: ${client}
|
|
84
|
+
- Store: TODO.myshopify.com
|
|
85
|
+
- Theme ID: TODO
|
|
86
|
+
- Storefront password: TODO
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
\`\`\`bash
|
|
91
|
+
# 1. Edit runwell.config.json with the real store handle + brand values
|
|
92
|
+
# 2. Sync baseline + toolkit + tenant overrides into the theme dir
|
|
93
|
+
runwell-shopify sync
|
|
94
|
+
|
|
95
|
+
# 3. Code QA before any push
|
|
96
|
+
runwell-shopify qa
|
|
97
|
+
|
|
98
|
+
# 4. Push to a NEW unpublished theme on the store
|
|
99
|
+
shopify theme push -u -s <store>.myshopify.com
|
|
100
|
+
|
|
101
|
+
# 5. Once happy, push to the live theme
|
|
102
|
+
shopify theme push -t <THEME_ID> -l -a -s <store>.myshopify.com
|
|
103
|
+
\`\`\`
|
|
104
|
+
|
|
105
|
+
## Layers
|
|
106
|
+
|
|
107
|
+
1. \`@runwell/dawn-runwell\` baseline (pinned in runwell.config.json).
|
|
108
|
+
2. \`@runwell/shopify-toolkit\` modules (declared in runwell.config.json modules block).
|
|
109
|
+
3. This repo: \`tenant-overrides/\` directory + \`templates/*.json\` + bespoke sections + brand assets.
|
|
110
|
+
|
|
111
|
+
\`runwell-manifest.json\` is regenerated on every sync and tracks file provenance.
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
console.log(`Scaffolded ${client} tenant theme at ${targetDir}`);
|
|
116
|
+
console.log(`\nNext steps:`);
|
|
117
|
+
console.log(` 1. cd ${path.relative(process.cwd(), targetDir) || '.'}`);
|
|
118
|
+
console.log(` 2. Edit runwell.config.json: set store handle + brand values`);
|
|
119
|
+
console.log(` 3. runwell-shopify sync`);
|
|
120
|
+
console.log(` 4. runwell-shopify qa`);
|
|
121
|
+
console.log(` 5. shopify theme push -u -s <store>`);
|
|
122
|
+
}
|
package/lib/sync.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfig, loadModuleManifest, listAvailableModules, resolveVariant } from './config-loader.js';
|
|
4
4
|
import { interpolate } from './template.js';
|
|
5
|
+
import { resolveBaseline, syncBaseline, applyTenantOverrides } from './baseline.js';
|
|
5
6
|
|
|
6
7
|
export async function sync(flags) {
|
|
7
8
|
const configPath = flags.config || './runwell.config.json';
|
|
@@ -18,6 +19,14 @@ export async function sync(flags) {
|
|
|
18
19
|
const writtenFiles = [];
|
|
19
20
|
const removedFiles = [];
|
|
20
21
|
|
|
22
|
+
// Stage 1: baseline (if configured). Owned files + patches first;
|
|
23
|
+
// toolkit module files in stage 2 will overlay on top, and tenant
|
|
24
|
+
// overrides in stage 3 win last.
|
|
25
|
+
const baselineRoot = resolveBaseline(flags, config);
|
|
26
|
+
if (baselineRoot) {
|
|
27
|
+
await syncBaseline({ baselineRoot, targetDir, config, dryRun, written: writtenFiles });
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
const moduleNames = Object.keys(config.modules);
|
|
22
31
|
for (const moduleName of moduleNames) {
|
|
23
32
|
const cfgEntry = config.modules[moduleName];
|
|
@@ -77,15 +86,21 @@ export async function sync(flags) {
|
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
//
|
|
89
|
+
// Stage 3: tenant overrides (last; wins over baseline + toolkit).
|
|
90
|
+
await applyTenantOverrides({ targetDir, dryRun, written: writtenFiles });
|
|
91
|
+
|
|
92
|
+
// Manifest with provenance per file.
|
|
81
93
|
const manifestPath = path.join(targetDir, 'runwell-manifest.json');
|
|
82
94
|
const manifestData = {
|
|
83
95
|
generated_at: new Date().toISOString(),
|
|
84
96
|
toolkit_version: getToolkitVersion(flags.toolkitRoot),
|
|
97
|
+
baseline_version: writtenFiles.find(f => f.source === 'baseline')?.baseline_version || null,
|
|
85
98
|
client: config.client,
|
|
86
99
|
files: writtenFiles.map(f => ({
|
|
100
|
+
source: f.source || 'toolkit',
|
|
87
101
|
module: f.module,
|
|
88
102
|
bucket: f.bucket,
|
|
103
|
+
baseline_version: f.baseline_version,
|
|
89
104
|
path: path.relative(targetDir, f.target)
|
|
90
105
|
}))
|
|
91
106
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfig } from './config-loader.js';
|
|
4
|
+
|
|
5
|
+
/* runwell-shopify upgrade-baseline <pin>: bump the baseline version pin
|
|
6
|
+
in runwell.config.json. Subsequent runwell-shopify sync uses the new
|
|
7
|
+
pin to fetch the new baseline. Caller is responsible for running sync
|
|
8
|
+
+ qa + push afterwards.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export async function upgradeBaseline(flags) {
|
|
12
|
+
const newPin = flags.positional && flags.positional[0];
|
|
13
|
+
if (!newPin) {
|
|
14
|
+
console.error('Usage: runwell-shopify upgrade-baseline <package@version>');
|
|
15
|
+
console.error('Example: runwell-shopify upgrade-baseline @runwell/dawn-runwell@1.1.0');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const configPath = path.resolve(flags.config || './runwell.config.json');
|
|
19
|
+
const { config } = loadConfig(configPath);
|
|
20
|
+
const oldPin = config.baseline || '(unset)';
|
|
21
|
+
config.baseline = newPin;
|
|
22
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
23
|
+
console.log(`baseline: ${oldPin} -> ${newPin}`);
|
|
24
|
+
console.log(`Run 'runwell-shopify sync' to apply, then 'runwell-shopify qa' before pushing.`);
|
|
25
|
+
}
|
package/modules/comparison-table/variants/feature-columns/sections/runwell-comparison-table.liquid
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell product comparison table. Drops on PDP (or any template) to
|
|
3
3
|
compare attributes across Lushi products or against a competitor.
|
|
4
4
|
Editorial style, not a feature dump. Per CRO §5.2 (decision-aid for
|
|
5
5
|
high-trust niches).
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
|
|
85
85
|
{% schema %}
|
|
86
86
|
{
|
|
87
|
-
"name": "
|
|
87
|
+
"name": "Runwell comparison table",
|
|
88
88
|
"tag": "section",
|
|
89
89
|
"class": "section-runwell-comparison",
|
|
90
90
|
"settings": [
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"max_blocks": 4,
|
|
146
146
|
"presets": [
|
|
147
147
|
{
|
|
148
|
-
"name": "
|
|
148
|
+
"name": "Runwell comparison table",
|
|
149
149
|
"blocks": [
|
|
150
150
|
{ "type": "column", "settings": { "column_title": "Lushi pick", "column_subtitle": "This product" } },
|
|
151
151
|
{ "type": "column", "settings": { "column_title": "Drugstore", "column_subtitle": "Average mass-market option" } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell delivery estimate. Renders a placeholder span filled by JS with
|
|
3
3
|
a date range based on cutoff time and standard transit. Native
|
|
4
4
|
replacement for the Estimated Delivery Date widget in Vitals etc.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell editorial section: eyebrow + serif italic heading + lede + body.
|
|
3
3
|
Used for the homepage section flow (Approach, Press, Partnering equivalents).
|
|
4
4
|
{%- endcomment -%}
|
|
5
5
|
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
|
|
67
67
|
{% schema %}
|
|
68
68
|
{
|
|
69
|
-
"name": "
|
|
69
|
+
"name": "Runwell editorial block",
|
|
70
70
|
"tag": "section",
|
|
71
71
|
"class": "section-runwell-editorial",
|
|
72
72
|
"settings": [
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
],
|
|
144
144
|
"presets": [
|
|
145
145
|
{
|
|
146
|
-
"name": "
|
|
146
|
+
"name": "Runwell editorial block",
|
|
147
147
|
"blocks": [
|
|
148
148
|
{ "type": "card" },
|
|
149
149
|
{ "type": "card" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell video hero. Full-bleed background video with editorial copy
|
|
3
3
|
bottom-left. Mirrors the v1 lushi.co Hero pattern but pivots messaging
|
|
4
4
|
to general health and wellness.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
|
|
77
77
|
{% schema %}
|
|
78
78
|
{
|
|
79
|
-
"name": "
|
|
79
|
+
"name": "Runwell video hero",
|
|
80
80
|
"tag": "section",
|
|
81
81
|
"class": "section-runwell-video-hero",
|
|
82
82
|
"settings": [
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
],
|
|
151
151
|
"presets": [
|
|
152
152
|
{
|
|
153
|
-
"name": "
|
|
153
|
+
"name": "Runwell video hero"
|
|
154
154
|
}
|
|
155
155
|
]
|
|
156
156
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell exit-intent newsletter popup. Native replacement for Privy /
|
|
3
3
|
Justuno / Uno popups. Triggers on mouseleave at top of viewport
|
|
4
4
|
(desktop) or after 30 seconds (mobile fallback). Suppresses with a
|
|
5
5
|
localStorage cookie for 30 days after dismiss or sign-up.
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
{% schema %}
|
|
35
35
|
{
|
|
36
|
-
"name": "
|
|
36
|
+
"name": "Runwell exit popup",
|
|
37
37
|
"tag": "section",
|
|
38
38
|
"class": "section-runwell-exit",
|
|
39
39
|
"settings": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{ "type": "textarea", "id": "lede", "label": "Lede", "default": "Join the journal and get the founder's take on what we picked, why we picked it, and which products to skip altogether." }
|
|
43
43
|
],
|
|
44
44
|
"presets": [
|
|
45
|
-
{ "name": "
|
|
45
|
+
{ "name": "Runwell exit popup" }
|
|
46
46
|
]
|
|
47
47
|
}
|
|
48
48
|
{% endschema %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell FAQ accordion. Native replacement for FAQ widgets in Vitals,
|
|
3
3
|
HelpCenter, etc. Uses native HTML <details> elements so it works
|
|
4
4
|
without any JS.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
{% schema %}
|
|
32
32
|
{
|
|
33
|
-
"name": "
|
|
33
|
+
"name": "Runwell FAQ",
|
|
34
34
|
"tag": "section",
|
|
35
35
|
"class": "section-runwell-faq",
|
|
36
36
|
"settings": [
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"max_blocks": 12,
|
|
53
53
|
"presets": [
|
|
54
54
|
{
|
|
55
|
-
"name": "
|
|
55
|
+
"name": "Runwell FAQ",
|
|
56
56
|
"blocks": [
|
|
57
|
-
{ "type": "qa", "settings": { "question": "Are these supplements third-party tested?", "answer": "<p>Every product we list ships with a published Certificate of Analysis from the supplier. We confirm the lot numbers match what the bottle claims before any product gets a page on
|
|
57
|
+
{ "type": "qa", "settings": { "question": "Are these supplements third-party tested?", "answer": "<p>Every product we list ships with a published Certificate of Analysis from the supplier. We confirm the lot numbers match what the bottle claims before any product gets a page on the site.</p>" } },
|
|
58
58
|
{ "type": "qa", "settings": { "question": "How fast do orders ship?", "answer": "<p>Most orders leave the warehouse within one to two business days. Free standard shipping on US orders over $75; flat $7 below. International rates calculated at checkout.</p>" } },
|
|
59
59
|
{ "type": "qa", "settings": { "question": "What is the return policy?", "answer": "<p>If a product does not earn a place on your counter, write to us within 30 days for a full refund. We do not make returns hard.</p>" } },
|
|
60
|
-
{ "type": "qa", "settings": { "question": "Do these products treat or cure conditions?", "answer": "<p>No. Statements about supplements and skincare on
|
|
60
|
+
{ "type": "qa", "settings": { "question": "Do these products treat or cure conditions?", "answer": "<p>No. Statements about supplements and skincare on this site have not been evaluated by the FDA and are not intended to diagnose, treat, cure, or prevent any disease. Consult your physician before starting any new regimen.</p>" } },
|
|
61
61
|
{ "type": "qa", "settings": { "question": "How does the journal work?", "answer": "<p>Every product is paired with a journal post explaining how it works, who it is for, and what the research actually says. Editorial, not advertorial.</p>" } }
|
|
62
62
|
]
|
|
63
63
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell "How it works" section. Side-image left + content right (or
|
|
3
3
|
reversible). Eyebrow + headline + lede + 3 numbered service cards.
|
|
4
4
|
Mirrors v1 .conciergeSection / "Nationwide Access to Concierge Care"
|
|
5
5
|
but pivoted to wellness curation flow.
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
|
|
61
61
|
{% schema %}
|
|
62
62
|
{
|
|
63
|
-
"name": "
|
|
63
|
+
"name": "Runwell how it works",
|
|
64
64
|
"tag": "section",
|
|
65
65
|
"class": "section-runwell-how",
|
|
66
66
|
"settings": [
|
|
67
|
-
{ "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "How
|
|
67
|
+
{ "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "How it works" },
|
|
68
68
|
{ "type": "text", "id": "heading", "label": "Heading", "default": "Curated, written, shipped." },
|
|
69
69
|
{ "type": "textarea", "id": "lede", "label": "Lede", "default": "Three simple steps from the brands we trust to your counter. No filler in between." },
|
|
70
70
|
{ "type": "image_picker", "id": "image", "label": "Side image (uploaded)" },
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"max_blocks": 4,
|
|
88
88
|
"presets": [
|
|
89
89
|
{
|
|
90
|
-
"name": "
|
|
90
|
+
"name": "Runwell how it works",
|
|
91
91
|
"blocks": [
|
|
92
|
-
{ "type": "step", "settings": { "title": "We curate.", "body": "Every product
|
|
92
|
+
{ "type": "step", "settings": { "title": "We curate.", "body": "Every product is hand-picked and clears our standards before it gets a page." } },
|
|
93
93
|
{ "type": "step", "settings": { "title": "We write.", "body": "Each product pairs with a journal post: how it works, who it's for, and what the research actually says. Editorial, not advertorial." } },
|
|
94
94
|
{ "type": "step", "settings": { "title": "We ship.", "body": "Suppliers ship direct from their facility. Free shipping over $75; 30-day no-hassle returns." } }
|
|
95
95
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell inventory urgency. Shows "Only N left" near ATC when stock is
|
|
3
3
|
low. Replaces the urgency widget from Vitals / similar apps.
|
|
4
4
|
Threshold and display are conservative: only fires when inventory is
|
|
5
5
|
tracked AND visible on the PDP product object.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell ingredient grid. Drops below a PDP description (or anywhere) and
|
|
3
3
|
spotlights 3 to 6 hero ingredients with name, role, and dose. Reads
|
|
4
|
-
product.metafields
|
|
4
|
+
product.metafields.<namespace>.ingredients (list.metaobject, default namespace runwell) when set, with
|
|
5
5
|
manual blocks as the fallback.
|
|
6
6
|
{%- endcomment -%}
|
|
7
7
|
|
|
8
|
-
{%- assign
|
|
8
|
+
{%- assign ns = section.settings.metafield_namespace | default: 'runwell' -%}
|
|
9
|
+
{%- assign ingredients_meta = product.metafields[ns].ingredients.value -%}
|
|
9
10
|
|
|
10
11
|
<section
|
|
11
12
|
class="runwell-ingredients"
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
|
|
75
76
|
{% schema %}
|
|
76
77
|
{
|
|
77
|
-
"name": "
|
|
78
|
+
"name": "Runwell ingredients",
|
|
78
79
|
"tag": "section",
|
|
79
80
|
"class": "section-runwell-ingredients",
|
|
80
81
|
"settings": [
|
|
@@ -84,6 +85,13 @@
|
|
|
84
85
|
"label": "Eyebrow",
|
|
85
86
|
"default": "What's inside"
|
|
86
87
|
},
|
|
88
|
+
{
|
|
89
|
+
"type": "text",
|
|
90
|
+
"id": "metafield_namespace",
|
|
91
|
+
"label": "Ingredients metafield namespace",
|
|
92
|
+
"default": "runwell",
|
|
93
|
+
"info": "Reads product.metafields.<namespace>.ingredients (list.metaobject)."
|
|
94
|
+
},
|
|
87
95
|
{
|
|
88
96
|
"type": "text",
|
|
89
97
|
"id": "heading",
|
|
@@ -110,7 +118,7 @@
|
|
|
110
118
|
},
|
|
111
119
|
{
|
|
112
120
|
"type": "paragraph",
|
|
113
|
-
"content": "If product.metafields
|
|
121
|
+
"content": "If product.metafields.<namespace>.ingredients is set, those are used and the blocks below are ignored."
|
|
114
122
|
}
|
|
115
123
|
],
|
|
116
124
|
"blocks": [
|
|
@@ -127,7 +135,7 @@
|
|
|
127
135
|
"max_blocks": 8,
|
|
128
136
|
"presets": [
|
|
129
137
|
{
|
|
130
|
-
"name": "
|
|
138
|
+
"name": "Runwell ingredients",
|
|
131
139
|
"blocks": [
|
|
132
140
|
{ "type": "ingredient", "settings": { "name": "Magnesium glycinate", "dose": "200mg", "role": "The most bioavailable form of magnesium. Supports sleep onset and muscle recovery." } },
|
|
133
141
|
{ "type": "ingredient", "settings": { "name": "L-theanine", "dose": "100mg", "role": "Amino acid from green tea. Calms without sedating; pairs well with magnesium." } },
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
3
|
-
explains why
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
Runwell PDP journal link. Connects a product to the editorial article
|
|
3
|
+
that explains why we carry it. Uses the product metafield
|
|
4
|
+
`<namespace>.journal_article` (default namespace 'runwell', overridable
|
|
5
|
+
per-client). Falls back to the section settings article picker, then
|
|
6
|
+
the manual title.
|
|
6
7
|
{%- endcomment -%}
|
|
7
8
|
|
|
8
|
-
{%- assign
|
|
9
|
+
{%- assign ns = section.settings.metafield_namespace | default: 'runwell' -%}
|
|
10
|
+
{%- assign linked_article = product.metafields[ns].journal_article.value -%}
|
|
9
11
|
{%- if linked_article == blank -%}
|
|
10
12
|
{%- assign linked_article = section.settings.article -%}
|
|
11
13
|
{%- endif -%}
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
|
|
60
62
|
{% schema %}
|
|
61
63
|
{
|
|
62
|
-
"name": "
|
|
64
|
+
"name": "Runwell PDP journal link",
|
|
63
65
|
"tag": "section",
|
|
64
66
|
"class": "section-runwell-pdp-journal",
|
|
65
67
|
"settings": [
|
|
@@ -69,11 +71,18 @@
|
|
|
69
71
|
"label": "Eyebrow",
|
|
70
72
|
"default": "From the journal"
|
|
71
73
|
},
|
|
74
|
+
{
|
|
75
|
+
"type": "text",
|
|
76
|
+
"id": "metafield_namespace",
|
|
77
|
+
"label": "Journal metafield namespace",
|
|
78
|
+
"default": "runwell",
|
|
79
|
+
"info": "Reads product.metafields.<namespace>.journal_article. Default: runwell."
|
|
80
|
+
},
|
|
72
81
|
{
|
|
73
82
|
"type": "article",
|
|
74
83
|
"id": "article",
|
|
75
84
|
"label": "Linked article (override)",
|
|
76
|
-
"info": "Optional. If set, overrides the product's
|
|
85
|
+
"info": "Optional. If set, overrides the product's <namespace>.journal_article metafield."
|
|
77
86
|
},
|
|
78
87
|
{
|
|
79
88
|
"type": "color",
|
|
@@ -101,7 +110,7 @@
|
|
|
101
110
|
"type": "textarea",
|
|
102
111
|
"id": "fallback_body",
|
|
103
112
|
"label": "Fallback body",
|
|
104
|
-
"default": "Every product
|
|
113
|
+
"default": "Every product we carry pairs with a journal article. Browse the full archive to dig deeper."
|
|
105
114
|
},
|
|
106
115
|
{
|
|
107
116
|
"type": "text",
|
|
@@ -117,7 +126,7 @@
|
|
|
117
126
|
],
|
|
118
127
|
"presets": [
|
|
119
128
|
{
|
|
120
|
-
"name": "
|
|
129
|
+
"name": "Runwell PDP journal link"
|
|
121
130
|
}
|
|
122
131
|
]
|
|
123
132
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell PDP trust block. Shows the four-check standards on every PDP.
|
|
3
3
|
Builds buyer trust the way Goop's "What it is / what it does" panels do.
|
|
4
4
|
Drop into product.json after the main-product section.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
{% schema %}
|
|
58
58
|
{
|
|
59
|
-
"name": "
|
|
59
|
+
"name": "Runwell PDP trust",
|
|
60
60
|
"tag": "section",
|
|
61
61
|
"class": "section-runwell-pdp-trust",
|
|
62
62
|
"settings": [
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"type": "textarea",
|
|
77
77
|
"id": "lede",
|
|
78
78
|
"label": "Lede",
|
|
79
|
-
"default": "We do not stock everything. Every product
|
|
79
|
+
"default": "We do not stock everything. Every product has to clear these four checks before it gets a page."
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"type": "color",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
],
|
|
129
129
|
"presets": [
|
|
130
130
|
{
|
|
131
|
-
"name": "
|
|
131
|
+
"name": "Runwell PDP trust",
|
|
132
132
|
"blocks": [
|
|
133
133
|
{ "type": "check", "settings": { "eyebrow": "Check 01", "title": "Ingredients we trust.", "body": "Whole-food forms over synthetic where the science holds. Synthetic where it works better, with a clear reason why." } },
|
|
134
134
|
{ "type": "check", "settings": { "eyebrow": "Check 02", "title": "Doses that match research.", "body": "We check actual doses against published trials. No buzzword ingredients at one-tenth the studied dose." } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell press bar. Logos in a row with optional eyebrow text and link.
|
|
3
3
|
Used on homepage and about page. Keep tight; the wins do the talking.
|
|
4
4
|
|
|
5
5
|
Block options (in priority order):
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
{% schema %}
|
|
54
54
|
{
|
|
55
|
-
"name": "
|
|
55
|
+
"name": "Runwell press bar",
|
|
56
56
|
"tag": "section",
|
|
57
57
|
"class": "section-runwell-press-bar",
|
|
58
58
|
"settings": [
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"max_blocks": 9,
|
|
105
105
|
"presets": [
|
|
106
106
|
{
|
|
107
|
-
"name": "
|
|
107
|
+
"name": "Runwell press bar",
|
|
108
108
|
"blocks": [
|
|
109
109
|
{ "type": "logo", "settings": { "label": "Bloomberg", "asset_filename": "runwell-press-bloomberg.png" } },
|
|
110
110
|
{ "type": "logo", "settings": { "label": "Yahoo Finance", "asset_filename": "runwell-press-yahoo-finance.png" } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell recently-viewed products. Reads from localStorage (populated on
|
|
3
3
|
PDP visits) and renders up to 4 cards. Native replacement for the
|
|
4
4
|
recently-viewed feature in Vitals / similar apps.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
{% schema %}
|
|
25
25
|
{
|
|
26
|
-
"name": "
|
|
26
|
+
"name": "Runwell recently viewed",
|
|
27
27
|
"tag": "section",
|
|
28
28
|
"class": "section-runwell-rv",
|
|
29
29
|
"settings": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{ "type": "color", "id": "background_color", "label": "Background", "default": "#FFFFFF" }
|
|
33
33
|
],
|
|
34
34
|
"presets": [
|
|
35
|
-
{ "name": "
|
|
35
|
+
{ "name": "Runwell recently viewed" }
|
|
36
36
|
]
|
|
37
37
|
}
|
|
38
38
|
{% endschema %}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
3
|
-
Reads from product metafield
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
Runwell native PDP reviews. Replaces Stamped / Loox display layer.
|
|
3
|
+
Reads from product metafield `<namespace>.reviews` (json type, default
|
|
4
|
+
namespace 'runwell', overridable per-client via section setting).
|
|
5
|
+
Each entry is { name, location, rating, body, date, verified }.
|
|
6
|
+
Falls back to a "no reviews yet" state with a mailto CTA pointing
|
|
7
|
+
to shop.email (merchant configures in Shopify admin > settings).
|
|
6
8
|
{%- endcomment -%}
|
|
7
9
|
|
|
8
10
|
{%- if product != blank -%}
|
|
9
|
-
{%- assign
|
|
11
|
+
{%- assign ns = section.settings.metafield_namespace | default: 'runwell' -%}
|
|
12
|
+
{%- assign reviews_json = product.metafields[ns].reviews.value -%}
|
|
10
13
|
<section class="runwell-reviews" data-runwell-reviews>
|
|
11
14
|
<div class="runwell-reviews__inner">
|
|
12
15
|
<header class="runwell-reviews__header">
|
|
@@ -45,7 +48,9 @@
|
|
|
45
48
|
</ul>
|
|
46
49
|
{%- else -%}
|
|
47
50
|
<div class="runwell-reviews__empty">
|
|
48
|
-
|
|
51
|
+
{%- comment -%}brand.support_email is interpolated at sync time from runwell.config.json; falls back to Shopify admin shop.email if the brand var is unset.{%- endcomment -%}
|
|
52
|
+
{%- assign reply_to = '{{brand.support_email}}' | default: shop.email | default: shop.url -%}
|
|
53
|
+
<p>No reviews on this one yet. If you have tried it, write us at <a href="mailto:{{ reply_to }}?subject=Review for {{ product.title | escape }}">{{ reply_to }}</a> and we will publish thoughtful reviews on the page.</p>
|
|
49
54
|
</div>
|
|
50
55
|
{%- endif -%}
|
|
51
56
|
|
|
@@ -79,14 +84,15 @@
|
|
|
79
84
|
|
|
80
85
|
{% schema %}
|
|
81
86
|
{
|
|
82
|
-
"name": "
|
|
87
|
+
"name": "Runwell PDP reviews",
|
|
83
88
|
"tag": "section",
|
|
84
89
|
"class": "section-runwell-pdp-reviews",
|
|
85
90
|
"settings": [
|
|
86
|
-
{ "type": "text", "id": "heading", "label": "Heading", "default": "What customers wrote in." }
|
|
91
|
+
{ "type": "text", "id": "heading", "label": "Heading", "default": "What customers wrote in." },
|
|
92
|
+
{ "type": "text", "id": "metafield_namespace", "label": "Reviews metafield namespace", "default": "runwell", "info": "Reads product.metafields.<namespace>.reviews. Default: runwell." }
|
|
87
93
|
],
|
|
88
94
|
"presets": [
|
|
89
|
-
{ "name": "
|
|
95
|
+
{ "name": "Runwell PDP reviews" }
|
|
90
96
|
]
|
|
91
97
|
}
|
|
92
98
|
{% endschema %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell risk-reversal block. Small, calm reassurance that drops directly
|
|
3
3
|
under the PDP buy form (or anywhere the editor wants reassurance).
|
|
4
4
|
Per CRO §5.3 (foundational trust signal under add-to-cart).
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
{% schema %}
|
|
41
41
|
{
|
|
42
|
-
"name": "
|
|
42
|
+
"name": "Runwell risk reversal",
|
|
43
43
|
"tag": "section",
|
|
44
44
|
"class": "section-runwell-risk-reversal",
|
|
45
45
|
"settings": [
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
],
|
|
88
88
|
"presets": [
|
|
89
89
|
{
|
|
90
|
-
"name": "
|
|
90
|
+
"name": "Runwell risk reversal"
|
|
91
91
|
}
|
|
92
92
|
]
|
|
93
93
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell free-shipping announcement bar. Sits at the very top of every
|
|
3
3
|
page. Reads cart total and shows a progress message + bar.
|
|
4
4
|
{%- endcomment -%}
|
|
5
5
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
{% schema %}
|
|
42
42
|
{
|
|
43
|
-
"name": "
|
|
43
|
+
"name": "Runwell shipping bar",
|
|
44
44
|
"tag": "aside",
|
|
45
45
|
"class": "section-runwell-shipping-bar",
|
|
46
46
|
"enabled_on": {
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
],
|
|
89
89
|
"presets": [
|
|
90
90
|
{
|
|
91
|
-
"name": "
|
|
91
|
+
"name": "Runwell shipping bar"
|
|
92
92
|
}
|
|
93
93
|
]
|
|
94
94
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell sticky add-to-cart for PDP. Slides up from the bottom on scroll
|
|
3
3
|
past the main buy button, hides again when buy area returns to view.
|
|
4
4
|
Mobile-first; on desktop reveals only after a meaningful scroll depth.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
|
|
69
69
|
{% schema %}
|
|
70
70
|
{
|
|
71
|
-
"name": "
|
|
71
|
+
"name": "Runwell sticky ATC",
|
|
72
72
|
"tag": "section",
|
|
73
73
|
"class": "section-runwell-sticky-atc",
|
|
74
74
|
"settings": [],
|
|
75
75
|
"presets": [
|
|
76
|
-
{ "name": "
|
|
76
|
+
{ "name": "Runwell sticky ATC" }
|
|
77
77
|
]
|
|
78
78
|
}
|
|
79
79
|
{% endschema %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell testimonials / UGC strip. Three-up grid of customer quotes with
|
|
3
3
|
source name + product/lifestyle context. CRO tips 6, 8, 9: social proof
|
|
4
4
|
scattered across the site, unique angles, customer voices.
|
|
5
5
|
{%- endcomment -%}
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
{% schema %}
|
|
46
46
|
{
|
|
47
|
-
"name": "
|
|
47
|
+
"name": "Runwell testimonials",
|
|
48
48
|
"tag": "section",
|
|
49
49
|
"class": "section-runwell-testimonials",
|
|
50
50
|
"settings": [
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"max_blocks": 6,
|
|
76
76
|
"presets": [
|
|
77
77
|
{
|
|
78
|
-
"name": "
|
|
78
|
+
"name": "Runwell testimonials",
|
|
79
79
|
"blocks": [
|
|
80
80
|
{ "type": "quote", "settings": { "quote": "First magnesium I have actually felt working. Read the journal post twice before ordering and now I get why they only carry the glycinate form.", "author": "Margaux, Brooklyn", "context": "Daily Magnesium Glycinate", "rating": "5" } },
|
|
81
81
|
{ "type": "quote", "settings": { "quote": "I have tried four collagen brands. This one mixes clean in cold coffee and does not have that fishy aftertaste I gave up trying to mask.", "author": "Priya, Austin", "context": "Marine Collagen Peptides", "rating": "5" } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
|
|
2
|
+
Runwell trust badges row. Drops on PDP under the buy form (or on cart,
|
|
3
3
|
collection, or homepage). Renders 3 to 6 trust signals: secure
|
|
4
4
|
checkout, returns, shipping, payment logos. Per Blueprint §3.4 and
|
|
5
5
|
CRO §5.3 (foundational trust signals).
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
{% schema %}
|
|
36
36
|
{
|
|
37
|
-
"name": "
|
|
37
|
+
"name": "Runwell trust badges",
|
|
38
38
|
"tag": "section",
|
|
39
39
|
"class": "section-runwell-trust-badges",
|
|
40
40
|
"settings": [
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
"max_blocks": 6,
|
|
81
81
|
"presets": [
|
|
82
82
|
{
|
|
83
|
-
"name": "
|
|
83
|
+
"name": "Runwell trust badges",
|
|
84
84
|
"blocks": [
|
|
85
85
|
{ "type": "badge", "settings": { "icon": "✓", "title": "Free shipping over $75", "body": "On all US orders" } },
|
|
86
86
|
{ "type": "badge", "settings": { "icon": "↺", "title": "30-day returns", "body": "No-hassle satisfaction promise" } },
|
|
87
|
-
{ "type": "badge", "settings": { "icon": "★", "title": "Editorially curated", "body": "Every product
|
|
87
|
+
{ "type": "badge", "settings": { "icon": "★", "title": "Editorially curated", "body": "Every product clears 4 checks" } },
|
|
88
88
|
{ "type": "badge", "settings": { "icon": "⊕", "title": "Secure checkout", "body": "Encrypted at every step" } }
|
|
89
89
|
]
|
|
90
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwell/shopify-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Reusable Shopify theme modules from Runwell. Replaces typically app-driven features (reviews, wishlist, urgency, FAQ, post-purchase upsell, exit popups, free-ship progress, sticky ATC, testimonials, badges, bundles) with native Liquid + JS + CSS that ship across multiple client themes via a config-driven sync CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|