@deneb-ui/cli 2.0.14 → 2.0.15

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/index.js CHANGED
@@ -199,6 +199,32 @@ function detectPages(projectDir) {
199
199
  ...(isContact ? { required: true } : {}),
200
200
  });
201
201
  }
202
+
203
+ // Check for dynamic product detail routes (e.g. products/[slug] or shop/[id])
204
+ const subDirPath = path.join(cDir, routeName);
205
+ try {
206
+ if (fs.existsSync(subDirPath)) {
207
+ const subEntries = fs.readdirSync(subDirPath, { withFileTypes: true });
208
+ const hasDynamicSlug = subEntries.some((se) => se.isDirectory() && se.name.startsWith('['));
209
+ if (hasDynamicSlug && !foundRoutes.has('/' + routeName + '/detail')) {
210
+ foundRoutes.add('/' + routeName + '/detail');
211
+ let sampleSlug = 'vanta-aero-x';
212
+ const siteDataPath = path.join(projectDir, 'src', 'data', 'site-data.json');
213
+ if (fs.existsSync(siteDataPath)) {
214
+ try {
215
+ const sd = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
216
+ if (sd.content?.product?.slug) sampleSlug = sd.content.product.slug;
217
+ else if (sd.content?.home?.product1Slug) sampleSlug = sd.content.home.product1Slug;
218
+ } catch {}
219
+ }
220
+ pages.push({
221
+ id: 'product',
222
+ label: 'Product Detail',
223
+ route: `/${routeName}/${sampleSlug}`,
224
+ });
225
+ }
226
+ }
227
+ } catch {}
202
228
  }
203
229
  }
204
230
  } catch {
@@ -291,6 +317,28 @@ function getDefaultManifest(projectName, pages) {
291
317
  },
292
318
  ],
293
319
  },
320
+ ...(pages.some((p) => p.id === 'product')
321
+ ? [
322
+ {
323
+ id: 'product',
324
+ path: 'product',
325
+ type: 'object',
326
+ label: 'Product Detail Content',
327
+ fields: [
328
+ { key: 'name', type: 'text', label: 'Product Title', required: true },
329
+ { key: 'price', type: 'text', label: 'Product Price', required: true },
330
+ { key: 'description', type: 'textarea', label: 'Product Description' },
331
+ { key: 'badge', type: 'text', label: 'Product Badge' },
332
+ { key: 'featuredImage', type: 'image', label: 'Featured Product Image' },
333
+ { key: 'addToSelectionLabel', type: 'text', label: 'Add to Selection Label' },
334
+ { key: 'specsTitle', type: 'text', label: 'Specifications Heading' },
335
+ { key: 'shippingTitle', type: 'text', label: 'Shipping Heading' },
336
+ { key: 'shippingSummary', type: 'text', label: 'Shipping Summary' },
337
+ { key: 'shippingReturns', type: 'text', label: 'Shipping Returns Note' },
338
+ ],
339
+ },
340
+ ]
341
+ : []),
294
342
  ],
295
343
  },
296
344
  };
@@ -352,6 +400,24 @@ function getDefaultSiteData(projectName, pages) {
352
400
  primaryCtaLabel: 'Shop Now',
353
401
  secondaryCtaLabel: 'Learn More',
354
402
  },
403
+ ...(pages.some((p) => p.id === 'product')
404
+ ? {
405
+ product: {
406
+ name: 'Signature Performance Edition',
407
+ price: 'LKR 32,500',
408
+ description: 'Lightweight performance runner with responsive foam midsole and breathable engineered mesh.',
409
+ badge: 'NEW',
410
+ featuredImage: '/products/vanta-aero-x.jpg',
411
+ addToSelectionLabel: 'Add to Selection',
412
+ specsTitle: 'Specifications',
413
+ shippingTitle: 'Shipping & Returns',
414
+ shippingSummary: 'Free Islandwide Delivery within 2-3 business days. Cash on delivery available.',
415
+ shippingReturns: '14-day hassle-free exchanges for unworn footwear in original condition.',
416
+ relatedLabel: 'You May Also Like',
417
+ relatedTitle: 'Related Products',
418
+ },
419
+ }
420
+ : {}),
355
421
  },
