@deneb-ui/cli 2.0.71 → 2.0.73

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
@@ -467,15 +467,49 @@ function getDefaultSiteData(projectName, pages) {
467
467
  secondaryColor: '#0a1931',
468
468
  accentColor: '#00adb5',
469
469
  backgroundColor: '#ffffff',
470
+ cardBackgroundColor: '#f8fafc',
470
471
  textColor: '#0f172a',
472
+ borderColor: '#e2e8f0',
471
473
  headingFont: 'Inter',
472
474
  bodyFont: 'Inter',
473
475
  baseSize: '16px',
474
476
  heroMinHeight: '70vh',
475
477
  sectionPadding: '4rem',
478
+ dark: {
479
+ primaryColor: '#00adb5',
480
+ secondaryColor: '#f8fafc',
481
+ accentColor: '#016a7e',
482
+ backgroundColor: '#0b0f19',
483
+ cardBackgroundColor: '#111827',
484
+ textColor: '#f9fafb',
485
+ borderColor: '#1f2937',
486
+ },
476
487
  },
477
488
  },
478
489
  },
490
+ theme: {
491
+ primaryColor: '#016a7e',
492
+ secondaryColor: '#0a1931',
493
+ accentColor: '#00adb5',
494
+ backgroundColor: '#ffffff',
495
+ cardBackgroundColor: '#f8fafc',
496
+ textColor: '#0f172a',
497
+ borderColor: '#e2e8f0',
498
+ headingFont: 'Inter',
499
+ bodyFont: 'Inter',
500
+ baseSize: '16px',
501
+ heroMinHeight: '70vh',
502
+ sectionPadding: '4rem',
503
+ dark: {
504
+ primaryColor: '#00adb5',
505
+ secondaryColor: '#f8fafc',
506
+ accentColor: '#016a7e',
507
+ backgroundColor: '#0b0f19',
508
+ cardBackgroundColor: '#111827',
509
+ textColor: '#f9fafb',
510
+ borderColor: '#1f2937',
511
+ },
512
+ },
479
513
  requirements: {
480
514
  requiredPages: pages.filter((p) => p.required).map((p) => p.id),
481
515
  requiredFeatures: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.71",
3
+ "version": "2.0.73",
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.71",
52
+ "@deneb-ui/core": "^2.0.73",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -1627,6 +1627,88 @@ test('learning loadFingerprintBoost returns verified boost for baseline trained
1627
1627
  assert.equal(boost3.boost, 0.08);
1628
1628
  });
1629
1629
 
