@runwell/shopify-toolkit 0.8.0 → 0.10.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.
@@ -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':
@@ -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
- // Manifest
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/INDEX.md CHANGED
@@ -26,7 +26,7 @@ Total modules: 34.
26
26
  | `delivery-estimate` | pdp | (native build) | snippets:1 | (none) | (none) | (none) |
27
27
  | `editorial-block` | storefront | (native build) | sections:1 | eyebrow, heading, lede, background_color, text_color | (none) | (none) |
28
28
  | `editorial-hero` | storefront | Dawn's image-banner / video-banner | | eyebrow, heading, subheading | video-bg, hero-with-card | (none) |
29
- | `exit-intent` | conversion | (native build) | sections:1 assets:1 | eyebrow, heading, lede | (none) | (none) |
29
+ | `exit-intent` | conversion | Privy / Justuno display layer | sections:1 assets:1 | eyebrow, heading, lede | (none) | (none) |
30
30
  | `faq` | storefront | (native build) | sections:1 | eyebrow, heading, background_color, text_color | (none) | (none) |
31
31
  | `gift-with-purchase` | conversion | (native build) | snippets:1 assets:1 | threshold_cents, gift_handle, unlocked_message, locked_message_suffix | (none) | create-gift-product + create-gift-discount |
32
32
  | `how-it-works` | storefront | (native build) | sections:1 | eyebrow, heading, lede, image, asset_filename, background_color, text_color, cta_label, cta_link | (none) | (none) |
@@ -97,13 +97,13 @@ Total modules: 34.
97
97
  ### delivery-estimate
98
98
 
99
99
  - Category: pdp
100
- - What: delivery-estimate module migrated from Lushi.
100
+ - What: PDP delivery-date estimate (JS-rendered date range based on cutoff time and standard transit).
101
101
  - Files: snippets:1
102
102
 
103
103
  ### editorial-block
104
104
 
105
105
  - Category: storefront
106
- - What: Lushi editorial block module migrated from Lushi.
106
+ - What: Editorial introduction block: eyebrow + serif italic heading + lede + body.
107
107
  - Files: sections:1
108
108
  - Config: eyebrow, heading, lede, background_color, text_color
109
109
 
@@ -118,14 +118,15 @@ Total modules: 34.
118
118
  ### exit-intent
119
119
 
120
120
  - Category: conversion
121
- - What: Lushi exit popup module migrated from Lushi.
121
+ - Replaces: Privy / Justuno display layer
122
+ - What: Exit-intent popup with email capture.
122
123
  - Files: sections:1 assets:1
123
124
  - Config: eyebrow, heading, lede
124
125
 
125
126
  ### faq
126
127
 
127
128
  - Category: storefront
128
- - What: Lushi FAQ module migrated from Lushi.
129
+ - What: FAQ accordion.
129
130
  - Files: sections:1
130
131
  - Config: eyebrow, heading, background_color, text_color
131
132
 
@@ -140,14 +141,14 @@ Total modules: 34.
140
141
  ### how-it-works
141
142
 
142
143
  - Category: storefront
143
- - What: Lushi how it works module migrated from Lushi.
144
+ - What: How-it-works section: side-image + multi-step explainer.
144
145
  - Files: sections:1
145
146
  - Config: eyebrow, heading, lede, image, asset_filename, background_color, text_color, cta_label, cta_link
146
147
 
147
148
  ### inventory-urgency
148
149
 
149
150
  - Category: pdp
150
- - What: inventory-urgency module migrated from Lushi.
151
+ - What: PDP inventory urgency message (Only N left) when stock tracking is enabled and quantity is low.
151
152
  - Files: snippets:1
152
153
 
153
154
  ### loyalty-tiers
@@ -161,21 +162,21 @@ Total modules: 34.
161
162
  ### pdp-ingredients
162
163
 
163
164
  - Category: pdp
164
- - What: Lushi ingredients module migrated from Lushi.
165
+ - What: PDP ingredients block (sources from product metafield list.metaobject).
165
166
  - Files: sections:1
166
167
  - Config: eyebrow, heading, lede, background_color, text_color
167
168
 
168
169
  ### pdp-journal-link
169
170
 
170
171
  - Category: pdp
171
- - What: Lushi PDP journal link module migrated from Lushi.
172
+ - What: PDP journal link.
172
173
  - Files: sections:1
173
174
  - Config: eyebrow, article, background_color, text_color, fallback_title, fallback_body, fallback_link_label, fallback_link_url
174
175
 
175
176
  ### pdp-trust-checks
176
177
 
177
178
  - Category: pdp
178
- - What: Lushi PDP trust module migrated from Lushi.
179
+ - What: PDP trust.
179
180
  - Files: sections:1
180
181
  - Config: eyebrow, heading, lede, background_color, text_color, link_label, link_url