356
422
  };
357
423
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "Official developer CLI for DENEB UI — Validating, scaffolding, testing, and packaging storefront templates. Created by Chamika Gayashan & Induranga Kawishwara.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -2,7 +2,7 @@
2
2
  "name": "vanta-shoes-store",
3
3
  "label": "Vanta Shoes Storefront",
4
4
  "description": "Calibrated Vanta shoes store recipe with WhatsApp CTA, Instagram social link, product grid, and hero slides.",
5
- "savedAt": "2026-09-08T17:20:49.206Z",
5
+ "savedAt": "2026-09-09T04:15:03.327Z",
6
6
  "signatures": {
7
7
  "keywords": [
8
8
  "vanta-shoes-store",
@@ -53,7 +53,16 @@
53
53
  }
54
54
  ],
55
55
  "autoIndex": true,
56
- "defaultCardPrefix": "product"
56
+ "defaultCardPrefix": "product",
57
+ "withBasePathImages": true,
58
+ "safeNavigationButtons": true
59
+ },
60
+ "productDetailRules": {
61
+ "enabled": true,
62
+ "sectionPath": "product",
63
+ "sampleRoute": "/products/vanta-aero-x",
64
+ "galleryWithBasePath": true,
65
+ "noStaticOnEditableAncestors": true
57
66
  },
58
67
  "actionRules": {
59
68
  "splitActionAndLabel": true,
@@ -89,6 +98,40 @@
89
98
  ],
90
99
  "targetPath": "common.footer"
91
100
  },