1630
+ test('instrumentLayoutSource injects ThemeStyles and ThemeToggle with dual mode support into root layout', () => {
1631
+ const { instrumentLayoutSource } = require('../transformer.cjs');
1632
+ const code = `
1633
+ import React from 'react';
1634
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1635
+ return (
1636
+ <html lang="en">
1637
+ <body>
1638
+ <main>{children}</main>
1639
+ </body>
1640
+ </html>
1641
+ );
1642
+ }
1643
+ `;
1644
+ const result = instrumentLayoutSource(code, '@/data/site-data.json', '@deneb-ui/ui');
1645
+ assert.equal(result.updated, true);
1646
+ assert.ok(result.code.includes('ThemeStyles'));
1647
+ assert.ok(result.code.includes('ThemeToggle'));
1648
+ assert.ok(result.code.includes('SiteDataProvider'));
1649
+ assert.ok(result.code.includes('enableDualMode'));
1650
+ assert.ok(result.code.includes('fixed bottom-6 left-6 z-40'));
1651
+ });
1652
+
1653
+ test('semantic prop harvester detects user-facing copy props and transformer binds them to siteData', () => {
1654
+ const { analyzeFile } = require('../semantic.cjs');
1655
+ const { planTransformations } = require('../planner.cjs');
1656
+ const { applyFilePlan } = require('../transformer.cjs');
1657
+ const code = `
1658
+ export function Showcase() {
1659
+ return (
1660
+ <div className="features">
1661
+ <FeatureCard title="Lightning Fast" description="Instant response times" />
1662
+ </div>
1663
+ );
1664
+ }
1665
+ `;
1666
+ const profile = { root: '/tmp/test', hasSrc: true, jsxFiles: ['Showcase.tsx'], routes: [{ id: 'home', route: '/' }] };
1667
+ const analysis = analyzeFile({
1668
+ code,
1669
+ relativeFile: 'Showcase.tsx',
1670
+ profile,
1671
+ graph: { routesByFile: { 'Showcase.tsx': ['home'] } },
1672
+ ownerScope: 'home',
1673
+ });
1674
+ const propCandidates = analysis.candidates.filter((c) => c.operation === 'extract-prop');
1675
+ assert.ok(propCandidates.length >= 2);
1676
+ assert.ok(propCandidates.some((c) => c.value === 'Lightning Fast' && c.extra?.propName === 'title'));
1677
+ assert.ok(propCandidates.some((c) => c.value === 'Instant response times' && c.extra?.propName === 'description'));
1678
+
1679
+ const plan = planTransformations({
1680
+ profile,
1681
+ analyses: [{ ...analysis, relativeFile: 'Showcase.tsx', code }],
1682
+ });
1683
+ const filePlan = plan.files[0];
1684
+ const transformed = applyFilePlan(filePlan, profile);
1685
+ assert.equal(transformed.changed, true);
1686
+ assert.ok(transformed.code.includes('title={siteData?.content?.home'));
1687
+ assert.ok(transformed.code.includes('description={siteData?.content?.home'));
1688
+ });
1689
+
1690
+ test('manifest buildSiteDataAndManifest creates default dual-mode palette with dark mode overrides', () => {
1691
+ const { buildSiteDataAndManifest } = require('../manifest.cjs');
1692
+ const profile = {
1693
+ packageName: 'test-store',
1694
+ hasSrc: true,
1695
+ routes: [{ id: 'home', label: 'Home', route: '/', required: true }],
1696
+ };
1697
+ const plan = { files: [], usedPaths: [] };
1698
+ const bundle = buildSiteDataAndManifest({
1699
+ projectDir: '/tmp/test',
1700
+ projectName: 'test-store',
1701
+ profile,
1702
+ plan,
1703
+ });
1704
+ assert.ok(bundle.siteData.theme);
1705
+ assert.ok(bundle.siteData.theme.dark);
1706
+ assert.equal(bundle.siteData.theme.backgroundColor, '#ffffff');
1707
+ assert.equal(bundle.siteData.theme.dark.backgroundColor, '#0b0f19');
1708
+ assert.equal(bundle.siteData.theme.dark.textColor, '#f9fafb');
1709
+ });
1710
+
1711
+
1630
1712
 
1631
1713
 
1632
1714
 
package/src/arc/ast.cjs CHANGED
@@ -240,6 +240,50 @@ function jsxTemplatePathAttr(attrName, prefix, indexName, suffix = '') {
240
240
  );
241
241
  }
242
242
 
