openyida 2026.9.19 → 2026.9.21

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.
Files changed (195) hide show
  1. package/README.md +4 -4
  2. package/lib/app/application-style.js +69 -0
  3. package/lib/app/create-form/args.js +37 -12
  4. package/lib/app/create-form/batch.js +32 -5
  5. package/lib/app/create-form.js +94 -57
  6. package/lib/app/form-field-validator.js +0 -36
  7. package/lib/app/services/form-compiler.js +0 -2
  8. package/lib/app/theme-from-design.js +40 -14
  9. package/lib/app/update-app.js +63 -13
  10. package/lib/core/command-manifest.js +81 -25
  11. package/lib/core/locales/en.js +20 -3
  12. package/lib/core/locales/zh.js +20 -3
  13. package/lib/core/sample.js +36 -14
  14. package/lib/design/document.js +1 -0
  15. package/lib/design-plan/confirmation.js +13 -0
  16. package/lib/design-plan/design-plan.js +44 -2
  17. package/lib/design-plan/entry-navigation.js +10 -2
  18. package/lib/design-plan/init.js +16 -9
  19. package/lib/design-plan/materialize.js +85 -45
  20. package/lib/design-plan/normalize.js +13 -14
  21. package/lib/design-plan/parallel.js +62 -13
  22. package/lib/design-plan/patch.js +21 -7
  23. package/lib/design-plan/rebase.js +60 -0
  24. package/lib/design-plan/themes.js +16 -1
  25. package/lib/design-plan/validate.js +16 -1
  26. package/lib/design-plan/visual-policy.js +29 -4
  27. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +3 -3
  28. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +6 -4
  29. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +5 -5
  30. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -2
  31. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +1 -1
  32. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +5 -4
  33. package/package.json +1 -1
  34. package/yida-skills/SKILL.md +2 -2
  35. package/yida-skills/references/official-example-schema-patterns.md +1 -1
  36. package/yida-skills/references/yida-api.md +4 -0
  37. package/yida-skills/skills/yida-app/SKILL.md +15 -4
  38. package/yida-skills/skills/yida-app/references/entry-navigation.md +11 -9
  39. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +1 -1
  40. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +11 -4
  41. package/yida-skills/skills/yida-app/workflow/step-2-design.md +3 -3
  42. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +2 -2
  43. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +3 -3
  44. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +3 -2
  45. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -2
  46. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +1 -1
  47. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +1 -1
  48. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +140 -1
  49. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +1 -1
  50. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +1 -1
  51. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +7 -0
  52. package/yida-skills/skills/yida-create-form-page/SKILL.md +34 -43
  53. package/yida-skills/skills/yida-create-form-page/references/advanced-form-modes.md +5 -5
  54. package/yida-skills/skills/yida-create-form-page/references/association-form-field.md +4 -4
  55. package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +2 -2
  56. package/yida-skills/skills/yida-create-form-page/references/field-definition-guide.md +4 -16
  57. package/yida-skills/skills/yida-create-form-page/references/form-field-properties.md +14 -37
  58. package/yida-skills/skills/yida-design/SKILL.md +7 -5
  59. package/yida-skills/skills/yida-design/references/application-style-library.md +93 -0
  60. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +48 -7
  61. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +1 -1
  62. package/yida-skills/skills/yida-design/references/native-form-styles.md +208 -0
  63. package/yida-skills/skills/yida-design/references/navigation-decision.md +6 -4
  64. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +110 -1
  65. package/yida-skills/skills/yida-design/references/theme/application-style-recipes.css +58 -0
  66. package/yida-skills/skills/yida-design/references/theme-selection.md +12 -11
  67. package/yida-skills/skills/yida-design/references/visual-decision-engine.md +1 -1
  68. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +49 -6
  69. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +13 -3
  70. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +6 -2
  71. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +5 -3
  72. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-design.md +3 -1
  73. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +22 -23
  74. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +2 -0
  75. package/yida-skills/skills/yida-design/templates/application-styles.json +398 -0
  76. package/yida-skills/skills/yida-design/templates/design-themes/README.md +15 -10
  77. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +748 -0
  78. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +274 -0
  79. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/form-layout.json +15 -0
  80. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +748 -0
  81. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +274 -0
  82. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/form-layout.json +14 -0
  83. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +748 -0
  84. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +274 -0
  85. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/form-layout.json +13 -0
  86. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +748 -0
  87. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +274 -0
  88. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/form-layout.json +14 -0
  89. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +748 -0
  90. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +274 -0
  91. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/form-layout.json +14 -0
  92. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +748 -0
  93. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +274 -0
  94. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/form-layout.json +14 -0
  95. package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +748 -0
  96. package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +274 -0
  97. package/yida-skills/skills/yida-design/templates/design-themes/app-line/form-layout.json +13 -0
  98. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +748 -0
  99. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +274 -0
  100. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/form-layout.json +14 -0
  101. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +754 -0
  102. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +279 -0
  103. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/form-layout.json +13 -0
  104. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +748 -0
  105. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +274 -0
  106. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/form-layout.json +13 -0
  107. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +748 -0
  108. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +274 -0
  109. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/form-layout.json +14 -0
  110. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +748 -0
  111. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +274 -0
  112. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/form-layout.json +14 -0
  113. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +758 -0
  114. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +281 -0
  115. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/form-layout.json +14 -0
  116. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +748 -0
  117. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +274 -0
  118. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/form-layout.json +14 -0
  119. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +748 -0
  120. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +274 -0
  121. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/form-layout.json +14 -0
  122. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +754 -0
  123. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +279 -0
  124. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/form-layout.json +14 -0
  125. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +752 -0
  126. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +278 -0
  127. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/form-layout.json +13 -0
  128. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +748 -0
  129. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +274 -0
  130. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/form-layout.json +14 -0
  131. package/yida-skills/skills/yida-design/templates/design-themes/basic-tokens.json +2 -1
  132. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +690 -0
  133. package/yida-skills/skills/yida-design/templates/design-themes/{dark-inset-hairline.md → dark-inset-hairline/design.md} +107 -58
  134. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/form-layout.json +13 -0
  135. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +691 -0
  136. package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-fine-lines.md → dark-rail-fine-lines/design.md} +106 -50
  137. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/form-layout.json +13 -0
  138. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/app_theme.css +696 -0
  139. package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-signal-panels.md → dark-rail-signal-panels/design.md} +66 -12
  140. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/form-layout.json +13 -0
  141. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +689 -0
  142. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +238 -0
  143. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/form-layout.json +13 -0
  144. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/app_theme.css +692 -0
  145. package/yida-skills/skills/yida-design/templates/design-themes/{graphite-bevel-grid.md → graphite-bevel-grid/design.md} +64 -10
  146. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/form-layout.json +13 -0
  147. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/app_theme.css +691 -0
  148. package/yida-skills/skills/yida-design/templates/design-themes/{hairline-shared-bands.md → hairline-shared-bands/design.md} +66 -12
  149. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/form-layout.json +13 -0
  150. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +691 -0
  151. package/yida-skills/skills/yida-design/templates/design-themes/{hairline-soft-blocks.md → hairline-soft-blocks/design.md} +61 -9
  152. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/form-layout.json +13 -0
  153. package/yida-skills/skills/yida-design/templates/design-themes/index.json +399 -31
  154. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +701 -0
  155. package/yida-skills/skills/yida-design/templates/design-themes/{ink-glow-soft-panels.md → ink-glow-soft-panels/design.md} +62 -8
  156. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/form-layout.json +13 -0
  157. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +691 -0
  158. package/yida-skills/skills/yida-design/templates/design-themes/{inset-frame-pixel-rhythm.md → inset-frame-pixel-rhythm/design.md} +63 -9
  159. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/form-layout.json +13 -0
  160. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +698 -0
  161. package/yida-skills/skills/yida-design/templates/design-themes/{outlined-texture-duotone.md → outlined-texture-duotone/design.md} +66 -12
  162. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/form-layout.json +13 -0
  163. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +691 -0
  164. package/yida-skills/skills/yida-design/templates/design-themes/{soft-inset-surfaces.md → soft-inset-surfaces/design.md} +63 -11
  165. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/form-layout.json +13 -0
  166. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +690 -0
  167. package/yida-skills/skills/yida-design/templates/design-themes/{soft-outline-rhythm.md → soft-outline-rhythm/design.md} +64 -26
  168. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/form-layout.json +13 -0
  169. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +698 -0
  170. package/yida-skills/skills/yida-design/templates/design-themes/{soft-rail-bold-band.md → soft-rail-bold-band/design.md} +103 -49
  171. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/form-layout.json +13 -0
  172. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/app_theme.css +692 -0
  173. package/yida-skills/skills/yida-design/templates/design-themes/{soft-spectral-panels.md → soft-spectral-panels/design.md} +67 -13
  174. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/form-layout.json +13 -0
  175. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +711 -0
  176. package/yida-skills/skills/yida-design/templates/design-themes/{warm-canvas-contrast-panels.md → warm-canvas-contrast-panels/design.md} +64 -10
  177. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/form-layout.json +13 -0
  178. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +692 -0
  179. package/yida-skills/skills/yida-design/templates/design-themes/{warm-rail-muted-panels.md → warm-rail-muted-panels/design.md} +102 -50
  180. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/form-layout.json +13 -0
  181. package/yida-skills/skills/yida-design/templates/navigation-styles.json +769 -0
  182. package/yida-skills/skills/yida-design/workflow/output-design.md +9 -6
  183. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +12 -4
  184. package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +1 -1
  185. package/yida-skills/skills/yida-form-permission/SKILL.md +6 -0
  186. package/yida-skills/skills/yida-nav-shell/SKILL.md +1 -1
  187. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +6 -0
  188. package/yida-skills/skills/yida-page-config/SKILL.md +9 -0
  189. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
  190. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  191. package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +1 -1
  192. package/yida-skills/skills/yida-requirement-analysis/references/experience-groups.md +45 -9
  193. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +5 -5
  194. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +17 -4
  195. package/yida-skills/skills-index.json +2 -2