101
+ "pages": [
102
+ {
103
+ "id": "home",
104
+ "label": "Home",
105
+ "route": "/",
106
+ "required": true
107
+ },
108
+ {
109
+ "id": "about",
110
+ "label": "About",
111
+ "route": "/about"
112
+ },
113
+ {
114
+ "id": "contact",
115
+ "label": "Contact",
116
+ "route": "/contact",
117
+ "required": true
118
+ },
119
+ {
120
+ "id": "products",
121
+ "label": "Products",
122
+ "route": "/products"
123
+ },
124
+ {
125
+ "id": "shop",
126
+ "label": "Shop",
127
+ "route": "/shop"
128
+ },
129
+ {
130
+ "id": "product",
131
+ "label": "Product Detail",
132
+ "route": "/products/vanta-aero-x"
133
+ }
134
+ ],
92
135
  "sections": [
93
136
  {
94
137
  "id": "common",
@@ -937,6 +980,7 @@
937
980
  },
938
981
  {
939
982
  "id": "product",
983
+ "path": "product",
940
984
  "label": "Product Detail Page",
941
985
  "fields": [
942
986
  {
@@ -1119,7 +1163,7 @@
1119
1163
  "product2Name": "VANTA Flux 01",
1120
1164
  "product2Category": "Sneakers",
1121
1165
  "product2Price": "LKR 28,900",
1122
- "product2Badge": "",
1166
+ "product2Badge": "Trending",
1123
1167
  "product2Image": "/products/vanta-flux-01.jpg",
1124
1168
  "product3Name": "VANTA Core Runner",
1125
1169
  "product3Category": "Performance",
@@ -1129,12 +1173,12 @@
1129
1173
  "product4Name": "VANTA Mono",
1130
1174
  "product4Category": "Lifestyle",
1131
1175
  "product4Price": "LKR 24,800",
1132
- "product4Badge": "",
1176
+ "product4Badge": "Exclusive",
1133
1177
  "product4Image": "/products/vanta-mono.jpg",
1134
1178
  "product5Name": "VANTA Street 02",
1135
1179
  "product5Category": "Sneakers",
1136
1180
  "product5Price": "LKR 27,500",
1137
- "product5Badge": "",
1181
+ "product5Badge": "Popular",
1138
1182
  "product5Image": "/products/vanta-street-02.jpg",
1139
1183
  "product6Name": "VANTA Velocity",
1140
1184
  "product6Category": "Performance",
@@ -1144,7 +1188,7 @@
1144
1188
  "product7Name": "VANTA Apex Hi",
1145
1189
  "product7Category": "Sneakers",
1146
1190
  "product7Price": "LKR 31,200",
1147
- "product7Badge": "",
1191
+ "product7Badge": "Featured",
1148
1192
  "product7Image": "/products/vanta-apex-hi.jpg",
1149
1193
  "product8Name": "VANTA Train Pro",
1150
1194
  "product8Category": "Training",
@@ -1159,7 +1203,7 @@
1159
1203
  "product10Name": "VANTA Flux 01",
1160
1204
  "product10Category": "Sneakers",
1161
1205
  "product10Price": "LKR 28,900",
1162
- "product10Badge": "",
1206
+ "product10Badge": "Trending",
1163
1207
  "product10Image": "/products/vanta-flux-01.jpg",
1164
1208
  "product11Name": "VANTA Core Runner",
1165
1209
  "product11Category": "Performance",
@@ -1169,7 +1213,7 @@
1169
1213
  "product12Name": "VANTA Mono",
1170
1214
  "product12Category": "Lifestyle",
1171
1215
  "product12Price": "LKR 24,800",
1172
- "product12Badge": "",
1216
+ "product12Badge": "Exclusive",
1173
1217
  "product12Image": "/products/vanta-mono.jpg"
1174
1218
  },
1175
1219
  "product": {
@@ -132,6 +132,15 @@ function saveRecipeFromProject(projectDir, recipeName = 'custom-storefront', opt
132
132
  ],
133
133
  autoIndex: true,
134
134
  defaultCardPrefix: 'product',
135
+ withBasePathImages: true,
136
+ safeNavigationButtons: true,
137
+ },
138
+ productDetailRules: {
139
+ enabled: true,
140
+ sectionPath: 'product',
141
+ sampleRoute: '/products/vanta-aero-x',
142
+ galleryWithBasePath: true,
143
+ noStaticOnEditableAncestors: true,
135
144
  },
136
145
  actionRules: {
137
146
  splitActionAndLabel: true,
@@ -146,7 +155,11 @@ function saveRecipeFromProject(projectDir, recipeName = 'custom-storefront', opt
146
155
  platforms: ['instagram', 'facebook', 'twitter', 'tiktok', 'youtube', 'linkedin'],
147
156
  targetPath: 'common.footer',
148
157
  },
149
- sections: manifest.editorSchema?.sections || [],
158
+ pages: manifest.pages || [],
159
+ sections: (manifest.editorSchema?.sections || []).map((s) => ({
160
+ ...s,
161
+ path: s.path || s.id,
162
+ })),
150
163
  defaults: siteData.content || {},
151
164
  };
152
165
 
@@ -579,13 +579,18 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
579
579
  });
580
580
 
581
581
  // 14. Sensitive Element Guardian: Auto-annotate non-bound <a> and <img> tags with data-preview-static
582
- // Ensures 100% compliance with Fivora strict mode so unannotated links/images don't fail certification
583
- code = code.replace(/<a(\s+[^>]*?href="[^"]*"[^>]*?)>/gi, (match, attrs) => {
582
+ // CRITICAL FIVORA RULE: Never mark an <a> as static if it encloses any children with data-preview-field-path
583
+ // This completely eliminates "markers cannot be on or inside data-preview-static" errors across cards and links.
584
+ code = code.replace(/<a(\s+[^>]*?href="[^"]*"[^>]*?)>([\s\S]*?)<\/a>/gi, (match, attrs, inner) => {
584
585
  if (attrs.includes('data-preview-field-path') || attrs.includes('data-preview-static')) {
585
586
  return match;
586
587
  }
588
+ // If the inner content has editable field markers or is a card wrapper, NEVER mark the anchor static!
589
+ if (inner.includes('data-preview-field-path') || inner.includes('data-preview-item-path') || /card|product|shoe|item/i.test(attrs)) {
590
+ return match;
591
+ }
587
592
  fileModified = true;
588
- return `<a${attrs} data-preview-static="navigation-link">`;
593
+ return `<a${attrs} data-preview-static="navigation-link">${inner}</a>`;
589
594
  });
590
595
 
591
596
  code = code.replace(/<img(\s+[^>]*?src="[^"]*"[^>]*?)>/gi, (match, attrs) => {
@@ -735,13 +740,25 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
735
740
  },
736
741
  ];
