@deneb-ui/cli 2.0.12 → 2.0.14
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
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",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanta-shoes-store",
|
|
3
|
-
"label": "
|
|
4
|
-
"description": "
|
|
5
|
-
"savedAt": "2026-09-
|
|
3
|
+
"label": "Vanta Shoes Storefront",
|
|
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",
|
|
6
6
|
"signatures": {
|
|
7
7
|
"keywords": [
|
|
8
8
|
"vanta-shoes-store",
|
|
@@ -55,6 +55,40 @@
|
|
|
55
55
|
"autoIndex": true,
|
|
56
56
|
"defaultCardPrefix": "product"
|
|
57
57
|
},
|
|
58
|
+
"actionRules": {
|
|
59
|
+
"splitActionAndLabel": true,
|
|
60
|
+
"whatsappKeywords": [
|
|
61
|
+
"whatsapp",
|
|
62
|
+
"wa.me",
|
|
63
|
+
"order via whatsapp",
|
|
64
|
+
"chat on whatsapp"
|
|
65
|
+
],
|
|
66
|
+
"actionFieldMapping": {
|
|
67
|
+
"whatsapp": {
|
|
68
|
+
"urlField": "whatsappCtaUrl",
|
|
69
|
+
"labelField": "whatsappCtaLabel"
|
|
70
|
+
},
|
|
71
|
+
"order": {
|
|
72
|
+
"urlField": "orderCtaUrl",
|
|
73
|
+
"labelField": "orderCtaLabel"
|
|
74
|
+
},
|
|
75
|
+
"promo": {
|
|
76
|
+
"urlField": "promoCtaUrl",
|
|
77
|
+
"labelField": "promoCtaLabel"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"socialRules": {
|
|
82
|
+
"platforms": [
|
|
83
|
+
"instagram",
|
|
84
|
+
"facebook",
|
|
85
|
+
"twitter",
|
|
86
|
+
"tiktok",
|
|
87
|
+
"youtube",
|
|
88
|
+
"linkedin"
|
|
89
|
+
],
|
|
90
|
+
"targetPath": "common.footer"
|
|
91
|
+
},
|
|
58
92
|
"sections": [
|
|
59
93
|
{
|
|
60
94
|
"id": "common",
|
|
@@ -133,6 +133,19 @@ function saveRecipeFromProject(projectDir, recipeName = 'custom-storefront', opt
|
|
|
133
133
|
autoIndex: true,
|
|
134
134
|
defaultCardPrefix: 'product',
|
|
135
135
|
},
|
|
136
|
+
actionRules: {
|
|
137
|
+
splitActionAndLabel: true,
|
|
138
|
+
whatsappKeywords: ['whatsapp', 'wa.me', 'order via whatsapp', 'chat on whatsapp'],
|
|
139
|
+
actionFieldMapping: {
|
|
140
|
+
whatsapp: { urlField: 'whatsappCtaUrl', labelField: 'whatsappCtaLabel' },
|
|
141
|
+
order: { urlField: 'orderCtaUrl', labelField: 'orderCtaLabel' },
|
|
142
|
+
promo: { urlField: 'promoCtaUrl', labelField: 'promoCtaLabel' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
socialRules: {
|
|
146
|
+
platforms: ['instagram', 'facebook', 'twitter', 'tiktok', 'youtube', 'linkedin'],
|
|
147
|
+
targetPath: 'common.footer',
|
|
148
|
+
},
|
|
136
149
|
sections: manifest.editorSchema?.sections || [],
|
|
137
150
|
defaults: siteData.content || {},
|
|
138
151
|
};
|
|
@@ -406,7 +406,94 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
406
406
|
return `<span data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</span>`;
|
|
407
407
|
});
|
|
408
408
|
|
|
409
|
-
// 9.
|
|
409
|
+
// 9. Adaptive Action-Link Splitter & Social Media Handler (100% Fivora Contract Safe)
|
|
410
|
+
// 9a. WhatsApp & Direct Communication Links
|
|
411
|
+
const waRegex = /<a(\s+[^>]*?)href="([^"]*(?:wa\.me|whatsapp)[^"]*)"([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
412
|
+
code = code.replace(waRegex, (match, preHref = '', href, postHref = '', innerContent) => {
|
|
413
|
+
if (preHref.includes('data-preview-field-path') || postHref.includes('data-preview-field-path')) {
|
|
414
|
+
return match;
|
|
415
|
+
}
|
|
416
|
+
const urlKey = 'whatsappCtaUrl';
|
|
417
|
+
const labelKey = 'whatsappCtaLabel';
|
|
418
|
+
extractedData[urlKey] = href;
|
|
419
|
+
|
|
420
|
+
// Extract text from innerContent
|
|
421
|
+
const textMatch = innerContent.match(/>([^<>{}]+)</) || [null, innerContent.replace(/<[^>]+>/g, '').trim()];
|
|
422
|
+
const text = (textMatch[1] || 'Order via WhatsApp').trim();
|
|
423
|
+
extractedData[labelKey] = text;
|
|
424
|
+
|
|
425
|
+
elementCount += 2;
|
|
426
|
+
fileModified = true;
|
|
427
|
+
|
|
428
|
+
// Wrap inner label cleanly in span if not already wrapped
|
|
429
|
+
let updatedInner = innerContent;
|
|
430
|
+
if (/<span(\s+[^>]*)?>([^<>{}]+)<\/span>/i.test(innerContent)) {
|
|
431
|
+
updatedInner = innerContent.replace(
|
|
432
|
+
/<span(\s+[^>]*)?>([^<>{}]+)<\/span>/i,
|
|
433
|
+
`<span data-preview-field-path="${pageKey}.${labelKey}"$1>{siteData?.content?.${pageKey}?.${labelKey} || ${JSON.stringify(text)}}</span>`
|
|
434
|
+
);
|
|
435
|
+
} else if (text && innerContent.includes(text)) {
|
|
436
|
+
updatedInner = innerContent.replace(
|
|
437
|
+
text,
|
|
438
|
+
`<span data-preview-field-path="${pageKey}.${labelKey}">{siteData?.content?.${pageKey}?.${labelKey} || ${JSON.stringify(text)}}</span>`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return `<a${preHref}href={siteData?.content?.${pageKey}?.${urlKey} || ${JSON.stringify(href)}} data-preview-field-path="${pageKey}.${urlKey}"${postHref}>${updatedInner}</a>`;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// 9b. Social Media Icon Links (Instagram, Facebook, TikTok, Twitter/X, YouTube, LinkedIn)
|
|
446
|
+
const socialRegex = /<a(\s+[^>]*?)href="([^"]*(?:instagram|facebook|tiktok|twitter|youtube|linkedin)\.com[^"]*)"([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
447
|
+
code = code.replace(socialRegex, (match, preHref = '', href, postHref = '', innerContent) => {
|
|
448
|
+
if (preHref.includes('data-preview-field-path') || postHref.includes('data-preview-field-path')) {
|
|
449
|
+
return match;
|
|
450
|
+
}
|
|
451
|
+
const platMatch = href.match(/(instagram|facebook|tiktok|twitter|youtube|linkedin)\.com/i);
|
|
452
|
+
const platform = platMatch ? platMatch[1].toLowerCase() : 'social';
|
|
453
|
+
const fieldKey = `${platform}Url`;
|
|
454
|
+
|
|
455
|
+
extractedData[fieldKey] = href;
|
|
456
|
+
elementCount++;
|
|
457
|
+
fileModified = true;
|
|
458
|
+
|
|
459
|
+
return `<a${preHref}href={siteData?.content?.common?.footer?.${fieldKey} || siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(href)}} data-preview-field-path="common.footer.${fieldKey}"${postHref}>${innerContent}</a>`;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// 9c. Button / CTA Action Links with Text (Split URL on <a> and Label on <span>)
|
|
463
|
+
const btnLinkRegex = /<a(\s+[^>]*?class(?:Name)?="[^"]*(?:btn|button|cta|action|rounded|bg-)[^"]*"[^>]*?)href="([^"]+)"([^>]*)>([\s\S]*?)<\/a>/gi;
|
|
464
|
+
code = code.replace(btnLinkRegex, (match, preHref = '', href, postHref = '', innerContent) => {
|
|
465
|
+
if (preHref.includes('data-preview-field-path') || postHref.includes('data-preview-field-path') || href.startsWith('#') || href.includes('javascript:')) {
|
|
466
|
+
return match;
|
|
467
|
+
}
|
|
468
|
+
const textOnly = innerContent.replace(/<[^>]+>/g, '').trim();
|
|
469
|
+
if (!textOnly || textOnly.length < 2) return match;
|
|
470
|
+
|
|
471
|
+
const rawUrlKey = toFieldKey(textOnly, 'ctaUrl', elementCount + 1);
|
|
472
|
+
const urlKey = getUniqueKey(rawUrlKey);
|
|
473
|
+
const labelKey = urlKey.replace(/Url$/, 'Label') || getUniqueKey('ctaLabel');
|
|
474
|
+
|
|
475
|
+
extractedData[urlKey] = href;
|
|
476
|
+
extractedData[labelKey] = textOnly;
|
|
477
|
+
elementCount += 2;
|
|
478
|
+
fileModified = true;
|
|
479
|
+
|
|
480
|
+
let updatedInner = innerContent;
|
|
481
|
+
if (/<span(\s+[^>]*)?>([^<>{}]+)<\/span>/i.test(innerContent)) {
|
|
482
|
+
updatedInner = innerContent.replace(
|
|
483
|
+
/<span(\s+[^>]*)?>([^<>{}]+)<\/span>/i,
|
|
484
|
+
`<span data-preview-field-path="${pageKey}.${labelKey}"$1>{siteData?.content?.${pageKey}?.${labelKey} || ${JSON.stringify(textOnly)}}</span>`
|
|
485
|
+
);
|
|
486
|
+
} else {
|
|
487
|
+
updatedInner = innerContent.replace(
|
|
488
|
+
textOnly,
|
|
489
|
+
`<span data-preview-field-path="${pageKey}.${labelKey}">{siteData?.content?.${pageKey}?.${labelKey} || ${JSON.stringify(textOnly)}}</span>`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return `<a${preHref}href={siteData?.content?.${pageKey}?.${urlKey} || ${JSON.stringify(href)}} data-preview-field-path="${pageKey}.${urlKey}"${postHref}>${updatedInner}</a>`;
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// 9d. Standard Anchor Links with Plain Text
|
|
410
497
|
const linkRegex = /<a(\s+[^>]*)?>([^<>{}]+)<\/a>/g;
|
|
411
498
|
code = code.replace(linkRegex, (match, attrs = '', text) => {
|
|
412
499
|
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
@@ -651,16 +738,32 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
|
|
|
651
738
|
// If active recipe has defined sections, seed them
|
|
652
739
|
if (activeRecipe && Array.isArray(activeRecipe.sections)) {
|
|
653
740
|
for (const sec of activeRecipe.sections) {
|
|
654
|
-
if (sec.id === 'common')
|
|
741
|
+
if (sec.id === 'common') {
|
|
742
|
+
const commonSec = editorSections.find((s) => s.id === 'common');
|
|
743
|
+
if (commonSec && Array.isArray(sec.fields)) {
|
|
744
|
+
for (const rf of sec.fields) {
|
|
745
|
+
if (!commonSec.fields.some((f) => f.key === rf.key)) {
|
|
746
|
+
commonSec.fields.push(rf);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
655
752
|
editorSections.push(sec);
|
|
656
753
|
}
|
|
657
754
|
}
|
|
658
755
|
|
|
659
|
-
// Seed recipe defaults if available
|
|
756
|
+
// Seed recipe defaults if available with deep merge for common/footer
|
|
660
757
|
if (activeRecipe && activeRecipe.defaults) {
|
|
661
758
|
for (const [secKey, secVal] of Object.entries(activeRecipe.defaults)) {
|
|
662
|
-
if (!content[secKey])
|
|
663
|
-
|
|
759
|
+
if (!content[secKey]) {
|
|
760
|
+
content[secKey] = JSON.parse(JSON.stringify(secVal));
|
|
761
|
+
} else if (typeof secVal === 'object' && !Array.isArray(secVal)) {
|
|
762
|
+
content[secKey] = { ...secVal, ...content[secKey] };
|
|
763
|
+
if (secVal.footer && content[secKey].footer) {
|
|
764
|
+
content[secKey].footer = { ...secVal.footer, ...content[secKey].footer };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
664
767
|
}
|
|
665
768
|
}
|
|
666
769
|
|