@deneb-ui/cli 2.0.49 → 2.0.51

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
@@ -233,8 +233,15 @@ function detectPages(projectDir) {
233
233
  }
234
234
  }
235
235
 
236
- const hasContact = pages.some((p) => p.id === 'contact' || p.route === '/contact');
237
- if (!hasContact) {
236
+ const contactExistsOnDisk = candidateDirs.some((cDir) =>
237
+ fs.existsSync(path.join(cDir, 'contact.tsx')) ||
238
+ fs.existsSync(path.join(cDir, 'contact.jsx')) ||
239
+ fs.existsSync(path.join(cDir, 'contact.js')) ||
240
+ fs.existsSync(path.join(cDir, 'contact', 'page.tsx')) ||
241
+ fs.existsSync(path.join(cDir, 'contact', 'page.jsx')) ||
242
+ fs.existsSync(path.join(cDir, 'contact', 'page.js'))
243
+ );
244
+ if (contactExistsOnDisk && !pages.some((p) => p.id === 'contact' || p.route === '/contact')) {
238
245
  pages.push({ id: 'contact', label: 'Contact', route: '/contact', required: true });
239
246
  }
240
247
 
@@ -597,7 +604,7 @@ function getComponentRegistry(importPkg) {
597
604
  };
598
605
  }
599
606
 
