@deneb-ui/cli 2.0.50 → 2.0.52
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 +12 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +219 -10
- package/src/arc/adapters.cjs +19 -0
- package/src/arc/field-paths.cjs +4 -0
- package/src/arc/fivora-contract.cjs +266 -5
- package/src/arc/index.cjs +129 -30
- package/src/arc/learning.cjs +20 -1
- package/src/arc/manifest.cjs +89 -11
- package/src/arc/planner.cjs +10 -6
- package/src/arc/printer.cjs +9 -0
- package/src/arc/residual.cjs +400 -0
- package/src/arc/scanner.cjs +46 -2
- package/src/arc/semantic.cjs +47 -8
- package/src/arc/style-candidates.cjs +2 -1
- package/src/arc/transformer.cjs +335 -20
- package/src/arc/validator.cjs +43 -3
- package/src/arc/version.cjs +1 -1
package/bin/index.js
CHANGED
|
@@ -187,6 +187,18 @@ function detectPages(projectDir) {
|
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
189
|
if (!foundRoutes.has('/' + routeName)) {
|
|
190
|
+
const pageFile = ['page.tsx', 'page.jsx', 'page.js']
|
|
191
|
+
.map((name) => path.join(cDir, routeName, name))
|
|
192
|
+
.find((file) => fs.existsSync(file));
|
|
193
|
+
const pagesFile = ['tsx', 'jsx', 'js']
|
|
194
|
+
.map((ext) => path.join(cDir, `${routeName}.${ext}`))
|
|
195
|
+
.find((file) => fs.existsSync(file));
|
|
196
|
+
if (!pageFile && !pagesFile && path.basename(cDir) !== 'pages' && path.basename(cDir) !== 'src') {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!pageFile && path.basename(cDir) !== 'pages' && !cDir.endsWith(`${path.sep}pages`)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
190
202
|
foundRoutes.add('/' + routeName);
|
|
191
203
|
const label = routeName
|
|
192
204
|
.replace(/[-_]/g, ' ')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.52",
|
|
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.
|
|
52
|
+
"@deneb-ui/core": "^2.0.52",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -158,8 +158,8 @@ test('AST transformer preserves className and uses nullish fallbacks', () => {
|
|
|
158
158
|
assert.match(result.code, /data-preview-field-path=/);
|
|
159
159
|
assert.match(result.code, /\?\?/);
|
|
160
160
|
assert.match(result.code, /<span[\s\S]*data-preview-field-path="/);
|
|
161
|
-
assert.match(result.code, /<
|
|
162
|
-
assert.
|
|
161
|
+
assert.match(result.code, /<a\b[^>]*data-preview-field-path=/);
|
|
162
|
+
assert.doesNotMatch(result.code, /<span[^>]*hidden[^>]*data-preview-field-path/);
|
|
163
163
|
assert.doesNotMatch(result.code, /'use client'/);
|
|
164
164
|
assert.match(result.code, /site-data\.json|@\/data\/site-data\.json/);
|
|
165
165
|
assert.match(result.code, /data-preview-style-target=/);
|
|
@@ -409,12 +409,14 @@ test('static-array collections become list contracts without changing render log
|
|
|
409
409
|
assert.equal(siteData.content.home.products[0].title, 'Minimalist Smart Watch');
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
-
test('collections holding component references
|
|
412
|
+
test('collections holding component references still bind primitive item fields', () => {
|
|
413
413
|
const dir = copyOf(STOREFRONT_FIXTURE);
|
|
414
414
|
silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
|
|
415
415
|
const features = fs.readFileSync(path.join(dir, 'src', 'components', 'Features.tsx'), 'utf8');
|
|
416
|
-
assert.
|
|
416
|
+
assert.match(features, /data-preview-list-path=/);
|
|
417
417
|
assert.match(features, /icon: Truck/);
|
|
418
|
+
assert.match(features, /data-preview-static="component-ref"/);
|
|
419
|
+
assert.match(features, /data-preview-field-path=\{`[^`]*\.title`\}/);
|
|
418
420
|
});
|
|
419
421
|
|
|
420
422
|
test('literal text inside a broad container is wrapped instead of marked illegally', () => {
|
|
@@ -435,8 +437,8 @@ test('shadcn Button asChild keeps the action on the link and the label in a span
|
|
|
435
437
|
assert.match(hero, /<Button asChild>/);
|
|
436
438
|
assert.match(hero, /href=\{siteData\?\.content\?\.home\?\.hero\?\.shopCollectionUrl \?\? "\/products"\}/);
|
|
437
439
|
assert.match(hero, /<span[\s\S]*data-preview-field-path="home\.hero\.shopCollectionLabel"/);
|
|
438
|
-
// The decorative icon stays static.
|
|
439
|
-
assert.match(hero, /<ArrowRight
|
|
440
|
+
// The decorative icon stays static (may also receive data-preview-static).
|
|
441
|
+
assert.match(hero, /<ArrowRight[^>]*className="ml-2 size-4"[^>]*aria-hidden="true"/);
|
|
440
442
|
});
|
|
441
443
|
|
|
442
444
|
test('a second init run converges on identical sources and manifest', () => {
|
|
@@ -729,7 +731,12 @@ test('classifyActionIntent identifies action keywords and assigns correct fallba
|
|
|
729
731
|
assert.equal(shop?.defaultUrl, '/shop');
|
|
730
732
|
assert.equal(shop?.external, false);
|
|
731
733
|
|
|
732
|
-
const
|
|
734
|
+
const formSubmit = classifyActionIntent('Submit Form', null);
|
|
735
|
+
assert.equal(formSubmit?.action, 'form-submit');
|
|
736
|
+
assert.equal(formSubmit?.defaultUrl, 'https://wa.me/1234567890');
|
|
737
|
+
assert.equal(formSubmit?.external, true);
|
|
738
|
+
|
|
739
|
+
const nonAction = classifyActionIntent('Random Content Text', null);
|
|
733
740
|
assert.equal(nonAction, null);
|
|
734
741
|
});
|
|
735
742
|
|
|
@@ -816,11 +823,12 @@ export function ActionPanel() {
|
|
|
816
823
|
assert.match(result.code, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.hero\?\.whatsappUrl \?\? "https:\/\/wa\.me\/1234567890"\}/);
|
|
817
824
|
assert.match(result.code, /target="_blank"/);
|
|
818
825
|
assert.match(result.code, /rel="noopener noreferrer"/);
|
|
819
|
-
|
|
826
|
+
// URL field path is bound directly to outer <a> element (never hidden, never static conflict)
|
|
827
|
+
assert.match(result.code, /<a\s+[^>]*data-preview-field-path=\{?"home\.hero\.whatsappUrl"?\}/);
|
|
820
828
|
// Text label wrapped in editable span
|
|
821
829
|
assert.match(result.code, /<span\s+data-preview-field-path="home\.hero\.whatsappLabel"[^>]*>\{siteData\?\.content\?\.home\?\.hero\?\.whatsappLabel \?\? "Order on WhatsApp"\}<\/span>/);
|
|
822
|
-
//
|
|
823
|
-
assert.
|
|
830
|
+
// Never emit hidden preview markers
|
|
831
|
+
assert.doesNotMatch(result.code, /<span[^>]*hidden[^>]*data-preview-field-path/);
|
|
824
832
|
// No parse errors
|
|
825
833
|
assert.doesNotThrow(() => parseSource(result.code, 'ActionPanel.tsx'));
|
|
826
834
|
});
|
|
@@ -1215,3 +1223,204 @@ export function CartDrawer() {
|
|
|
1215
1223
|
assert.match(splitAction.urlField, /whatsapp/i);
|
|
1216
1224
|
});
|
|
1217
1225
|
|
|
1226
|
+
test('classifyActionIntent recognizes form submit action keywords', () => {
|
|
1227
|
+
const submit = classifyActionIntent('Submit', null);
|
|
1228
|
+
assert.equal(submit?.action, 'form-submit');
|
|
1229
|
+
assert.equal(submit?.defaultUrl, 'https://wa.me/1234567890');
|
|
1230
|
+
|
|
1231
|
+
const sendMessage = classifyActionIntent('Send Message', null);
|
|
1232
|
+
assert.equal(sendMessage?.action, 'form-submit');
|
|
1233
|
+
|
|
1234
|
+
const confirmBooking = classifyActionIntent('Confirm Booking', null);
|
|
1235
|
+
assert.equal(confirmBooking?.action, 'form-submit');
|
|
1236
|
+
|
|
1237
|
+
const getQuote = classifyActionIntent('Get Quote', null);
|
|
1238
|
+
assert.equal(getQuote?.action, 'form-submit');
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
test('semantic engine recognizes form inputs, labels, placeholders, and form-submit-action buttons', () => {
|
|
1242
|
+
const code = `
|
|
1243
|
+
export function BookingForm() {
|
|
1244
|
+
return (
|
|
1245
|
+
<form className="booking-form">
|
|
1246
|
+
<h2>Book Appointment</h2>
|
|
1247
|
+
<label>Full Name</label>
|
|
1248
|
+
<input type="text" placeholder="Enter your full name" />
|
|
1249
|
+
<label>Email Address</label>
|
|
1250
|
+
<input type="email" placeholder="you@example.com" />
|
|
1251
|
+
<label>Special Instructions</label>
|
|
1252
|
+
<textarea placeholder="Describe your request..." />
|
|
1253
|
+
<button type="submit">Confirm Booking</button>
|
|
1254
|
+
</form>
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
`;
|
|
1258
|
+
const profile = {
|
|
1259
|
+
root: os.tmpdir(),
|
|
1260
|
+
framework: 'nextjs',
|
|
1261
|
+
router: 'next-app',
|
|
1262
|
+
language: 'typescript',
|
|
1263
|
+
cssSystems: ['tailwind'],
|
|
1264
|
+
hasSrc: true,
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
const analysis = analyzeFile({
|
|
1268
|
+
code,
|
|
1269
|
+
relativeFile: 'src/components/BookingForm.tsx',
|
|
1270
|
+
profile,
|
|
1271
|
+
graph: { sharedFiles: [] },
|
|
1272
|
+
ownerScope: 'home',
|
|
1273
|
+
componentMeta: { name: 'BookingForm', role: 'form' },
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
const placeholders = analysis.candidates.filter((c) => c.kind === 'placeholder');
|
|
1277
|
+
assert.equal(placeholders.length, 3, 'expected 3 placeholder candidates');
|
|
1278
|
+
|
|
1279
|
+
const labels = analysis.candidates.filter((c) => c.kind === 'text' && c.tag === 'label');
|
|
1280
|
+
assert.equal(labels.length, 3, 'expected 3 label candidates');
|
|
1281
|
+
|
|
1282
|
+
const submitAction = analysis.candidates.find((c) => c.kind === 'form-submit-action');
|
|
1283
|
+
assert.ok(submitAction, 'expected form-submit-action candidate');
|
|
1284
|
+
assert.equal(submitAction.label, 'Confirm Booking');
|
|
1285
|
+
assert.equal(submitAction.extra.action, 'form-submit');
|
|
1286
|
+
assert.equal(submitAction.extra.insideForm, true);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
test('planner and transformer make entire form editable with submit button and WhatsApp URL binding', () => {
|
|
1290
|
+
const code = `
|
|
1291
|
+
import React from 'react';
|
|
1292
|
+
|
|
1293
|
+
export function ContactSection() {
|
|
1294
|
+
return (
|
|
1295
|
+
<form className="contact-form">
|
|
1296
|
+
<label>Your Name</label>
|
|
1297
|
+
<input type="text" placeholder="John Doe" />
|
|
1298
|
+
<button type="submit">Send Message</button>
|
|
1299
|
+
</form>
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
`;
|
|
1303
|
+
const profile = {
|
|
1304
|
+
root: os.tmpdir(),
|
|
1305
|
+
framework: 'nextjs',
|
|
1306
|
+
router: 'next-app',
|
|
1307
|
+
language: 'typescript',
|
|
1308
|
+
cssSystems: ['tailwind'],
|
|
1309
|
+
hasSrc: true,
|
|
1310
|
+
aliasMap: { '@/*': ['src/*'] },
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const analysis = analyzeFile({
|
|
1314
|
+
code,
|
|
1315
|
+
relativeFile: 'src/components/ContactSection.tsx',
|
|
1316
|
+
profile,
|
|
1317
|
+
graph: { sharedFiles: [] },
|
|
1318
|
+
ownerScope: 'home',
|
|
1319
|
+
componentMeta: { name: 'ContactSection', role: 'contact' },
|
|
1320
|
+
});
|
|
1321
|
+
analysis.code = code;
|
|
1322
|
+
analysis.relativeFile = 'src/components/ContactSection.tsx';
|
|
1323
|
+
|
|
1324
|
+
const plan = planTransformations({
|
|
1325
|
+
profile,
|
|
1326
|
+
analyses: [analysis],
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
const filePlan = plan.files[0];
|
|
1330
|
+
const formSubmit = filePlan.transformations.find((t) => t.operation === 'form-submit-action');
|
|
1331
|
+
assert.ok(formSubmit, 'form-submit-action must be planned');
|
|
1332
|
+
assert.match(formSubmit.urlField, /formWhatsappUrl/i);
|
|
1333
|
+
assert.match(formSubmit.labelField, /formSubmitLabel/i);
|
|
1334
|
+
|
|
1335
|
+
const transformed = applyFilePlan(filePlan, profile);
|
|
1336
|
+
assert.ok(transformed.changed, 'file must be changed');
|
|
1337
|
+
assert.match(transformed.code, /data-preview-field-path/);
|
|
1338
|
+
assert.match(transformed.code, /formSubmitLabel/);
|
|
1339
|
+
assert.match(transformed.code, /type="submit"/);
|
|
1340
|
+
assert.doesNotMatch(transformed.code, /<span[^>]*hidden/);
|
|
1341
|
+
assert.doesNotMatch(transformed.code, /data-preview-static[\s\S]*data-preview-field-path/);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
test('auditMarkerPlacement detects and rejects hidden preview markers', () => {
|
|
1345
|
+
const hiddenCode = `
|
|
1346
|
+
<div>
|
|
1347
|
+
<span className="hidden" aria-hidden="true" data-preview-field-path="home.hero.buttonUrl">https://wa.me/123</span>
|
|
1348
|
+
<span>Click me</span>
|
|
1349
|
+
</div>
|
|
1350
|
+
`;
|
|
1351
|
+
const errors = contract.auditMarkerPlacement(hiddenCode, 'TestHidden.tsx');
|
|
1352
|
+
assert.ok(errors.some((e) => e.includes('data-preview field/list/item marker is hidden')));
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
test('auditCoupledListMarkers detects and rejects coupling parallel list arrays by index', () => {
|
|
1356
|
+
const coupledCode = `
|
|
1357
|
+
<div data-preview-item-path="seasonal.items[0]">
|
|
1358
|
+
<span data-preview-field-path="seasonal.preOrderCta[0].buttonLabel">Pre-Order</span>
|
|
1359
|
+
</div>
|
|
1360
|
+
`;
|
|
1361
|
+
const errors = contract.auditCoupledListMarkers(coupledCode, 'TestCoupled.tsx');
|
|
1362
|
+
assert.ok(errors.some((e) => e.includes('belongs to a different list')));
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
test('detectedPages without a page file are not invented in the manifest', () => {
|
|
1366
|
+
const dir = copyOf(FIXTURE);
|
|
1367
|
+
silence(() =>
|
|
1368
|
+
runDenebArc(dir, 'basic-store', {
|
|
1369
|
+
telemetry: 'off',
|
|
1370
|
+
detectedPages: [{ id: 'contact', label: 'Contact', route: '/contact', required: true }],
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
const { manifest } = auditFivora(dir);
|
|
1374
|
+
assert.ok(!manifest.pages.some((page) => page.id === 'contact' || page.route === '/contact'));
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
test('learning records contract failure instead of success', () => {
|
|
1378
|
+
const { honestOutcome } = require('../learning.cjs');
|
|
1379
|
+
assert.equal(
|
|
1380
|
+
honestOutcome({ syntaxPassed: true, contractPassed: true, fivoraContractPassed: false }, 'success'),
|
|
1381
|
+
'failure'
|
|
1382
|
+
);
|
|
1383
|
+
assert.equal(
|
|
1384
|
+
honestOutcome({ syntaxPassed: true, contractPassed: true, fivoraContractPassed: true, uncoveredVisibleText: 0 }, 'success'),
|
|
1385
|
+
'success'
|
|
1386
|
+
);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
test('residual pass marks leftover decorative copy static with a reason', () => {
|
|
1390
|
+
const { applyResidualPass } = require('../residual.cjs');
|
|
1391
|
+
const code = `export function Chrome() { return <button aria-hidden="true">Close</button>; }`;
|
|
1392
|
+
const result = applyResidualPass({
|
|
1393
|
+
code,
|
|
1394
|
+
file: 'src/components/Chrome.tsx',
|
|
1395
|
+
ownerScope: 'home',
|
|
1396
|
+
usedPaths: new Set(),
|
|
1397
|
+
});
|
|
1398
|
+
assert.equal(result.changed, true);
|
|
1399
|
+
assert.match(result.code, /data-preview-static="[^"]+"/);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
test('style-bind grid and card attributes are written onto collection nodes', () => {
|
|
1403
|
+
const dir = copyOf(STOREFRONT_FIXTURE);
|
|
1404
|
+
silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
|
|
1405
|
+
const grid = fs.readFileSync(path.join(dir, 'src', 'components', 'ProductGrid.tsx'), 'utf8');
|
|
1406
|
+
assert.match(grid, /data-preview-style-type="grid"/);
|
|
1407
|
+
assert.match(grid, /data-preview-style-type="card"/);
|
|
1408
|
+
assert.match(grid, /data-preview-style-target="home\.products\.grid"/);
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
test('ContactActions stays mounted when all channels are empty', () => {
|
|
1412
|
+
const sourcePath = path.join(__dirname, '..', '..', '..', '..', '..', 'packages', 'deneb-ui', 'src', 'contact', 'ContactActions.tsx');
|
|
1413
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
1414
|
+
assert.doesNotMatch(source, /if \(!hasPhone && !hasWhatsApp && !hasEmail\)/);
|
|
1415
|
+
assert.doesNotMatch(source, /return null/);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
test('empty-state source audit flags gated preview markers', () => {
|
|
1419
|
+
const errors = contract.auditEmptyStateSource(
|
|
1420
|
+
'{title && <span data-preview-field-path="home.hero.title">{title}</span>}',
|
|
1421
|
+
'Hero.tsx'
|
|
1422
|
+
);
|
|
1423
|
+
assert.ok(errors.some((error) => error.includes('gated behind')));
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
|
package/src/arc/adapters.cjs
CHANGED
|
@@ -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,6 +197,18 @@ 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
213
|
if (
|
|
197
214
|
/wa\.me|whatsapp/i.test(h) ||
|
|
@@ -280,6 +297,8 @@ module.exports = {
|
|
|
280
297
|
TEXT_TAGS,
|
|
281
298
|
ACTION_TAGS,
|
|
282
299
|
IMAGE_TAGS,
|
|
300
|
+
FORM_INPUT_TAGS,
|
|
301
|
+
FORM_CONTAINER_TAGS,
|
|
283
302
|
collectJsxText,
|
|
284
303
|
getJsxAttributeLiteral,
|
|
285
304
|
};
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -47,6 +47,7 @@ function inferSection(context) {
|
|
|
47
47
|
['testimonials', /testimonial/],
|
|
48
48
|
['map', /map\b|location-map|google-map/],
|
|
49
49
|
['faq', /faq|accordion/],
|
|
50
|
+
['form', /form|booking|inquiry|registration/],
|
|
50
51
|
['contact', /contact|whatsapp|mailto/],
|
|
51
52
|
['featuredProducts', /featured|product-grid|collection/],
|
|
52
53
|
['newsletter', /newsletter|subscribe/],
|
|
@@ -65,6 +66,7 @@ function inferSection(context) {
|
|
|
65
66
|
|
|
66
67
|
function inferFieldName(kind, tag, text, extra = {}) {
|
|
67
68
|
if (kind === 'url') {
|
|
69
|
+
if (extra.action === 'form-submit') return 'formWhatsappUrl';
|
|
68
70
|
if (extra.platform) return `${extra.platform}Url`;
|
|
69
71
|
if (extra.action === 'whatsapp') return 'whatsappUrl';
|
|
70
72
|
if (extra.action === 'phone') return 'phoneUrl';
|
|
@@ -74,6 +76,7 @@ function inferFieldName(kind, tag, text, extra = {}) {
|
|
|
74
76
|
return fromText || (extra.action ? `${extra.action}Url` : 'ctaUrl');
|
|
75
77
|
}
|
|
76
78
|
if (kind === 'label' && extra.paired) {
|
|
79
|
+
if (extra.action === 'form-submit') return 'formSubmitLabel';
|
|
77
80
|
if (extra.action === 'whatsapp') return 'whatsappLabel';
|
|
78
81
|
if (extra.action === 'phone') return 'phoneLabel';
|
|
79
82
|
if (extra.action === 'email') return 'emailLabel';
|
|
@@ -100,6 +103,7 @@ function inferFieldName(kind, tag, text, extra = {}) {
|
|
|
100
103
|
Description: 'description',
|
|
101
104
|
Typography: 'text',
|
|
102
105
|
Badge: 'badge',
|
|
106
|
+
label: 'label',
|
|
103
107
|
button: 'label',
|
|
104
108
|
Button: 'label',
|
|
105
109
|
span: 'label',
|