181
182
 
@@ -191,7 +192,7 @@ Total modules: 34.
191
192
  ### press-bar
192
193
 
193
194
  - Category: social-proof
194
- - What: Lushi press bar module migrated from Lushi.
195
+ - What: press bar.
195
196
  - Files: sections:1
196
197
  - Config: eyebrow, background_color, text_color
197
198
 
@@ -219,21 +220,21 @@ Total modules: 34.
219
220
  ### recently-viewed
220
221
 
221
222
  - Category: pdp
222
- - What: Lushi recently viewed module migrated from Lushi.
223
+ - What: recently viewed.
223
224
  - Files: sections:1 assets:1
224
225
  - Config: eyebrow, heading, background_color
225
226
 
226
227
  ### reviews
227
228
 
228
229
  - Category: social-proof
229
- - What: Lushi PDP reviews module migrated from Lushi.
230
+ - What: PDP reviews.
230
231
  - Files: sections:1
231
232
  - Config: heading
232
233
 
233
234
  ### risk-reversal
234
235
 
235
236
  - Category: conversion
236
- - What: Lushi risk reversal module migrated from Lushi.
237
+ - What: risk reversal.
237
238
  - Files: sections:1
238
239
  - Config: icon, heading, body, link_label, link_url, background_color, text_color
239
240
 
@@ -247,7 +248,7 @@ Total modules: 34.
247
248
  ### shipping-bar
248
249
 
249
250
  - Category: conversion
250
- - What: Lushi shipping bar module migrated from Lushi.
251
+ - What: shipping bar.
251
252
  - Files: sections:1
252
253
  - Config: threshold_cents, message_below, message_qualified, message_default, background_color, text_color
253
254
 
@@ -292,7 +293,7 @@ Total modules: 34.
292
293
  ### trust-badges
293
294
 
294
295
  - Category: social-proof
295
- - What: Lushi trust badges module migrated from Lushi.
296
+ - What: trust badges.
296
297
  - Files: sections:1
297
298
  - Config: background_color, text_color
298
299
 
@@ -9,9 +9,9 @@
9
9
  "config": {
10
10
  "schema": {
11
11
  "icon": { "type": "string", "default": "&#10003;", "label": "Bullet icon (HTML entity or emoji)" },
12
- "usp_1": { "type": "string", "default": "30-day satisfaction promise" },
13
- "usp_2": { "type": "string", "default": "Editorially curated, not bulk-stocked" },
14
- "usp_3": { "type": "string", "default": "Every product paired with a journal post" }
12
+ "usp_1": { "type": "string", "default": "Free shipping over $50" },
13
+ "usp_2": { "type": "string", "default": "30-day satisfaction guarantee" },
14
+ "usp_3": { "type": "string", "default": "Secure checkout" }
15
15
  }
16
16
  }
