@deneb-ui/cli 2.0.71 → 2.0.72

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.72",
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.72",
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
 
@@ -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;
@@ -384,7 +415,7 @@ function ensurePlatformSections(sections, content, boundListPaths = []) {
384
415
  { key: 'businessSummary', type: 'textarea', label: 'Business summary' },
385
416
  { key: 'additionalBusinessDetails', type: 'textarea', label: 'Additional business details' },
386
417
  { key: 'referenceWebsiteUrl', type: 'url', label: 'Reference website URL' },
387
- { key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes' },
418
+ { key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes', fields: [] },
388
419
  ],
389
420
  });
390
421
  }
@@ -572,7 +603,10 @@ function buildSiteDataAndManifest({
572
603
  id: `${projectName}-template`,
573
604
  name: projectName,
574
605
  engine: 'NEXT_STATIC_EXPORT',
575
- structure: { pages: routes.map((p) => p.id) },
606
+ structure: {
607
+ pages: routes.map((p) => p.id),
608
+ theme: defaultDualModeTheme(existingSiteData?.template?.structure?.theme || existingSiteData?.theme),
609
+ },
576
610
  },
577
611
  requirements: existingSiteData?.requirements || {
578
612
  requiredPages: routes.filter((p) => p.required).map((p) => p.id),
@@ -580,7 +614,7 @@ function buildSiteDataAndManifest({
580
614
  },
581
615
  content,
582
616
  styles: collectStylesFromPlan(plan, existingSiteData?.styles),
583
- ...(isPlainObject(existingSiteData?.theme) ? { theme: { ...existingSiteData.theme } } : {}),
617
+ theme: defaultDualModeTheme(existingSiteData?.theme || existingSiteData?.template?.structure?.theme),
584
618
  };
585
619
 
586
620
  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".
@@ -97,7 +97,7 @@ function inMapCallback(pathNode) {
97
97
 
98
98
  function isMeaningfulVisibleText(text) {
99
99
  const value = String(text || '').replace(/\s+/g, ' ').trim();
100
- if (!value || value.length <= 2) return false;
100
+ if (!value || value.length < 2) return false;
101
101
  if (!/\p{L}/u.test(value)) return false;
102
102
  if (isStaticSkipText(value)) return false;
103
103
  if (CHROME_TEXT_RE.test(value)) return false;
@@ -372,10 +372,30 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
372
372
  fields.push({ path: field, type: fieldType, value: text });
373
373
  applied++;
374
374
  }
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++;
375
+ } else {
376
+ const nextChildren = [];
377
+ let wrapped = false;
378
+ for (const child of node.children || []) {
379
+ if (!wrapped && child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
380
+ const leading = child.value.match(/^\s*/)?.[0] || '';
381
+ const trailing = child.value.match(/\s*$/)?.[0] || '';
382
+ if (leading) nextChildren.push(b.jsxText(leading));
383
+ nextChildren.push(wrapTextInEditableSpan(field, text.trim(), fieldType));
384
+ if (trailing) nextChildren.push(b.jsxText(trailing));
385
+ wrapped = true;
386
+ continue;
387
+ }
388
+ nextChildren.push(child);
389
+ }
390
+ if (wrapped) {
391
+ node.children = nextChildren;
392
+ fields.push({ path: field, type: fieldType, value: text.trim() });
393
+ applied++;
394
+ } else if (!BROAD_CONTENT_CONTAINERS.has(lower)) {
395
+ if (ensureStaticOnLeaf(node, 'non-leaf-copy')) applied++;
396
+ } else if (wrapFirstLiteralAsStatic(node, 'container-copy')) {
397
+ applied++;
398
+ }
379
399
  }
380
400
  this.traverse(pathNode);
381
401
  return;
@@ -33,6 +33,20 @@ const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
33
33
  const TECHNICAL_TEXT_RE = /^(true|false|null|undefined|px|rem|em|auto|hidden|flex|grid|sr-only)$/i;
34
34
  const ARIA_ONLY_RE = /^(aria-|data-state|data-slot|data-orientation)/;
35
35
  const SKIP_ATTR_NAMES = new Set(['className', 'class', 'style', 'key', 'id', 'role', 'type', 'name', 'htmlFor', 'suppressHydrationWarning']);
36
+ const USER_FACING_PROP_NAMES = new Set([
37
+ 'title',
38
+ 'heading',
39
+ 'subheading',
40
+ 'subtitle',
41
+ 'label',
42
+ 'description',
43
+ 'caption',
44
+ 'badge',
45
+ 'buttonText',
46
+ 'ctaText',
47
+ 'helperText',
48
+ 'summary',
49
+ ]);
36
50
 
37
51
  function fingerprintCandidate(features) {
38
52
  return shortHash(JSON.stringify(features));
@@ -569,6 +583,43 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
569
583
  });
570
584
  }