@@ -9,13 +9,13 @@ const { normalizePlan, assertThemeProfile } = require('./normalize');
9
9
  const { readJson, writeFiles } = require('./files');
10
10
  const { prepareUpdates } = require('./artifact-updates');
11
11
  const { validateAuthoring } = require('./validate');
12
- const { buildEntryNavigation } = require('./entry-navigation');
12
+ const { buildEntryNavigation, ENTRY_MODE_SUMMARIES } = require('./entry-navigation');
13
13
  const { buildPageNavigationPolicy } = require('./navigation-policy');
14
14
  const { buildAssetTasks } = require('../asset/asset-plan');
15
15
  const { mergeParts } = require('./parallel');
16
16
  const { performance } = require('perf_hooks');
17
17
  const { readDesignTokens, applyDesignTokens } = require('../app/theme-from-design');
18
- const { PLAN_SKILL_ROOT, loadThemeIndex, themeTemplatePath, resolveThemeColors, resolveColorToken } = require('./themes');
18
+ const { PLAN_SKILL_ROOT, loadThemeIndex, themeTemplatePath, themeNavTheme, resolveThemeColors, resolveColorToken } = require('./themes');
19
19
  const { parseDesignDocument, serializeDesignDocument, validateDesignDocument } = require('../design/document');
20
20
 
21
21
  const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
@@ -65,11 +65,10 @@ function visualSelections(plan) {
65
65
  description: forUser.styleSummary || '保持清晰、稳定并适合持续业务操作。',
66
66
  source: forUser.styleSource || selectedTheme.source || 'legacy_default',
67
67
  };
68
- const navigationStyle = forUser.navigationStyle || {
69
- structure: 'side',
70
- tone: 'light',
71
- source: 'legacy_default',
72
- selectionReason: '兼容旧版搭建计划;新计划必须显式记录导航选择。',
68
+ const theme = loadThemeIndex().themes.find(item => item.themeId === selectedTheme.themeId);
69
+ const navigationStyle = {
70
+ ...(forUser.navigationStyle || {}),
71
+ ...(theme && theme.mode !== 'creative' ? { tone: themeNavTheme(theme), toneSource: 'theme_derived' } : {}),
73
72
  };
74
73
  return { visual, forUser, selectedTheme, visualDirection, navigationStyle };
75
74
  }
@@ -78,6 +77,15 @@ function navigationToneLabel(value) {
78
77
  return value === 'dark' ? '深色' : '浅色';
79
78
  }
80
79
 