17
17
  }
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "variants": {
17
17
  "feature-columns": {
18
- "description": "Theme Editor blocks are columns (e.g. Lushi pick / Drugstore / Hype brand). Rows come from a textarea (pipe-delimited). Best when comparing many products across the same attributes.",
18
+ "description": "Theme Editor blocks are columns (e.g. Brand pick / Generic / Competitor). Rows come from a textarea (pipe-delimited). Best when comparing many products across the same attributes.",
19
19
  "files": {
20
20
  "sections": ["variants/feature-columns/sections/runwell-comparison-table.liquid"]
21
21
  },
@@ -2,7 +2,7 @@
2
2
  "name": "delivery-estimate",
3
3
  "version": "0.1.0",
4
4
  "category": "pdp",
5
- "description": "delivery-estimate module migrated from Lushi.",
5
+ "description": "PDP delivery-date estimate (JS-rendered date range based on cutoff time and standard transit).",
6
6
  "files": {
7
7
  "snippets": [
8
8
  "snippets/runwell-delivery-estimate.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "editorial-block",
3
3
  "version": "0.1.0",
4
4
  "category": "storefront",
5
- "description": "Lushi editorial block module migrated from Lushi.",
5
+ "description": "Editorial introduction block: eyebrow + serif italic heading + lede + body. Native replacement for Dawn rich-text section with brand-aware typography.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-editorial-block.liquid"
@@ -17,12 +17,12 @@
17
17
  },
18
18
  "heading": {
19
19
  "type": "string",
20
- "default": "Wellness, made simple.",
20
+ "default": "Why we built this.",
21
21
  "label": "Heading"
22
22
  },
23
23
  "lede": {
24
24
  "type": "string",
25
- "default": "Curated supplements, skincare, and rituals from the brands we trust most. Editorial-first, science-aware, beautifully made.",
25
+ "default": "Brief introduction text. Replace with your brand voice.",
26
26
  "label": "Lede"
27
27
  },
28
28
  "background_color": {
@@ -2,13 +2,13 @@
2
2
  "name": "editorial-hero",
3
3
  "version": "0.2.0",
4
4
  "category": "storefront",
5
- "description": "Editorial hero section. Two variants: video-bg (default) renders a full-bleed background video with eyebrow + heading + subhead + dual CTA, ideal for Lushi-style editorial brands; hero-with-card renders a background image with a centered text card (Hero with Card pattern), ideal for product-led brands. Replaces Dawn's image-banner / video-banner.",
5
+ "description": "Editorial hero section. Two variants: video-bg (default) renders a full-bleed background video with eyebrow + heading + subhead + dual CTA, ideal for editorial brands; hero-with-card renders a background image with a centered text card (Hero with Card pattern), ideal for product-led brands. Replaces Dawn's image-banner / video-banner.",
6
6
  "default_variant": "video-bg",
7
7
  "config": {
8
8
  "schema": {
9
- "eyebrow": { "type": "string", "default": "Wellness, curated", "label": "Eyebrow text" },
10
- "heading": { "type": "string", "default": "Glow from within.", "label": "Headline" },
11
- "subheading": { "type": "string", "default": "Lushi is a modern wellness brand offering curated supplements, skincare, and rituals for everyday health.", "label": "Subheading" }
9
+ "eyebrow": { "type": "string", "default": "Brand eyebrow", "label": "Eyebrow text" },
10
+ "heading": { "type": "string", "default": "Tagline goes here.", "label": "Headline" },
11
+ "subheading": { "type": "string", "default": "Short brand subhead. Replace with your value proposition.", "label": "Subheading" }
12
12
  }
13
13
  },
14
14
  "variants": {
@@ -22,9 +22,9 @@
22
22
  "poster_image": { "type": "string", "label": "Poster image (and fallback)" },
23
23
  "fallback_asset": { "type": "string", "label": "Bundled fallback asset filename (e.g. lushi-hero-bg.jpg)" },
24
24
  "min_height": { "type": "number", "default": 100, "label": "Hero min-height (vh)" },
25
- "button_label_primary": { "type": "string", "default": "Shop wellness", "label": "Primary button label" },
25
+ "button_label_primary": { "type": "string", "default": "Shop now", "label": "Primary button label" },
26
26
  "button_link_primary": { "type": "string", "label": "Primary button link" },
27
- "button_label_secondary": { "type": "string", "default": "Read the journal", "label": "Secondary button label" },
27
+ "button_label_secondary": { "type": "string", "default": "Learn more", "label": "Secondary button label" },
28
28
  "button_link_secondary": { "type": "string", "label": "Secondary button link" }
29
29
  }
30
30
  },
@@ -2,7 +2,7 @@
2
2
  "name": "exit-intent",
3
3
  "version": "0.1.0",
4
4
  "category": "conversion",
5
- "description": "Lushi exit popup module migrated from Lushi.",
5
+ "description": "Exit-intent popup with email capture. Replaces Privy / Justuno display layer.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-exit-intent.liquid"
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "lede": {
27
27
  "type": "string",
28
- "default": "Join the journal and get the founder's take on what we picked, why we picked it, and which products to skip altogether.",
28
+ "default": "Subscribe for early access and exclusive offers.",
29
29
  "label": "Lede"
30
30
  }
31
31
  }
@@ -2,7 +2,7 @@
2
2
  "name": "faq",
3
3
  "version": "0.1.0",
4
4
  "category": "storefront",
5
- "description": "Lushi FAQ module migrated from Lushi.",
5
+ "description": "FAQ accordion. Native replacement for FAQ widgets in Vitals and similar apps.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-faq.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "how-it-works",
3
3
  "version": "0.1.0",
4
4
  "category": "storefront",
5
- "description": "Lushi how it works module migrated from Lushi.",
5
+ "description": "How-it-works section: side-image + multi-step explainer.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-how-it-works.liquid"
@@ -12,12 +12,12 @@
12
12
  "schema": {
13
13
  "eyebrow": {
14
14
  "type": "string",
15
- "default": "How Lushi works",
15
+ "default": "How it works",
16
16
  "label": "Eyebrow"
17
17
  },
18
18
  "heading": {
19
19
  "type": "string",
20
- "default": "Curated, written, shipped.",
20
+ "default": "How we work.",
21
21
  "label": "Heading"
22
22
  },
23
23
  "lede": {
@@ -2,7 +2,7 @@
2
2
  "name": "inventory-urgency",
3
3
  "version": "0.1.0",
4
4
  "category": "pdp",
5
- "description": "inventory-urgency module migrated from Lushi.",
5
+ "description": "PDP inventory urgency message (Only N left) when stock tracking is enabled and quantity is low.",
6
6
  "files": {
7
7
  "snippets": [
8
8
  "snippets/runwell-inventory-urgency.liquid"
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "config": {
10
10
  "schema": {
11
- "tier_0_name": { "type": "string", "default": "Curator" },
11
+ "tier_0_name": { "type": "string", "default": "Member" },
12
12
  "tier_1_name": { "type": "string", "default": "Insider" },
13
13
  "tier_1_threshold": { "type": "number", "default": 250 },
14
14
  "tier_1_perk_pct": { "type": "number", "default": 10 },
@@ -2,7 +2,7 @@
2
2
  "name": "pdp-ingredients",
3
3
  "version": "0.1.0",
4
4
  "category": "pdp",
5
- "description": "Lushi ingredients module migrated from Lushi.",
5
+ "description": "PDP ingredients block (sources from product metafield list.metaobject).",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-ingredients.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "pdp-journal-link",
3
3
  "version": "0.1.0",
4
4
  "category": "pdp",
5
- "description": "Lushi PDP journal link module migrated from Lushi.",
5
+ "description": "PDP journal link.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-pdp-journal.liquid"
@@ -12,7 +12,7 @@
12
12
  "schema": {
13
13
  "eyebrow": {
14
14
  "type": "string",
15
- "default": "From the journal",
15
+ "default": "Editorial",
16
16
  "label": "Eyebrow"
17
17
  },
18
18
  "article": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "fallback_body": {
38
38
  "type": "string",
39
- "default": "Every product on Lushi pairs with a journal article. Browse the full archive to dig deeper.",
39
+ "default": "Every product pairs with a journal article. Browse the full archive to dig deeper.",
40
40
  "label": "Fallback body"
41
41
  },
42
42
  "fallback_link_label": {
@@ -2,7 +2,7 @@
2
2
  "name": "pdp-trust-checks",
3
3
  "version": "0.1.0",
4
4
  "category": "pdp",
5
- "description": "Lushi PDP trust module migrated from Lushi.",
5
+ "description": "PDP trust.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-pdp-trust.liquid"
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "lede": {
24
24
  "type": "string",
25
- "default": "We do not stock everything. Every product on Lushi has to clear these four checks before it gets a page.",
25
+ "default": "We do not stock everything. Every product has to clear these four checks before it gets a page.",
26
26
  "label": "Lede"
27
27
  },
28
28
  "background_color": {
@@ -2,7 +2,7 @@
2
2
  "name": "press-bar",
3
3
  "version": "0.1.0",
4
4
  "category": "social-proof",
5
- "description": "Lushi press bar module migrated from Lushi.",
5
+ "description": "press bar.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-press-bar.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "recently-viewed",
3
3
  "version": "0.1.0",
4
4
  "category": "pdp",
5
- "description": "Lushi recently viewed module migrated from Lushi.",
5
+ "description": "recently viewed.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-recently-viewed.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "reviews",
3
3
  "version": "0.1.0",
4
4
  "category": "social-proof",
5
- "description": "Lushi PDP reviews module migrated from Lushi.",
5
+ "description": "PDP reviews.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-pdp-reviews.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "risk-reversal",
3
3
  "version": "0.1.0",
4
4
  "category": "conversion",
5
- "description": "Lushi risk reversal module migrated from Lushi.",
5
+ "description": "risk reversal.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-risk-reversal.liquid"
@@ -2,7 +2,7 @@
2
2
  "name": "shipping-bar",
3
3
  "version": "0.1.0",
4
4
  "category": "conversion",
5
- "description": "Lushi shipping bar module migrated from Lushi.",
5
+ "description": "shipping bar.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-shipping-bar.liquid"
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "variants": {
16
16
  "grid-quotes": {
17
- "description": "3 to 6 customer quote cards in a responsive grid. Each block is a quote with author + context + 0-5 star rating. Best for editorial / wellness brands.",
17
+ "description": "3 to 6 customer quote cards in a responsive grid. Each block is a quote with author + context + 0-5 star rating. Best for editorial brands.",
18
18
  "files": {
19
19
  "sections": ["variants/grid-quotes/sections/runwell-testimonials.liquid"]
20
20
  }
@@ -2,7 +2,7 @@
2
2
  "name": "trust-badges",
3
3
  "version": "0.1.0",
4
4
  "category": "social-proof",
5
- "description": "Lushi trust badges module migrated from Lushi.",
5
+ "description": "trust badges.",
6
6
  "files": {
7
7
  "sections": [
8
8
  "sections/runwell-trust-badges.liquid"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runwell/shopify-toolkit",
3
- "version": "0.8.0",
3
+ "version": "0.10.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",
@@ -42,4 +42,4 @@
42
42
  "type": "git",
43
43
  "url": "https://github.com/louayelbiche/runwell-shopify-toolkit.git"
44
44
  }
45
- }
45
+ }