571
585
 
586
+ const isCustomComponent = Boolean(name && ((name[0] >= 'A' && name[0] <= 'Z') || name.includes('.')));
587
+ if (isCustomComponent && !apiOwned && node.openingElement && Array.isArray(node.openingElement.attributes)) {
588
+ for (const attr of node.openingElement.attributes) {
589
+ if (attr.type === 'JSXAttribute' && attr.name && USER_FACING_PROP_NAMES.has(attr.name.name)) {
590
+ const propName = attr.name.name;
591
+ let propValue = '';
592
+ if (attr.value) {
593
+ if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
594
+ propValue = String(attr.value.value || '');
595
+ } else if (attr.value.type === 'JSXExpressionContainer') {
596
+ const expr = attr.value.expression;
597
+ if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
598
+ propValue = String(expr.value || '');
599
+ }
600
+ }
601
+ }
602
+ if (propValue && !isStaticSkipText(propValue)) {
603
+ const propLoc = `${loc}:${propName}`;
604
+ if (!usedLocs.has(propLoc)) {
605
+ usedLocs.add(propLoc);
606
+ candidates.push({
607
+ ...baseMeta,
608
+ loc: propLoc,
609
+ kind: 'text',
610
+ operation: 'extract-prop',
611
+ value: propValue,
612
+ extra: { propName, tag: name },
613
+ confidence: 0.88,
614
+ reason: `component-prop-${propName}`,
615
+ fingerprint: fingerprintCandidate({ tag: name, kind: 'prop', propName }),
616
+ });
617
+ }
618
+ }
619
+ }
620
+ }
621
+ }
622
+
572
623
  const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
573
624
 
574
625
  const actionableHref = href || (name === 'Button' ? getJsxAttributeLiteral(node, 'href') : null);
@@ -27,10 +27,14 @@ const { toPosix } = require('./fs-utils.cjs');
27
27
  const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
28
28
 
