@decantr/cli 1.4.0 → 1.5.2

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.
@@ -120,6 +120,12 @@ function composeSections(composeEntries, archetypeResults, overrides) {
120
120
  }
121
121
  }
122
122
  }
123
+ const primaryShell = sections.find((s) => s.role === "primary")?.shell || defaultShell;
124
+ for (const section of sections) {
125
+ if (section.shell === "inherit") {
126
+ section.shell = primaryShell;
127
+ }
128
+ }
123
129
  let features = [...new Set(allFeatures)];
124
130
  if (overrides?.features_add) {
125
131
  for (const f of overrides.features_add) {
@@ -292,11 +298,11 @@ function generateTokensCSS(themeData, mode) {
292
298
  "--d-radius-lg": "0.75rem",
293
299
  "--d-radius-xl": "1rem",
294
300
  "--d-radius-full": "9999px",
295
- // Shadows
296
- "--d-shadow-sm": "0 1px 2px rgba(0,0,0,0.05)",
297
- "--d-shadow": "0 1px 3px rgba(0,0,0,0.1)",
298
- "--d-shadow-md": "0 4px 6px rgba(0,0,0,0.1)",
299
- "--d-shadow-lg": "0 10px 15px rgba(0,0,0,0.1)",
301
+ // Shadows — dark mode needs higher opacity to be visible on dark backgrounds
302
+ "--d-shadow-sm": tokenMode === "light" ? "0 1px 2px rgba(0,0,0,0.05)" : "0 1px 2px rgba(0,0,0,0.2)",
303
+ "--d-shadow": tokenMode === "light" ? "0 1px 3px rgba(0,0,0,0.1)" : "0 1px 3px rgba(0,0,0,0.25)",
304
+ "--d-shadow-md": tokenMode === "light" ? "0 4px 6px rgba(0,0,0,0.1)" : "0 4px 6px rgba(0,0,0,0.3)",
305
+ "--d-shadow-lg": tokenMode === "light" ? "0 10px 15px rgba(0,0,0,0.1)" : "0 10px 15px rgba(0,0,0,0.4)",
300
306
  // Status colors
301
307
  "--d-success": themeData.tokens?.base?.success || "#22c55e",
302
308
  "--d-error": themeData.tokens?.base?.danger || "#ef4444",
@@ -313,7 +319,19 @@ ${lines}
313
319
  `;
314
320
  if (mode === "auto") {
315
321
  const lightTokens = buildTokens("light");
316
- const paletteKeys = ["--d-bg", "--d-surface", "--d-surface-raised", "--d-border", "--d-text", "--d-text-muted", "--d-primary-hover"];
322
+ const paletteKeys = [
323
+ "--d-bg",
324
+ "--d-surface",
325
+ "--d-surface-raised",
326
+ "--d-border",
327
+ "--d-text",
328
+ "--d-text-muted",
329
+ "--d-primary-hover",
330
+ "--d-shadow-sm",
331
+ "--d-shadow",
332
+ "--d-shadow-md",
333
+ "--d-shadow-lg"
334
+ ];
317
335
  const lightLines = Object.entries(lightTokens).filter(([key]) => paletteKeys.includes(key)).map(([key, value]) => ` ${key}: ${value};`).join("\n");
318
336
  css += `
319
337
  @media (prefers-color-scheme: light) {
@@ -351,17 +369,132 @@ function generateDecoratorsCSS(recipeData, themeName) {
351
369
  `);
352
370
  return css.join("\n");
353
371
  }
372
+ function generateGlobalCSS(personality) {
373
+ const personalityText = personality.join(" ").toLowerCase();
374
+ let fontBody = "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
375
+ if (personalityText.includes("inter")) {
376
+ fontBody = `'Inter', ${fontBody}`;
377
+ } else if (personalityText.includes("geist")) {
378
+ fontBody = `'Geist', ${fontBody}`;
379
+ }
380
+ return `/* Generated by @decantr/cli \u2014 global reset + body styles */
381
+
382
+ *, *::before, *::after {
383
+ box-sizing: border-box;
384
+ margin: 0;
385
+ padding: 0;
386
+ }
387
+
388
+ html {
389
+ color-scheme: dark;
390
+ -webkit-font-smoothing: antialiased;
391
+ -moz-osx-font-smoothing: grayscale;
392
+ text-rendering: optimizeLegibility;
393
+ }
394
+
395
+ body {
396
+ font-family: ${fontBody};
397
+ background: var(--d-bg);
398
+ color: var(--d-text);
399
+ line-height: 1.6;
400
+ min-height: 100dvh;
401
+ }
402
+
403
+ img, picture, video, canvas, svg {
404
+ display: block;
405
+ max-width: 100%;
406
+ }
407
+
408
+ input, button, textarea, select {
409
+ font: inherit;
410
+ color: inherit;
411
+ }
412
+
413
+ :focus-visible {
414
+ outline: 2px solid var(--d-primary);
415
+ outline-offset: 2px;
416
+ }
417
+
418
+ .sr-only {
419
+ position: absolute;
420
+ width: 1px;
421
+ height: 1px;
422
+ padding: 0;
423
+ margin: -1px;
424
+ overflow: hidden;
425
+ clip: rect(0, 0, 0, 0);
426
+ white-space: nowrap;
427
+ border-width: 0;
428
+ }
429
+ `;
430
+ }
431
+ function generateDecoratorsContext(recipeData, recipeName) {
432
+ const lines = [];
433
+ lines.push(`# Recipe Decorators: ${recipeName}`);
434
+ lines.push("");
435
+ lines.push("## Available Classes");
436
+ lines.push("");
437
+ if (recipeData?.decorators && Object.keys(recipeData.decorators).length > 0) {
438
+ lines.push("| Decorator | Description |");
439
+ lines.push("|-----------|-------------|");
440
+ for (const [name, description] of Object.entries(recipeData.decorators)) {
441
+ lines.push(`| ${name} | ${description} |`);
442
+ }
443
+ } else {
444
+ lines.push("No decorators defined.");
445
+ }
446
+ lines.push("");
447
+ lines.push("## Usage");
448
+ lines.push("");
449
+ lines.push("Decorators are plain CSS class names from `src/styles/decorators.css`. Combine with atoms:");
450
+ lines.push("");
451
+ lines.push("```tsx");
452
+ lines.push("<div className={css('_flex _col _gap4') + ' " + recipeName + "-card'}>");
453
+ lines.push(" <pre className={css('_p3') + ' " + recipeName + "-code'}>{code}</pre>");
454
+ lines.push("</div>");
455
+ lines.push("```");
456
+ lines.push("");
457
+ lines.push("Atoms use `css()` function. Decorators are plain class strings. Combined via string concatenation.");
458
+ lines.push("");
459
+ return lines.join("\n");
460
+ }
354
461
  function generateDecoratorRule(name, description) {
355
462
  const rules = [];
356
463
  const descLower = description.toLowerCase();
357
- if (descLower.includes("surface background") || descLower.includes("surface elevation")) {
464
+ const nameLower = name.toLowerCase();
465
+ const isCard = descLower.includes("card") || descLower.includes("panel") || nameLower.includes("card") || nameLower.includes("panel");
466
+ const isInput = descLower.includes("input") || descLower.includes("field") || descLower.includes("textarea") || nameLower.includes("input") || nameLower.includes("textarea");
467
+ const isGlass = descLower.includes("glassmorphic") || descLower.includes("glass") || nameLower.includes("glass");
468
+ const isInteractive = isCard || isInput || isGlass;
469
+ const isNonInteractive = descLower.includes("divider") || descLower.includes("skeleton") || descLower.includes("keyframe") || descLower.includes("canvas") || nameLower.includes("divider") || nameLower.includes("skeleton") || nameLower.includes("canvas");
470
+ if (descLower.includes("monospace") || descLower.includes("mono font")) {
471
+ rules.push("font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace");
472
+ }
473
+ if (descLower.includes("surface-raised") || descLower.includes("surface raised")) {
474
+ rules.push("background: var(--d-surface-raised)");
475
+ } else if (descLower.includes("surface background") || descLower.includes("surface elevation")) {
358
476
  rules.push("background: var(--d-surface)");
359
477
  } else if (descLower.includes("background") && descLower.includes("theme")) {
360
478
  rules.push("background: var(--d-bg)");
361
479
  } else if (descLower.includes("primary-tinted") || descLower.includes("primary background")) {
362
480
  rules.push("background: color-mix(in srgb, var(--d-primary) 15%, var(--d-surface))");
363
481
  }
364
- if (descLower.includes("1px border") || descLower.includes("subtle border")) {
482
+ if (isInput) {
483
+ if (!rules.some((r) => r.startsWith("background"))) {
484
+ rules.push("background: var(--d-surface)");
485
+ }
486
+ rules.push("color: var(--d-text)");
487
+ rules.push("padding: 0.5rem 0.75rem");
488
+ rules.push("border-radius: var(--d-radius)");
489
+ rules.push("width: 100%");
490
+ rules.push("outline: none");
491
+ }
492
+ const leftBorderMatch = descLower.match(/(\d+)px\s+left\s+border/);
493
+ if (leftBorderMatch) {
494
+ rules.push(`border-left: ${leftBorderMatch[1]}px solid var(--d-primary)`);
495
+ } else if (descLower.includes("left border")) {
496
+ rules.push("border-left: 3px solid var(--d-primary)");
497
+ } else if (descLower.includes("1px border") || descLower.includes("subtle border")) {
365
498
  rules.push("border: 1px solid var(--d-border)");
366
499
  } else if (descLower.includes("border") && !descLower.includes("radius")) {
367
500
  rules.push("border: 1px solid var(--d-border)");
@@ -369,23 +502,40 @@ function generateDecoratorRule(name, description) {
369
502
  const radiusMatch = descLower.match(/(\d+)px radius/);
370
503
  if (radiusMatch) {
371
504
  rules.push(`border-radius: ${radiusMatch[1]}px`);
372
- } else if (descLower.includes("radius") || descLower.includes("rounded")) {
505
+ } else if ((descLower.includes("radius") || descLower.includes("rounded")) && !isInput) {
373
506
  rules.push("border-radius: var(--d-radius)");
374
507
  }
375
- if (descLower.includes("hover shadow") || descLower.includes("shadow transition")) {
376
- rules.push("transition: box-shadow 0.15s ease");
377
- }
378
508
  if (descLower.includes("elevation") || descLower.includes("shadow")) {
379
509
  rules.push("box-shadow: var(--d-shadow)");
380
510
  }
511
+ if (isInteractive && !isNonInteractive) {
512
+ rules.push("transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease");
513
+ }
381
514
  if (descLower.includes("entrance animation") || descLower.includes("fade")) {
382
515
  rules.push("animation: decantr-fade-in 0.2s ease-out");
383
516
  }
384
517
  if (descLower.includes("pulse animation") || descLower.includes("skeleton")) {
385
518
  rules.push("animation: decantr-pulse 1.5s ease-in-out infinite");
386
519
  }
387
- if (descLower.includes("blur") || descLower.includes("glass")) {
520
+ const blurMatch = descLower.match(/blur\((\d+)px\)/);
521
+ if (blurMatch) {
522
+ rules.push(`backdrop-filter: blur(${blurMatch[1]}px)`);
523
+ rules.push(`-webkit-backdrop-filter: blur(${blurMatch[1]}px)`);
524
+ } else if (isGlass) {
525
+ rules.push("backdrop-filter: blur(12px)");
526
+ rules.push("-webkit-backdrop-filter: blur(12px)");
527
+ } else if (descLower.includes("blur")) {
388
528
  rules.push("backdrop-filter: blur(8px)");
529
+ rules.push("-webkit-backdrop-filter: blur(8px)");
530
+ }
531
+ if (descLower.includes("semi-transparent") || descLower.includes("glassmorphic")) {
532
+ const bgIdx = rules.findIndex((r) => r.startsWith("background:"));
533
+ const rgbaBg = "background: rgba(31, 31, 35, 0.8)";
534
+ if (bgIdx !== -1) {
535
+ rules[bgIdx] = rgbaBg;
536
+ } else {
537
+ rules.push(rgbaBg);
538
+ }
389
539
  }
390
540
  if (descLower.includes("right-aligned")) {
391
541
  rules.push("margin-left: auto");
@@ -397,12 +547,45 @@ function generateDecoratorRule(name, description) {
397
547
  rules.push("border-radius: var(--d-radius-lg)");
398
548
  rules.push("max-width: 80%");
399
549
  }
550
+ if (descLower.includes("monospace") || descLower.includes("code")) {
551
+ if (!rules.some((r) => r.startsWith("padding"))) {
552
+ rules.push("padding: 0.75rem 1rem");
553
+ }
554
+ if (!rules.some((r) => r.startsWith("border-radius"))) {
555
+ rules.push("border-radius: var(--d-radius-sm)");
556
+ }
557
+ rules.push("overflow-x: auto");
558
+ }
400
559
  if (rules.length === 0) {
401
560
  return `/* .${name}: ${description} */`;
402
561
  }
403
- return `.${name} {
562
+ let css = `.${name} {
404
563
  ${rules.join(";\n ")};
405
564
  }`;
565
+ if (isInteractive && !isNonInteractive) {
566
+ const stateRules = [];
567
+ if (isCard || isGlass) {
568
+ stateRules.push(`.${name}:hover {
569
+ border-color: var(--d-primary-hover, var(--d-border));
570
+ box-shadow: var(--d-shadow-md);
571
+ }`);
572
+ }
573
+ if (isInput) {
574
+ stateRules.push(`.${name}:focus {
575
+ border-color: var(--d-primary);
576
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--d-primary) 25%, transparent);
577
+ }`);
578
+ stateRules.push(`.${name}::placeholder {
579
+ color: var(--d-text-muted);
580
+ }`);
581
+ stateRules.push(`.${name}:disabled {
582
+ opacity: 0.5;
583
+ cursor: not-allowed;
584
+ }`);
585
+ }
586
+ css += "\n\n" + stateRules.join("\n\n");
587
+ }
588
+ return css;
406
589
  }
407
590
  function serializeLayoutItem(item) {
408
591
  if (typeof item === "string") {
@@ -833,6 +1016,32 @@ The \`css()\` function processes atom strings and injects CSS at runtime:
833
1016
  | \`_trans\` | \`transition:all 0.15s ease\` |
834
1017
  | \`_visible\`, \`_invisible\` | visibility |
835
1018
 
1019
+ ### Using Recipe Decorators
1020
+
1021
+ Recipe decorators (from \`src/styles/decorators.css\`) are regular CSS class names, NOT atoms. They are applied directly as class names and combined with atoms using string concatenation:
1022
+
1023
+ \`\`\`tsx
1024
+ // Atoms use css() function, decorators are plain class names
1025
+ <div className={css('_flex _col _gap4') + ' carbon-card'}>
1026
+ <div className={css('_p4') + ' carbon-glass'}>
1027
+ <pre className={css('_p3') + ' carbon-code'}>{code}</pre>
1028
+ </div>
1029
+ </div>
1030
+ \`\`\`
1031
+
1032
+ **Key difference:**
1033
+ - Atoms: \`css('_flex _col _gap4')\` \u2014 processed by @decantr/css runtime
1034
+ - Decorators: \`'carbon-card'\`, \`'carbon-glass'\` \u2014 plain CSS classes from decorators.css
1035
+ - Combined: \`css('_flex _col') + ' carbon-card'\`
1036
+
1037
+ ### Routing
1038
+
1039
+ Check \`decantr.essence.json\` \u2192 \`meta.platform.routing\` for the routing strategy:
1040
+ - \`"hash"\` \u2192 use \`HashRouter\` (e.g., for static hosting, GitHub Pages)
1041
+ - \`"history"\` \u2192 use \`BrowserRouter\` (e.g., for server-rendered apps)
1042
+
1043
+ Routes are defined in \`decantr.essence.json\` \u2192 \`blueprint.routes\` and listed in \`.decantr/context/scaffold.md\`.
1044
+
836
1045
  ### CSS Architecture
837
1046
 
838
1047
  The CSS is organized into two parts:
@@ -925,33 +1134,29 @@ function generateTaskContext(templateName, essence) {
925
1134
  };
926
1135
  return renderTemplate(template, vars);
927
1136
  }
928
- function generateEssenceSummary(essence) {
929
- const template = loadTemplate("essence-summary.md.template");
930
- const pagesTable = `| Page | Shell | Layout |
931
- |------|-------|--------|
932
- ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.map(serializeLayoutItem).join(", ") || "none"} |`).join("\n")}`;
933
- const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
934
- const dnaEnforcement = essence.guard.enforce_style ? "error" : "off";
935
- const blueprintEnforcement = essence.guard.enforce_recipe ? "warn" : "off";
1137
+ function generateTaskContextV3(templateName, essence) {
1138
+ const template = loadTemplate(templateName);
1139
+ const pages = essence.blueprint.sections && essence.blueprint.sections.length > 0 ? essence.blueprint.sections.flatMap((s) => s.pages) : essence.blueprint.pages || [];
1140
+ const defaultShell = essence.blueprint.sections?.[0]?.shell || essence.blueprint.shell || "sidebar-main";
1141
+ const layout = pages[0]?.layout?.map(serializeLayoutItem).join(", ") || "none";
1142
+ const scaffoldStructure = pages.map((p) => {
1143
+ const patterns = p.layout.length > 0 ? `
1144
+ - Patterns: ${p.layout.map(serializeLayoutItem).join(", ")}` : "";
1145
+ return `- **${p.id}** (${defaultShell})${patterns}`;
1146
+ }).join("\n");
1147
+ const densityLevel = essence.dna.spacing?.density || "comfortable";
1148
+ const contentGap = essence.dna.spacing?.content_gap || "_gap4";
936
1149
  const vars = {
937
- ARCHETYPE: essence.archetype || "custom",
938
- BLUEPRINT: essence.blueprint || "none",
939
- PERSONALITY: essence.personality.join(", "),
940
- TARGET: essence.target,
941
- THEME_STYLE: essence.theme.style,
942
- THEME_MODE: essence.theme.mode,
943
- THEME_RECIPE: essence.theme.recipe,
944
- SHAPE: essence.theme.shape,
945
- PAGES_TABLE: pagesTable,
946
- FEATURES_LIST: featuresList,
947
- GUARD_MODE: essence.guard.mode,
948
- ENFORCE_STYLE: String(essence.guard.enforce_style),
949
- ENFORCE_RECIPE: String(essence.guard.enforce_recipe),
950
- DNA_ENFORCEMENT: dnaEnforcement,
951
- BLUEPRINT_ENFORCEMENT: blueprintEnforcement,
952
- DENSITY: essence.density.level,
953
- CONTENT_GAP: essence.density.content_gap,
954
- LAST_UPDATED: (/* @__PURE__ */ new Date()).toISOString()
1150
+ TARGET: essence.meta.target || "react",
1151
+ THEME_STYLE: essence.dna.theme.style,
1152
+ THEME_MODE: essence.dna.theme.mode,
1153
+ THEME_RECIPE: essence.dna.theme.recipe || essence.dna.theme.style,
1154
+ DEFAULT_SHELL: defaultShell,
1155
+ GUARD_MODE: essence.meta.guard.mode,
1156
+ LAYOUT: layout,
1157
+ DENSITY: densityLevel,
1158
+ CONTENT_GAP: contentGap,
1159
+ SCAFFOLD_STRUCTURE: scaffoldStructure
955
1160
  };
956
1161
  return renderTemplate(template, vars);
957
1162
  }
@@ -1035,15 +1240,6 @@ async function scaffoldProject(projectRoot, options, detected, registry, archety
1035
1240
  const scaffoldPath = join(contextDir, "task-scaffold.md");
1036
1241
  writeFileSync(scaffoldPath, generateTaskContext("task-scaffold.md.template", essence));
1037
1242
  contextFiles.push(scaffoldPath);
1038
- const addPagePath = join(contextDir, "task-add-page.md");
1039
- writeFileSync(addPagePath, generateTaskContext("task-add-page.md.template", essence));
1040
- contextFiles.push(addPagePath);
1041
- const modifyPath = join(contextDir, "task-modify.md");
1042
- writeFileSync(modifyPath, generateTaskContext("task-modify.md.template", essence));
1043
- contextFiles.push(modifyPath);
1044
- const summaryPath = join(contextDir, "essence-summary.md");
1045
- writeFileSync(summaryPath, generateEssenceSummary(essence));
1046
- contextFiles.push(summaryPath);
1047
1243
  if (composedSections) {
1048
1244
  essenceV3.version = "3.1.0";
1049
1245
  essenceV3.blueprint = {
@@ -1052,7 +1248,7 @@ async function scaffoldProject(projectRoot, options, detected, registry, archety
1052
1248
  routes: routeMap || {}
1053
1249
  };
1054
1250
  if (blueprintData?.personality?.length) {
1055
- essenceV3.dna.personality = blueprintData.personality;
1251
+ essenceV3.dna.personality = typeof blueprintData.personality === "string" ? [blueprintData.personality] : blueprintData.personality;
1056
1252
  }
1057
1253
  if (blueprintData?.design_constraints) {
1058
1254
  essenceV3.dna.constraints = blueprintData.design_constraints;
@@ -1065,7 +1261,7 @@ async function scaffoldProject(projectRoot, options, detected, registry, archety
1065
1261
  }
1066
1262
  writeFileSync(essencePath, JSON.stringify(essenceV3, null, 2) + "\n");
1067
1263
  }
1068
- const refreshResult = await refreshDerivedFiles(projectRoot, essenceV3, registry);
1264
+ const refreshResult = await refreshDerivedFiles(projectRoot, essenceV3, registry, themeData, recipeData, { isInitialScaffold: true });
1069
1265
  contextFiles.push(...refreshResult.contextFiles);
1070
1266
  const gitignoreUpdated = updateGitignore(projectRoot);
1071
1267
  return {
@@ -1267,7 +1463,7 @@ When available, use these tools:
1267
1463
  gitignoreUpdated
1268
1464
  };
1269
1465
  }
1270
- async function refreshDerivedFiles(projectRoot, essence, registry) {
1466
+ async function refreshDerivedFiles(projectRoot, essence, registry, prefetchedThemeData, prefetchedRecipeData, options) {
1271
1467
  const decantrDir = join(projectRoot, ".decantr");
1272
1468
  const contextDir = join(decantrDir, "context");
1273
1469
  mkdirSync(contextDir, { recursive: true });
@@ -1281,12 +1477,13 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1281
1477
  blueprint_enforcement: essence.meta.guard.blueprint_enforcement || "warn"
1282
1478
  };
1283
1479
  const personality = essence.dna.personality || [];
1284
- let themeData;
1285
- let recipeData;
1286
- try {
1480
+ let themeData = prefetchedThemeData;
1481
+ let recipeData = prefetchedRecipeData;
1482
+ if (!themeData) try {
1287
1483
  const themeResult = await registry.fetchTheme(themeName);
1288
1484
  if (themeResult?.data) {
1289
- const t = themeResult.data;
1485
+ const raw = themeResult.data;
1486
+ const t = raw.data ?? raw;
1290
1487
  themeData = {
1291
1488
  seed: t.seed,
1292
1489
  palette: t.palette,
@@ -1298,18 +1495,66 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1298
1495
  }
1299
1496
  } catch {
1300
1497
  }
1301
- try {
1498
+ if (!recipeData) try {
1302
1499
  const recipeResult = await registry.fetchRecipe(recipeName);
1303
1500
  if (recipeResult?.data) {
1304
- const r = recipeResult.data;
1501
+ const raw = recipeResult.data;
1502
+ const r = raw.data ?? raw;
1305
1503
  recipeData = {
1306
1504
  decorators: r.decorators,
1307
1505
  spatial_hints: r.spatial_hints,
1308
1506
  radius_hints: r.radius_hints
1309
1507
  };
1508
+ if (!recipeData.decorators && raw.data) {
1509
+ const inner = raw.data;
1510
+ if (inner.decorators) {
1511
+ recipeData.decorators = inner.decorators;
1512
+ recipeData.spatial_hints = inner.spatial_hints;
1513
+ recipeData.radius_hints = inner.radius_hints;
1514
+ }
1515
+ }
1310
1516
  }
1311
1517
  } catch {
1312
1518
  }
1519
+ if (!recipeData?.decorators || Object.keys(recipeData.decorators).length === 0) {
1520
+ try {
1521
+ const apiUrl = registry.apiUrl || "https://api.decantr.ai/v1";
1522
+ const resp = await fetch(`${apiUrl}/recipes/@official/${recipeName}`);
1523
+ if (resp.ok) {
1524
+ const apiData = await resp.json();
1525
+ const inner = apiData.data ?? apiData;
1526
+ if (inner.decorators && Object.keys(inner.decorators).length > 0) {
1527
+ recipeData = {
1528
+ decorators: inner.decorators,
1529
+ spatial_hints: inner.spatial_hints,
1530
+ radius_hints: inner.radius_hints
1531
+ };
1532
+ }
1533
+ }
1534
+ } catch {
1535
+ }
1536
+ }
1537
+ if (!themeData?.seed?.primary) {
1538
+ try {
1539
+ const apiUrl = registry.apiUrl || "https://api.decantr.ai/v1";
1540
+ const resp = await fetch(`${apiUrl}/themes/@official/${themeName}`);
1541
+ if (resp.ok) {
1542
+ const apiData = await resp.json();
1543
+ const inner = apiData.data ?? apiData;
1544
+ if (inner.seed) {
1545
+ themeData = {
1546
+ seed: inner.seed,
1547
+ palette: inner.palette,
1548
+ cvd_support: inner.cvd_support,
1549
+ tokens: inner.tokens,
1550
+ typography_hints: inner.typography_hints,
1551
+ motion_hints: inner.motion_hints
1552
+ };
1553
+ }
1554
+ }
1555
+ } catch {
1556
+ }
1557
+ }
1313
1558
  const stylesDir = join(projectRoot, "src", "styles");
1314
1559
  mkdirSync(stylesDir, { recursive: true });
1315
1560
  const tokensPath = join(stylesDir, "tokens.css");
@@ -1322,15 +1567,39 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1322
1567
  if (hasRealRecipeData || !existsSync(decoratorsPath)) {
1323
1568
  writeFileSync(decoratorsPath, generateDecoratorsCSS(recipeData, themeName));
1324
1569
  }
1325
- const cssFiles = [tokensPath, decoratorsPath];
1570
+ const globalPath = join(stylesDir, "global.css");
1571
+ if (!existsSync(globalPath)) {
1572
+ writeFileSync(globalPath, generateGlobalCSS(personality));
1573
+ }
1574
+ const cssFiles = [tokensPath, decoratorsPath, globalPath];
1575
+ const decoratorsMdPath = join(contextDir, "decorators.md");
1576
+ writeFileSync(decoratorsMdPath, generateDecoratorsContext(recipeData, recipeName));
1326
1577
  const decantrMdPath = join(projectRoot, "DECANTR.md");
1327
1578
  writeFileSync(decantrMdPath, generateDecantrMdV31(guardMode, CSS_APPROACH_CONTENT));
1328
- const summaryPath = join(contextDir, "essence-summary.md");
1329
- writeFileSync(summaryPath, generateEssenceSummaryV3(essence));
1330
- const contextFiles = [summaryPath];
1579
+ const hasSections = essence.blueprint.sections && essence.blueprint.sections.length > 0;
1580
+ const contextFiles = [decoratorsMdPath];
1581
+ if (!hasSections) {
1582
+ const summaryPath = join(contextDir, "essence-summary.md");
1583
+ writeFileSync(summaryPath, generateEssenceSummaryV3(essence));
1584
+ contextFiles.push(summaryPath);
1585
+ }
1586
+ if (!options?.isInitialScaffold) {
1587
+ const addPagePath = join(contextDir, "task-add-page.md");
1588
+ writeFileSync(addPagePath, generateTaskContextV3("task-add-page.md.template", essence));
1589
+ contextFiles.push(addPagePath);
1590
+ const modifyPath = join(contextDir, "task-modify.md");
1591
+ writeFileSync(modifyPath, generateTaskContextV3("task-modify.md.template", essence));
1592
+ contextFiles.push(modifyPath);
1593
+ }
1331
1594
  const blueprint = essence.blueprint;
1332
1595
  const sections = blueprint.sections && blueprint.sections.length > 0 ? blueprint.sections : [];
1333
1596
  if (sections.length > 0) {
1597
+ const primarySectionShell = sections.find((s) => s.role === "primary")?.shell || "sidebar-main";
1598
+ for (const section of sections) {
1599
+ if (section.shell === "inherit") {
1600
+ section.shell = primarySectionShell;
1601
+ }
1602
+ }
1334
1603
  const patternSpecs = {};
1335
1604
  const seenPatterns = /* @__PURE__ */ new Set();
1336
1605
  for (const section of sections) {
@@ -1347,13 +1616,36 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1347
1616
  const inner = raw.data ?? raw;
1348
1617
  const defaultPreset = inner.default_preset || "standard";
1349
1618
  const preset = inner.presets?.[defaultPreset];
1350
- patternSpecs[name] = {
1619
+ let slots = preset?.layout?.slots || {};
1620
+ if (Object.keys(slots).length === 0) {
1621
+ const synthetic = generateSyntheticSlots(name, inner.description || "");
1622
+ if (Object.keys(synthetic).length > 0) {
1623
+ slots = synthetic;
1624
+ }
1625
+ }
1626
+ const spec = {
1351
1627
  description: inner.description || "",
1352
1628
  components: inner.components || [],
1353
- slots: preset?.layout?.slots || {}
1629
+ slots
1354
1630
  };
1631
+ if (!spec.components || spec.components.length === 0) {
1632
+ const syntheticComps = generateSyntheticComponents(name, spec.description);
1633
+ if (syntheticComps.length > 0) spec.components = syntheticComps;
1634
+ }
1635
+ patternSpecs[name] = spec;
1636
+ } else {
1637
+ const synthetic = generateSyntheticSlots(name, "");
1638
+ const syntheticComps = generateSyntheticComponents(name, "");
1639
+ if (Object.keys(synthetic).length > 0 || syntheticComps.length > 0) {
1640
+ patternSpecs[name] = { description: "", components: syntheticComps, slots: synthetic };
1641
+ }
1355
1642
  }
1356
1643
  } catch {
1644
+ const synthetic = generateSyntheticSlots(name, "");
1645
+ const syntheticComps = generateSyntheticComponents(name, "");
1646
+ if (Object.keys(synthetic).length > 0 || syntheticComps.length > 0) {
1647
+ patternSpecs[name] = { description: "", components: syntheticComps, slots: synthetic };
1648
+ }
1357
1649
  }
1358
1650
  }
1359
1651
  }
@@ -1423,8 +1715,17 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1423
1715
  }
1424
1716
  }
1425
1717
  for (const section of sections) {
1426
- const zoneLabel = section.role === "primary" || section.role === "auxiliary" ? "App" : section.role.charAt(0).toUpperCase() + section.role.slice(1);
1427
- const zoneContext = `This section is in the **${zoneLabel}** zone (${section.shell} shell).` + (topologyMarkdown ? "\n\n" + topologyMarkdown : "");
1718
+ const zoneLabel = section.role === "primary" || section.role === "auxiliary" ? "App" : section.role === "gateway" ? "Gateway" : "Public";
1719
+ let zoneContext = `**Zone:** ${zoneLabel} (${section.role}) \u2014 ${section.shell} shell`;
1720
+ if (section.role === "gateway") {
1721
+ zoneContext += "\nAuth success \u2192 enters App zone. Sign out returns here.";
1722
+ } else if (section.role === "primary") {
1723
+ zoneContext += "\nAuthenticated users land here. Sign out \u2192 Gateway (/login).";
1724
+ } else if (section.role === "public") {
1725
+ zoneContext += "\nAnonymous visitors. CTAs lead to Gateway (/login, /register).";
1726
+ } else if (section.role === "auxiliary") {
1727
+ zoneContext += "\nSupporting section within App zone. Shares navigation with primary.";
1728
+ }
1428
1729
  const sectionPatterns = {};
1429
1730
  for (const page of section.pages) {
1430
1731
  for (const item of page.layout) {
@@ -1496,13 +1797,36 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1496
1797
  const inner = raw.data ?? raw;
1497
1798
  const defaultPreset = inner.default_preset || "standard";
1498
1799
  const preset = inner.presets?.[defaultPreset];
1499
- patternSpecs[name] = {
1800
+ let slots = preset?.layout?.slots || {};
1801
+ if (Object.keys(slots).length === 0) {
1802
+ const synthetic = generateSyntheticSlots(name, inner.description || "");
1803
+ if (Object.keys(synthetic).length > 0) {
1804
+ slots = synthetic;
1805
+ }
1806
+ }
1807
+ const spec = {
1500
1808
  description: inner.description || "",
1501
1809
  components: inner.components || [],
1502
- slots: preset?.layout?.slots || {}
1810
+ slots
1503
1811
  };
1812
+ if (!spec.components || spec.components.length === 0) {
1813
+ const syntheticComps = generateSyntheticComponents(name, spec.description);
1814
+ if (syntheticComps.length > 0) spec.components = syntheticComps;
1815
+ }
1816
+ patternSpecs[name] = spec;
1817
+ } else {
1818
+ const synthetic = generateSyntheticSlots(name, "");
1819
+ const syntheticComps = generateSyntheticComponents(name, "");
1820
+ if (Object.keys(synthetic).length > 0 || syntheticComps.length > 0) {
1821
+ patternSpecs[name] = { description: "", components: syntheticComps, slots: synthetic };
1822
+ }
1504
1823
  }
1505
1824
  } catch {
1825
+ const synthetic = generateSyntheticSlots(name, "");
1826
+ const syntheticComps = generateSyntheticComponents(name, "");
1827
+ if (Object.keys(synthetic).length > 0 || syntheticComps.length > 0) {
1828
+ patternSpecs[name] = { description: "", components: syntheticComps, slots: synthetic };
1829
+ }
1506
1830
  }
1507
1831
  }
1508
1832
  }
@@ -1565,8 +1889,95 @@ async function refreshDerivedFiles(projectRoot, essence, registry) {
1565
1889
  cssFiles
1566
1890
  };
1567
1891
  }
1892
+ function generateSyntheticSlots(patternId, description) {
1893
+ const desc = description.toLowerCase();
1894
+ const syntheticSlots = {};
1895
+ if (patternId.includes("feature") || desc.includes("feature")) {
1896
+ syntheticSlots["grid"] = "Grid of feature cards (icon + title + description)";
1897
+ syntheticSlots["feature-card"] = "Individual feature with icon, heading, and description text";
1898
+ }
1899
+ if (patternId.includes("pricing") || desc.includes("pricing")) {
1900
+ syntheticSlots["tiers"] = "Pricing tier cards (name, price, features list, CTA button)";
1901
+ syntheticSlots["toggle"] = "Monthly/annual billing toggle (optional)";
1902
+ }
1903
+ if (patternId.includes("testimonial") || desc.includes("testimonial")) {
1904
+ syntheticSlots["quotes"] = "Testimonial cards (quote text, author name, role, avatar)";
1905
+ }
1906
+ if (patternId.includes("cta") || desc.includes("call-to-action") || desc.includes("call to action")) {
1907
+ syntheticSlots["headline"] = "CTA headline text";
1908
+ syntheticSlots["description"] = "Supporting description text";
1909
+ syntheticSlots["actions"] = "CTA button(s)";
1910
+ }
1911
+ if (patternId.includes("how-it-works") || desc.includes("how it works") || desc.includes("timeline") || desc.includes("steps")) {
1912
+ syntheticSlots["steps"] = "Numbered steps (step number, title, description)";
1913
+ }
1914
+ if (patternId.includes("team") || desc.includes("team")) {
1915
+ syntheticSlots["members"] = "Team member cards (avatar, name, role)";
1916
+ }
1917
+ if (patternId.includes("story") || desc.includes("story") || desc.includes("about")) {
1918
+ syntheticSlots["content"] = "Story/about narrative text content";
1919
+ }
1920
+ if (patternId.includes("values") || desc.includes("values")) {
1921
+ syntheticSlots["values"] = "Value cards (icon/emoji, title, description)";
1922
+ }
1923
+ if (patternId.includes("form") || desc.includes("form") || desc.includes("contact")) {
1924
+ syntheticSlots["fields"] = "Form fields (name, email, message, etc.)";
1925
+ syntheticSlots["submit"] = "Submit button";
1926
+ }
1927
+ if (patternId.includes("content") || desc.includes("legal") || desc.includes("privacy") || desc.includes("policy")) {
1928
+ syntheticSlots["body"] = "Long-form text content with headings and paragraphs";
1929
+ syntheticSlots["toc"] = "Table of contents sidebar (optional)";
1930
+ }
1931
+ if (patternId.includes("settings") || desc.includes("settings") || desc.includes("preferences")) {
1932
+ syntheticSlots["sections"] = "Settings sections (label, description, input/toggle)";
1933
+ }
1934
+ if (patternId.includes("security") || desc.includes("security") || desc.includes("password")) {
1935
+ syntheticSlots["sections"] = "Security sections (password change, MFA toggle, session list)";
1936
+ }
1937
+ if (patternId.includes("session") || desc.includes("session")) {
1938
+ syntheticSlots["list"] = "Active sessions list (device, location, last active, revoke button)";
1939
+ }
1940
+ if (patternId.includes("message") || desc.includes("message") || desc.includes("chat")) {
1941
+ syntheticSlots["messages"] = "Message bubbles (user/assistant, content, timestamp)";
1942
+ }
1943
+ if (patternId.includes("input") && desc.includes("chat")) {
1944
+ syntheticSlots["textarea"] = "Auto-expanding message input";
1945
+ syntheticSlots["actions"] = "Attach file button, send button";
1946
+ }
1947
+ if (patternId.includes("empty") || desc.includes("empty")) {
1948
+ syntheticSlots["illustration"] = "Empty state illustration or icon";
1949
+ syntheticSlots["message"] = "Welcome/empty state message";
1950
+ syntheticSlots["suggestions"] = "Suggested actions or prompts";
1951
+ }
1952
+ if (patternId.includes("header") && desc.includes("chat")) {
1953
+ syntheticSlots["title"] = "Conversation title or model name";
1954
+ syntheticSlots["actions"] = "Header action buttons (new chat, settings)";
1955
+ }
1956
+ return syntheticSlots;
1957
+ }
1958
+ function generateSyntheticComponents(patternId, description) {
1959
+ const desc = description.toLowerCase();
1960
+ const syntheticComponents = [];
1961
+ if (patternId.includes("hero")) syntheticComponents.push("Button", "Icon", "Image");
1962
+ if (patternId.includes("feature")) syntheticComponents.push("Card", "Icon", "Text");
1963
+ if (patternId.includes("pricing")) syntheticComponents.push("Card", "Button", "Badge");
1964
+ if (patternId.includes("testimonial")) syntheticComponents.push("Card", "Avatar", "Text");
1965
+ if (patternId.includes("cta")) syntheticComponents.push("Button", "Text");
1966
+ if (patternId.includes("form") || patternId.includes("contact")) syntheticComponents.push("Input", "Textarea", "Button", "Label");
1967
+ if (patternId.includes("team")) syntheticComponents.push("Card", "Avatar", "Text");
1968
+ if (patternId.includes("settings") || patternId.includes("security")) syntheticComponents.push("Card", "Toggle", "Input", "Button");
1969
+ if (patternId.includes("message") || patternId.includes("chat")) syntheticComponents.push("Avatar", "Text", "CodeBlock");
1970
+ if (patternId.includes("input") && desc.includes("chat")) syntheticComponents.push("Textarea", "Button", "Icon");
1971
+ if (patternId.includes("header") && desc.includes("chat")) syntheticComponents.push("Button", "Icon", "Text");
1972
+ if (patternId.includes("content") || patternId.includes("legal")) syntheticComponents.push("Heading", "Text", "List");
1973
+ if (patternId.includes("how-it-works") || patternId.includes("steps")) syntheticComponents.push("Card", "Icon", "Text", "Badge");
1974
+ if (patternId.includes("values")) syntheticComponents.push("Card", "Icon", "Text");
1975
+ if (patternId.includes("story") || patternId.includes("about")) syntheticComponents.push("Text", "Image");
1976
+ if (patternId.includes("empty") || patternId.includes("new")) syntheticComponents.push("Icon", "Text", "Button");
1977
+ return [...new Set(syntheticComponents)];
1978
+ }
1568
1979
  function generateSectionContext(input) {
1569
- const { section, themeTokens, decorators, guardConfig, personality, themeName, recipeName, zoneContext, patternSpecs, recipeHints, constraints, shellInfo } = input;
1980
+ const { section, decorators, guardConfig, personality, themeName, recipeName, zoneContext, patternSpecs, recipeHints, constraints, shellInfo } = input;
1570
1981
  const lines = [];
1571
1982
  lines.push(`# Section: ${section.id}`);
1572
1983
  lines.push("");
@@ -1587,40 +1998,16 @@ function generateSectionContext(input) {
1587
1998
  lines.push("");
1588
1999
  lines.push("---");
1589
2000
  lines.push("");
1590
- lines.push("## Guard Rules");
2001
+ lines.push(`**Guard:** ${guardConfig.mode} mode | DNA violations = ${guardConfig.dna_enforcement} | Blueprint violations = ${guardConfig.blueprint_enforcement}`);
1591
2002
  lines.push("");
1592
- lines.push(`| Rule | Scope | Severity | Description |`);
1593
- lines.push(`|------|-------|----------|-------------|`);
1594
- lines.push(`| Style guard | DNA | ${guardConfig.dna_enforcement} | Code must use the ${themeName} theme |`);
1595
- lines.push(`| Recipe guard | DNA | ${guardConfig.dna_enforcement} | Visual recipe must match ${recipeName} |`);
1596
- lines.push(`| Density guard | DNA | ${guardConfig.dna_enforcement} | Content gap must match essence density |`);
1597
- lines.push(`| Accessibility guard | DNA | ${guardConfig.dna_enforcement} | Must meet WCAG level from essence |`);
1598
- lines.push(`| Structure guard | Blueprint | ${guardConfig.blueprint_enforcement} | Pages must exist in essence structure |`);
1599
- lines.push(`| Layout guard | Blueprint | ${guardConfig.blueprint_enforcement} | Pattern order must match essence layout |`);
1600
- lines.push(`| Pattern existence | Blueprint | ${guardConfig.blueprint_enforcement} | All patterns must exist in registry |`);
1601
- lines.push("");
1602
- lines.push(`**Guard mode:** ${guardConfig.mode}`);
1603
- lines.push("");
1604
- lines.push("---");
1605
- lines.push("");
1606
- lines.push(`## Theme: ${themeName}`);
1607
- lines.push("");
1608
- lines.push("```css");
1609
- lines.push(themeTokens);
1610
- lines.push("```");
1611
- lines.push("");
1612
- lines.push("---");
1613
- lines.push("");
1614
- lines.push(`## Decorators (${recipeName} recipe)`);
2003
+ lines.push(`**Theme tokens:** see \`src/styles/tokens.css\` \u2014 use \`var(--d-primary)\`, \`var(--d-bg)\`, etc.`);
1615
2004
  lines.push("");
1616
2005
  if (decorators.length > 0) {
1617
- lines.push("| Decorator | Description |");
1618
- lines.push("|-----------|-------------|");
1619
- for (const dec of decorators) {
1620
- lines.push(`| ${dec.name} | ${dec.description} |`);
1621
- }
2006
+ const names = decorators.map((d) => `\`${d.name}\``).join(", ");
2007
+ lines.push(`**Decorators:** ${names} (see \`src/styles/decorators.css\`)`);
2008
+ lines.push("Usage: `className={css('_flex _col') + ' carbon-card'}` \u2014 atoms via css(), decorators as plain class strings.");
1622
2009
  } else {
1623
- lines.push("No decorators defined.");
2010
+ lines.push("**Decorators:** none defined.");
1624
2011
  }
1625
2012
  lines.push("");
1626
2013
  if (recipeHints) {
@@ -1638,11 +2025,8 @@ function generateSectionContext(input) {
1638
2025
  lines.push("---");
1639
2026
  lines.push("");
1640
2027
  if (zoneContext) {
1641
- lines.push("## Zone Context");
1642
- lines.push("");
1643
2028
  lines.push(zoneContext);
1644
- lines.push("");
1645
- lines.push("---");
2029
+ lines.push("For full app topology, see `.decantr/context/scaffold.md`");
1646
2030
  lines.push("");
1647
2031
  }
1648
2032
  if (section.features.length > 0) {
@@ -1654,11 +2038,7 @@ function generateSectionContext(input) {
1654
2038
  lines.push("");
1655
2039
  }
1656
2040
  if (personality.length > 0) {
1657
- lines.push("## Personality");
1658
- lines.push("");
1659
- lines.push(personality.join(", "));
1660
- lines.push("");
1661
- lines.push("---");
2041
+ lines.push("**Personality:** See scaffold.md for personality and visual direction.");
1662
2042
  lines.push("");
1663
2043
  }
1664
2044
  if (constraints && Object.keys(constraints).length > 0) {
@@ -1671,6 +2051,34 @@ function generateSectionContext(input) {
1671
2051
  lines.push("---");
1672
2052
  lines.push("");
1673
2053
  }
2054
+ const uniquePatterns = /* @__PURE__ */ new Map();
2055
+ for (const page of section.pages) {
2056
+ const patternNames = page.layout.flatMap(extractPatternNames);
2057
+ for (const name of patternNames) {
2058
+ if (patternSpecs[name] && !uniquePatterns.has(name)) {
2059
+ uniquePatterns.set(name, patternSpecs[name]);
2060
+ }
2061
+ }
2062
+ }
2063
+ if (uniquePatterns.size > 0) {
2064
+ lines.push("## Pattern Reference");
2065
+ lines.push("");
2066
+ for (const [patternName, spec] of uniquePatterns) {
2067
+ lines.push(`### ${patternName}`);
2068
+ lines.push("");
2069
+ lines.push(spec.description);
2070
+ lines.push("");
2071
+ lines.push(`**Components:** ${spec.components.join(", ")}`);
2072
+ lines.push("");
2073
+ lines.push("**Layout slots:**");
2074
+ for (const [slot, desc] of Object.entries(spec.slots)) {
2075
+ lines.push(`- \`${slot}\`: ${desc}`);
2076
+ }
2077
+ lines.push("");
2078
+ }
2079
+ lines.push("---");
2080
+ lines.push("");
2081
+ }
1674
2082
  lines.push("## Pages");
1675
2083
  lines.push("");
1676
2084
  for (const page of section.pages) {
@@ -1691,22 +2099,6 @@ function generateSectionContext(input) {
1691
2099
  }
1692
2100
  lines.push("");
1693
2101
  }
1694
- const patternNames = page.layout.flatMap(extractPatternNames);
1695
- for (const patternName of patternNames) {
1696
- const spec = patternSpecs[patternName];
1697
- if (!spec) continue;
1698
- lines.push(`#### Pattern: ${patternName}`);
1699
- lines.push("");
1700
- lines.push(spec.description);
1701
- lines.push("");
1702
- lines.push(`**Components:** ${spec.components.join(", ")}`);
1703
- lines.push("");
1704
- lines.push("**Layout slots:**");
1705
- for (const [slot, desc] of Object.entries(spec.slots)) {
1706
- lines.push(`- \`${slot}\`: ${desc}`);
1707
- }
1708
- lines.push("");
1709
- }
1710
2102
  }
1711
2103
  return lines.join("\n");
1712
2104
  }
@@ -1925,12 +2317,22 @@ var RegistryClient = class {
1925
2317
  if (customResult) return customResult;
1926
2318
  if (id.startsWith("custom:")) return null;
1927
2319
  if (!this.offline) {
1928
- try {
1929
- const data = await this.apiClient.getContent(contentType, namespace, id);
1930
- saveToCache(this.cacheDir, contentType, id, data, namespace);
1931
- return { data, source: { type: "api", url: this.apiUrl } };
1932
- } catch {
2320
+ for (let attempt = 0; attempt < 2; attempt++) {
2321
+ try {
2322
+ const data = await this.apiClient.getContent(contentType, namespace, id);
2323
+ saveToCache(this.cacheDir, contentType, id, data, namespace);
2324
+ return { data, source: { type: "api", url: this.apiUrl } };
2325
+ } catch (e) {
2326
+ if (process.env.DECANTR_DEBUG) {
2327
+ console.error(` [debug] API fetch ${attempt === 0 ? "failed" : "retry failed"} for ${contentType}/${namespace}/${id}: ${e.message}`);
2328
+ }
2329
+ if (attempt === 0) {
2330
+ await new Promise((r) => setTimeout(r, 500));
2331
+ }
2332
+ }
1933
2333
  }
2334
+ } else if (process.env.DECANTR_DEBUG) {
2335
+ console.error(` [debug] Skipping API (offline mode) for ${contentType}/${namespace}/${id}`);
1934
2336
  }
1935
2337
  return loadFromCache(this.cacheDir, contentType, id, namespace);
1936
2338
  }
@@ -1992,9 +2394,12 @@ async function syncRegistry(cacheDir, apiUrl = DEFAULT_API_URL) {
1992
2394
  const result = await apiClient.listContent(type, { namespace: "@official" });
1993
2395
  saveToCache(cacheDir, type, null, result, "@official");
1994
2396
  for (const item of result.items) {
1995
- const id = item.id || item.slug;
1996
- if (id) {
1997
- saveToCache(cacheDir, type, id, item, "@official");
2397
+ const slug = item.slug;
2398
+ const data = item.data;
2399
+ const innerSlug = data?.id || data?.slug;
2400
+ const cacheKey = slug || innerSlug || item.id;
2401
+ if (cacheKey) {
2402
+ saveToCache(cacheDir, type, cacheKey, item, "@official");
1998
2403
  }
1999
2404
  }
2000
2405
  synced.push(type);