@o2vend/theme-cli 1.0.36 → 1.0.38
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/README.md +4 -0
- package/lib/lib/dev-server.js +344 -48
- package/lib/lib/liquid-engine.js +3 -1
- package/lib/lib/mock-data.js +473 -119
- package/lib/lib/widget-service.js +12 -4
- package/package.json +2 -2
- package/test-theme/assets/async-sections.js +32 -24
- package/test-theme/assets/cart-drawer.js +20 -22
- package/test-theme/assets/cart-manager.js +1 -15
- package/test-theme/assets/checkout-price-handler.js +12 -11
- package/test-theme/assets/checkout.css +1415 -0
- package/test-theme/assets/checkout.js +3174 -0
- package/test-theme/assets/components.css +178 -29
- package/test-theme/assets/delivery-zone.js +1 -1
- package/test-theme/assets/product-detail.css +1050 -0
- package/test-theme/assets/product-detail.js +2940 -0
- package/test-theme/assets/theme.css +95 -120
- package/test-theme/assets/theme.js +781 -186
- package/test-theme/layout/theme.liquid +91 -17
- package/test-theme/sections/content.liquid +64 -57
- package/test-theme/sections/footer-fallback.liquid +57 -7
- package/test-theme/sections/footer.liquid +63 -12
- package/test-theme/sections/header-fallback.liquid +41 -41
- package/test-theme/sections/header.liquid +41 -51
- package/test-theme/sections/hero-fallback.liquid +1 -1
- package/test-theme/sections/hero.liquid +159 -136
- package/test-theme/snippets/account-sidebar.liquid +121 -29
- package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
- package/test-theme/snippets/breadcrumbs.liquid +98 -11
- package/test-theme/snippets/cart-drawer.liquid +93 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
- package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
- package/test-theme/snippets/delivery-zone-search.liquid +104 -18
- package/test-theme/snippets/login-modal.liquid +269 -82
- package/test-theme/snippets/mega-menu.liquid +130 -43
- package/test-theme/snippets/news-thumbnail.liquid +120 -28
- package/test-theme/snippets/pagination.liquid +1 -1
- package/test-theme/snippets/price.liquid +100 -9
- package/test-theme/snippets/product-card-related.liquid +22 -4
- package/test-theme/snippets/product-card-simple.liquid +521 -25
- package/test-theme/snippets/product-card.liquid +145 -232
- package/test-theme/snippets/rating.liquid +100 -9
- package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
- package/test-theme/snippets/skeleton-product-card.liquid +102 -16
- package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
- package/test-theme/snippets/social-sharing.liquid +133 -32
- package/test-theme/templates/account/dashboard.liquid +30 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
- package/test-theme/templates/account/loyalty.liquid +45 -43
- package/test-theme/templates/account/order-detail.liquid +15 -8
- package/test-theme/templates/account/orders.liquid +189 -35
- package/test-theme/templates/account/profile.liquid +509 -114
- package/test-theme/templates/account/register.liquid +18 -8
- package/test-theme/templates/account/return-orders.liquid +31 -30
- package/test-theme/templates/account/store-credit.liquid +27 -26
- package/test-theme/templates/account/subscriptions.liquid +22 -5
- package/test-theme/templates/account/wishlist.liquid +88 -19
- package/test-theme/templates/address-book.liquid +166 -69
- package/test-theme/templates/categories.liquid +90 -30
- package/test-theme/templates/checkout.liquid +137 -3834
- package/test-theme/templates/error.liquid +23 -21
- package/test-theme/templates/index.liquid +29 -0
- package/test-theme/templates/login.liquid +33 -6
- package/test-theme/templates/order-confirmation.liquid +67 -9
- package/test-theme/templates/page.liquid +418 -206
- package/test-theme/templates/product-detail.liquid +124 -3878
- package/test-theme/templates/products.liquid +155 -30
- package/test-theme/templates/search.liquid +739 -225
- package/test-theme/widgets/brand-carousel.liquid +102 -82
- package/test-theme/widgets/brand.liquid +78 -50
- package/test-theme/widgets/carousel.liquid +253 -121
- package/test-theme/widgets/category-list-carousel.liquid +32 -8
- package/test-theme/widgets/category-list.liquid +21 -6
- package/test-theme/widgets/category.liquid +104 -37
- package/test-theme/widgets/discount-time.liquid +326 -119
- package/test-theme/widgets/footer-menu.liquid +115 -23
- package/test-theme/widgets/footer.liquid +118 -5
- package/test-theme/widgets/gallery.liquid +29 -5
- package/test-theme/widgets/header-menu.liquid +25 -13
- package/test-theme/widgets/header.liquid +64 -26
- package/test-theme/widgets/html.liquid +29 -6
- package/test-theme/widgets/news.liquid +6 -0
- package/test-theme/widgets/product-canvas.liquid +20 -12
- package/test-theme/widgets/product-carousel.liquid +118 -56
- package/test-theme/widgets/shared/product-grid.liquid +12 -0
- package/test-theme/widgets/single-product.liquid +688 -250
- package/test-theme/widgets/spacebar-carousel.liquid +39 -10
- package/test-theme/widgets/spacebar.liquid +77 -6
- package/test-theme/widgets/splash.liquid +40 -30
- package/test-theme/widgets/testimonial-carousel.liquid +111 -67
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
The O2VEND Theme CLI is a complete development environment for creating, testing, and packaging O2VEND themes. It works independently and doesn't require access to the main O2VEND webstore solution.
|
|
6
6
|
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- **Node.js 24.0 or later** is required. Download from [nodejs.org](https://nodejs.org/).
|
|
10
|
+
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
9
13
|
Install globally via npm:
|
package/lib/lib/dev-server.js
CHANGED
|
@@ -137,7 +137,7 @@ class DevServer {
|
|
|
137
137
|
// Only process if it contains actual Liquid variables, not just comments
|
|
138
138
|
if (cssContent.includes('{{') && cssContent.includes('settings.')) {
|
|
139
139
|
try {
|
|
140
|
-
const settings = this.loadThemeSettings();
|
|
140
|
+
const settings = { ...this.loadThemeSettings(), ...(this._realStoreSettings || {}) };
|
|
141
141
|
const processedCss = await this.liquid.parseAndRender(cssContent, { settings });
|
|
142
142
|
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
143
143
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -356,7 +356,6 @@ class DevServer {
|
|
|
356
356
|
if (fs.existsSync(altPath)) {
|
|
357
357
|
console.log(`[WIDGET] ✅ Using shared widget: ${templateSlug}.liquid`);
|
|
358
358
|
const widgetContent = fs.readFileSync(altPath, 'utf8');
|
|
359
|
-
// Get full context - use current rendering context if available, otherwise use passed context
|
|
360
359
|
const fullContext = liquidEngine.getCurrentContext ? liquidEngine.getCurrentContext() : null;
|
|
361
360
|
const widgetContext = fullContext
|
|
362
361
|
? { ...fullContext, widget: widget }
|
|
@@ -380,7 +379,6 @@ class DevServer {
|
|
|
380
379
|
|
|
381
380
|
console.log(`[WIDGET] ✅ Found widget template: ${widgetPath}`);
|
|
382
381
|
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
383
|
-
// Get full context - use current rendering context if available
|
|
384
382
|
const fullContext = liquidEngine.getCurrentContext ? liquidEngine.getCurrentContext() : null;
|
|
385
383
|
const widgetContext = fullContext
|
|
386
384
|
? { ...fullContext, widget: widget }
|
|
@@ -1056,6 +1054,19 @@ class DevServer {
|
|
|
1056
1054
|
}
|
|
1057
1055
|
});
|
|
1058
1056
|
|
|
1057
|
+
// Checkout page (use pageType 'checkout' so only header/footer widgets load, not home widgets)
|
|
1058
|
+
this.app.get('/checkout', async (req, res, next) => {
|
|
1059
|
+
try {
|
|
1060
|
+
const context = await this.buildContext(req, 'checkout');
|
|
1061
|
+
const html = await renderWithLayout(this.liquid, 'templates/checkout', context, this.themePath);
|
|
1062
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1063
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1064
|
+
res.send(html);
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
next(error);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1059
1070
|
// Search page
|
|
1060
1071
|
this.app.get('/search', async (req, res, next) => {
|
|
1061
1072
|
try {
|
|
@@ -1244,7 +1255,7 @@ class DevServer {
|
|
|
1244
1255
|
content: sectionContent,
|
|
1245
1256
|
type: 'section',
|
|
1246
1257
|
name: sectionName,
|
|
1247
|
-
backUrl: '/dev'
|
|
1258
|
+
backUrl: '/dev#sections'
|
|
1248
1259
|
});
|
|
1249
1260
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1250
1261
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1276,13 +1287,14 @@ class DevServer {
|
|
|
1276
1287
|
}
|
|
1277
1288
|
}
|
|
1278
1289
|
|
|
1279
|
-
// Get mock widget data for this type
|
|
1290
|
+
// Get mock widget data for this type (widgetType is usually template slug, e.g. spacebar-carousel)
|
|
1280
1291
|
const mockWidgets = this.mockApi?.mockData?.widgets || [];
|
|
1281
|
-
const mockWidget = mockWidgets.find(w =>
|
|
1282
|
-
w.type === widgetType ||
|
|
1283
|
-
w.type.toLowerCase() === widgetType.toLowerCase()
|
|
1284
|
-
w.
|
|
1285
|
-
|
|
1292
|
+
const mockWidget = mockWidgets.find(w => {
|
|
1293
|
+
if (w.type === widgetType || w.id?.includes(widgetType)) return true;
|
|
1294
|
+
if (w.type && typeof w.type === 'string' && w.type.toLowerCase().replace(/-/g, '') === widgetType.toLowerCase().replace(/-/g, '')) return true;
|
|
1295
|
+
if (this.widgetService && w.type && this.widgetService.getTemplateSlug(w.type) === widgetType) return true;
|
|
1296
|
+
return false;
|
|
1297
|
+
}) || {
|
|
1286
1298
|
id: `widget-${widgetType}-1`,
|
|
1287
1299
|
type: widgetType,
|
|
1288
1300
|
section: 'content',
|
|
@@ -1299,6 +1311,8 @@ class DevServer {
|
|
|
1299
1311
|
if (this.widgetService) {
|
|
1300
1312
|
widgetToRender = this.widgetService.normalizeWidget(mockWidget);
|
|
1301
1313
|
}
|
|
1314
|
+
// Enrich single widget with products/news/etc so preview has data
|
|
1315
|
+
this.enrichSingleWidget(widgetToRender, context);
|
|
1302
1316
|
|
|
1303
1317
|
const widgetContent = await this.renderWidget(widgetToRender, context);
|
|
1304
1318
|
|
|
@@ -1307,7 +1321,7 @@ class DevServer {
|
|
|
1307
1321
|
content: widgetContent,
|
|
1308
1322
|
type: 'widget',
|
|
1309
1323
|
name: widgetType,
|
|
1310
|
-
backUrl: '/dev'
|
|
1324
|
+
backUrl: '/dev#widgets'
|
|
1311
1325
|
});
|
|
1312
1326
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1313
1327
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1336,18 +1350,10 @@ class DevServer {
|
|
|
1336
1350
|
}
|
|
1337
1351
|
|
|
1338
1352
|
const context = await this.buildContext(req, 'home');
|
|
1339
|
-
|
|
1340
|
-
// Try to render snippet with common variables
|
|
1341
|
-
const snippetContext = {
|
|
1353
|
+
const snippetContext = this.getSnippetMockContext(snippetName, {
|
|
1342
1354
|
...context,
|
|
1343
|
-
snippet: {
|
|
1344
|
-
|
|
1345
|
-
},
|
|
1346
|
-
// Add common snippet variables
|
|
1347
|
-
product: context.products?.[0] || context.product,
|
|
1348
|
-
collection: context.collections?.[0] || context.collection,
|
|
1349
|
-
cart: context.cart
|
|
1350
|
-
};
|
|
1355
|
+
snippet: { name: snippetName }
|
|
1356
|
+
});
|
|
1351
1357
|
|
|
1352
1358
|
const snippetContent = await this.liquid.renderFile(snippetPath, snippetContext);
|
|
1353
1359
|
|
|
@@ -1356,7 +1362,8 @@ class DevServer {
|
|
|
1356
1362
|
content: snippetContent,
|
|
1357
1363
|
type: 'snippet',
|
|
1358
1364
|
name: snippetName,
|
|
1359
|
-
backUrl: '/dev'
|
|
1365
|
+
backUrl: '/dev#snippets',
|
|
1366
|
+
includeThemeCss: true
|
|
1360
1367
|
});
|
|
1361
1368
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1362
1369
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1392,7 +1399,7 @@ class DevServer {
|
|
|
1392
1399
|
else if (templateName.includes('search')) pageType = 'search';
|
|
1393
1400
|
else if (templateName.includes('page')) pageType = 'page';
|
|
1394
1401
|
else if (templateName.includes('account') || templateName.includes('login')) pageType = 'page';
|
|
1395
|
-
else if (templateName.includes('checkout')) pageType = '
|
|
1402
|
+
else if (templateName.includes('checkout')) pageType = 'checkout';
|
|
1396
1403
|
|
|
1397
1404
|
const context = await this.buildContext(req, pageType);
|
|
1398
1405
|
const html = await renderWithLayout(this.liquid, `templates/${templateName}`, context, this.themePath);
|
|
@@ -1411,7 +1418,7 @@ class DevServer {
|
|
|
1411
1418
|
const handle = req.params.handle;
|
|
1412
1419
|
|
|
1413
1420
|
// Skip if it's a known route path or static file extension
|
|
1414
|
-
const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'search', 'page', 'pages'];
|
|
1421
|
+
const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'checkout', 'search', 'page', 'pages'];
|
|
1415
1422
|
const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
|
|
1416
1423
|
const hasStaticExtension = staticExtensions.some(ext => handle.toLowerCase().endsWith(ext));
|
|
1417
1424
|
|
|
@@ -1840,7 +1847,7 @@ class DevServer {
|
|
|
1840
1847
|
};
|
|
1841
1848
|
|
|
1842
1849
|
// Load widgets filtered by page type (matches production behavior)
|
|
1843
|
-
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', brand: 'brand', checkout: 'checkout', page: 'home' };
|
|
1850
|
+
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', collections: 'categories', brand: 'brand', cart: 'cart', checkout: 'checkout', page: 'home' };
|
|
1844
1851
|
const widgetPageId = pageIdMap[pageType] || 'home';
|
|
1845
1852
|
if (this.widgetService) {
|
|
1846
1853
|
try {
|
|
@@ -2079,6 +2086,9 @@ class DevServer {
|
|
|
2079
2086
|
const favicon = (storeInfo && storeInfo.favouriteIconUrl) ? storeInfo.favouriteIconUrl : '/favicon.ico';
|
|
2080
2087
|
const storeName = (storeInfo && storeInfo.name) ? storeInfo.name : 'Store';
|
|
2081
2088
|
const storeSettings = (storeInfo && storeInfo.settings) || {};
|
|
2089
|
+
if (Object.keys(storeSettings).length > 0) {
|
|
2090
|
+
this._realStoreSettings = storeSettings;
|
|
2091
|
+
}
|
|
2082
2092
|
try {
|
|
2083
2093
|
context.shop = {
|
|
2084
2094
|
name: storeName,
|
|
@@ -2118,7 +2128,8 @@ class DevServer {
|
|
|
2118
2128
|
context.settings.layout_style = 'full-width';
|
|
2119
2129
|
}
|
|
2120
2130
|
|
|
2121
|
-
const
|
|
2131
|
+
const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', collections: 'categories', brand: 'brand', cart: 'cart', checkout: 'checkout', page: 'home' };
|
|
2132
|
+
const page = pageIdMap[pageType] || 'home';
|
|
2122
2133
|
context.widgets = await webstoreApi.fetchWidgetsBySections(page, webstoreApi.DEFAULT_SECTIONS, opts);
|
|
2123
2134
|
context.widgets = context.widgets || { hero: [], products: [], footer: [], content: [], header: [] };
|
|
2124
2135
|
|
|
@@ -2143,20 +2154,23 @@ class DevServer {
|
|
|
2143
2154
|
|
|
2144
2155
|
try {
|
|
2145
2156
|
const catRes = await webstoreApi.fetchCategories({ limit: 50, offset: 0 }, opts);
|
|
2146
|
-
const raw = catRes?.categories || catRes?.data?.categories || catRes || [];
|
|
2147
|
-
context.categories = raw.map((c) => {
|
|
2157
|
+
const raw = catRes?.categories || catRes?.data?.categories || catRes?.data?.items || catRes?.items || (Array.isArray(catRes) ? catRes : []);
|
|
2158
|
+
context.categories = Array.isArray(raw) ? raw.map((c) => {
|
|
2148
2159
|
const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().trim() || null;
|
|
2149
2160
|
if (h) {
|
|
2150
2161
|
c.handle = c.handle || h;
|
|
2151
2162
|
c.slug = c.slug || h;
|
|
2152
2163
|
}
|
|
2153
2164
|
return c;
|
|
2154
|
-
});
|
|
2165
|
+
}) : [];
|
|
2155
2166
|
context.collections = context.categories;
|
|
2156
2167
|
} catch (e) {
|
|
2157
2168
|
context.categories = [];
|
|
2158
2169
|
context.collections = [];
|
|
2159
2170
|
}
|
|
2171
|
+
if (pageType === 'collections' && !context.collections) {
|
|
2172
|
+
context.collections = context.categories || [];
|
|
2173
|
+
}
|
|
2160
2174
|
|
|
2161
2175
|
try {
|
|
2162
2176
|
const brandRes = await webstoreApi.fetchBrands({ limit: 50, offset: 0 }, opts);
|
|
@@ -2250,12 +2264,35 @@ class DevServer {
|
|
|
2250
2264
|
}
|
|
2251
2265
|
}
|
|
2252
2266
|
|
|
2267
|
+
// Align with mock: hide Splash on pages (show only in /dev); in mock only, hide Footer type in footer (only FooterMenu)
|
|
2268
|
+
this.filterWidgetsForDisplay(context.widgets, this.mode);
|
|
2269
|
+
|
|
2253
2270
|
// Enrich widgets with products, categories, and brands data
|
|
2254
2271
|
this.enrichWidgetsWithData(context);
|
|
2255
|
-
|
|
2272
|
+
|
|
2256
2273
|
return context;
|
|
2257
2274
|
}
|
|
2258
|
-
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Filter widgets for display: remove Splash from all sections (preview only in /dev).
|
|
2278
|
+
* In mock mode only, remove Footer type from footer section (only FooterMenu in footer). Real mode shows all footer widgets.
|
|
2279
|
+
*/
|
|
2280
|
+
filterWidgetsForDisplay(widgets, mode) {
|
|
2281
|
+
if (!widgets || typeof widgets !== 'object') return;
|
|
2282
|
+
const isMock = mode !== 'real';
|
|
2283
|
+
Object.keys(widgets).forEach(section => {
|
|
2284
|
+
const list = widgets[section];
|
|
2285
|
+
if (!Array.isArray(list)) return;
|
|
2286
|
+
const filtered = list.filter((w) => {
|
|
2287
|
+
const type = (w.type || '').toLowerCase().trim();
|
|
2288
|
+
if (type === 'splash') return false;
|
|
2289
|
+
if (isMock && section === 'footer' && type === 'footer') return false;
|
|
2290
|
+
return true;
|
|
2291
|
+
});
|
|
2292
|
+
widgets[section] = filtered;
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2259
2296
|
/**
|
|
2260
2297
|
* Enrich widgets with actual data (products, categories, brands)
|
|
2261
2298
|
* This is essential for product carousels, category lists, etc. to display content
|
|
@@ -2345,11 +2382,165 @@ class DevServer {
|
|
|
2345
2382
|
enrichedCount++;
|
|
2346
2383
|
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} recently viewed products`);
|
|
2347
2384
|
}
|
|
2385
|
+
|
|
2386
|
+
// Enrich News widgets with items (use existing or default)
|
|
2387
|
+
if (type === 'news') {
|
|
2388
|
+
let items = widget.data.items || widget.data.newsItems || widget.data.content?.items || widget.data.content?.Items;
|
|
2389
|
+
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
2390
|
+
items = this.getDefaultMockNewsItems();
|
|
2391
|
+
enrichedCount++;
|
|
2392
|
+
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${items.length} news items`);
|
|
2393
|
+
}
|
|
2394
|
+
widget.data.items = items;
|
|
2395
|
+
widget.data.newsItems = items;
|
|
2396
|
+
widget.data.content = widget.data.content || {};
|
|
2397
|
+
widget.data.content.items = items;
|
|
2398
|
+
widget.data.content.Items = items;
|
|
2399
|
+
}
|
|
2348
2400
|
});
|
|
2349
2401
|
});
|
|
2350
2402
|
|
|
2351
2403
|
console.log(`[CONTEXT] ✅ Enriched ${enrichedCount} widgets with data`);
|
|
2352
2404
|
}
|
|
2405
|
+
|
|
2406
|
+
/**
|
|
2407
|
+
* Enrich a single widget with products, categories, brands, or news items.
|
|
2408
|
+
* Used by /dev/widget/:type so the preview has data without being in context.widgets.
|
|
2409
|
+
*/
|
|
2410
|
+
enrichSingleWidget(widget, context) {
|
|
2411
|
+
if (!widget) return;
|
|
2412
|
+
widget.data = widget.data || {};
|
|
2413
|
+
const type = (widget.type || '').toLowerCase();
|
|
2414
|
+
const products = context.products || [];
|
|
2415
|
+
const categories = context.categories || [];
|
|
2416
|
+
const brands = context.brands || [];
|
|
2417
|
+
|
|
2418
|
+
if (type.includes('product') || type === 'featuredproducts' || type === 'bestsellerproducts' || type === 'newproducts' || type === 'simpleproduct' || type === 'singleproduct') {
|
|
2419
|
+
const widgetProducts = this.getProductsForWidget(widget, products);
|
|
2420
|
+
const formatted = this.mode === 'real'
|
|
2421
|
+
? this.normalizeProductsForRealMode(widgetProducts)
|
|
2422
|
+
: this.enrichProductsData(widgetProducts);
|
|
2423
|
+
widget.data.products = formatted;
|
|
2424
|
+
widget.data.Products = formatted;
|
|
2425
|
+
widget.products = formatted;
|
|
2426
|
+
widget.data.content = widget.data.content || {};
|
|
2427
|
+
widget.data.content.products = formatted;
|
|
2428
|
+
widget.data.content.Products = formatted;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
if (type === 'recentlyviewed' || type === 'recently-viewed') {
|
|
2432
|
+
const recent = products.slice(0, 6);
|
|
2433
|
+
const recentProducts = this.mode === 'real'
|
|
2434
|
+
? this.normalizeProductsForRealMode(recent)
|
|
2435
|
+
: this.enrichProductsData(recent);
|
|
2436
|
+
widget.data.products = recentProducts;
|
|
2437
|
+
widget.data.Products = recentProducts;
|
|
2438
|
+
widget.products = recentProducts;
|
|
2439
|
+
widget.data.content = widget.data.content || {};
|
|
2440
|
+
widget.data.content.products = recentProducts;
|
|
2441
|
+
widget.data.content.Products = recentProducts;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
if (type.includes('category') || type === 'categorylist' || type === 'categorylistcarousel') {
|
|
2445
|
+
const widgetCategories = categories.slice(0, 10);
|
|
2446
|
+
widget.data.categories = widgetCategories;
|
|
2447
|
+
widget.data.Categories = widgetCategories;
|
|
2448
|
+
widget.categories = widgetCategories;
|
|
2449
|
+
widget.data.content = widget.data.content || {};
|
|
2450
|
+
widget.data.content.categories = widgetCategories;
|
|
2451
|
+
widget.data.content.Categories = widgetCategories;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
if (type.includes('brand') || type === 'brandcarousel' || type === 'brandlist') {
|
|
2455
|
+
widget.data.brands = brands;
|
|
2456
|
+
widget.data.Brands = brands;
|
|
2457
|
+
widget.brands = brands;
|
|
2458
|
+
widget.data.content = widget.data.content || {};
|
|
2459
|
+
widget.data.content.brands = brands;
|
|
2460
|
+
widget.data.content.Brands = brands;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
if (type === 'news') {
|
|
2464
|
+
let items = widget.data.items || widget.data.newsItems || widget.data.content?.items || widget.data.content?.Items;
|
|
2465
|
+
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
2466
|
+
items = context.mockNewsItems || this.getDefaultMockNewsItems();
|
|
2467
|
+
}
|
|
2468
|
+
widget.data.items = items;
|
|
2469
|
+
widget.data.newsItems = items;
|
|
2470
|
+
widget.data.content = widget.data.content || {};
|
|
2471
|
+
widget.data.content.items = items;
|
|
2472
|
+
widget.data.content.Items = items;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
/** Default news items for dev preview when no mock data is present */
|
|
2477
|
+
getDefaultMockNewsItems() {
|
|
2478
|
+
return [
|
|
2479
|
+
{ id: 1, title: 'Welcome to Our Store', slug: 'welcome', excerpt: 'Learn about our new collection and special offers.', thumbnailImage: { url: 'https://picsum.photos/seed/n1/400/220' }, publishedAt: new Date().toISOString() },
|
|
2480
|
+
{ id: 2, title: 'Summer Sale 2025', slug: 'summer-sale', excerpt: 'Up to 50% off on selected items.', thumbnailImage: { url: 'https://picsum.photos/seed/n2/400/220' }, publishedAt: new Date().toISOString() },
|
|
2481
|
+
{ id: 3, title: 'New Arrivals', slug: 'new-arrivals', excerpt: 'Check out the latest products in our catalog.', thumbnailImage: { url: 'https://picsum.photos/seed/n3/400/220' }, publishedAt: new Date().toISOString() },
|
|
2482
|
+
{ id: 4, title: 'Tips & Guides', slug: 'tips-guides', excerpt: 'How to get the most from your purchase.', thumbnailImage: { url: 'https://picsum.photos/seed/n4/400/220' }, publishedAt: new Date().toISOString() }
|
|
2483
|
+
];
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Build mock context for snippet preview so all snippets have data to render.
|
|
2488
|
+
* Ensures product, collection, cart, news_item, pagination, page, etc. are present.
|
|
2489
|
+
*/
|
|
2490
|
+
getSnippetMockContext(snippetName, baseContext) {
|
|
2491
|
+
const ctx = { ...baseContext };
|
|
2492
|
+
const product = ctx.products?.[0] || ctx.product;
|
|
2493
|
+
const collection = ctx.collections?.[0] || ctx.collection || ctx.categories?.[0];
|
|
2494
|
+
const cart = ctx.cart || { items: [], itemCount: 0, total: 0, subTotal: 0 };
|
|
2495
|
+
|
|
2496
|
+
if (product) {
|
|
2497
|
+
ctx.product = this.mode === 'real' ? product : this.enrichProductData(product);
|
|
2498
|
+
if (!ctx.product.prices) {
|
|
2499
|
+
ctx.product.prices = { price: ctx.product.price || 0, mrp: ctx.product.compareAtPrice || 0, priceString: `$${Number(ctx.product.price || 0).toFixed(2)}`, mrpString: ctx.product.compareAtPrice ? `$${Number(ctx.product.compareAtPrice).toFixed(2)}` : '' };
|
|
2500
|
+
}
|
|
2501
|
+
if (!ctx.product.variations && !ctx.product.variants) {
|
|
2502
|
+
ctx.product.variations = [{ id: 1, productId: ctx.product.id, price: ctx.product.price, inStock: true, title: 'Default' }];
|
|
2503
|
+
ctx.product.variants = ctx.product.variations;
|
|
2504
|
+
}
|
|
2505
|
+
} else {
|
|
2506
|
+
const mockProduct = { id: 1, title: 'Sample Product', name: 'Sample Product', handle: 'sample-product', slug: 'sample-product', url: '/products/sample-product', price: 29.99, compareAtPrice: 39.99, imageUrl: 'https://picsum.photos/seed/p1/400/400', thumbnailImage: 'https://picsum.photos/seed/p1/400/400', prices: { price: 29.99, mrp: 39.99, priceString: '$29.99', mrpString: '$39.99' }, variations: [{ id: 1, productId: 1, price: 29.99, inStock: true, title: 'Default' }], variants: null, rating: 4.5, averageRating: 4.5, reviewCount: 12, reviewsCount: 12 };
|
|
2507
|
+
mockProduct.variants = mockProduct.variations;
|
|
2508
|
+
ctx.product = mockProduct;
|
|
2509
|
+
}
|
|
2510
|
+
if (ctx.product && (ctx.product.rating == null && ctx.product.averageRating == null)) {
|
|
2511
|
+
ctx.product.rating = ctx.product.rating ?? 4.5;
|
|
2512
|
+
ctx.product.averageRating = ctx.product.averageRating ?? ctx.product.rating;
|
|
2513
|
+
}
|
|
2514
|
+
if (ctx.product && (ctx.product.reviewCount == null && ctx.product.reviewsCount == null)) {
|
|
2515
|
+
ctx.product.reviewCount = ctx.product.reviewCount ?? 12;
|
|
2516
|
+
ctx.product.reviewsCount = ctx.product.reviewsCount ?? ctx.product.reviewCount;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
if (collection) {
|
|
2520
|
+
ctx.collection = { ...collection, title: collection.title || collection.name, handle: collection.handle || collection.slug, url: `/categories/${collection.handle || collection.slug}` };
|
|
2521
|
+
} else {
|
|
2522
|
+
ctx.collection = { id: 1, title: 'Sample Collection', name: 'Sample Collection', handle: 'sample', slug: 'sample', url: '/categories/sample' };
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
ctx.cart = cart && typeof cart === 'object' ? cart : { items: [], itemCount: 0, total: 0, subTotal: 0 };
|
|
2526
|
+
if (!ctx.cart.items) ctx.cart.items = [];
|
|
2527
|
+
if (ctx.cart.items.length === 0 && ctx.product) {
|
|
2528
|
+
ctx.cart.items = [{ id: '1', productId: ctx.product.id, title: ctx.product.title || ctx.product.name, quantity: 1, price: ctx.product.price, image: ctx.product.imageUrl || ctx.product.thumbnailImage }];
|
|
2529
|
+
ctx.cart.itemCount = 1;
|
|
2530
|
+
ctx.cart.total = ctx.product.price;
|
|
2531
|
+
ctx.cart.subTotal = ctx.product.price;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
ctx.news_item = ctx.news_item || this.getDefaultMockNewsItems()[0];
|
|
2535
|
+
ctx.pagination = ctx.pagination || { totalPages: 5, currentPage: 1, total: 50, pageSize: 10 };
|
|
2536
|
+
ctx.page = ctx.page || { title: 'Sample Page', handle: 'sample-page' };
|
|
2537
|
+
ctx.article = ctx.article || null;
|
|
2538
|
+
ctx.line_item = ctx.cart.items?.[0] || null;
|
|
2539
|
+
if (ctx.shop && !ctx.shop.currency) ctx.shop.currency = 'USD';
|
|
2540
|
+
if (ctx.shop && !ctx.shop.settings) ctx.shop.settings = { currencySymbol: '$' };
|
|
2541
|
+
|
|
2542
|
+
return ctx;
|
|
2543
|
+
}
|
|
2353
2544
|
|
|
2354
2545
|
/**
|
|
2355
2546
|
* Check if a product belongs to a category (for collection/category filtering).
|
|
@@ -2467,35 +2658,58 @@ class DevServer {
|
|
|
2467
2658
|
normalizeProductForRealMode(p) {
|
|
2468
2659
|
if (!p || typeof p !== 'object') return p;
|
|
2469
2660
|
const n = { ...p };
|
|
2661
|
+
const apiBase = (process.env.O2VEND_API_BASE_URL || '').replace(/\/$/, '');
|
|
2662
|
+
|
|
2470
2663
|
if (n.Description != null && n.description == null) n.description = n.Description;
|
|
2471
2664
|
if (n.HtmlContent != null && n.htmlContent == null) n.htmlContent = n.HtmlContent;
|
|
2472
2665
|
if (n.ShortDescription != null && n.shortDescription == null) n.shortDescription = n.ShortDescription;
|
|
2473
2666
|
if (n.Name != null && n.name == null) n.name = n.Name;
|
|
2474
2667
|
if (n.Title != null && n.title == null) n.title = n.Title;
|
|
2475
2668
|
if (n.Attributes != null && n.attributes == null) n.attributes = n.Attributes;
|
|
2669
|
+
if (n.Variations != null && n.variations == null) n.variations = n.Variations;
|
|
2670
|
+
if (n.Combinations != null && n.combinations == null) n.combinations = n.Combinations;
|
|
2671
|
+
if (n.Subscriptions != null && n.subscriptions == null) n.subscriptions = n.Subscriptions;
|
|
2672
|
+
if (n.ProductType != null && n.productType == null) n.productType = n.ProductType;
|
|
2673
|
+
|
|
2674
|
+
const resolveImageUrl = (u) => {
|
|
2675
|
+
if (!u || typeof u !== 'string') return u;
|
|
2676
|
+
if (u.startsWith('http://') || u.startsWith('https://') || u.startsWith('//')) return u;
|
|
2677
|
+
if (u.startsWith('/') && apiBase) return apiBase + u;
|
|
2678
|
+
return u;
|
|
2679
|
+
};
|
|
2476
2680
|
|
|
2477
|
-
const
|
|
2681
|
+
const extractUrl = (obj) => {
|
|
2682
|
+
if (!obj) return null;
|
|
2683
|
+
if (typeof obj === 'string') return obj;
|
|
2684
|
+
return obj.url || obj.Url || obj.imageUrl || obj.ImageUrl || obj.fileName || obj.FileName || obj.filePath || obj.FilePath || obj.imagePath || obj.ImagePath || null;
|
|
2685
|
+
};
|
|
2686
|
+
|
|
2687
|
+
const t1 = n.ThumbnailImage1 || n.thumbnailImage1 || n.thumbnailImage;
|
|
2478
2688
|
if (t1 != null) {
|
|
2479
|
-
const u =
|
|
2689
|
+
const u = resolveImageUrl(extractUrl(t1));
|
|
2480
2690
|
if (u) {
|
|
2481
2691
|
n.thumbnailImage = u;
|
|
2482
2692
|
if (!n.imageUrl) n.imageUrl = u;
|
|
2483
2693
|
}
|
|
2484
2694
|
}
|
|
2485
2695
|
|
|
2486
|
-
let imgs = n.images || n.Images;
|
|
2696
|
+
let imgs = n.images || n.Images || n.ProductImages || n.productImages;
|
|
2487
2697
|
if (Array.isArray(imgs) && imgs.length > 0) {
|
|
2488
2698
|
n.images = imgs.map((img) => {
|
|
2489
|
-
|
|
2490
|
-
const
|
|
2491
|
-
const a = img.altText || img.AltText || n.name || n.title;
|
|
2699
|
+
const u = resolveImageUrl(typeof img === 'string' ? img : extractUrl(img));
|
|
2700
|
+
const a = (typeof img === 'object' ? (img.altText || img.AltText) : null) || n.name || n.title;
|
|
2492
2701
|
return { url: u || '', altText: a };
|
|
2493
2702
|
});
|
|
2494
2703
|
} else {
|
|
2495
|
-
const single = n.imageUrl || n.ImageUrl;
|
|
2704
|
+
const single = resolveImageUrl(n.imageUrl || n.ImageUrl);
|
|
2496
2705
|
if (single) n.images = [{ url: single, altText: n.name || n.title }];
|
|
2497
2706
|
}
|
|
2498
2707
|
|
|
2708
|
+
if (n.images && n.images.length > 0) {
|
|
2709
|
+
n.featured_image = n.images[0];
|
|
2710
|
+
if (!n.imageUrl) n.imageUrl = n.images[0].url;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2499
2713
|
const handle = (n.handle || n.slug || n.id || '').toString().trim();
|
|
2500
2714
|
const h = handle && handle !== '#' ? handle.toLowerCase() : (n.productId ? String(n.productId) : null);
|
|
2501
2715
|
if (h) {
|
|
@@ -2854,10 +3068,14 @@ class DevServer {
|
|
|
2854
3068
|
|
|
2855
3069
|
try {
|
|
2856
3070
|
const templateSlug = this.widgetService.getTemplateSlug(widget.type);
|
|
2857
|
-
|
|
2858
|
-
|
|
3071
|
+
let widgetPath = path.join(this.themePath, 'widgets', `${templateSlug}.liquid`);
|
|
2859
3072
|
if (!fs.existsSync(widgetPath)) {
|
|
2860
|
-
|
|
3073
|
+
const sharedPath = path.join(this.themePath, 'widgets', 'shared', `${templateSlug}.liquid`);
|
|
3074
|
+
if (fs.existsSync(sharedPath)) {
|
|
3075
|
+
widgetPath = sharedPath;
|
|
3076
|
+
} else {
|
|
3077
|
+
return `<div class="error">Widget template not found: ${templateSlug}.liquid</div>`;
|
|
3078
|
+
}
|
|
2861
3079
|
}
|
|
2862
3080
|
|
|
2863
3081
|
const widgetContent = fs.readFileSync(widgetPath, 'utf8');
|
|
@@ -3099,7 +3317,7 @@ class DevServer {
|
|
|
3099
3317
|
</div>
|
|
3100
3318
|
</header>
|
|
3101
3319
|
|
|
3102
|
-
<div class="section">
|
|
3320
|
+
<div class="section" id="templates">
|
|
3103
3321
|
<h2>📄 Templates <span class="section-count">(${templates.length})</span></h2>
|
|
3104
3322
|
${templates.length > 0 ? `
|
|
3105
3323
|
<div class="file-grid">
|
|
@@ -3113,7 +3331,7 @@ class DevServer {
|
|
|
3113
3331
|
` : '<div class="empty">No templates found</div>'}
|
|
3114
3332
|
</div>
|
|
3115
3333
|
|
|
3116
|
-
<div class="section">
|
|
3334
|
+
<div class="section" id="sections">
|
|
3117
3335
|
<h2>🧩 Sections <span class="section-count">(${sections.length})</span></h2>
|
|
3118
3336
|
${sections.length > 0 ? `
|
|
3119
3337
|
<div class="file-grid">
|
|
@@ -3127,7 +3345,7 @@ class DevServer {
|
|
|
3127
3345
|
` : '<div class="empty">No sections found</div>'}
|
|
3128
3346
|
</div>
|
|
3129
3347
|
|
|
3130
|
-
<div class="section">
|
|
3348
|
+
<div class="section" id="widgets">
|
|
3131
3349
|
<h2>🎯 Widgets <span class="section-count">(${widgets.length})</span></h2>
|
|
3132
3350
|
${widgets.length > 0 ? `
|
|
3133
3351
|
<div class="file-grid">
|
|
@@ -3141,7 +3359,7 @@ class DevServer {
|
|
|
3141
3359
|
` : '<div class="empty">No widgets found</div>'}
|
|
3142
3360
|
</div>
|
|
3143
3361
|
|
|
3144
|
-
<div class="section">
|
|
3362
|
+
<div class="section" id="snippets">
|
|
3145
3363
|
<h2>✂️ Snippets <span class="section-count">(${snippets.length})</span></h2>
|
|
3146
3364
|
${snippets.length > 0 ? `
|
|
3147
3365
|
<div class="file-grid">
|
|
@@ -3163,14 +3381,91 @@ class DevServer {
|
|
|
3163
3381
|
<p style="margin-top: 8px; font-size: 0.85em; opacity: 0.7;">Version 1.0.0 | Running on port ${this.port || 3000}</p>
|
|
3164
3382
|
</div>
|
|
3165
3383
|
</div>
|
|
3384
|
+
<script>
|
|
3385
|
+
(function() {
|
|
3386
|
+
var hash = window.location.hash;
|
|
3387
|
+
if (hash) {
|
|
3388
|
+
var el = document.querySelector(hash);
|
|
3389
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
3390
|
+
}
|
|
3391
|
+
})();
|
|
3392
|
+
</script>
|
|
3166
3393
|
</body>
|
|
3167
3394
|
</html>`;
|
|
3168
3395
|
}
|
|
3169
3396
|
|
|
3397
|
+
/**
|
|
3398
|
+
* Return link tags for theme CSS files that exist (so snippet/section preview gets theme styles).
|
|
3399
|
+
* Includes theme, components, sections, and any other .css in assets (e.g. delivery-zone.css).
|
|
3400
|
+
*/
|
|
3401
|
+
getThemeCssLinkTags() {
|
|
3402
|
+
const assetsDir = path.join(this.themePath, 'assets');
|
|
3403
|
+
const links = [];
|
|
3404
|
+
const seen = new Set();
|
|
3405
|
+
const add = (base) => {
|
|
3406
|
+
if (seen.has(base)) return;
|
|
3407
|
+
seen.add(base);
|
|
3408
|
+
const minPath = path.join(assetsDir, `${base}.min.css`);
|
|
3409
|
+
const cssPath = path.join(assetsDir, `${base}.css`);
|
|
3410
|
+
if (fs.existsSync(minPath)) {
|
|
3411
|
+
links.push(`<link rel="stylesheet" href="/assets/${base}.min.css">`);
|
|
3412
|
+
} else if (fs.existsSync(cssPath)) {
|
|
3413
|
+
links.push(`<link rel="stylesheet" href="/assets/${base}.css">`);
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3416
|
+
['theme', 'components', 'sections'].forEach(add);
|
|
3417
|
+
if (fs.existsSync(assetsDir)) {
|
|
3418
|
+
try {
|
|
3419
|
+
fs.readdirSync(assetsDir)
|
|
3420
|
+
.filter((f) => f.endsWith('.css') && !f.endsWith('.min.css'))
|
|
3421
|
+
.forEach((f) => add(f.replace(/\.css$/, '')));
|
|
3422
|
+
} catch (_) { /* ignore */ }
|
|
3423
|
+
}
|
|
3424
|
+
return links.join('\n ');
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3170
3427
|
/**
|
|
3171
3428
|
* Generate preview page HTML
|
|
3172
3429
|
*/
|
|
3173
|
-
generatePreviewPage({ title, content, type, name, backUrl }) {
|
|
3430
|
+
generatePreviewPage({ title, content, type, name, backUrl, includeThemeCss = false }) {
|
|
3431
|
+
const themeCssLinks = includeThemeCss ? this.getThemeCssLinkTags() : '';
|
|
3432
|
+
const modalSnippets = ['login-modal', 'delivery-zone-modal', 'add-to-cart-modal'];
|
|
3433
|
+
const isModalSnippet = type === 'snippet' && name && modalSnippets.includes(name);
|
|
3434
|
+
const modalVisibilityCss = isModalSnippet ? `
|
|
3435
|
+
/* Force modal visible in /dev snippet preview (no trigger to open; overrides [aria-hidden="true"] { display: none }) */
|
|
3436
|
+
.preview-container .login-modal,
|
|
3437
|
+
.preview-container .login-modal[aria-hidden="true"],
|
|
3438
|
+
.preview-container .login-modal.login-modal--active {
|
|
3439
|
+
display: flex !important;
|
|
3440
|
+
align-items: center;
|
|
3441
|
+
justify-content: center;
|
|
3442
|
+
position: fixed;
|
|
3443
|
+
inset: 0;
|
|
3444
|
+
z-index: 9999;
|
|
3445
|
+
visibility: visible !important;
|
|
3446
|
+
}
|
|
3447
|
+
.preview-container .delivery-zone-modal,
|
|
3448
|
+
.preview-container .delivery-zone-modal[aria-hidden="true"],
|
|
3449
|
+
.preview-container .delivery-zone-modal.active {
|
|
3450
|
+
display: flex !important;
|
|
3451
|
+
align-items: center;
|
|
3452
|
+
justify-content: center;
|
|
3453
|
+
position: fixed;
|
|
3454
|
+
inset: 0;
|
|
3455
|
+
z-index: 9999;
|
|
3456
|
+
visibility: visible !important;
|
|
3457
|
+
}
|
|
3458
|
+
.preview-container .add-to-cart-modal,
|
|
3459
|
+
.preview-container .add-to-cart-modal[aria-hidden="true"] {
|
|
3460
|
+
display: flex !important;
|
|
3461
|
+
align-items: center;
|
|
3462
|
+
justify-content: center;
|
|
3463
|
+
position: fixed;
|
|
3464
|
+
inset: 0;
|
|
3465
|
+
z-index: 9999;
|
|
3466
|
+
visibility: visible !important;
|
|
3467
|
+
}
|
|
3468
|
+
` : '';
|
|
3174
3469
|
return `<!DOCTYPE html>
|
|
3175
3470
|
<html lang="en">
|
|
3176
3471
|
<head>
|
|
@@ -3178,7 +3473,7 @@ class DevServer {
|
|
|
3178
3473
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3179
3474
|
<title>${title} - O2VEND Dev</title>
|
|
3180
3475
|
<link rel="icon" type="image/png" href="/o2vend-assets/favicon.png">
|
|
3181
|
-
<style>
|
|
3476
|
+
${themeCssLinks ? ` ${themeCssLinks}\n ` : ''}<style>
|
|
3182
3477
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3183
3478
|
:root {
|
|
3184
3479
|
--o2vend-primary: #F9A825;
|
|
@@ -3251,6 +3546,7 @@ class DevServer {
|
|
|
3251
3546
|
border-radius: 6px;
|
|
3252
3547
|
color: #c62828;
|
|
3253
3548
|
}
|
|
3549
|
+
${modalVisibilityCss}
|
|
3254
3550
|
</style>
|
|
3255
3551
|
</head>
|
|
3256
3552
|
<body>
|
package/lib/lib/liquid-engine.js
CHANGED
|
@@ -233,9 +233,11 @@ function registerCustomTags(liquid, themePath) {
|
|
|
233
233
|
if (widget.template_path) {
|
|
234
234
|
const widgetPath = path.join(themePath, `${widget.template_path}.liquid`);
|
|
235
235
|
const altPath = path.join(themePath, widget.template_path);
|
|
236
|
+
const baseName = (widget.template_path.replace(/^widgets\//, '').replace(/\.liquid$/, '') || widget.template_path).split('/').pop();
|
|
237
|
+
const sharedPath = path.join(themePath, 'widgets', 'shared', `${baseName}.liquid`);
|
|
236
238
|
|
|
237
239
|
const finalPath = fs.existsSync(widgetPath) ? widgetPath :
|
|
238
|
-
(fs.existsSync(altPath) ? altPath : null);
|
|
240
|
+
(fs.existsSync(altPath) ? altPath : (fs.existsSync(sharedPath) ? sharedPath : null));
|
|
239
241
|
|
|
240
242
|
if (finalPath) {
|
|
241
243
|
const fullContext = currentRenderingContext
|