@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.12",
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": "vanta-shoes-store",
4
- "description": "Learned and saved recipe extracted from shoes_shop.",
5
- "savedAt": "2026-09-08T15:46:21.569Z",
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. Anchor Links: <a> (sensitive attribute handler)
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') continue;
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]) content[secKey] = {};
663
- Object.assign(content[secKey], secVal);
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