@deneb-ui/cli 2.0.97 → 2.0.99

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/README.md CHANGED
@@ -60,11 +60,20 @@ npm install -D @deneb-ui/cli
60
60
  | `deneb zip` | Create upload-ready `fivora-template.zip` |
61
61
  | `deneb doctor` | Environment, dependency, and export diagnostics |
62
62
  | `deneb add <component>` | Copy DENEB UI components into your project |
63
- | `deneb lab` | Launch local visual editing preview lab |
63
+ | `deneb lab` | Launch the local visual editor, preview changes, and save them to the declared `siteDataFile` |
64
64
  | `deneb update` | Update `@deneb-ui/*` packages and synced components |
65
65
 
66
66
  Full reference: [deneb.fivora.site/docs/cli](https://deneb.fivora.site/docs/cli)
67
67
 
68
+ ### Local preview before upload
69
+
70
+ Run `deneb lab .` in a template project, then paste the printed controller URL
71
+ and token into **Developer Portal → Local Test Lab**. Content and style edits are
72
+ applied to the preview immediately. Use **Save local** to persist the current
73
+ editor state to the `siteDataFile` declared in `fivora-template.json`; the write
74
+ stays on your machine and no template files are uploaded. Run **Test** after
75
+ saving to perform the same local preflight checks used before packaging.
76
+
68
77
  ---
69
78
 
70
79
  ## What `deneb init` does (ARC)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.97",
3
+ "version": "2.0.99",
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.97",
52
+ "@deneb-ui/core": "^2.0.99",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -12,7 +12,13 @@ const { scanProject, buildDependencyGraph } = require('../scanner.cjs');
12
12
  const { analyzeFile } = require('../semantic.cjs');
13
13
  const { planTransformations } = require('../planner.cjs');
14
14
  const { applyFilePlan } = require('../transformer.cjs');
15
- const { buildFieldPath, inferFieldName, isListActionCtaKey } = require('../field-paths.cjs');
15
+ const {
16
+ buildFieldPath,
17
+ classifyFieldType,
18
+ inferFieldName,
19
+ isListActionCtaKey,
20
+ } = require('../field-paths.cjs');
21
+ const { normalizeFieldType } = require('../fivora-schema-authority.cjs');
16
22
  const { enrichSchemasFromContent } = require('../manifest.cjs');
17
23
  const { runDenebArc } = require('../index.cjs');
18
24
  const { parseSource } = require('../ast.cjs');
@@ -67,6 +73,16 @@ test('field-paths recognizes list action CTA keys', () => {
67
73
  assert.equal(isListActionCtaKey('features'), false);
68
74
  });
69
75
 
76
+ test('price fields use numeric data while currency and formatted prices stay text', () => {
77
+ assert.equal(normalizeFieldType('price', 'price'), 'number');
78
+ assert.equal(normalizeFieldType('currency', 'currency'), 'text');
79
+ assert.equal(classifyFieldType('text', 1000, 'price'), 'number');
80
+ assert.equal(classifyFieldType('text', '1000', 'price'), 'number');
81
+ assert.equal(classifyFieldType('text', '1000.50', 'amount'), 'number');
82
+ assert.equal(classifyFieldType('text', 'Rs. 1,000', 'price'), 'text');
83
+ assert.equal(classifyFieldType('text', 'LKR 1000', 'price'), 'text');
84
+ });
85
+
70
86
  test('manifest enrichSchemasFromContent registers list CTA and paired directions fields', () => {
71
87
  const sections = [
72
88
  {
@@ -447,10 +463,30 @@ test('static-array collections become list contracts without changing render log
447
463
  const products = home.fields.find((f) => f.key === 'products');
448
464
  assert.equal(products.type, 'list');
449
465
  assert.deepEqual(products.fields.map((f) => f.key).sort(), ['description', 'image', 'price', 'title']);
466
+ assert.equal(products.fields.find((field) => field.key === 'price').type, 'text');
450
467
  assert.equal(siteData.content.home.products.length, 3);
451
468
  assert.equal(siteData.content.home.products[0].title, 'Minimalist Smart Watch');
452
469
  });
453
470
 
471
+ test('numeric catalog prices stay numbers in generated site data and editor schema', () => {
472
+ const dir = copyOf(STOREFRONT_FIXTURE);
473
+ const gridPath = path.join(dir, 'src', 'components', 'ProductGrid.tsx');
474
+ const grid = fs
475
+ .readFileSync(gridPath, 'utf8')
476
+ .replace(/price: '\$(\d+)\.00'/g, 'price: $1');
477
+ fs.writeFileSync(gridPath, grid, 'utf8');
478
+
479
+ silence(() => runDenebArc(dir, 'numeric-price-store', { telemetry: 'off' }));
480
+ const { manifest, siteData } = auditFivora(dir);
481
+ const home = manifest.editorSchema.sections.find((section) => section.id === 'home');
482
+ const products = home.fields.find((field) => field.key === 'products');
483
+ const price = products.fields.find((field) => field.key === 'price');
484
+
485
+ assert.equal(price.type, 'number');
486
+ assert.equal(siteData.content.home.products[0].price, 249);
487
+ assert.equal(typeof siteData.content.home.products[0].price, 'number');
488
+ });
489
+
454
490
  test('collections holding component references still bind primitive item fields', () => {
455
491
  const dir = copyOf(STOREFRONT_FIXTURE);
456
492
  silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
@@ -3923,4 +3959,3 @@ test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remed
3923
3959
 
3924
3960
 
3925
3961
 
3926
-
@@ -181,7 +181,14 @@ function classifyFieldType(kind, value, fieldName = '') {
181
181
  /price|amount|rating|count|score|stars|qty|quantity|discount|fee|cost/i.test(name)
182
182
  ) {
183
183
  if (typeof value === 'string' && /\$|lkr|usd|rs\.?\s*\d/i.test(value)) return 'text';
184
- return typeof value === 'number' ? 'number' : 'text';
184
+ if (typeof value === 'number') return 'number';
185
+ if (
186
+ typeof value === 'string' &&
187
+ /^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(value.trim())
188
+ ) {
189
+ return 'number';
190
+ }
191
+ return 'text';
185
192
  }
186
193
  if (typeof value === 'boolean' || /enabled|visible|show|active/i.test(name)) return 'boolean';
187
194
  if (kind === 'textarea' || (typeof value === 'string' && (value.length > 70 || value.includes('\n')))) return 'textarea';
@@ -25,8 +25,8 @@ const PLATFORM_ALLOWED_TYPES = new Set([
25
25
  ]);
26
26
 
27
27
  const TYPE_COERCION_MAP = {
28
- currency: 'text', // In Fivora storefronts, prices with symbols (e.g. '$249' or 'LKR 5,000') must be text or number
29
- price: 'text',
28
+ currency: 'text',
29
+ price: 'number',
30
30
  richtext: 'textarea',
31
31
  phone: 'tel',
32
32
  link: 'url',
@@ -370,7 +370,12 @@ const PLATFORM_CONTROLLED_PATHS = new Set(
370
370
  );
371
371
 
372
372
  function slimListItems(items, itemFields) {
373
- const keys = (itemFields || []).map((field) => field.key).filter(Boolean);
373
+ const fieldsByKey = new Map(
374
+ (itemFields || [])
375
+ .filter((field) => field?.key)
376
+ .map((field) => [field.key, field]),
377
+ );
378
+ const keys = [...fieldsByKey.keys()];
374
379
  if (!keys.length || !Array.isArray(items)) return items || [];
375
380
  return items.map((item) => {
376
381
  if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
@@ -378,7 +383,14 @@ function slimListItems(items, itemFields) {
378
383
  for (const key of keys) {
379
384
  if (!(key in item)) continue;
380
385
  if (Array.isArray(item[key])) continue;
381
- out[key] = item[key];
386
+ const field = fieldsByKey.get(key);
387
+ const value = item[key];
388
+ out[key] =
389
+ field?.type === 'number' &&
390
+ typeof value === 'string' &&
391
+ /^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(value.trim())
392
+ ? Number(value)
393
+ : value;
382
394
  }
383
395
  return Object.keys(out).length ? out : item;
384
396
  });
@@ -33,6 +33,7 @@ const { resolveImportSpecifier } = require('./scanner.cjs');
33
33
  const { resolvePublicAssetUrl } = require('./ir-builder.cjs');
34
34
  const { analyzeElementFragments } = require('./text-fragment-analyzer.cjs');
35
35
  const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
36
+ const { classifyFieldType } = require('./field-paths.cjs');
36
37
 
37
38
  const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
38
39
  const ARIA_ONLY_RE = /^(aria-|data-state|data-slot|data-orientation)/;
@@ -1105,6 +1106,21 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
1105
1106
  const stringItems = arr.items.every((item) => item.type === 'string');
1106
1107
  const rootTagName = getJsxName(mapInfo.rootElement);
1107
1108
  const isChildCustomComponent = /^[A-Z]/.test(rootTagName);
1109
+ const sampleItem = objectItems
1110
+ ? arr.items.find((item) => item?.value && typeof item.value === 'object')?.value
1111
+ : null;
1112
+
1113
+ // JSX text positions alone look like generic text, but commerce values
1114
+ // such as a bare numeric `price` must remain numeric for catalog range
1115
+ // filters, sorting, totals, and database persistence. Formatted values
1116
+ // such as "$249" intentionally remain text for legacy templates.
1117
+ if (sampleItem) {
1118
+ for (const [key, role] of itemUsage) {
1119
+ if (role !== 'text') continue;
1120
+ const inferredRole = classifyFieldType('text', sampleItem[key], key);
1121
+ if (inferredRole !== 'text') itemUsage.set(key, inferredRole);
1122
+ }
1123
+ }
1108
1124
 
1109
1125
  // If the map returns a custom child component (e.g. <ProductCard product={item} />),
1110
1126
  // derive item fields from the sample object items if direct JSX member usage was empty
@@ -1112,13 +1128,7 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
1112
1128
  const sampleObj = arr.items[0].value;
1113
1129
  for (const key of Object.keys(sampleObj)) {
1114
1130
  if (/^(id|_id|key|slug)$/i.test(key)) continue;
1115
- const role = /image|photo|avatar|icon|thumb|img/i.test(key)
1116
- ? 'image'
1117
- : /price|cost|amount|count|rating|stars/i.test(key)
1118
- ? 'number'
1119
- : /url|link|href/i.test(key)
1120
- ? 'url'
1121
- : 'text';
1131
+ const role = classifyFieldType('text', sampleObj[key], key);
1122
1132
  itemUsage.set(key, role);
1123
1133
  }
1124
1134
  }
@@ -1207,4 +1217,3 @@ module.exports = {
1207
1217
  isStaticSkipText,
1208
1218
  resolveChildText,
1209
1219
  };
1210
-