29
29
  function findElementByLoc(ast, loc) {
30
+ if (!loc) return null;
31
+ const parts = loc.split(':');
32
+ const targetLoc = parts.length > 4 ? parts.slice(0, 4).join(':') : loc;
30
33
  let found = null;
31
34
  recast.types.visit(ast, {
32
35
  visitJSXElement(pathNode) {
33
- if (locKey(pathNode.node) === loc) {
36
+ const k = locKey(pathNode.node);
37
+ if (k === targetLoc || k === loc) {
34
38
  found = pathNode;
35
39
  return false;
36
40
  }
@@ -243,6 +247,14 @@ function applyTransformToElement(pathNode, transform) {
243
247
  ensurePreviewPath(node, transform.field);
244
248
  return;
245
249
  }
250
+ if (transform.operation === 'extract-prop') {
251
+ const propName = transform.propName || 'title';
252
+ replaceAttrValue(node, propName, siteDataBinding(transform.field.split('.'), transform.fallback, transform.fieldType || 'text'));
253
+ if (!hasJsxAttribute(node, 'data-preview-field-path') && !hasJsxAttribute(node, 'data-preview-list-path')) {
254
+ ensurePreviewPath(node, transform.field);
255
+ }
256
+ return;
257
+ }
246
258
  if (transform.operation === 'wrap-text-span') {
247
259
  wrapLiteralTextChildren(node, transform.field, transform.fallback);
248
260
  return;
@@ -823,13 +835,15 @@ function injectSiteDataHook(ast) {
823
835
  }
824
836
 
825
837
  function resolveSiteDataSpecifier(profile, fromRelativeFile) {
826
- const aliases = profile.aliasMap || {};
838
+ const aliases = (profile && profile.aliasMap) || {};
827
839
  const hasAt = Object.keys(aliases).some((k) => k === '@/*' || k.startsWith('@/'));
828
- const siteDataRel = profile.hasSrc ? 'src/data/site-data.json' : 'data/site-data.json';
829
- if (hasAt && profile.hasSrc) return '@/data/site-data.json';
840
+ const hasSrc = Boolean(profile && profile.hasSrc);
841
+ const siteDataRel = hasSrc ? 'src/data/site-data.json' : 'data/site-data.json';
842
+ if (hasAt && hasSrc) return '@/data/site-data.json';
830
843
 
831
- const fromAbs = path.join(profile.root, fromRelativeFile);
832
- const toAbs = path.join(profile.root, siteDataRel);
844
+ const root = profile && profile.root ? profile.root : process.cwd();
845
+ const fromAbs = path.join(root, fromRelativeFile || 'page.tsx');
846
+ const toAbs = path.join(root, siteDataRel);
833
847
  let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
834
848
  if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
835
849
  return relSpec;
@@ -853,6 +867,7 @@ function applyFilePlan(filePlan, profile) {
853
867
  'extract-alt',
854
868
  'extract-placeholder',
855
869
  'extract-text',
870
+ 'extract-prop',
856
871
  'wrap-text-span',
857
872
  'collection-conversion',
858
873
  'style-bind',
@@ -1152,11 +1167,98 @@ function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-u
1152
1167
 
1153
1168
  ensureProviderInitialData(ast, jsonIdent);
1154
1169
  injectPlatformAdditionalPages(ast, providerImport);
1170
+ injectDualModeTheme(ast, jsonIdent, providerImport);
1155
1171
  sanitizeDuplicateBindings(ast);
1156
1172
  const next = printSource(ast, code);
1157
1173
  return { code: next, updated: next !== code };
1158
1174
  }
1159
1175
 
1176
+ /**
1177
+ * Injects <ThemeStyles /> and <ThemeToggle /> into the layout JSX tree.
1178
+ * ThemeStyles applies light/dark variables and auto-contrast rules.
1179
+ * ThemeToggle provides an out-of-the-box floating theme switcher.
1180
+ */
1181
+ function injectDualModeTheme(ast, jsonIdent, providerImport = '@deneb-ui/ui') {
1182
+ if (!ast) return;
1183
+ let hasThemeStyles = false;
1184
+ let hasThemeToggle = false;
1185
+ recast.types.visit(ast, {
1186
+ visitJSXIdentifier(pathNode) {
1187
+ if (pathNode.node.name === 'ThemeStyles') hasThemeStyles = true;
1188
+ if (pathNode.node.name === 'ThemeToggle') hasThemeToggle = true;
1189
+ this.traverse(pathNode);
1190
+ },
1191
+ });
1192
+
1193
+ const importsToAdd = [];
1194
+ if (!hasThemeStyles) importsToAdd.push('ThemeStyles');
1195
+ if (!hasThemeToggle) importsToAdd.push('ThemeToggle');
1196
+ if (importsToAdd.length === 0) return;
1197
+
1198
+ if (!hasThemeStyles) {
1199
+ let stylesInjected = false;
1200
+ const snippet = parseSource(
1201
+ `<ThemeStyles theme={${jsonIdent}?.template?.structure?.theme || ${jsonIdent}?.theme} enableDualMode />`,
1202
+ 'snippet.tsx'
1203
+ );
1204
+ const stylesEl = snippet.program.body[0].expression;
1205
+
1206
+ // Try <head> first
1207
+ recast.types.visit(ast, {
1208
+ visitJSXElement(pathNode) {
1209
+ if (stylesInjected) return false;
1210
+ const name = getJsxName(pathNode.node);
1211
+ if (name === 'head' || name === 'Head') {
1212
+ pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
1213
+ stylesInjected = true;
1214
+ return false;
1215
+ }
1216
+ this.traverse(pathNode);
1217
+ },
1218
+ });
1219
+
1220
+ // If no <head>, inject inside SiteDataProvider or <body>
1221
+ if (!stylesInjected) {
1222
+ recast.types.visit(ast, {
1223
+ visitJSXElement(pathNode) {
1224
+ if (stylesInjected) return false;
1225
+ const name = getJsxName(pathNode.node);
1226
+ if (name === 'SiteDataProvider' || name === 'body') {
1227
+ pathNode.node.children = [b.jsxText('\n '), stylesEl, ...(pathNode.node.children || [])];
1228
+ stylesInjected = true;
1229
+ return false;
1230
+ }
1231
+ this.traverse(pathNode);
1232
+ },
1233
+ });
1234
+ }
1235
+ }
1236
+
1237
+ if (!hasThemeToggle) {
1238
+ let toggleInjected = false;
1239
+ const snippet = parseSource(
1240
+ '<ThemeToggle showLabel className="fixed bottom-6 left-6 z-40" />',
1241
+ 'snippet.tsx'
1242
+ );
1243
+ const toggleEl = snippet.program.body[0].expression;
1244
+
1245
+ recast.types.visit(ast, {
1246
+ visitJSXElement(pathNode) {
1247
+ if (toggleInjected) return false;
1248
+ const name = getJsxName(pathNode.node);
1249
+ if (name === 'SiteDataProvider' || name === 'body') {
1250
+ pathNode.node.children = [...(pathNode.node.children || []), b.jsxText('\n '), toggleEl];
1251
+ toggleInjected = true;
1252
+ return false;
1253
+ }
1254
+ this.traverse(pathNode);
1255
+ },
1256
+ });
1257
+ }
1258
+
1259
+ ensureImport(ast, providerImport || '@deneb-ui/ui', importsToAdd);
1260
+ }
1261
+
1160
1262
  /**
1161
1263
  * Injects <PlatformAdditionalPages /> into the layout JSX tree after <main>.
1162
1264
  * This component handles all additionalPages visual-editing markers correctly