243
+ function unwrapExpr(node) {
244
+ let curr = node;
245
+ while (
246
+ curr &&
247
+ (curr.type === 'TSAsExpression' ||
248
+ curr.type === 'TSTypeAssertion' ||
249
+ curr.type === 'TypeCastExpression' ||
250
+ curr.type === 'TSNonNullExpression' ||
251
+ curr.type === 'ParenthesizedExpression')
252
+ ) {
253
+ curr = curr.expression;
254
+ }
255
+ return curr;
256
+ }
257
+
258
+ function extractItemMemberName(expr, binding) {
259
+ if (!expr) return null;
260
+ const unwrapped = unwrapExpr(expr);
261
+ if (!unwrapped) return null;
262
+
263
+ if (unwrapped.type === 'MemberExpression' && !unwrapped.computed) {
264
+ if (
265
+ (unwrapped.object?.type === 'Identifier' && unwrapped.object.name === binding) ||
266
+ (unwrapped.object?.type === 'JSXIdentifier' && unwrapped.object.name === binding)
267
+ ) {
268
+ return unwrapped.property?.name || null;
269
+ }
270
+ }
271
+
272
+ if (unwrapped.type === 'LogicalExpression' || unwrapped.type === 'BinaryExpression') {
273
+ return extractItemMemberName(unwrapped.left, binding) || extractItemMemberName(unwrapped.right, binding);
274
+ }
275
+
276
+ if (unwrapped.type === 'ConditionalExpression') {
277
+ return (
278
+ extractItemMemberName(unwrapped.consequent, binding) ||
279
+ extractItemMemberName(unwrapped.alternate, binding) ||
280
+ extractItemMemberName(unwrapped.test, binding)
281
+ );
282
+ }
283
+
284
+ return null;
285
+ }
286
+
243
287
  function wrapTextInEditableSpan(fieldPath, fallback, fieldType) {
244
288
  return b.jsxElement(
245
289
  b.jsxOpeningElement(
@@ -489,6 +533,8 @@ module.exports = {
489
533
  jsxStaticAttr,
490
534
  jsxTemplatePathAttr,
491
535
  wrapTextInEditableSpan,
536
+ unwrapExpr,
537
+ extractItemMemberName,
492
538
  hasDirective,
493
539
  ensureImport,
494
540
  ensureDefaultImport,
@@ -83,6 +83,10 @@ function inferFieldName(kind, tag, text, extra = {}) {
83
83
  if (extra.cta) return extra.cta === 'primary' ? 'primaryCtaLabel' : `${extra.cta}Label`;
84
84
  return toCamel([text || '', 'label']) || (extra.action ? `${extra.action}Label` : 'ctaLabel');
85
85
  }
86
+ if (extra.isBrandLogo) {
87
+ if (kind === 'image') return 'logoUrl';
88
+ return 'logoText';
89
+ }
86
90
  if (kind === 'image') return extra.alt ? toCamel([extra.alt, 'image']) || 'image' : 'image';
87
91
  if (kind === 'alt') return extra.imageField ? extra.imageField.replace(/Image$/, 'ImageAlt').replace(/image$/, 'imageAlt') : 'imageAlt';
88
92
  if (kind === 'placeholder') return toCamel([text || '', 'placeholder']) || 'placeholder';
@@ -62,6 +62,37 @@ function setDeep(target, pathStr, value) {
62
62
  if (curr[last] === undefined) curr[last] = value;
63
63
  }
64
64
 
65
+ function defaultDualModeTheme(existingTheme = {}) {
66
+ const base = {
67
+ primaryColor: '#0284c7',
68
+ secondaryColor: '#0f172a',
69
+ accentColor: '#38bdf8',
70
+ backgroundColor: '#ffffff',
71
+ cardBackgroundColor: '#f8fafc',
72
+ textColor: '#0f172a',
73
+ borderColor: '#e2e8f0',
74
+ headingFont: 'Outfit, sans-serif',
75
+ bodyFont: 'Inter, sans-serif',
76
+ borderRadius: '12px',
77
+ dark: {
78
+ primaryColor: '#38bdf8',
79
+ secondaryColor: '#f8fafc',
80
+ accentColor: '#0284c7',
81
+ backgroundColor: '#0b0f19',
82
+ cardBackgroundColor: '#111827',
83
+ textColor: '#f9fafb',
84
+ borderColor: '#1f2937',
85
+ },
86
+ };
87
+ if (isPlainObject(existingTheme)) {
88
+ Object.assign(base, existingTheme);
89
+ if (isPlainObject(existingTheme.dark)) {
90
+ base.dark = { ...base.dark, ...existingTheme.dark };
91
+ }
92
+ }
93
+ return base;
94
+ }
95
+
65
96
  function getDeep(target, pathStr) {
66
97
  const parts = String(pathStr).split('.').filter(Boolean);
67
98
  let curr = target;
@@ -149,11 +180,13 @@ function upsertSchemaList(sections, listPath, itemFields, items) {
149
180
  const key = rest[rest.length - 1];
150
181
  if (fields.some((f) => f.key === key)) return;
151
182
 
183
+ const isPrimitiveList = (itemFields || []).length === 0;
152
184
  fields.push({
153
185
  key,
154
186
  type: 'list',
155
187
  label: humanLabel(key),
156
188
  itemLabel: humanLabel(key).replace(/s$/, '') || 'Item',
189
+ itemType: isPrimitiveList ? 'string' : 'object',
157
190
  minItems: 0,
158
191
  maxItems: Math.max((items || []).length, 12),
159
192
  fields: (itemFields || []).map((field) => ({
@@ -384,7 +417,7 @@ function ensurePlatformSections(sections, content, boundListPaths = []) {
384
417
  { key: 'businessSummary', type: 'textarea', label: 'Business summary' },
385
418
  { key: 'additionalBusinessDetails', type: 'textarea', label: 'Additional business details' },
386
419
  { key: 'referenceWebsiteUrl', type: 'url', label: 'Reference website URL' },
387
- { key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes' },
420
+ { key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes', fields: [] },
388
421
  ],
389
422
  });
390
423
  }
@@ -572,7 +605,10 @@ function buildSiteDataAndManifest({
572
605
  id: `${projectName}-template`,
573
606
  name: projectName,
574
607
  engine: 'NEXT_STATIC_EXPORT',
575
- structure: { pages: routes.map((p) => p.id) },
608
+ structure: {
609
+ pages: routes.map((p) => p.id),
610
+ theme: defaultDualModeTheme(existingSiteData?.template?.structure?.theme || existingSiteData?.theme),
611
+ },
576
612
  },
577
613
  requirements: existingSiteData?.requirements || {
578
614
  requiredPages: routes.filter((p) => p.required).map((p) => p.id),
@@ -580,7 +616,7 @@ function buildSiteDataAndManifest({
580
616
  },
581
617
  content,
582
618
  styles: collectStylesFromPlan(plan, existingSiteData?.styles),
583
- ...(isPlainObject(existingSiteData?.theme) ? { theme: { ...existingSiteData.theme } } : {}),
619
+ theme: defaultDualModeTheme(existingSiteData?.theme || existingSiteData?.template?.structure?.theme),
584
620
  };
585
621
 
586
622
  applyFontTheme(siteData, collectFontIdsFromSiteData(siteData));
@@ -159,6 +159,16 @@ function planTransformations({ profile, analyses, recipe }) {
159
159
  field: inferFieldName('placeholder', candidate.tag, candidate.value, extra),
160
160
  used: usedPaths,
161
161
  });
162
+ } else if (candidate.operation === 'extract-prop') {
163
+ const propName = extra.propName || 'text';
164
+ transform.field = buildFieldPath({
165
+ scope,
166
+ section,
167
+ field: inferFieldName(propName, candidate.tag, candidate.value, extra),
168
+ used: usedPaths,
169
+ });
170
+ transform.propName = propName;
171
+ transform.fieldType = classifyFieldType('text', candidate.value);
162
172
  } else if (candidate.operation === 'collection-conversion') {
163
173
  // A collection is named after the developer's own array variable so the
164
174
  // merchant sees "products", not "items2".
@@ -95,11 +95,12 @@ function inMapCallback(pathNode) {
95
95
  return false;
96
96
  }
97
97
 
98
- function isMeaningfulVisibleText(text) {
98
+ function isMeaningfulVisibleText(text, isLogoOrList = false) {
99
99
  const value = String(text || '').replace(/\s+/g, ' ').trim();
100
- if (!value || value.length <= 2) return false;
100
+ if (!value) return false;
101
+ if (value.length < 2 && !isLogoOrList) return false;
101
102
  if (!/\p{L}/u.test(value)) return false;
102
- if (isStaticSkipText(value)) return false;
103
+ if (isStaticSkipText(value, isLogoOrList)) return false;
103
104
  if (CHROME_TEXT_RE.test(value)) return false;
104
105
  return true;
105
106
  }
@@ -126,8 +127,16 @@ function classifyResidual(node, text, tag) {
126
127
  if (IMAGE_TAGS.has(tag) && attrLiteral(node, 'src')) {
127
128
  return { bind: true, kind: 'image', value: attrLiteral(node, 'src') };
128
129
  }
129
- if (isMeaningfulVisibleText(text)) {
130
- return { bind: true, kind: 'text', value: text };
130
+ const isExplicitLogo = /\b(site-logo|brand-logo|nav-logo|header-logo|monogram)\b/i.test(className);
131
+ const ariaLabel = attrLiteral(node, 'aria-label');
132
+ const isLogoContext =
133
+ isExplicitLogo ||
134
+ ((tag === 'a' || tag === 'Link' || tag === 'span') &&
135
+ (attrLiteral(node, 'href') === '/' || /\b(logo|home)\b/i.test(ariaLabel)));
136
+ const isListContext = tag === 'li' || /list-item|bullet/i.test(className);
137
+
138
+ if (isMeaningfulVisibleText(text, isLogoContext || isListContext)) {
139
+ return { bind: true, kind: 'text', value: text, isBrandLogo: isLogoContext };
131
140
  }
132
141
  if (text && text.trim()) {
133
142
  return { bind: false, reason: 'decorative-copy' };
@@ -298,7 +307,8 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
298
307
  applied++;
299
308
  }
300
309
 
301
- if (decision && decision.bind && decision.kind === 'text' && isMeaningfulVisibleText(text)) {
310
+ if (decision && decision.bind && decision.kind === 'text') {
311
+ const isLogoText = Boolean(decision.isBrandLogo);
302
312
  const section = inferSection({
303
313
  componentName,
304
314
  fileName: file,
@@ -307,9 +317,9 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
307
317
  role,
308
318
  });
309
319
  const field = buildFieldPath({
310
- scope: ownerScope || 'home',
320
+ scope: (isLogoText && (role === 'navigation' || /header|footer|nav/i.test(file))) ? 'common' : (ownerScope || 'home'),
311
321
  section,
312
- field: inferFieldName('text', tag, text, { tag }),
322
+ field: inferFieldName('text', tag, text, { tag, isBrandLogo: isLogoText }),
313
323
  used,
314
324
  });
315
325
  const fieldType = classifyFieldType('text', text);
@@ -372,10 +382,30 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
372
382
  fields.push({ path: field, type: fieldType, value: text });
373
383
  applied++;
374
384
  }
375
- } else if (!BROAD_CONTENT_CONTAINERS.has(lower)) {
376
- if (ensureStaticOnLeaf(node, 'non-leaf-copy')) applied++;
377
- } else if (wrapFirstLiteralAsStatic(node, 'container-copy')) {
378
- applied++;
385
+ } else {
386
+ const nextChildren = [];
387
+ let wrapped = false;
388
+ for (const child of node.children || []) {
389
+ if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
390
+ const leading = child.value.match(/^\s*/)?.[0] || '';
391
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
392
+ if (leading) nextChildren.push(b.jsxText(leading));
393
+ nextChildren.push(wrapTextInEditableSpan(field, text.trim(), fieldType));
394
+ if (trailing) nextChildren.push(b.jsxText(trailing));
395
+ wrapped = true;
396
+ continue;
397
+ }
398
+ nextChildren.push(child);
399
+ }
400
+ if (wrapped) {
401
+ node.children = nextChildren;
402
+ fields.push({ path: field, type: fieldType, value: text.trim() });
403
+ applied++;
404
+ } else if (!BROAD_CONTENT_CONTAINERS.has(lower)) {
405
+ if (ensureStaticOnLeaf(node, 'non-leaf-copy')) applied++;
406
+ } else if (wrapFirstLiteralAsStatic(node, 'container-copy')) {
407
+ applied++;
408
+ }
379
409
  }
380
410
  this.traverse(pathNode);
381
411
  return;