600
- function initProject(targetInput, options = {}) {
607
+ async function initProject(targetInput, options = {}) {
601
608
  const targetDir = path.resolve(process.cwd(), targetInput || '.');
602
609
  const pkgPath = path.join(targetDir, 'package.json');
603
610
 
@@ -645,7 +652,7 @@ function initProject(targetInput, options = {}) {
645
652
  conversionRes = runUniversalTemplateConversion(targetDir, projectName, detectedPages, options);
646
653
  } else {
647
654
  const { runDenebArc } = require('../src/arc/index.cjs');
648
- conversionRes = runDenebArc(targetDir, projectName, {
655
+ conversionRes = await runDenebArc(targetDir, projectName, {
649
656
  ...options,
650
657
  detectedPages,
651
658
  });
@@ -1048,7 +1055,10 @@ if (command === 'init') {
1048
1055
  targetInput = arg;
1049
1056
  }
1050
1057
  }
1051
- initProject(targetInput, { recipeName, dryRun, explain, legacy, telemetry, aiEnabled, aiDryRun });
1058
+ initProject(targetInput, { recipeName, dryRun, explain, legacy, telemetry, aiEnabled, aiDryRun }).catch((err) => {
1059
+ console.error(`\x1b[31mError:\x1b[0m ${err.message}`);
1060
+ process.exit(1);
1061
+ });
1052
1062
  } else if (command === 'create') {
1053
1063
  createTemplate(commandArgs[0]);
1054
1064
  } else if (command === 'add') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.49",
3
+ "version": "2.0.51",
4
4
  "description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -49,7 +49,7 @@
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
51
  "@babel/parser": "^7.28.0",
52
- "@deneb-ui/core": "^2.0.49",
52
+ "@deneb-ui/core": "^2.0.51",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -16,6 +16,8 @@ const { runDenebArc } = require('../index.cjs');
16
16
  const { parseSource } = require('../ast.cjs');
17
17
  const { loadFingerprintBoost } = require('../learning.cjs');
18
18
  const { classifyActionIntent } = require('../adapters.cjs');
19
+ const { validateGeneratedCode } = require('../ai-agent.cjs');
20
+ const { auditRuntimeIntegrity, healRuntimeIntegrity, runAiEvaluatorPipeline } = require('../ai-evaluator.cjs');
19
21
 
20
22
  test('field-paths recognizes list action CTA keys', () => {
21
23
  assert.equal(isListActionCtaKey('preOrderCta'), true);
@@ -727,7 +729,12 @@ test('classifyActionIntent identifies action keywords and assigns correct fallba
727
729
  assert.equal(shop?.defaultUrl, '/shop');
728
730
  assert.equal(shop?.external, false);
729
731
 
730
- const nonAction = classifyActionIntent('Submit Form', null);
732
+ const formSubmit = classifyActionIntent('Submit Form', null);
733
+ assert.equal(formSubmit?.action, 'form-submit');
734
+ assert.equal(formSubmit?.defaultUrl, 'https://wa.me/1234567890');
735
+ assert.equal(formSubmit?.external, true);
736
+
737
+ const nonAction = classifyActionIntent('Random Content Text', null);
731
738
  assert.equal(nonAction, null);
732
739
  });
733
740
 
@@ -875,3 +882,460 @@ export function ActionPage() {
875
882
  assert.deepEqual(errors, []);
876
883
  });
877
884
 
885
+ test('parseSource parses TypeScript interface declarations without experimental syntax errors', () => {
886
+ const tsCode = `
887
+ import React from 'react';
888
+ import { EditableText } from './EditableText';
889
+
890
+ export interface EditableTradeinSectionProps {
891
+ itemPath: string;
892
+ title: string;
893
+ }
894
+
895
+ export function EditableTradeinSection({ itemPath, title }: EditableTradeinSectionProps) {
896
+ return (
897
+ <div data-preview-item-path={itemPath}>
898
+ <EditableText as="h2" id={\`\${itemPath}.title\`} data-preview-field-path={\`\${itemPath}.title\`} defaultValue={title} />
899
+ </div>
900
+ );
901
+ }
902
+ `;
903
+ assert.doesNotThrow(() => {
904
+ parseSource(tsCode, 'EditableTradeinSection.tsx');
905
+ });
906
+ });
907
+
908
+ test('validateGeneratedCode catches data-preview-field-path placed on broad <div> containers', () => {
909
+ const invalidCode = `
910
+ import React from 'react';
911
+ import { EditableText } from './EditableText';
912
+
913
+ export interface EditableCardProps {
914
+ itemPath: string;
915
+ }
916
+
917
+ export function EditableCard({ itemPath }: EditableCardProps) {
918
+ return (
919
+ <div data-preview-item-path={itemPath}>
920
+ <div data-preview-field-path={\`\${itemPath}.content\`}>Some broad content</div>
921
+ </div>
922
+ );
923
+ }
924
+ `;
925
+ const res = validateGeneratedCode(invalidCode, 'EditableCard.tsx');
926
+ assert.equal(res.passed, false);
927
+ assert.ok(res.errors.some((e) => e.includes('cannot be placed on broad <div> content containers')));
928
+ });
929
+
930
+ test('AST transformer wraps literal text inside <div> with <span> instead of placing field path on <div>', () => {
931
+ const code = `
932
+ export function Card() {
933
+ return (
934
+ <div className="card">
935
+ <div className="title-row">In-Store VIP Lab</div>
936
+ </div>
937
+ );
938
+ }
939
+ `;
940
+ const profile = {
941
+ root: os.tmpdir(),
942
+ framework: 'nextjs',
943
+ router: 'next-app',
944
+ language: 'typescript',
945
+ cssSystems: ['tailwind'],
946
+ hasSrc: true,
947
+ aliasMap: { '@/*': ['src/*'] },
948
+ };
949
+
950
+ const analysis = analyzeFile({
951
+ code,
952
+ relativeFile: 'src/components/Card.tsx',
953
+ profile,
954
+ graph: { sharedFiles: [] },
955
+ ownerScope: 'home',
956
+ componentMeta: { name: 'Card', role: 'card' },
957
+ });
958
+ analysis.code = code;
959
+ analysis.relativeFile = 'src/components/Card.tsx';
960
+
961
+ const plan = planTransformations({ profile, analyses: [analysis] });
962
+ const res = applyFilePlan(plan.files[0], profile);
963
+ assert.equal(res.changed, true);
964
+ // The outer <div> must NOT have data-preview-field-path
965
+ assert.doesNotMatch(res.code, /<div[^>]*data-preview-field-path/);
966
+ // The inner text must be wrapped in a <span> with data-preview-field-path
967
+ assert.match(res.code, /<span[^>]*data-preview-field-path=/);
968
+ });
969
+
970
+ test('AI Evaluator audits and heals RSC duplicate SiteDataProvider in layout.tsx', async () => {
971
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-eval-'));
972
+ const appDir = path.join(tmp, 'src', 'app');
973
+ fs.mkdirSync(appDir, { recursive: true });
974
+
975
+ const brokenLayout = `
976
+ import { SiteDataProvider } from '@deneb-ui/ui';
977
+ import { Providers } from '@/components/providers';
978
+ import initialSiteData from '@/data/site-data.json';
979
+
980
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
981
+ return (
982
+ <html lang="en">
983
+ <body>
984
+ <Providers>
985
+ <SiteDataProvider initialSiteData={initialSiteData}>
986
+ {children}
987
+ </SiteDataProvider>
988
+ </Providers>
989
+ </body>
990
+ </html>
991
+ );
992
+ }
993
+ `;
994
+ fs.writeFileSync(path.join(appDir, 'layout.tsx'), brokenLayout, 'utf8');
995
+
996
+ const profile = {
997
+ root: tmp,
998
+ framework: 'nextjs',
999
+ router: 'next-app',
1000
+ appDir: 'src/app',
1001
+ language: 'typescript',
1002
+ jsxFiles: ['src/app/layout.tsx'],
1003
+ };
1004
+
1005
+ const issues = auditRuntimeIntegrity(tmp, profile);
1006
+ assert.equal(issues.length, 1);
1007
+ assert.equal(issues[0].type, 'rsc-duplicate-provider');
1008
+
1009
+ const healRes = await healRuntimeIntegrity(tmp, profile, issues);
1010
+ assert.equal(healRes.healedCount, 1);
1011
+ assert.equal(healRes.remainingCount, 0);
1012
+
1013
+ const fixed = fs.readFileSync(path.join(appDir, 'layout.tsx'), 'utf8');
1014
+ assert.doesNotMatch(fixed, /<SiteDataProvider/);
1015
+ assert.match(fixed, /<Providers>\s*\{children\}\s*<\/Providers>/);
1016
+
1017
+ fs.rmSync(tmp, { recursive: true, force: true });
1018
+ });
1019
+
1020
+ test('AI Evaluator detects missing component exports in page.tsx imports', () => {
1021
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-eval-exp-'));
1022
+ const appDir = path.join(tmp, 'src', 'app');
1023
+ const compDir = path.join(tmp, 'src', 'components');
1024
+ fs.mkdirSync(appDir, { recursive: true });
1025
+ fs.mkdirSync(compDir, { recursive: true });
1026
+
1027
+ fs.writeFileSync(
1028
+ path.join(appDir, 'page.tsx'),
1029
+ `import { ProductList, MissingHero } from '@/components/Widgets';\nexport default function Page() { return <div><ProductList /></div>; }`,
1030
+ 'utf8'
1031
+ );
1032
+
1033
+ fs.writeFileSync(
1034
+ path.join(compDir, 'Widgets.tsx'),
1035
+ `export function ProductList() { return <div>Products</div>; }`,
1036
+ 'utf8'
1037
+ );
1038
+
1039
+ const profile = {
1040
+ root: tmp,
1041
+ framework: 'nextjs',
1042
+ router: 'next-app',
1043
+ appDir: 'src/app',
1044
+ language: 'typescript',
1045
+ jsxFiles: ['src/app/page.tsx', 'src/components/Widgets.tsx'],
1046
+ };
1047
+
1048
+ const issues = auditRuntimeIntegrity(tmp, profile);
1049
+ assert.equal(issues.length, 1);
1050
+ assert.equal(issues[0].type, 'missing-named-export');
1051
+ assert.equal(issues[0].meta?.componentName, 'MissingHero');
1052
+
1053
+ fs.rmSync(tmp, { recursive: true, force: true });
1054
+ });
1055
+
1056
+ test('AI Evaluator runAiEvaluatorPipeline passes on clean valid project', async () => {
1057
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-eval-clean-'));
1058
+ const appDir = path.join(tmp, 'src', 'app');
1059
+ fs.mkdirSync(appDir, { recursive: true });
1060
+
1061
+ fs.writeFileSync(
1062
+ path.join(appDir, 'layout.tsx'),
1063
+ `import { Providers } from '@/components/providers';\nexport default function RootLayout({ children }: { children: React.ReactNode }) { return <html><body><Providers>{children}</Providers></body></html>; }`,
1064
+ 'utf8'
1065
+ );
1066
+
1067
+ const profile = {
1068
+ root: tmp,
1069
+ framework: 'nextjs',
1070
+ router: 'next-app',
1071
+ appDir: 'src/app',
1072
+ language: 'typescript',
1073
+ jsxFiles: ['src/app/layout.tsx'],
1074
+ };
1075
+
1076
+ const res = await runAiEvaluatorPipeline(tmp, profile);
1077
+ assert.equal(res.passed, true);
1078
+ assert.equal(res.issuesFound, 0);
1079
+
1080
+ fs.rmSync(tmp, { recursive: true, force: true });
1081
+ });
1082
+
1083
+ test('AI Evaluator audits and heals missing global CSS stylesheet import in layout.tsx', async () => {
1084
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-eval-css-'));
1085
+ const appDir = path.join(tmp, 'src', 'app');
1086
+ fs.mkdirSync(appDir, { recursive: true });
1087
+ fs.writeFileSync(path.join(appDir, 'globals.css'), '@import "tailwindcss";', 'utf8');
1088
+
1089
+ const unstyledLayout = `
1090
+ import { Providers } from '@/components/providers';
1091
+
1092
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1093
+ return <html><body><Providers>{children}</Providers></body></html>;
1094
+ }
1095
+ `;
1096
+ fs.writeFileSync(path.join(appDir, 'layout.tsx'), unstyledLayout, 'utf8');
1097
+
1098
+ const profile = {
1099
+ root: tmp,
1100
+ framework: 'nextjs',
1101
+ router: 'next-app',
1102
+ appDir: 'src/app',
1103
+ language: 'typescript',
1104
+ jsxFiles: ['src/app/layout.tsx'],
1105
+ };
1106
+
1107
+ const issues = auditRuntimeIntegrity(tmp, profile);
1108
+ assert.equal(issues.length, 1);
1109
+ assert.equal(issues[0].type, 'missing-global-css-import');
1110
+
1111
+ const healRes = await healRuntimeIntegrity(tmp, profile, issues);
1112
+ assert.equal(healRes.healedCount, 1);
1113
+
1114
+ const fixed = fs.readFileSync(path.join(appDir, 'layout.tsx'), 'utf8');
1115
+ assert.match(fixed, /import\s+['"]\.\/globals\.css['"]/);
1116
+
1117
+ fs.rmSync(tmp, { recursive: true, force: true });
1118
+ });
1119
+
1120
+ test('collections with nested arrays, objects, and TS as const convert to editable list contracts', () => {
1121
+ const code = `
1122
+ const phones = [
1123
+ {
1124
+ name: 'iPhone 16 Pro Max',
1125
+ brand: 'Apple',
1126
+ subtitle: 'Grade 5 Titanium' as const,
1127
+ price: 1199,
1128
+ storageOptions: ['256GB', '512GB', '1TB'],
1129
+ colors: [{ name: 'Desert Titanium', hex: '#bba795' }],
1130
+ },
1131
+ {
1132
+ name: 'Galaxy S25 Ultra',
1133
+ brand: 'Samsung',
1134
+ subtitle: 'Armor Titanium' as const,
1135
+ price: 1299,
1136
+ storageOptions: ['256GB', '512GB'],
1137
+ colors: [{ name: 'Titanium Gray', hex: '#5e6166' }],
1138
+ },
1139
+ ];
1140
+
1141
+ export function FeaturedPhones() {
1142
+ return (
1143
+ <div className="phones-grid">
1144
+ {phones.map((phone, idx) => (
1145
+ <div key={phone.name} className="phone-card">
1146
+ <h4>{phone.brand}</h4>
1147
+ <h3>{phone.name}</h3>
1148
+ <p>{phone.subtitle}</p>
1149
+ <span>Rs {phone.price}</span>
1150
+ </div>
1151
+ ))}
1152
+ </div>
1153
+ );
1154
+ }
1155
+ `;
1156
+
1157
+ const profile = { framework: 'nextjs', router: 'next-app', appDir: 'src/app', language: 'typescript' };
1158
+ const analysis = analyzeFile({
1159
+ code,
1160
+ relativeFile: 'src/components/FeaturedPhones.tsx',
1161
+ profile,
1162
+ graph: { sharedFiles: [] },
1163
+ ownerScope: 'home',
1164
+ componentMeta: { name: 'FeaturedPhones', role: 'shop' },
1165
+ });
1166
+
1167
+ const collectionCandidate = analysis.candidates.find((c) => c.kind === 'collection');
1168
+ assert.ok(collectionCandidate, 'collection with nested arrays and TS as const must be detected');
1169
+ assert.equal(collectionCandidate.extra.objectItems, true);
1170
+ assert.ok(collectionCandidate.confidence >= 0.8, 'collection must have high confidence');
1171
+
1172
+ const plan = planTransformations({
1173
+ profile,
1174
+ analyses: [analysis],
1175
+ });
1176
+
1177
+ assert.equal(plan.files.length, 1);
1178
+ const collectionTransform = plan.files[0].transformations.find((t) => t.fieldType === 'list');
1179
+ assert.ok(collectionTransform, 'collection must be planned as list field');
1180
+ assert.equal(collectionTransform.listField, 'home.phones');
1181
+ assert.ok(collectionTransform.itemFields.some((f) => f.key === 'name'));
1182
+ assert.ok(collectionTransform.itemFields.some((f) => f.key === 'price'));
1183
+ });
1184
+
1185
+ test('proceed to order button is recognized as WhatsApp order split-action contract', () => {
1186
+ const code = `
1187
+ export function CartDrawer() {
1188
+ return (
1189
+ <div className="cart-footer">
1190
+ <button type="button" className="btn-primary w-full py-4 font-bold">
1191
+ Proceed to Order · RS 1299
1192
+ </button>
1193
+ </div>
1194
+ );
1195
+ }
1196
+ `;
1197
+
1198
+ const profile = { framework: 'nextjs', router: 'next-app', appDir: 'src/app', language: 'typescript' };
1199
+ const analysis = analyzeFile({
1200
+ code,
1201
+ relativeFile: 'src/components/CartDrawer.tsx',
1202
+ profile,
1203
+ graph: { sharedFiles: [] },
1204
+ ownerScope: 'home',
1205
+ componentMeta: { name: 'CartDrawer', role: 'cart' },
1206
+ });
1207
+
1208
+ const actionCandidate = analysis.candidates.find((c) => c.kind === 'split-action-contract');
1209
+ assert.ok(actionCandidate, 'proceed to order must be recognized as split-action-contract');
1210
+ assert.equal(actionCandidate.extra.action, 'whatsapp');
1211
+
1212
+ const plan = planTransformations({
1213
+ profile,
1214
+ analyses: [analysis],
1215
+ });
1216
+
1217
+ const filePlan = plan.files[0];
1218
+ const splitAction = filePlan.transformations.find((t) => t.operation === 'split-action-contract');
1219
+ assert.ok(splitAction, 'split action must be planned');
1220
+ assert.match(splitAction.urlField, /whatsapp/i);
1221
+ });
1222
+
1223
+ test('classifyActionIntent recognizes form submit action keywords', () => {
1224
+ const submit = classifyActionIntent('Submit', null);
1225
+ assert.equal(submit?.action, 'form-submit');
1226
+ assert.equal(submit?.defaultUrl, 'https://wa.me/1234567890');
1227
+
1228
+ const sendMessage = classifyActionIntent('Send Message', null);
1229
+ assert.equal(sendMessage?.action, 'form-submit');
1230
+
1231
+ const confirmBooking = classifyActionIntent('Confirm Booking', null);
1232
+ assert.equal(confirmBooking?.action, 'form-submit');
1233
+
1234
+ const getQuote = classifyActionIntent('Get Quote', null);
1235
+ assert.equal(getQuote?.action, 'form-submit');
1236
+ });
1237
+
1238
+ test('semantic engine recognizes form inputs, labels, placeholders, and form-submit-action buttons', () => {
1239
+ const code = `
1240
+ export function BookingForm() {
1241
+ return (
1242
+ <form className="booking-form">
1243
+ <h2>Book Appointment</h2>
1244
+ <label>Full Name</label>
1245
+ <input type="text" placeholder="Enter your full name" />
1246
+ <label>Email Address</label>
1247
+ <input type="email" placeholder="you@example.com" />
1248
+ <label>Special Instructions</label>
1249
+ <textarea placeholder="Describe your request..." />
1250
+ <button type="submit">Confirm Booking</button>
1251
+ </form>
1252
+ );
1253
+ }
1254
+ `;
1255
+ const profile = {
1256
+ root: os.tmpdir(),
1257
+ framework: 'nextjs',
1258
+ router: 'next-app',
1259
+ language: 'typescript',
1260
+ cssSystems: ['tailwind'],
1261
+ hasSrc: true,
1262
+ };
1263
+
1264
+ const analysis = analyzeFile({
1265
+ code,
1266
+ relativeFile: 'src/components/BookingForm.tsx',
1267
+ profile,
1268
+ graph: { sharedFiles: [] },
1269
+ ownerScope: 'home',
1270
+ componentMeta: { name: 'BookingForm', role: 'form' },
1271
+ });
1272
+
1273
+ const placeholders = analysis.candidates.filter((c) => c.kind === 'placeholder');
1274
+ assert.equal(placeholders.length, 3, 'expected 3 placeholder candidates');
1275
+
1276
+ const labels = analysis.candidates.filter((c) => c.kind === 'text' && c.tag === 'label');
1277
+ assert.equal(labels.length, 3, 'expected 3 label candidates');
1278
+
1279
+ const submitAction = analysis.candidates.find((c) => c.kind === 'form-submit-action');
1280
+ assert.ok(submitAction, 'expected form-submit-action candidate');
1281
+ assert.equal(submitAction.label, 'Confirm Booking');
1282
+ assert.equal(submitAction.extra.action, 'form-submit');
1283
+ assert.equal(submitAction.extra.insideForm, true);
1284
+ });
1285
+
1286
+ test('planner and transformer make entire form editable with submit button and WhatsApp URL binding', () => {
1287
+ const code = `
1288
+ import React from 'react';
1289
+
1290
+ export function ContactSection() {
1291
+ return (
1292
+ <form className="contact-form">
1293
+ <label>Your Name</label>
1294
+ <input type="text" placeholder="John Doe" />
1295
+ <button type="submit">Send Message</button>
1296
+ </form>
1297
+ );
1298
+ }
1299
+ `;
1300
+ const profile = {
1301
+ root: os.tmpdir(),
1302
+ framework: 'nextjs',
1303
+ router: 'next-app',
1304
+ language: 'typescript',
1305
+ cssSystems: ['tailwind'],
1306
+ hasSrc: true,
1307
+ aliasMap: { '@/*': ['src/*'] },
1308
+ };
1309
+
1310
+ const analysis = analyzeFile({
1311
+ code,
1312
+ relativeFile: 'src/components/ContactSection.tsx',
1313
+ profile,
1314
+ graph: { sharedFiles: [] },
1315
+ ownerScope: 'home',
1316
+ componentMeta: { name: 'ContactSection', role: 'contact' },
1317
+ });
1318
+ analysis.code = code;
1319
+ analysis.relativeFile = 'src/components/ContactSection.tsx';
1320
+
1321
+ const plan = planTransformations({
1322
+ profile,
1323
+ analyses: [analysis],
1324
+ });
1325
+
1326
+ const filePlan = plan.files[0];
1327
+ const formSubmit = filePlan.transformations.find((t) => t.operation === 'form-submit-action');
1328
+ assert.ok(formSubmit, 'form-submit-action must be planned');
1329
+ assert.match(formSubmit.urlField, /formWhatsappUrl/i);
1330
+ assert.match(formSubmit.labelField, /formSubmitLabel/i);
1331
+
1332
+ const transformed = applyFilePlan(filePlan, profile);
1333
+ assert.ok(transformed.changed, 'file must be changed');
1334
+ assert.match(transformed.code, /data-preview-field-path/);
1335
+ assert.match(transformed.code, /formSubmitLabel/);
1336
+ assert.match(transformed.code, /formWhatsappUrl/);
1337
+ assert.match(transformed.code, /type="submit"/);
1338
+ assert.match(transformed.code, /hidden/);
1339
+ });
1340
+
1341
+
@@ -23,6 +23,8 @@ const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'Heading', 'Ti
23
23
  const TEXT_TAGS = new Set(['p', 'span', 'li', 'blockquote', 'figcaption', 'label', 'CardDescription', 'Description', 'Subtitle', 'Typography', 'Text', 'Badge', 'badge']);
24
24
  const ACTION_TAGS = new Set(['a', 'Link', 'NavLink', 'Button', 'button', 'IconButton', 'NavbarBrand']);
25
25
  const IMAGE_TAGS = new Set(['img', 'Image', 'Img', 'BackgroundImage']);
26
+ const FORM_INPUT_TAGS = new Set(['input', 'Input', 'textarea', 'Textarea', 'select', 'Select']);
27
+ const FORM_CONTAINER_TAGS = new Set(['form', 'Form']);
26
28
 
27
29
  const shadcn = createAdapter('shadcn', {
28
30
  detect: (project) => detectFromProfile(project, 'shadcn') || project.shadcn,
@@ -106,6 +108,9 @@ const deneb = createAdapter('deneb', {
106
108
  Boolean(project.dependencies && project.dependencies['@deneb-ui/ui']),
107
109
  recognizeNode(node) {
108
110
  const name = getJsxName(node);
111
+ if (['EditableContactForm', 'ContactForm'].includes(name)) {
112
+ return { library: 'deneb', kind: 'form', tag: name };
113
+ }
109
114
  if (['EditableGoogleFeedback', 'EditableCustomerReviews', 'CustomerReviews', 'GoogleFeedback'].includes(name)) {
110
115
  return { library: 'deneb', kind: 'feedback', tag: name };
111
116
  }
@@ -192,8 +197,24 @@ function classifyActionIntent(text, href) {
192
197
  const t = String(text || '').trim().toLowerCase();
193
198
  const h = String(href || '').trim().toLowerCase();
194
199
 
200
+ // 0. Form Submit / WhatsApp Form Action
201
+ if (
202
+ /^(submit|send\s*message|send\s*inquiry|confirm\s*booking|place\s*order|book\s*now|confirm|get\s*quote|request\s*quote|send\s*request|schedule|register|sign\s*up|subscribe|apply|enroll)$/i.test(t) ||
203
+ /\b(?:send\s*message|confirm\s*booking|submit\s*form|submit\s*inquiry|send\s*inquiry|request\s*quote|get\s*quote|send\s*request)\b/i.test(t)
204
+ ) {
205
+ return {
206
+ action: 'form-submit',
207
+ defaultUrl: /^https?:\/\//i.test(h) && !h.includes('#') ? href : 'https://wa.me/1234567890',
208
+ external: true,
209
+ };
210
+ }
211
+
195
212
  // 1. WhatsApp
196
- if (/wa\.me|whatsapp/i.test(h) || /\b(?:whatsapp|wa\.me)\b/i.test(t) || /order on whatsapp|chat on whatsapp|message on whatsapp/i.test(t)) {
213
+ if (
214
+ /wa\.me|whatsapp/i.test(h) ||
215
+ /\b(?:whatsapp|wa\.me)\b/i.test(t) ||
216
+ /order on whatsapp|chat on whatsapp|message on whatsapp|whatsapp order|order via whatsapp|proceed to order|proceed to checkout|complete order|confirm priority repair|book repair|schedule repair|confirm repair/i.test(t)
217
+ ) {
197
218
  return {
198
219
  action: 'whatsapp',
199
220
  defaultUrl: /^https?:\/\//i.test(h) && !h.includes('#') ? href : 'https://wa.me/1234567890',
@@ -276,6 +297,8 @@ module.exports = {
276
297
  TEXT_TAGS,
277
298
  ACTION_TAGS,
278
299
  IMAGE_TAGS,
300
+ FORM_INPUT_TAGS,
301
+ FORM_CONTAINER_TAGS,
279
302
  collectJsxText,
280
303
  getJsxAttributeLiteral,
281
304
  };
@@ -167,6 +167,12 @@ function validateGeneratedCode(code, filename) {
167
167
  errors.push('Missing data-preview-field-path markers — the component has no editable fields.');
168
168
  }
169
169
 
170
+ // 2b. data-preview-field-path cannot be placed on broad containers
171
+ const broadMatch = code.match(/<(div|section|article|aside|header|footer|nav|main|form|ul|ol|table|thead|tbody|tr)\b[^>]*\bdata-preview-field-path\s*=/i);
172
+ if (broadMatch) {
173
+ errors.push(`data-preview-field-path cannot be placed on broad <${broadMatch[1]}> content containers. Put the marker on the exact visible text, media, link, or control element (like <span>, <p>, <h1>, <a>, <button>, or <EditableText>).`);
174
+ }
175
+
170
176
  // 3. Must have data-preview-item-path on the root wrapper
171
177
  if (!code.includes('data-preview-item-path')) {
172
178
  errors.push('Missing data-preview-item-path on root wrapper element.');