80
+ function resolveContentTone(theme, tokens) {
81
+ if (['light', 'dark'].includes(theme.contentTone)) {return theme.contentTone;}
82
+ requireFact(theme.mode === 'creative', `主题 ${theme.themeId} 缺少 contentTone`);
83
+ const pageColor = resolveColorToken(tokens, '--pod-page-bg-color');
84
+ requireFact(/^#[\dA-F]{6}$/i.test(pageColor || ''), '自由创意必须提供可判断界面明暗的 --pod-page-bg-color');
85
+ const rgb = [1, 3, 5].map(index => parseInt(pageColor.slice(index, index + 2), 16));
86
+ return rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722 < 128 ? 'dark' : 'light';
87
+ }
88
+
81
89
  function requireFact(condition, message) {
82
90
  if (!condition) {throw new CliError(message, { code: 'DESIGN_PLAN_INCOMPLETE_CONTRACT' });}
83
91
  }
@@ -356,6 +364,7 @@ function buildExecution(plan, options = {}) {
356
364
  const entryNavigation = buildEntryNavigation(plan, handoff.resourceBlueprint, handoff.pages);
357
365
  if (entryNavigation) {
358
366
  handoff.entryRecommendation = entryNavigation.recommendation;
367
+ handoff.entryModeSummary = ENTRY_MODE_SUMMARIES[entryNavigation.recommendation.mode];
359
368
  const skipOrder = resourceOnly || navigation.navigationType === 'custom' || entryNavigation.recommendation.mode === 'frontend-only';
360
369
  if (execution.navigationOrder !== undefined) {
361
370
  requireFact(JSON.stringify(execution.navigationOrder) === JSON.stringify(skipOrder ? [] : entryNavigation.platformOrder), 'navigationOrder 与入口管理菜单冲突,请修改入口菜单这一份源事实');
@@ -382,7 +391,7 @@ function buildExecution(plan, options = {}) {
382
391
 
383
392
  function renderEntryNavigation(recommendation) {
384
393
  if (!Array.isArray(recommendation?.entries)) {return [];}
385
- const lines = ['### 访问态入口与权限', '访客端与业务管理端共享资源,分别组织菜单;页面隐藏导航不授予业务权限。', ''];
394
+ const lines = ['### 访问态入口与权限', ENTRY_MODE_SUMMARIES[recommendation.mode], '页面隐藏导航不授予业务权限。', ''];
386
395
  for (const entry of recommendation.entries) {
387
396
  const rows = [];
388
397
  const visit = items => {
@@ -476,35 +485,11 @@ function normalizeHex(value) {
476
485
  return match ? `#${match[1].toUpperCase()}` : null;
477
486
  }
478
487
 
479
- function mixHex(hex, target, targetRatio) {
480
- const source = [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16));
481
- const mixed = source.map((channel, index) => Math.round(channel * (1 - targetRatio) + target[index] * targetRatio));
482
- return `#${mixed.map(channel => channel.toString(16).padStart(2, '0')).join('').toUpperCase()}`;
483
- }
484
-
485
- function navigationPalette(primaryColor, tone, tokens) {
486
- const shell = resolveColorToken(tokens, '--pod-shell-theme-bg-color');
487
- const isDark = shell && [1, 3, 5].every(index => parseInt(shell.slice(index, index + 2), 16) < 128);
488
- if (!shell || isDark === (tone === 'dark')) {return {};}
489
- const dark = tone === 'dark';
490
- const background = dark
491
- ? mixHex(primaryColor, [23, 23, 23], 0.62)
492
- : mixHex(primaryColor, [255, 255, 255], 0.96);
493
- return {
494
- '--pod-shell-theme-bg-color': background,
495
- '--pod-nav-item-text-color': dark ? '#D6D6D6' : '#606060',
496
- '--pod-nav-item-text-hover-color': dark ? '#FFFFFF' : '#303030',
497
- '--pod-nav-item-text-selected-color': dark ? '#FFFFFF' : '#171717',
498
- '--pod-nav-menu-bg-hover-color': mixHex(background, dark ? [255, 255, 255] : [0, 0, 0], dark ? 0.08 : 0.04),
499
- '--pod-nav-menu-bg-selected-color': mixHex(background, dark ? [255, 255, 255] : [0, 0, 0], dark ? 0.16 : 0.08),
500
- };
501
- }
502
-
503
488
  function resolveTheme(plan) {
504
489
  const { selectedTheme: selected } = visualSelections(plan);
505
490
  const index = loadThemeIndex();
506
491
  const theme = (index.themes || []).find(item => item.themeId === selected.themeId);
507
- if (!theme || theme.templatePath !== selected.templatePath) {
492
+ if (!theme || (theme.templatePath !== selected.templatePath && (!theme.legacyTemplatePath || theme.legacyTemplatePath !== selected.templatePath))) {
508
493
  throw new CliError('selectedTheme 的 themeId 与 templatePath 不属于主题索引中的同一记录', {
509
494
  code: 'DESIGN_PLAN_THEME_MISMATCH',
510
495
  details: { selectedTheme: selected },
@@ -537,14 +522,38 @@ function renderNavigationDesign(plan, navigationStyle, tokens) {
537
522
  '--pod-shell-theme-bg-color': '导航背景', '--pod-nav-item-text-color': '普通文字',
538
523
  '--pod-nav-item-text-hover-color': '悬停文字', '--pod-nav-item-text-selected-color': '选中文字',
539
524
  '--pod-nav-menu-bg-hover-color': '悬停背景', '--pod-nav-menu-bg-selected-color': '选中背景',
525
+ '--pod-page-header-bg-color': '页头背景', '--pod-nav-logo-text': '应用名称',
526
+ '--pod-nav-l-group-label-color': '分组文字', '--pod-nav-sub-divider-color': '分组边界',
527
+ '--pod-nav-search-bg-color': '搜索背景', '--pod-nav-search-text-color': '搜索文字',
528
+ '--pod-nav-popup-bg-color': '弹层背景', '--pod-nav-popup-border-radius': '弹层圆角',
529
+ '--pod-nav-menu-item-height': '菜单高度', '--pod-nav-menu-item-radius': '菜单圆角',
530
+ '--pod-nav-menu-item-border': '普通边框', '--pod-nav-menu-item-hover-border': '悬停边框',
531
+ '--pod-nav-menu-item-selected-border': '选中边框', '--pod-nav-menu-item-padding': '菜单内距',
532
+ '--pod-nav-menu-font-size': '菜单字号', '--pod-nav-menu-line-height': '菜单文字行高', '--pod-nav-menu-gap': '菜单间距',
533
+ '--pod-nav-menu-item-selected-shadow': '选中项阴影',
534
+ '--pod-nav-slide-aside-gap': '侧栏分区间距', '--pod-nav-slide-aside-padding': '侧栏内距',
535
+ '--pod-nav-platform-header-height': '顶部框架高度', '--pod-nav-top-header-padding': '顶栏内距',
536
+ '--pod-nav-top-tab-height': '顶部菜单高度', '--pod-nav-top-tab-item-padding': '顶部菜单内距',
537
+ '--pod-nav-top-tab-item-max-width': '顶部菜单最大宽度',
538
+ '--pod-nav-l-header-padding': 'L 型顶栏内距', '--pod-nav-l-aside-padding': 'L 型侧栏内距',
539
+ '--pod-shell-lshape-border-radius': '框架圆角',
540
540
  };
541
+ // Layout reasons can come from an older visual choice. Keep their business
542
+ // clauses while stating the current navigation tone separately.
543
+ const reason = navigationStyle.toneSource === 'theme_derived'
544
+ ? String(navigationStyle.selectionReason || '').split(/[,,;;。]/)
545
+ .filter(clause => !/(?:深色|浅色|暗色|亮色|黑色|白色).*(?:导航|菜单|侧栏)|(?:导航|菜单|侧栏).*(?:深色|浅色|暗色|亮色|黑色|白色)|(?:dark|light).*(?:nav|menu|sidebar)|(?:nav|menu|sidebar).*(?:dark|light)/i.test(clause))
546
+ .filter(clause => clause.trim()).join(';')
547
+ : navigationStyle.selectionReason;
541
548
  return [
542
- '#### 本项目导航配色', '',
549
+ '#### 本项目导航与应用框架', '',
543
550
  `- 导航类型:${NAVIGATION_TYPES[navigationConfig(plan).navigationType].label}`,
544
551
  `- 导航明暗:${navigationToneLabel(navigationStyle.tone)}`,
545
552
  '- 导航背景:`--pod-shell-theme-bg-color`(最终值见 tokens)',
546
- `- 选择依据:${text(navigationStyle.selectionReason)}`, '',
547
- mdTable(['角色', '变量', '本项目取值'], Object.entries(roles).map(([name, role]) => [role, name, tokens[name]])), '',
553
+ `- 布局依据:${text(reason, '按业务入口、模块层级和切换频率安排菜单')}`,
554
+ `- 明暗依据:${navigationStyle.toneSource === 'theme_derived' ? '沿用已选应用风格的导航明暗' : '采用本项目明确设计的导航明暗'},当前为${navigationToneLabel(navigationStyle.tone)}。`, '',
555
+ '- 导航、表单、自定义页面和记录详情共用本项目设计;完整导航状态与尺寸见 tokens.application-global.appearance.navigation。', '',
556
+ mdTable(['角色', '变量', '本项目取值'], Object.entries(roles).filter(([name]) => tokens[name]).map(([name, role]) => [role, name, tokens[name]])), '',
548
557
  ...(plan.pages?.customPageDetails || []).map(page => [
549
558
  `#### ${text(page.name)}的导航`, '', mdTable(['项目', '方案'], pageNavigationRows(page, plan)),
550
559
  ].join('\n')),
@@ -610,7 +619,7 @@ function renderProjectApplications(plan, tokens, color, chapter) {
610
619
  '## 5. 项目应用与调整规则', '',
611
620
  themeImplementationRules(chapter), '',
612
621
  '### 本项目实现边界', '',
613
- '本项目显式 Token 与页面决定优先于默认值;原生导航采用第二章列出的六项实际配色,自绘导航按本项目入口方案实施。业务内容、操作和数据来自 PRD,条件配方仅用于本项目已有内容。',
622
+ '本项目显式 Token 与页面决定优先于默认值;原生导航采用第二章的导航设计和完整导航 token,自绘导航按本项目入口方案实施。导航、应用框架、表单、自定义页面与详情保持统一风格。业务内容、操作和数据来自 PRD,条件配方用于本项目已有内容。',
614
623
  `已确认约束:${text(plan.visualStyle?.constraints, '无额外约束')}。`, '',
615
624
  '### 配色与语义', '',
616
625
  `本项目主色为 ${text(color.primaryColorName, '')} ${color.primaryColor};来源为 ${text(color.source, 'AI 推断')}。${text(color.usage, '用于主要品牌操作和少量焦点,独立数据色和平台状态色保持各自语义。')}`, '',
@@ -704,10 +713,19 @@ function renderDesign(plan, options = {}) {
704
713
  const parsed = parseDesignDocument(source);
705
714
  const themeTokens = readDesignTokens(source);
706
715
  const explicitTokens = visual.tokens || {};
716
+ if (theme.mode === 'creative') {
717
+ require('../app/application-style').validateCreativeDirection(visual.creativeDirection, explicitTokens);
718
+ }
707
719
  if (typeof explicitTokens !== 'object' || Array.isArray(explicitTokens)) {
708
720
  throw new CliError('visualStyle.tokens 必须是 CSS token 对象', { code: 'DESIGN_PLAN_INVALID_TOKENS' });
709
721
  }
710
- const overrides = { ...navigationPalette(primaryColor, navigationStyle.tone, themeTokens), ...explicitTokens };
722
+ if (theme.mode !== 'creative' && themeNavTheme(theme) !== navigationStyle.tone) {
723
+ throw new CliError('navigationStyle.tone 必须由所选主题模板的 navTheme 派生', {
724
+ code: 'DESIGN_PLAN_NAVIGATION_TONE_MISMATCH',
725
+ details: { themeId: theme.themeId, expected: themeNavTheme(theme), actual: navigationStyle.tone },
726
+ });
727
+ }
728
+ const overrides = { ...explicitTokens };
711
729
  for (const [token, value] of Object.entries(overrides)) {
712
730
  if (!/^--[\w-]+$/.test(token) || /^--color-brand/.test(token) || typeof value !== 'string') {
713
731
  throw new CliError(`无效视觉 token:${token};品牌色由 primaryColor 推导`, { code: 'DESIGN_PLAN_INVALID_TOKENS' });
@@ -722,11 +740,25 @@ function renderDesign(plan, options = {}) {
722
740
  if (chapters.length !== 5) {
723
741
  throw new CliError('主题缺少五章设计结构,请检查模板', { code: 'DESIGN_PLAN_THEME_CONTEXT_MISSING' });
724
742
  }
725
- chapters[0] = chapters[0].replace(/(^## 1\. 风格摘要\n\s*)\*\*([^\n]+)\*\*[ \t]*\n(?:[ \t]*\n)?/m,
743
+ const summaryTitle = new RegExp('(^## 1\\. 风格摘要\\n\\s*)\\*\\*([^\\n]+)\\*\\*[ \\t]*\\n(?:[ \\t]*\\n)?', 'm');
744
+ chapters[0] = chapters[0].replace(summaryTitle,
726
745
  (line, heading, title) => title.includes(theme.label) || title.includes(theme.themeId) ? heading : line);
727
746
  chapters[0] += `\n\n${renderProjectFacts(plan, visualDirection, { ...color, primaryColor })}`;
728
- chapters[1] = chapters[1].replace(/^(### 2\.2 应用导航\n)/m,
729
- (_, heading) => `${heading}\n${renderNavigationDesign(plan, navigationStyle, tokens)}\n\n`);
747
+ if (theme.mode === 'creative') {
748
+ const decision = visual.creativeDirection;
749
+ chapters[0] += `\n\n### 业务推演依据\n\n${decision.businessRationale}`;
750
+ chapters[1] += `\n\n### 自主设计决策\n\n${decision.composition}\n\n${decision.typography}\n\n${decision.material}`;
751
+ chapters[2] += `\n\n### 表单布局决策\n\n${decision.formLayout}`;
752
+ }
753
+ chapters[1] = chapters[1].replace(/^(### 2\.2 应用导航\n)([\s\S]*?)(?=^### |(?![\s\S]))/m,
754
+ (_, heading, guidance) => {
755
+ // The resolved project values replace the preset's color description;
756
+ // keep its token consumption, interaction and acceptance guidance.
757
+ const sharedGuidance = (theme.navigationSummary ? guidance.replace(theme.navigationSummary, '') : guidance)
758
+ .replace(/^导航示例:[^\n]*\n?/gm, '')
759
+ .replace(/当前模板使用 contentTone:\s*(?:light|dark)、navTheme:\s*(?:light|dark)。/g, '').trim();
760
+ return `${heading}\n${renderNavigationDesign(plan, navigationStyle, tokens)}\n\n${sharedGuidance}\n\n`;
761
+ });
730
762
  const themeGuidance = fs.readFileSync(THEME_CONSISTENCY_PATH, 'utf8');
731
763
  for (const section of ['指标卡与按钮配色', '分组标题与分割线']) {
732
764
  const guidance = themeGuidance.match(new RegExp(`(?:^|\\n)(## ${section}\\n[\\s\\S]*?)(?=\\n## |$)`))?.[1];
@@ -754,10 +786,12 @@ function renderDesign(plan, options = {}) {
754
786
  }
755
787
  const metadata = {
756
788
  schemaVersion: '1.0', name, description: parsed.metadata.description,
789
+ ...(theme.collection === 'application-styles' ? { applicationStyle: { recipe: 'application-style-v1', mode: theme.mode } } : {}),
790
+ ...(theme.mode === 'creative' ? { creativeDirection: visual.creativeDirection } : {}),
757
791
  buildPlanRevision: text(meta.revision, 'unversioned'), tokens: parsed.metadata.tokens,
758
792
  themeProfile: {
759
793
  name: visualDirection.label, themeColor: primaryColor, themeColorSource: colorSource,
760
- navTheme: navigationStyle.tone, themeDelivery: 'app-custom-theme-file',
794
+ contentTone: resolveContentTone(theme, tokens), navTheme: navigationStyle.tone, themeDelivery: 'app-custom-theme-file',
761
795
  themeFile: path.join(options.outputDir || path.join('prd', meta.projectName || name), 'app-theme.css'),
762
796
  ...navigationConfig(plan), logoSource: 'appIcon',
763
797
  },
@@ -804,8 +838,9 @@ function renderHtml(plan, sections) {
804
838
 
805
839
  // Draft sections share the final PRD renderer; implementation details are filled at final validation.
806
840
  function renderDraftPrd(plan) {
841
+ const { navigationStyle } = visualSelections(plan);
807
842
  return renderPrd(plan, {
808
- appConfig: { ...navigationConfig(plan), navTheme: plan.visualStyle?.forUser?.navigationStyle?.tone },
843
+ appConfig: { ...navigationConfig(plan), navTheme: navigationStyle.tone },
809
844
  pageNavigation: [], sampleDataPlan: [],
810
845
  pages: (plan.pages?.customPageDetails || []).map(page => ({ pageSpecHandoff: page.pageSpecHandoff || {} })),
811
846
  interactionStates: {}, resourceBlueprint: [], resourceCreationOrder: [], pageImplementationOrder: [],
@@ -899,6 +934,9 @@ function materializeUnlocked(inputPath, options = {}) {
899
934
  const readMs = performance.now() - start;
900
935
  const mergeStart = performance.now();
901
936
  const merging = options.businessFile !== undefined || options.visualFile !== undefined;
937
+ if (options.rebaseParts && !merging) {
938
+ throw new CliError('--rebase-parts requires --business-file and --visual-file', { code: 'DESIGN_PLAN_PARTS_REQUIRED' });
939
+ }
902
940
  if (merging && (!options.businessFile || !options.visualFile)) {
903
941
  throw new CliError('--business-file 与 --visual-file 需同时提供', { code: 'DESIGN_PLAN_PARTS_REQUIRED' });
904
942
  }
@@ -907,7 +945,7 @@ function materializeUnlocked(inputPath, options = {}) {
907
945
  }
908
946
  if (merging && options.fromPreview) { throw new CliError('选择一种规划来源', { code: 'DESIGN_PLAN_PART_CONFLICT' }); }
909
947
  const merged = options.fromPreview ? require('./preview').finalizePreview(source, path.join(path.dirname(input), 'preview/.state.json'))
910
- : merging ? mergeParts(source, options.businessFile, options.visualFile) : source;
948
+ : merging ? mergeParts(source, options.businessFile, options.visualFile, { input, rebaseParts: options.rebaseParts }) : source;
911
949
  const updatesSource = merging || options.fromPreview;
912
950
  const mergeMs = performance.now() - mergeStart;
913
951
  const previewDir = path.join(path.dirname(input), 'preview');
@@ -932,7 +970,9 @@ function materializeUnlocked(inputPath, options = {}) {
932
970
  }
933
971
  const timings = { readMs, ...(merging ? { mergeMs } : {}), ...artifacts.timings, writeMs: performance.now() - writeStart, totalMs: performance.now() - start };
934
972
  for (const key of Object.keys(timings)) {timings[key] = Math.round(timings[key] * 100) / 100;}
935
- return { success: true, checked: options.check === true, input, outputDir, revision: artifacts.revision, outputs: artifacts.outputs, updated: options.check ? [] : updates.updated, assetTasks: artifacts.assetTasks, timings, ...(updatesSource ? { merged: true, previousRevision: source.meta.revision } : {}) };
973
+ return { success: true, checked: options.check === true, input, outputDir, revision: artifacts.revision, outputs: artifacts.outputs, updated: options.check ? [] : updates.updated, assetTasks: artifacts.assetTasks, timings,
974
+ ...(!options.check ? { confirmation: require('./confirmation').confirmationPayload(artifacts.outputs, artifacts.revision) } : {}),
975
+ ...(updatesSource ? { merged: true, previousRevision: source.meta.revision } : {}) };
936
976
  }
937
977
 
938
978
  function materialize(inputPath, options = {}) {
@@ -5,7 +5,7 @@ const { CliError } = require('../core/cli-error');
5
5
  const { t } = require('../core/i18n');
6
6
  const { readJson } = require('./files');
7
7
 
8
- const { PLAN_SKILL_ROOT, loadThemeIndex } = require('./themes');
8
+ const { PLAN_SKILL_ROOT, loadThemeIndex, themeNavTheme } = require('./themes');
9
9
  const PAGE_PATTERN_INDEX_PATH = path.join(PLAN_SKILL_ROOT, 'templates', 'page-patterns', 'index.json');
10
10
 
11
11
  function clone(value) {
@@ -27,7 +27,7 @@ function assertVisualFieldObjects(forUser, options = {}) {
27
27
  const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
28
28
  const examples = {
29
29
  visualDirection: { label: '<label>', description: '<description>' },
30
- navigationStyle: { structure: 'top', tone: 'light' },
30
+ navigationStyle: { structure: 'top' },
31
31
  };
32
32
  const issues = [];
33
33
  const check = (value, field, example) => {
@@ -270,7 +270,7 @@ function selectedTheme(plan, themeIndex) {
270
270
  details: { themeId: selected.themeId },
271
271
  });
272
272
  }
273
- if (selected.templatePath && selected.templatePath !== theme.templatePath) {
273
+ if (selected.templatePath && selected.templatePath !== theme.templatePath && selected.templatePath !== theme.legacyTemplatePath) {
274
274
  throw new CliError('selectedTheme 的 themeId 与 templatePath 不属于主题索引中的同一记录', {
275
275
  code: 'DESIGN_PLAN_THEME_MISMATCH',
276
276
  details: { selectedTheme: selected },
@@ -281,7 +281,7 @@ function selectedTheme(plan, themeIndex) {
281
281
  selected.summary = selected.summary || [theme.styleSummary, selected.customText].filter(Boolean).join(';');
282
282
  assertThemeProfile(forUser.themeProfile, theme);
283
283
  forUser.themeProfile = { ...(theme.defaultProfile || {}) };
284
- return theme;
284
+ return { ...theme, navTheme: themeNavTheme(theme) };
285
285
  }
286
286
 
287
287
  function pageApplicationDefaults(page) {
@@ -302,13 +302,18 @@ function pageApplicationDefaults(page) {
302
302
  };
303
303
  }
304
304
 
305
- function ensureVisual(plan) {
305
+ function ensureVisual(plan, theme) {
306
306
  const visual = plan.visualStyle;
307
307
  const forUser = visual.forUser;
308
308
  const selected = visual.internal.selectedTheme;
309
309
  const direction = forUser.visualDirection || {};
310
310
  const color = forUser.colorStrategy = forUser.colorStrategy || {};
311
- const navigation = forUser.navigationStyle || {};
311
+ const navigation = forUser.navigationStyle = forUser.navigationStyle || {};
312
+ if (theme.mode !== 'creative') {navigation.tone = theme.navTheme;}
313
+ navigation.toneSource = theme.mode === 'creative' ? 'project_defined' : 'theme_derived';
314
+ if (!['light', 'dark'].includes(navigation.tone)) {
315
+ throw new CliError('visualStyle.forUser.navigationStyle.tone 必须是 light 或 dark', { code: 'DESIGN_PLAN_INVALID_NAVIGATION_TONE' });
316
+ }
312
317
  if (!direction.label || !direction.description) {
313
318
  throw new CliError('schemaVersion 2.0 必须提供 visualStyle.forUser.visualDirection.label 和 description', {
314
319
  code: 'DESIGN_PLAN_VISUAL_DIRECTION_REQUIRED',
@@ -320,12 +325,6 @@ function ensureVisual(plan) {
320
325
  details: { structure: navigation.structure },
321
326
  });
322
327
  }
323
- if (!['light', 'dark'].includes(navigation.tone)) {
324
- throw new CliError('visualStyle.forUser.navigationStyle.tone 必须是 light 或 dark', {
325
- code: 'DESIGN_PLAN_INVALID_NAVIGATION_TONE',
326
- details: { tone: navigation.tone },
327
- });
328
- }
329
328
  const sourceApplications = new Map(list(forUser.pageApplications).map(item => [item.pageId || item.pageName, item]));
330
329
  forUser.pageApplications = list((plan.pages || {}).customPageDetails).map(page => {
331
330
  const source = sourceApplications.get(page.pageId) || sourceApplications.get(page.name) || {};
@@ -404,8 +403,8 @@ function normalizePlan(sourcePlan) {
404
403
  const pagePatternIndex = readJson(PAGE_PATTERN_INDEX_PATH, '页面模式索引');
405
404
  ensureOverview(plan);
406
405
  ensurePageRules(plan, pagePatternIndex);
407
- selectedTheme(plan, themeIndex);
408
- ensureVisual(plan);
406
+ const theme = selectedTheme(plan, themeIndex);
407
+ ensureVisual(plan, theme);
409
408
  return plan;
410
409
  }
411
410
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { createHash } = require('crypto');
4
4
  const { CliError } = require('../core/cli-error');
5
+ const { t } = require('../core/i18n');
5
6
  const { readJson } = require('./files');
6
7
  const { assertVisualFieldObjects } = require('./normalize');
7
8
 
@@ -11,13 +12,14 @@ function planBase(plan) {
11
12
  return { revision: plan.meta?.revision, digest: createHash('sha256').update(JSON.stringify(plan)).digest('hex') };
12
13
  }
13
14
 
14
- function readPart(file, role, base, allowed) {
15
- const part = readJson(file, `${role} 规划片段`);
15
+ function readPart(file, role, base, allowed, supplied) {
16
+ const part = supplied || readJson(file, `${role} 规划片段`);
16
17
  if (!object(part) || part.ready !== true || !object(part.facts)) {
17
18
  throw new CliError(`${role} 规划尚未完成,请补齐 facts 并设置 ready=true`, { code: 'DESIGN_PLAN_PART_NOT_READY' });
18
19
  }
19
20
  if (part.base?.revision !== base.revision || part.base?.digest !== base.digest) {
20
- throw new CliError(`${role} 规划来自旧版本,请根据当前计划重新整理`, { code: 'DESIGN_PLAN_STALE_PART' });
21
+ throw new CliError(`${role} 规划来自旧版本,请根据当前计划重新整理`, { code: 'DESIGN_PLAN_STALE_PART',
22
+ details: { sourcePath: file, expectedBase: base, actualBase: part.base, nextStep: t('design_plan.rebase_required') } });
21
23
  }
22
24
  const unknown = Object.keys(part.facts).filter(key => !allowed.includes(key));
23
25
  if (unknown.length) {
@@ -26,10 +28,63 @@ function readPart(file, role, base, allowed) {
26
28
  return part.facts;
27
29
  }
28
30
 
29
- function mergeParts(source, businessFile, visualFile) {
31
+ // Report all binding defects at once so agents can repair the existing part.
32
+ function assertPageBindings(pages, applications, visualFile, businessFile) {
33
+ const prefix = 'facts.visualStyle.forUser.pageApplications';
34
+ const issues = [];
35
+ const pageIds = [];
36
+ pages.forEach((page, index) => {
37
+ const field = `facts.pages.customPageDetails[${index}].pageId`;
38
+ if (!object(page) || typeof page.pageId !== 'string' || !page.pageId.trim()) {
39
+ issues.push({ sourcePath: businessFile, path: field, code: 'expected_nonempty_string' });
40
+ } else if (pageIds.includes(page.pageId)) {
41
+ issues.push({ sourcePath: businessFile, path: field, code: 'duplicate_page', pageId: page.pageId });
42
+ } else { pageIds.push(page.pageId); }
43
+ });
44
+ const seen = new Set();
45
+ if (!Array.isArray(applications)) {
46
+ issues.push({ sourcePath: visualFile, path: prefix, code: 'expected_array' });
47
+ } else {
48
+ applications.forEach((item, index) => {
49
+ const field = `${prefix}[${index}]`;
50
+ if (!object(item)) {
51
+ issues.push({ sourcePath: visualFile, path: field, code: 'expected_object' });
52
+ return;
53
+ }
54
+ if (typeof item.pageId !== 'string' || !item.pageId.trim()) {
55
+ issues.push({ sourcePath: visualFile, path: `${field}.pageId`, code: 'expected_nonempty_string' });
56
+ } else {
57
+ if (!pageIds.includes(item.pageId)) {
58
+ issues.push({ sourcePath: visualFile, path: `${field}.pageId`, code: 'unexpected_page', pageId: item.pageId });
59
+ }
60
+ if (seen.has(item.pageId)) {
61
+ issues.push({ sourcePath: visualFile, path: `${field}.pageId`, code: 'duplicate_page', pageId: item.pageId });
62
+ }
63
+ seen.add(item.pageId);
64
+ }
65
+ if (!Array.isArray(item.visualMemoryApplications)) {
66
+ issues.push({ sourcePath: visualFile, path: `${field}.visualMemoryApplications`, code: 'expected_array' });
67
+ }
68
+ });
69
+ }
70
+ pageIds.filter(pageId => !seen.has(pageId)).forEach(pageId => {
71
+ issues.push({ sourcePath: visualFile, path: prefix, code: 'missing_page', pageId });
72
+ });
73
+ if (issues.length) {
74
+ const nextStep = t('design_plan.repair_page_bindings');
75
+ const summary = issues.map(issue => `${issue.path}: ${issue.code}${issue.pageId ? ` (${issue.pageId})` : ''}`).join('; ');
76
+ throw new CliError(`${nextStep} ${summary}`, {
77
+ code: 'DESIGN_PLAN_PAGE_BINDINGS_REQUIRED',
78
+ details: { sourcePath: visualFile, expectedPageIds: pageIds, issues, nextStep },
79
+ });
80
+ }
81
+ }
82
+
83
+ function mergeParts(source, businessFile, visualFile, options = {}) {
30
84
  const base = planBase(source);
31
- const business = readPart(businessFile, 'business', base, ['overview', 'dataModels', 'businessFlows', 'pages', 'execution', 'meta']);
32
- const visual = readPart(visualFile, 'visual', base, ['visualStyle']);
85
+ const rebased = options.rebaseParts ? require('./rebase').rebaseParts(source, options.input, businessFile, visualFile) : [];
86
+ const business = readPart(businessFile, 'business', base, ['overview', 'dataModels', 'businessFlows', 'pages', 'execution', 'meta'], rebased[0]);
87
+ const visual = readPart(visualFile, 'visual', base, ['visualStyle'], rebased[1]);
33
88
  assertVisualFieldObjects(visual.visualStyle?.forUser, { prefix: 'facts.visualStyle.forUser', sourcePath: visualFile });
34
89
  if (!object(business.overview) || !Array.isArray(business.dataModels) || !Array.isArray(business.businessFlows)
35
90
  || !Array.isArray(business.pages?.customPageDetails) || !object(visual.visualStyle)) {
@@ -39,13 +94,7 @@ function mergeParts(source, businessFile, visualFile) {
39
94
  || Object.keys(business.meta).some(key => !['businessDomain', 'experienceTopology'].includes(key)))) {
40
95
  throw new CliError('业务 meta 仅填写 businessDomain/experienceTopology,版本由 CLI 维护', { code: 'DESIGN_PLAN_PART_CONFLICT' });
41
96
  }
42
- const pageIds = business.pages.customPageDetails.map(page => page.pageId);
43
- const applications = visual.visualStyle.forUser?.pageApplications;
44
- if (!Array.isArray(applications) || applications.length !== pageIds.length
45
- || new Set(applications.map(item => item.pageId)).size !== pageIds.length
46
- || applications.some(item => !pageIds.includes(item.pageId) || !Array.isArray(item.visualMemoryApplications))) {
47
- throw new CliError('视觉规划需按业务页面逐一完成 pageApplications,再合并产物', { code: 'DESIGN_PLAN_PAGE_BINDINGS_REQUIRED' });
48
- }
97
+ assertPageBindings(business.pages.customPageDetails, visual.visualStyle.forUser?.pageApplications, visualFile, businessFile);
49
98
  const plan = JSON.parse(JSON.stringify({ ...source, ...business, ...visual, meta: { ...source.meta, ...business.meta },
50
99
  ...(source.execution || business.execution ? { execution: { ...source.execution, ...business.execution,
51
100
  ...(source.execution?.appConfig || business.execution?.appConfig
@@ -12,10 +12,13 @@ const PROTECTED_PATHS = [
12
12
  'meta.updatedAt',
13
13
  'meta.planState',
14
14
  'visualStyle.forUser.themeProfile',
15
+ 'visualStyle.forUser.navigationStyle.tone',
16
+ 'visualStyle.forUser.navigationStyle.toneSource',
17
+ 'execution.appConfig.navTheme',
15
18
  ];
16
19
  const DANGEROUS_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
17
20
 
18
- function parsePath(pathExpression) {
21
+ function parsePath(pathExpression, options = {}) {
19
22
  const expression = String(pathExpression || '').trim();
20
23
  if (!expression || !/^[A-Za-z_$][\w$-]*(?:(?:\.(?:[A-Za-z_$][\w$-]*|--[\w-]+))|(?:\[\d+\]))*$/.test(expression)) {
21
24
  throw new CliError(`无效字段路径:${expression || '(empty)'}`, {
@@ -28,11 +31,16 @@ function parsePath(pathExpression) {
28
31
  code: 'DESIGN_PLAN_UNSAFE_PATH',
29
32
  });
30
33
  }
31
- if (PROTECTED_PATHS.some(protectedPath => expression === protectedPath
34
+ const protectedPaths = options.creative ? PROTECTED_PATHS.filter(field => field !== 'visualStyle.forUser.navigationStyle.tone') : PROTECTED_PATHS;
35
+ if (protectedPaths.some(protectedPath => expression === protectedPath
32
36
  || expression.startsWith(`${protectedPath}.`)
33
37
  || expression.startsWith(`${protectedPath}[`)
34
38
  || protectedPath.startsWith(`${expression}.`))) {
35
- const hint = expression.includes('themeProfile') ? ';themeProfile 只读,请改 visualStyle.tokens 或 colorStrategy.primaryColor' : '';
39
+ const hint = expression.includes('themeProfile')
40
+ ? ';themeProfile 只读,请改 visualStyle.tokens 或 colorStrategy.primaryColor'
41
+ : /(?:navigationStyle\.(?:tone|toneSource)|appConfig\.navTheme)/.test(expression)
42
+ ? ';导航明暗由所选主题模板派生,请改选主题'
43
+ : '';
36
44
  throw new CliError(`字段 ${expression} 由 design-plan patch 自动维护,不能通过 --set 修改${hint}`, {
37
45
  code: 'DESIGN_PLAN_PROTECTED_PATH',
38
46
  });
@@ -49,7 +57,7 @@ function parseValue(rawValue) {
49
57
  }
50
58
  }
51
59
 
52
- function parseSetExpression(expression) {
60
+ function parseSetExpression(expression, options = {}) {
53
61
  const separator = String(expression || '').indexOf('=');
54
62
  if (separator <= 0) {
55
63
  throw new CliError(`--set 必须使用 path=value 格式:${expression || '(empty)'}`, {
@@ -59,7 +67,7 @@ function parseSetExpression(expression) {
59
67
  const pathExpression = expression.slice(0, separator).trim();
60
68
  return {
61
69
  path: pathExpression,
62
- segments: parsePath(pathExpression),
70
+ segments: parsePath(pathExpression, options),
63
71
  value: parseValue(expression.slice(separator + 1)),
64
72
  };
65
73
  }
@@ -67,7 +75,7 @@ function parseSetExpression(expression) {
67
75
  const OPTIONAL_PATHS = [
68
76
  /^execution$/,
69
77
  /^execution\.(resourceBlueprint|resourceCreationOrder|pageImplementationOrder|navigationOrder|navigationFallback|entryRecommendation|sampleDataPlan|interactionStates|acceptanceCriteria|explicitScope|appConfig)$/,
70
- /^execution\.appConfig\.(appType|corpId|baseUrl|navigationType|hideAppNav|layoutDirection|navTheme|logoSource)$/,
78
+ /^execution\.appConfig\.(appType|corpId|baseUrl|navigationType|hideAppNav|layoutDirection|logoSource)$/,
71
79
  /^execution\.interactionStates\.(empty|loading|error|formEntry|detail)$/,
72
80
  /^visualStyle\.tokens(?:\.--[\w-]+)?$/,
73
81
  /^visualStyle\.forUser\.iconSystem(?:\.(library|mappings|colorPairs))?$/,
@@ -154,12 +162,17 @@ function patchPlan(inputPath, setExpressions, options = {}) {
154
162
  }
155
163
  const original = readJson(resolvedInput);
156
164
  const patched = JSON.parse(JSON.stringify(original));
157
- const changes = expressions.map(parseSetExpression).map(change => ({
165
+ const selectedTheme = original.visualStyle?.internal?.selectedTheme || original.visualStyle?.forUser?.selectedTheme;
166
+ const creative = selectedTheme?.themeId === 'free-creative';
167
+ const changes = expressions.map(expression => parseSetExpression(expression, { creative })).map(change => ({
158
168
  ...change,
159
169
  previousValue: setExistingPath(patched, change),
160
170
  }));
161
171
  const effectiveChanges = changes.filter(change => JSON.stringify(change.previousValue) !== JSON.stringify(change.value));
162
172
  if (JSON.stringify(original) === JSON.stringify(patched)) {
173
+ if (options.materialize) {
174
+ return { ...require('./materialize').materialize(resolvedInput, { outputDir: options.outputDir }), changed: false, materialized: true, changedPaths: [], confirmationInvalidated: false };
175
+ }
163
176
  return {
164
177
  success: true,
165
178
  changed: false,
@@ -195,6 +208,7 @@ function patchPlan(inputPath, setExpressions, options = {}) {
195
208
  confirmationInvalidated: !assetProgressOnly,
196
209
  materialized: options.materialize === true,
197
210
  outputs: options.materialize ? artifacts.outputs : undefined,
211
+ ...(options.materialize ? { confirmation: require('./confirmation').confirmationPayload(artifacts.outputs, artifacts.revision) } : {}),
198
212
  updated: updates?.updated || [],
199
213
  };
200
214
  }
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { CliError } = require('../core/cli-error');
6
+ const { readJson } = require('./files');
7
+ const { t } = require('../core/i18n');
8
+
9
+ const BASE_FILE = '.build-plan-base.json';
10
+ const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
11
+ const object = value => value && typeof value === 'object' && !Array.isArray(value);
12
+
13
+ // Arrays represent ordered business facts; conflicting edits require a decision.
14
+ function reconcile(base, current, proposed, field, conflicts) {
15
+ if (equal(current, proposed) || equal(proposed, base)) { return current; }
16
+ if (equal(current, base)) { return proposed; }
17
+ if (object(base) && object(current) && object(proposed)) {
18
+ const result = {};
19
+ for (const key of new Set([...Object.keys(base), ...Object.keys(current), ...Object.keys(proposed)])) {
20
+ const value = reconcile(base[key], current[key], proposed[key], field ? `${field}.${key}` : key, conflicts);
21
+ if (value !== undefined) { result[key] = value; }
22
+ }
23
+ return result;
24
+ }
25
+ conflicts.push(field);
26
+ return proposed;
27
+ }
28
+
29
+ function rebaseParts(source, input, businessFile, visualFile) {
30
+ const { planBase } = require('./parallel');
31
+ const baselinePath = path.join(path.dirname(path.resolve(input)), BASE_FILE);
32
+ const fail = (code, details) => { throw new CliError(t('design_plan.rebase_required'), { code, details: { baselinePath, ...details } }); };
33
+ if (!fs.existsSync(baselinePath)) { fail('DESIGN_PLAN_BASELINE_MISSING', { nextStep: t('design_plan.baseline_missing') }); }
34
+ const saved = readJson(baselinePath);
35
+ if (saved.schemaVersion !== 1 || !object(saved.plan) || !equal(saved.base, planBase(saved.plan))) {
36
+ fail('DESIGN_PLAN_BASELINE_INVALID', { nextStep: t('design_plan.baseline_missing') });
37
+ }
38
+ if (source.meta?.status !== 'draft' || source.meta?.planState?.presentedRevision
39
+ || source.meta?.planState?.planConfirmed || !equal(source.meta, saved.plan.meta)) {
40
+ fail('DESIGN_PLAN_REBASE_STAGE_INVALID', { nextStep: t('design_plan.rebase_stage_invalid') });
41
+ }
42
+ const parts = [businessFile, visualFile].map(file => readJson(file));
43
+ for (const [index, part] of parts.entries()) {
44
+ if (!equal(part.base, saved.base) || !object(part.facts)) {
45
+ fail('DESIGN_PLAN_BASELINE_MISMATCH', { sourcePath: [businessFile, visualFile][index], nextStep: t('design_plan.baseline_missing') });
46
+ }
47
+ }
48
+ return parts.map((part, index) => {
49
+ const conflicts = [];
50
+ const facts = Object.fromEntries(Object.keys(part.facts).map(key => [key,
51
+ reconcile(saved.plan[key], source[key], part.facts[key], key, conflicts),
52
+ ]));
53
+ if (conflicts.length) {
54
+ fail('DESIGN_PLAN_REBASE_CONFLICT', { sourcePath: [businessFile, visualFile][index], conflicts, nextStep: t('design_plan.rebase_conflict') });
55
+ }
56
+ return { ...part, facts, base: planBase(source) };
57
+ });
58
+ }
59
+
60
+ module.exports = { BASE_FILE, rebaseParts };
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { CliError } = require('../core/cli-error');
6
6
  const { readJson } = require('./files');
7
+ const { parseDesignDocument } = require('../design/document');
7
8
 
8
9
  const DESIGN_SKILL_ROOT = path.resolve(__dirname, '../../yida-skills/skills/yida-design');
9
10
  const PLAN_SKILL_ROOT = path.join(DESIGN_SKILL_ROOT, 'sub_skill/yida-design-plan');
@@ -21,6 +22,20 @@ function themeTemplatePath(theme) {
21
22
  return file;
22
23
  }
23
24
 
25
+ function themeNavTheme(theme) {
26
+ if (theme.mode === 'creative') {return undefined;}
27
+ const file = themeTemplatePath(theme);
28
+ const metadata = parseDesignDocument(fs.readFileSync(file, 'utf8')).metadata;
29
+ const navTheme = metadata.themeProfile?.navTheme || metadata.navTheme;
30
+ if (!['light', 'dark'].includes(navTheme)) {
31
+ throw new CliError(`主题模板 navTheme 无效:${theme.themeId}`, {
32
+ code: 'DESIGN_PLAN_THEME_NAV_THEME_INVALID',
33
+ details: { themeId: theme.themeId, navTheme: metadata.navTheme, templatePath: theme.templatePath },
34
+ });
35
+ }
36
+ return navTheme;
37
+ }
38
+
24
39
  // Color recipes live with the visual theme. Resolve their token dependencies rather
25
40
  // than duplicating per-theme palettes in JavaScript.
26
41
  function resolveThemeColors(source) {
@@ -83,4 +98,4 @@ function resolveColorToken(tokens, name, seen = new Set()) {
83
98
  return alias ? resolveColorToken(tokens, alias[1], seen) || alias[2] || null : null;
84
99
  }
85
100
 
86
- module.exports = { DESIGN_SKILL_ROOT, PLAN_SKILL_ROOT, loadThemeIndex, themeTemplatePath, resolveThemeColors, resolveColorToken };
101
+ module.exports = { DESIGN_SKILL_ROOT, PLAN_SKILL_ROOT, loadThemeIndex, themeTemplatePath, themeNavTheme, resolveThemeColors, resolveColorToken };