737
742
 
738
- // If active recipe has defined sections, seed them
743
+ // Merge pages from detectedPages and activeRecipe.pages
744
+ const mergedPages = [...detectedPages];
745
+ if (activeRecipe && Array.isArray(activeRecipe.pages)) {
746
+ for (const rp of activeRecipe.pages) {
747
+ if (!mergedPages.some((p) => p.id === rp.id || p.route === rp.route)) {
748
+ mergedPages.push(rp);
749
+ }
750
+ }
751
+ }
752
+
753
+ // If active recipe has defined sections, seed them with guaranteed path property
739
754
  if (activeRecipe && Array.isArray(activeRecipe.sections)) {
740
755
  for (const sec of activeRecipe.sections) {
741
- if (sec.id === 'common') {
756
+ const clonedSec = { ...sec };
757
+ if (!clonedSec.path) clonedSec.path = clonedSec.id;
758
+ if (clonedSec.id === 'common') {
742
759
  const commonSec = editorSections.find((s) => s.id === 'common');
743
- if (commonSec && Array.isArray(sec.fields)) {
744
- for (const rf of sec.fields) {
760
+ if (commonSec && Array.isArray(clonedSec.fields)) {
761
+ for (const rf of clonedSec.fields) {
745
762
  if (!commonSec.fields.some((f) => f.key === rf.key)) {
746
763
  commonSec.fields.push(rf);
747
764
  }
@@ -749,7 +766,7 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
749
766
  }
750
767
  continue;
751
768
  }
752
- editorSections.push(sec);
769
+ editorSections.push(clonedSec);
753
770
  }
754
771
  }
755
772
 
@@ -796,6 +813,7 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
796
813
  }
797
814
 
798
815
  if (existingSection) {
816
+ if (!existingSection.path) existingSection.path = existingSection.id;
799
817
  existingSection.fields.push(...newFieldDefs);
800
818
  } else if (newFieldDefs.length > 0) {
801
819
  editorSections.push({
@@ -808,6 +826,11 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
808
826
  }
809
827
  }
810
828
 
829
+ // Ensure all editorSections have a valid path attribute
830
+ for (const sec of editorSections) {
831
+ if (!sec.path) sec.path = sec.id;
832
+ }
833
+
811
834
  const siteData = {
812
835
  project: {
813
836
  id: `${projectName}-project`,
@@ -823,11 +846,11 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
823
846
  name: projectName,
824
847
  engine: 'NEXT_STATIC_EXPORT',
825
848
  structure: {
826
- pages: detectedPages.map((p) => p.id),
849
+ pages: mergedPages.map((p) => p.id),
827
850
  },
828
851
  },
829
852
  requirements: {
830
- requiredPages: detectedPages.filter((p) => p.required).map((p) => p.id),
853
+ requiredPages: mergedPages.filter((p) => p.required).map((p) => p.id),
831
854
  requiredFeatures: [],
832
855
  },
833
856
  content: content,
@@ -846,7 +869,7 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
846
869
  installCommand: 'npm install',
847
870
  buildCommand: 'npm run build',
848
871
  basePathEnvVar: 'NEXT_PUBLIC_SITE_BASE_PATH',
849
- pages: detectedPages,
872
+ pages: mergedPages,
850
873
  editorSchema: {
851
874
  version: 1,
852
875
  sections: editorSections,