openyida 2026.9.20 → 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 (162) hide show
  1. package/README.md +1 -1
  2. package/lib/app/application-style.js +3 -1
  3. package/lib/app/theme-from-design.js +38 -13
  4. package/lib/app/update-app.js +63 -13
  5. package/lib/core/command-manifest.js +32 -20
  6. package/lib/core/locales/en.js +17 -0
  7. package/lib/core/locales/zh.js +17 -0
  8. package/lib/core/sample.js +17 -14
  9. package/lib/design-plan/confirmation.js +13 -0
  10. package/lib/design-plan/design-plan.js +44 -2
  11. package/lib/design-plan/entry-navigation.js +10 -2
  12. package/lib/design-plan/init.js +11 -7
  13. package/lib/design-plan/materialize.js +62 -43
  14. package/lib/design-plan/normalize.js +13 -14
  15. package/lib/design-plan/parallel.js +62 -13
  16. package/lib/design-plan/patch.js +21 -7
  17. package/lib/design-plan/rebase.js +60 -0
  18. package/lib/design-plan/themes.js +16 -1
  19. package/lib/design-plan/validate.js +6 -1
  20. package/lib/design-plan/visual-policy.js +18 -4
  21. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +3 -3
  22. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +6 -4
  23. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +5 -5
  24. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -2
  25. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +1 -1
  26. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +5 -4
  27. package/package.json +1 -1
  28. package/yida-skills/SKILL.md +2 -2
  29. package/yida-skills/references/yida-api.md +4 -0
  30. package/yida-skills/skills/yida-app/SKILL.md +12 -4
  31. package/yida-skills/skills/yida-app/references/entry-navigation.md +11 -9
  32. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +1 -1
  33. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +11 -4
  34. package/yida-skills/skills/yida-app/workflow/step-2-design.md +3 -3
  35. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +2 -2
  36. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +3 -3
  37. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +3 -2
  38. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -2
  39. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +1 -1
  40. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +1 -1
  41. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +140 -1
  42. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +1 -1
  43. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +1 -1
  44. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +7 -0
  45. package/yida-skills/skills/yida-create-form-page/SKILL.md +1 -1
  46. package/yida-skills/skills/yida-design/SKILL.md +5 -3
  47. package/yida-skills/skills/yida-design/references/application-style-library.md +22 -7
  48. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +45 -5
  49. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +1 -1
  50. package/yida-skills/skills/yida-design/references/native-form-styles.md +1 -1
  51. package/yida-skills/skills/yida-design/references/navigation-decision.md +6 -4
  52. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +62 -0
  53. package/yida-skills/skills/yida-design/references/theme-selection.md +10 -9
  54. package/yida-skills/skills/yida-design/references/visual-decision-engine.md +1 -1
  55. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +35 -9
  56. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +13 -3
  57. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +4 -2
  58. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +4 -2
  59. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-design.md +3 -1
  60. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +15 -17
  61. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +2 -0
  62. package/yida-skills/skills/yida-design/templates/design-themes/README.md +15 -10
  63. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +224 -83
  64. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +69 -14
  65. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +224 -83
  66. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +69 -14
  67. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +244 -103
  68. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +68 -13
  69. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +223 -82
  70. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +68 -13
  71. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +225 -84
  72. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +69 -14
  73. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +244 -103
  74. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +68 -13
  75. package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +224 -83
  76. package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +69 -14
  77. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +244 -103
  78. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +68 -13
  79. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +236 -89
  80. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +74 -14
  81. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +224 -83
  82. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +68 -13
  83. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +223 -82
  84. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +68 -13
  85. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +224 -83
  86. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +68 -13
  87. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +240 -89
  88. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +76 -14
  89. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +224 -83
  90. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +69 -14
  91. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +224 -83
  92. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +68 -13
  93. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +254 -107
  94. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +74 -14
  95. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +233 -88
  96. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +73 -14
  97. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +224 -83
  98. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +69 -14
  99. package/yida-skills/skills/yida-design/templates/design-themes/basic-tokens.json +2 -1
  100. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +690 -0
  101. package/yida-skills/skills/yida-design/templates/design-themes/{dark-inset-hairline.md → dark-inset-hairline/design.md} +103 -58
  102. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/form-layout.json +13 -0
  103. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +691 -0
  104. package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-fine-lines.md → dark-rail-fine-lines/design.md} +102 -50
  105. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/form-layout.json +13 -0
  106. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/app_theme.css +696 -0
  107. package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-signal-panels.md → dark-rail-signal-panels/design.md} +62 -12
  108. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/form-layout.json +13 -0
  109. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +278 -115
  110. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +57 -10
  111. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/app_theme.css +692 -0
  112. package/yida-skills/skills/yida-design/templates/design-themes/{graphite-bevel-grid.md → graphite-bevel-grid/design.md} +60 -10
  113. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/form-layout.json +13 -0
  114. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/app_theme.css +691 -0
  115. package/yida-skills/skills/yida-design/templates/design-themes/{hairline-shared-bands.md → hairline-shared-bands/design.md} +62 -12
  116. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/form-layout.json +13 -0
  117. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +691 -0
  118. package/yida-skills/skills/yida-design/templates/design-themes/{hairline-soft-blocks.md → hairline-soft-blocks/design.md} +57 -9
  119. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/form-layout.json +13 -0
  120. package/yida-skills/skills/yida-design/templates/design-themes/index.json +141 -48
  121. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +701 -0
  122. package/yida-skills/skills/yida-design/templates/design-themes/{ink-glow-soft-panels.md → ink-glow-soft-panels/design.md} +58 -8
  123. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/form-layout.json +13 -0
  124. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +691 -0
  125. package/yida-skills/skills/yida-design/templates/design-themes/{inset-frame-pixel-rhythm.md → inset-frame-pixel-rhythm/design.md} +59 -9
  126. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/form-layout.json +13 -0
  127. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +698 -0
  128. package/yida-skills/skills/yida-design/templates/design-themes/{outlined-texture-duotone.md → outlined-texture-duotone/design.md} +62 -12
  129. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/form-layout.json +13 -0
  130. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +691 -0
  131. package/yida-skills/skills/yida-design/templates/design-themes/{soft-inset-surfaces.md → soft-inset-surfaces/design.md} +59 -11
  132. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/form-layout.json +13 -0
  133. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +690 -0
  134. package/yida-skills/skills/yida-design/templates/design-themes/{soft-outline-rhythm.md → soft-outline-rhythm/design.md} +60 -26
  135. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/form-layout.json +13 -0
  136. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +698 -0
  137. package/yida-skills/skills/yida-design/templates/design-themes/{soft-rail-bold-band.md → soft-rail-bold-band/design.md} +99 -49
  138. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/form-layout.json +13 -0
  139. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/app_theme.css +692 -0
  140. package/yida-skills/skills/yida-design/templates/design-themes/{soft-spectral-panels.md → soft-spectral-panels/design.md} +63 -13
  141. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/form-layout.json +13 -0
  142. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +711 -0
  143. package/yida-skills/skills/yida-design/templates/design-themes/{warm-canvas-contrast-panels.md → warm-canvas-contrast-panels/design.md} +60 -10
  144. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/form-layout.json +13 -0
  145. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +692 -0
  146. package/yida-skills/skills/yida-design/templates/design-themes/{warm-rail-muted-panels.md → warm-rail-muted-panels/design.md} +98 -50
  147. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/form-layout.json +13 -0
  148. package/yida-skills/skills/yida-design/templates/navigation-styles.json +769 -0
  149. package/yida-skills/skills/yida-design/workflow/output-design.md +5 -5
  150. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +5 -3
  151. package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +1 -1
  152. package/yida-skills/skills/yida-form-permission/SKILL.md +6 -0
  153. package/yida-skills/skills/yida-nav-shell/SKILL.md +1 -1
  154. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +6 -0
  155. package/yida-skills/skills/yida-page-config/SKILL.md +9 -0
  156. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
  157. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  158. package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +1 -1
  159. package/yida-skills/skills/yida-requirement-analysis/references/experience-groups.md +45 -9
  160. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +5 -5
  161. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +17 -4
  162. package/yida-skills/skills-index.json +2 -2
@@ -112,7 +112,7 @@ const SAMPLE_HINTS = {
112
112
  'openyida-page-template/canvas-nav-top': '顶部默认贴顶通栏;长页先合并 canvas-nav-content,CanvasNav 传 headerOnly,有首屏背景时传 overlay;悬浮外观仅显式 floating=true。',
113
113
  'openyida-page-template/canvas-nav-content': '合并 CanvasNavigationContent;长页用 layout=document,CanvasNav 传 headerOnly/overlay;默认 sticky,受宿主限制时用 navigationPosition=fixed。工作区用 workspace,宿主已分配高度时传 height。',
114
114
  'openyida-page-template/canvas-nav-data': '页内展示用 mode=local(key/viewKey/targetType=local),不请求平台菜单;platform 需真实资源 ID;independent 或声明 access 时必须接入真实 resolveAccess。保留 PRD 顺序与分组。',
115
- 'openyida-page-template/form-open-container': '将 import、CanvasDrawer、FormOpenContainer、useYidaFormOpen 和辅助函数合并到现有 Canvas 页面;表单按钮调用 openForm(request),页面必须渲染返回的 formOpenContainer;保留 iframe、标题栏操作、调宽和关闭刷新。普通业务内容使用 CanvasDrawer;抽屉背景为 --pod-shell-theme-bg-color。',
115
+ 'openyida-page-template/form-open-container': '将 import、CanvasDrawer、FormOpenContainer、useYidaFormOpen 和辅助函数合并到现有 Canvas 页面;表单按钮调用 openForm(request),页面必须渲染返回的 formOpenContainer;保留 iframe、标题栏操作、调宽和关闭刷新。普通业务内容使用 CanvasDrawer;抽屉背景使用 --pod-page-bg-color,文字使用 --color-text1-4,background 可按设计覆盖。',
116
116
  'yida-design/app-theme': '已复制应用主题模板;请由 yida-design 按 design.md 定点修改需要变化的 token。',
117
117
  };
118
118
 
@@ -150,6 +150,8 @@ function printSampleList() {
150
150
  console.log('');
151
151
  console.log(' 应用主题:openyida sample yida-design app-theme --output <app-theme.css> --design-file <design.md>');
152
152
  console.log(' Application style: openyida sample yida-design application-style --style-id <themeId|free-creative> --output <directory>');
153
+ console.log(` ${t('sample_options.theme_help')}`);
154
+ console.log(` ${t('sample_options.style_help')}`);
153
155
  }
154
156
 
155
157
  /**
@@ -164,10 +166,10 @@ function ensureDirectoryExists(filePath) {
164
166
  }
165
167
 
166
168
  /**
167
- * 解析 --output 与 --var KEY=VALUE 参数。
169
+ * 解析 sample 输出路径、设计文件、主题 ID 与模板变量;拒绝未声明参数。
168
170
  * @param {string[]} rest
169
171
  * @param {string} defaultOutputPath
170
- * @returns {{ outputPath: string, variables: Object }}
172
+ * @returns {{ outputPath: string, variables: Object, designFile?: string, styleId?: string }}
171
173
  */
172
174
  function parseOptions(rest, defaultOutputPath) {
173
175
  let outputPath = defaultOutputPath;
@@ -177,17 +179,16 @@ function parseOptions(rest, defaultOutputPath) {
177
179
 
178
180
  for (let i = 0; i < rest.length; i++) {
179
181
  const arg = rest[i];
182
+ const invalid = () => {throw new CliError(t('cli_argument.invalid', arg), { code: 'SAMPLE_ARGUMENT_INVALID' });};
183
+ if (!['--output', '--var', '--design-file', '--style-id'].includes(arg)) {invalid();}
184
+ if (!rest[i + 1] || rest[i + 1].startsWith('--')) {invalid();}
180
185
 
181
186
  if (arg === '--style-id') {
182
187
  styleId = rest[++i];
183
- if (!styleId || styleId.startsWith('--')) {throw new CliError(t('design_document.invalid', '--style-id', 'APPLICATION_STYLE_REQUIRED'), { code: 'APPLICATION_STYLE_REQUIRED' });}
184
188
  continue;
185
189
  }
186
190
 
187
191
  if (arg === '--design-file') {
188
- if (!rest[i + 1] || rest[i + 1].startsWith('--')) {
189
- throw new CliError('--design-file 缺少 design.md 路径', { code: 'DESIGN_THEME_FILE_REQUIRED' });
190
- }
191
192
  designFile = rest[++i];
192
193
  continue;
193
194
  }
@@ -201,13 +202,9 @@ function parseOptions(rest, defaultOutputPath) {
201
202
  if (arg === '--var' && rest[i + 1]) {
202
203
  const pair = rest[i + 1];
203
204
  const eqIndex = pair.indexOf('=');
204
- if (eqIndex > 0) {
205
- const key = pair.slice(0, eqIndex).trim();
206
- const value = pair.slice(eqIndex + 1);
207
- if (key) {
208
- variables[key] = value;
209
- }
210
- }
205
+ if (eqIndex <= 0 || !pair.slice(0, eqIndex).trim()) {invalid();}
206
+ const key = pair.slice(0, eqIndex).trim();
207
+ variables[key] = pair.slice(eqIndex + 1);
211
208
  i++;
212
209
  continue;
213
210
  }
@@ -248,6 +245,9 @@ async function run(args) {
248
245
  if (options.designFile || Object.keys(options.variables).length) {
249
246
  throw new CliError(t('design_document.invalid', 'application-style', 'APPLICATION_STYLE_SAMPLE_INVALID'), { code: 'APPLICATION_STYLE_SAMPLE_INVALID' });
250
247
  }
248
+ if (!options.styleId) {
249
+ throw new CliError(t('cli_argument.invalid', '--style-id'), { code: 'APPLICATION_STYLE_REQUIRED' });
250
+ }
251
251
  const result = require('../app/application-style').exportApplicationStyle(options.styleId, options.outputPath);
252
252
  console.log(JSON.stringify(result, null, 2));
253
253
  return result;
@@ -294,6 +294,9 @@ async function run(args) {
294
294
  throw new CliError('--design-file 仅用于 yida-design app-theme', { code: 'DESIGN_THEME_SAMPLE_INVALID' });
295
295
  }
296
296
  if (sampleKey === 'yida-design/app-theme') {
297
+ if (Object.keys(variables).length) {
298
+ throw new CliError(t('cli_argument.invalid', '--var (yida-design app-theme: --design-file)'), { code: 'SAMPLE_ARGUMENT_INVALID' });
299
+ }
297
300
  const snapshotPath = `${outputPath}.tokens.md`;
298
301
  if (designFile && path.resolve(snapshotPath) === path.resolve(designFile)) {
299
302
  throw new CliError('输出路径不能覆盖模板或 design.md', { code: 'SAMPLE_OUTPUT_CONFLICT' });
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ function confirmationPayload(outputs, revision) {
4
+ return {
5
+ question: '请确认当前搭建方案',
6
+ options: [{ label: '确认并开始搭建', value: 'confirm_build' }, { label: '继续调整', value: 'continue_editing' }],
7
+ submitLabel: '提交选择',
8
+ attachments: [{ name: '搭建方案.html', path: outputs.html }],
9
+ revision,
10
+ };
11
+ }
12
+
13
+ module.exports = { confirmationPayload };
@@ -12,7 +12,7 @@ function usage() {
12
12
  ' openyida design-plan catalog [--json]',
13
13
  ' openyida design-plan init <requirement-brief.json> [--theme-id <id>] [--output-dir <dir>] [--json]',
14
14
  ' openyida design-plan preview <build-plan.json> --part-file <module.json> [--json]',
15
- ' openyida design-plan materialize <build-plan.json> [--from-preview | --business-file <json> --visual-file <json>] [--output-dir <dir>] [--check] [--json]',
15
+ ' openyida design-plan materialize <build-plan.json> [--from-preview | --business-file <json> --visual-file <json>] [--output-dir <dir>] [--rebase-parts] [--check] [--json]',
16
16
  ' openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]',
17
17
  ].join('\n');
18
18
  }
@@ -50,6 +50,46 @@ function setValues(args) {
50
50
  return values;
51
51
  }
52
52
 
53
+ const OPTION_SCHEMA = Object.freeze({
54
+ catalog: { flags: ['--json'], values: [] },
55
+ init: { flags: ['--json'], values: ['--theme-id', '--output-dir'] },
56
+ preview: { flags: ['--json'], values: ['--part-file'] },
57
+ materialize: { flags: ['--from-preview', '--rebase-parts', '--check', '--json'], values: ['--business-file', '--visual-file', '--output-dir'] },
58
+ patch: { flags: ['--materialize', '--json'], values: ['--set', '--output-dir'], inlineValues: ['--set'] },
59
+ });
60
+
61
+ function assertKnownOptions(subcommand, args) {
62
+ const schema = OPTION_SCHEMA[subcommand];
63
+ const flags = new Set(schema.flags);
64
+ const values = new Set(schema.values);
65
+ const inlineValues = new Set(schema.inlineValues || []);
66
+ for (let index = 0; index < args.length; index += 1) {
67
+ const token = args[index];
68
+ const equalsAt = token.indexOf('=');
69
+ const option = equalsAt === -1 ? token : token.slice(0, equalsAt);
70
+ if (equalsAt !== -1 && inlineValues.has(option)) {
71
+ continue;
72
+ }
73
+ if (flags.has(token)) {
74
+ continue;
75
+ }
76
+ if (values.has(token)) {
77
+ if (!args[index + 1] || args[index + 1].startsWith('--')) {
78
+ throw new CliError(`${token} 缺少参数值`, {
79
+ code: 'DESIGN_PLAN_INVALID_ARGUMENT',
80
+ usage: usage(),
81
+ });
82
+ }
83
+ index += 1;
84
+ continue;
85
+ }
86
+ throw new CliError(`未知 ${subcommand} 参数:${token}`, {
87
+ code: 'DESIGN_PLAN_INVALID_ARGUMENT',
88
+ usage: usage(),
89
+ });
90
+ }
91
+ }
92
+
53
93
  async function run(args) {
54
94
  const subcommand = args[0];
55
95
  if (!subcommand || args.includes('--help') || args.includes('-h')) {
@@ -63,6 +103,7 @@ async function run(args) {
63
103
  });
64
104
  }
65
105
  if (subcommand === 'catalog') {
106
+ assertKnownOptions(subcommand, args.slice(1));
66
107
  console.log(JSON.stringify(catalog(), null, 2));
67
108
  return;
68
109
  }
@@ -74,13 +115,14 @@ async function run(args) {
74
115
  });
75
116
  }
76
117
  const options = args.slice(2);
118
+ assertKnownOptions(subcommand, options);
77
119
  const outputDir = optionValue(options, '--output-dir');
78
120
  const result = subcommand === 'init'
79
121
  ? initialize(input, { outputDir, themeId: optionValue(options, '--theme-id') })
80
122
  : subcommand === 'preview'
81
123
  ? preview(input, { partFile: optionValue(options, '--part-file') })
82
124
  : subcommand === 'materialize'
83
- ? materialize(input, { outputDir, check: options.includes('--check'), fromPreview: options.includes('--from-preview'), businessFile: optionValue(options, '--business-file'), visualFile: optionValue(options, '--visual-file') })
125
+ ? materialize(input, { outputDir, check: options.includes('--check'), rebaseParts: options.includes('--rebase-parts'), fromPreview: options.includes('--from-preview'), businessFile: optionValue(options, '--business-file'), visualFile: optionValue(options, '--visual-file') })
84
126
  : patchPlan(input, setValues(options), { outputDir, materialize: options.includes('--materialize') });
85
127
 
86
128
  if (options.includes('--json') || subcommand === 'preview') {
@@ -2,6 +2,13 @@
2
2
 
3
3
  const { CliError } = require('../core/cli-error');
4
4
  const { t } = require('../core/i18n');
5
+ const ENTRY_MODES = Object.freeze(['unified', 'service-management', 'frontend-only', 'backend-only']);
6
+ const ENTRY_MODE_SUMMARIES = Object.freeze({
7
+ unified: '不分前后台:使用同一套业务页面,按职责显示菜单、数据和操作。',
8
+ 'service-management': '分前后台:办事的人使用前台,处理业务的人使用后台,两边共享业务数据。',
9
+ 'frontend-only': '只有访问前台:本次提供办理、查询或展示页面;需要处理的业务由已有系统、人员入口或自动流程承接。',
10
+ 'backend-only': '只有访问后台:工作人员录入、受理、处理或维护业务,办事入口在别处或本次不需要。',
11
+ });
5
12
  const filled = value => typeof value === 'string' && value.trim().length > 0;
6
13
  const object = value => value && typeof value === 'object' && !Array.isArray(value);
7
14
  const assert = (condition, message, details) => {
@@ -12,7 +19,7 @@ const assert = (condition, message, details) => {
12
19
  function buildEntryNavigation(plan, resources, pages) {
13
20
  const recommendation = plan.execution?.entryRecommendation;
14
21
  if (recommendation === undefined) {return null;}
15
- assert(object(recommendation) && ['unified', 'service-management', 'frontend-only'].includes(recommendation.mode), '入口方案需要明确 unified/service-management/frontend-only');
22
+ assert(object(recommendation) && ENTRY_MODES.includes(recommendation.mode), t('cli.design_plan_entry_mode_required'));
16
23
  assert(Array.isArray(recommendation.entries) && recommendation.entries.length > 0, '入口方案缺少 entries');
17
24
  const names = new Set(resources.map(resource => resource.name));
18
25
  const entryKeys = new Set();
@@ -83,7 +90,8 @@ function buildEntryNavigation(plan, resources, pages) {
83
90
  const roles = recommendation.entries.map(entry => entry.role);
84
91
  assert(recommendation.mode !== 'service-management' || (roles.includes('service') && roles.includes('management')), '前后台方案必须同时包含访客端和业务管理端');
85
92
  assert(recommendation.mode !== 'frontend-only' || roles.every(role => role === 'service'), '仅前台方案不能包含管理入口');
93
+ assert(recommendation.mode !== 'backend-only' || roles.every(role => role === 'management'), t('cli.design_plan_backend_only_roles'));
86
94
  return { recommendation: JSON.parse(JSON.stringify(recommendation)), platformOrder: [...new Set(platformOrder)] };
87
95
  }
88
96
 
89
- module.exports = { buildEntryNavigation };
97
+ module.exports = { buildEntryNavigation, ENTRY_MODES, ENTRY_MODE_SUMMARIES };
@@ -9,7 +9,7 @@ const { planBase } = require('./parallel');
9
9
  const { collectIssues } = require('./validate');
10
10
  const { assertVisualFieldObjects } = require('./normalize');
11
11
 
12
- const { PLAN_SKILL_ROOT: ROOT, loadThemeIndex, themeTemplatePath } = require('./themes');
12
+ const { PLAN_SKILL_ROOT: ROOT, loadThemeIndex, themeTemplatePath, themeNavTheme } = require('./themes');
13
13
 
14
14
  function list(value) {
15
15
  return Array.isArray(value) ? value : [];
@@ -139,13 +139,14 @@ function initialize(inputPath, options = {}) {
139
139
  code: 'DESIGN_PLAN_THEME_UNKNOWN', details: { themes: themes.map(({ themeId, label }) => ({ themeId, label })), catalogCommand: 'openyida design-plan catalog --json' },
140
140
  });
141
141
  }
142
+ const navTheme = theme.mode === 'creative' ? brief.visualSelection?.navigationStyle?.tone || '' : themeNavTheme(theme);
142
143
  const outputDir = path.resolve(options.outputDir || path.join('prd', project));
143
144
  const output = path.join(outputDir, 'build-plan.json');
144
145
  const guide = path.join(outputDir, 'authoring-context.md');
145
146
  const business = path.join(outputDir, 'business.json');
146
147
  const visual = path.join(outputDir, 'visual.json');
147
148
  const materializeCommand = `openyida design-plan materialize ${quoteArgument(output)} --business-file ${quoteArgument(business)} --visual-file ${quoteArgument(visual)} --json`;
148
- if ([output, guide, business, visual].some(file => fs.existsSync(file))) {
149
+ if ([output, guide, business, visual, path.join(outputDir, require('./rebase').BASE_FILE)].some(file => fs.existsSync(file))) {
149
150
  throw new CliError('计划或编写上下文已存在,请继续补充现有文件', { code: 'DESIGN_PLAN_ALREADY_EXISTS' });
150
151
  }
151
152
  const scopedPages = list(brief.explicitScope?.pages);
@@ -215,7 +216,7 @@ function initialize(inputPath, options = {}) {
215
216
  ...(choice.creativeDirection ? { creativeDirection: choice.creativeDirection } : {}),
216
217
  forUser: { visualDirection: { ...(choice.visualDirection || { label: theme.label, description: theme.styleSummary }), source: visualSource },
217
218
  colorStrategy: { primaryColor: '', primaryColorName: '', source: '', usage: '', ...normalizeColorStrategy(choice.colorStrategy) },
218
- navigationStyle: choice.navigationStyle || { structure: '', tone: '', source: '', selectionReason: '' },
219
+ navigationStyle: { structure: '', source: '', selectionReason: '', ...(choice.navigationStyle || {}), tone: navTheme, toneSource: theme.mode === 'creative' ? 'project_defined' : 'theme_derived' },
219
220
  pageApplications: pages.map(page => ({ pageId: page.pageId, firstScreenFocus: '', layout: '', responsive: '', primaryAction: '', acceptanceChecks: [],
220
221
  visualMemoryApplications: [], ...list(choice.pageApplications).find(item => item?.pageId === page.pageId) })),
221
222
  ...(choice.assetStrategy ? { assetStrategy: choice.assetStrategy } : {}),
@@ -233,9 +234,11 @@ function initialize(inputPath, options = {}) {
233
234
  `读取本上下文、编写契约和 business.json;按 authoring.pendingFields 补齐事实,保留 base 与已有需求。business facts 填写 overview、dataModels、businessFlows、pages 和可选 execution;视觉填写 visual.json。复核业务覆盖后设置片段 ready=true,再执行:${materializeCommand}。使用成功结果中的 HTML 路径展示“当前方案”,revision 仅放在内部确认字段,不写入用户可见标题、摘要或问题。视觉片段按每页实际任务补齐焦点、布局、主操作、响应式与验收;已完成的选择直接复用。`, '',
234
235
  '## 本轮编写重点', '设计功能范围、数据与规则、页面组织、关键交互和业务验收。页面 blocks 按优先顺序填写 {name,purpose};CLI 派生默认内容优先级与内容层次,保留明确填写的独立设计。通用状态和验收检查由 CLI 补齐,interactionStates 与 acceptanceCriteria 填写项目差异。', '',
235
236
  '首页按使用者的主要任务选择:集中办事可用门户式工作台(workbench),单项任务可直达原生业务页;只有已确认的指标分析或投屏监控任务才规划 dashboard/screen。工作台按需求组织待办、操作队列、业务列表和常用入口,辅助统计不改变页面主任务。不得从后台身份、系统名称或暗色风格自动增加驾驶舱。完整规则见 yida-requirement-analysis/references/experience-groups.md 的“首页按任务选择”。', '',
236
- '前台、后台及共用工作区都要确定默认页面、菜单及分组顺序、打开后的首屏任务。menu 数组记录展示顺序,defaultMenuKey 记录默认任务;按角色需求决定,不按资源创建先后排列。平台导航通过排序命令落实,自定义导航通过页面菜单落实,frontend-only 也必须完成入口排序与首页验收。', '',
237
- '入口方案沿用 execution.entryRecommendation;逐入口补齐 role、menu 和 defaultMenuKey,访客端关联独立页面 sceneKey。任何身份的入口含 local 菜单时,entry.sceneKey 必须关联承载页面,menu.resource 等于页面 name,viewKey 非空。管理端可直接落到原生业务视图,不强制创建首页。所有叶子菜单按资源、视图和操作记录 access 权限依赖,平台导航排序从管理菜单派生。详见 yida-app/references/entry-navigation.md。', '',
238
- '保留 visual.json 中 facts.visualStyle.forUser.visualDirection 和 navigationStyle 的对象结构,只补齐内部字段;前者包含 label/description,后者包含 structure=top|side 和 tone=light|dark,不改写为文字字符串。', '',
237
+ '无论是否分前后台,每个业务入口都要确定默认页面、菜单及分组顺序、打开后的首屏任务。menu 数组记录展示顺序,defaultMenuKey 记录默认任务;按角色需求决定,不按资源创建先后排列。平台导航通过排序命令落实,自定义导航通过页面菜单落实,frontend-only 也必须完成入口排序与首页验收。', '',
238
+ '入口方案沿用 execution.entryRecommendation:unified 表示不分前后台,service-management 表示分前后台,frontend-only 表示只有访问前台,backend-only 表示只有访问后台。向用户说明谁使用、看到什么、能做什么;只有前台且提交后需要处理时,在 reason 和业务流程中说明承接方式;逐入口补齐 role、menu 和 defaultMenuKey,前台关联独立页面 sceneKey。任何身份的入口含 local 菜单时,entry.sceneKey 必须关联承载页面,menu.resource 等于页面 name,viewKey 非空。管理端可直接落到原生业务视图,不强制创建首页。所有叶子菜单按资源、视图和操作记录 access 权限依赖,平台导航排序从管理菜单派生。详见 yida-app/references/entry-navigation.md。', '',
239
+ theme.mode === 'creative'
240
+ ? '保留 visual.json 中 facts.visualStyle.forUser.visualDirection 和 navigationStyle 的对象结构;自由创意需明确填写 structure=top|side 和 tone=light|dark,并配套导航 Token。'
241
+ : `保留 visual.json 中 facts.visualStyle.forUser.visualDirection 和 navigationStyle 的对象结构,只补齐内部字段;前者包含 label/description,后者只需维护 structure=top|side。tone=${navTheme} 由已选主题模板派生,不要修改。`, '',
239
242
  '## 页面模式', ...patterns.map(item => `- ${item.id}(${item.label}):${item.mustKeep.join(';')}`), '',
240
243
  `## 已选风格:${theme.label}`, overview.trim(), '',
241
244
  '平台变量清单是基础契约,不是主题能力上限。visualStyle.tokens 可新增项目命名空间的材质、布局、字体、动效和组件状态变量,并在设计正文说明消费位置;平台变量可引用同一主题中已声明且无循环的扩展变量。不要为一次性页面样式强制新增 token。', '',
@@ -264,7 +267,8 @@ function initialize(inputPath, options = {}) {
264
267
  const visualPart = `${JSON.stringify({ base, ready: visualReady, facts: { visualStyle: plan.visualStyle } }, null, 2)}\n`;
265
268
  const authoring = authoringGuidance(plan, business, visual, businessReady, visualReady);
266
269
  const formatContext = ['## 输入格式示例', '示例仅说明格式,按当前业务填写。', '```json', JSON.stringify(authoring.examples, null, 2), '```', '', authoring.completion, ''].join('\n');
267
- writeFiles([[output, `${JSON.stringify(plan, null, 2)}\n`], [guide, context + formatContext], [business, businessPart], [visual, visualPart]]);
270
+ const baseline = path.join(outputDir, require('./rebase').BASE_FILE);
271
+ writeFiles([[output, `${JSON.stringify(plan, null, 2)}\n`], [baseline, `${JSON.stringify({ schemaVersion: 1, base, plan }, null, 2)}\n`], [guide, context + formatContext], [business, businessPart], [visual, visualPart]]);
268
272
  return { success: true, input, output, context: guide, contract: path.join(ROOT, 'references/build-plan-compact-schema.md'), draft: true,
269
273
  materialize: { mode: 'complete_files_once', maxCalls: 1,
270
274
  shell: process.platform === 'win32' ? 'powershell' : 'posix',
@@ -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
  }
@@ -365,6 +364,7 @@ function buildExecution(plan, options = {}) {
365
364
  const entryNavigation = buildEntryNavigation(plan, handoff.resourceBlueprint, handoff.pages);
366
365
  if (entryNavigation) {
367
366
  handoff.entryRecommendation = entryNavigation.recommendation;
367
+ handoff.entryModeSummary = ENTRY_MODE_SUMMARIES[entryNavigation.recommendation.mode];
368
368
  const skipOrder = resourceOnly || navigation.navigationType === 'custom' || entryNavigation.recommendation.mode === 'frontend-only';
369
369
  if (execution.navigationOrder !== undefined) {
370
370
  requireFact(JSON.stringify(execution.navigationOrder) === JSON.stringify(skipOrder ? [] : entryNavigation.platformOrder), 'navigationOrder 与入口管理菜单冲突,请修改入口菜单这一份源事实');
@@ -391,7 +391,7 @@ function buildExecution(plan, options = {}) {
391
391
 
392
392
  function renderEntryNavigation(recommendation) {
393
393
  if (!Array.isArray(recommendation?.entries)) {return [];}
394
- const lines = ['### 访问态入口与权限', '访客端与业务管理端共享资源,分别组织菜单;页面隐藏导航不授予业务权限。', ''];
394
+ const lines = ['### 访问态入口与权限', ENTRY_MODE_SUMMARIES[recommendation.mode], '页面隐藏导航不授予业务权限。', ''];
395
395
  for (const entry of recommendation.entries) {
396
396
  const rows = [];
397
397
  const visit = items => {
@@ -485,35 +485,11 @@ function normalizeHex(value) {
485
485
  return match ? `#${match[1].toUpperCase()}` : null;
486
486
  }
487
487
 
488
- function mixHex(hex, target, targetRatio) {
489
- const source = [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16));
490
- const mixed = source.map((channel, index) => Math.round(channel * (1 - targetRatio) + target[index] * targetRatio));
491
- return `#${mixed.map(channel => channel.toString(16).padStart(2, '0')).join('').toUpperCase()}`;
492
- }
493
-
494
- function navigationPalette(primaryColor, tone, tokens) {
495
- const shell = resolveColorToken(tokens, '--pod-shell-theme-bg-color');
496
- const isDark = shell && [1, 3, 5].every(index => parseInt(shell.slice(index, index + 2), 16) < 128);
497
- if (!shell || isDark === (tone === 'dark')) {return {};}
498
- const dark = tone === 'dark';
499
- const background = dark
500
- ? mixHex(primaryColor, [23, 23, 23], 0.62)
501
- : mixHex(primaryColor, [255, 255, 255], 0.96);
502
- return {
503
- '--pod-shell-theme-bg-color': background,
504
- '--pod-nav-item-text-color': dark ? '#D6D6D6' : '#606060',
505
- '--pod-nav-item-text-hover-color': dark ? '#FFFFFF' : '#303030',
506
- '--pod-nav-item-text-selected-color': dark ? '#FFFFFF' : '#171717',
507
- '--pod-nav-menu-bg-hover-color': mixHex(background, dark ? [255, 255, 255] : [0, 0, 0], dark ? 0.08 : 0.04),
508
- '--pod-nav-menu-bg-selected-color': mixHex(background, dark ? [255, 255, 255] : [0, 0, 0], dark ? 0.16 : 0.08),
509
- };
510
- }
511
-
512
488
  function resolveTheme(plan) {
513
489
  const { selectedTheme: selected } = visualSelections(plan);
514
490
  const index = loadThemeIndex();
515
491
  const theme = (index.themes || []).find(item => item.themeId === selected.themeId);
516
- if (!theme || theme.templatePath !== selected.templatePath) {
492
+ if (!theme || (theme.templatePath !== selected.templatePath && (!theme.legacyTemplatePath || theme.legacyTemplatePath !== selected.templatePath))) {
517
493
  throw new CliError('selectedTheme 的 themeId 与 templatePath 不属于主题索引中的同一记录', {
518
494
  code: 'DESIGN_PLAN_THEME_MISMATCH',
519
495
  details: { selectedTheme: selected },
@@ -546,14 +522,38 @@ function renderNavigationDesign(plan, navigationStyle, tokens) {
546
522
  '--pod-shell-theme-bg-color': '导航背景', '--pod-nav-item-text-color': '普通文字',
547
523
  '--pod-nav-item-text-hover-color': '悬停文字', '--pod-nav-item-text-selected-color': '选中文字',
548
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': '框架圆角',
549
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;
550
548
  return [
551
- '#### 本项目导航配色', '',
549
+ '#### 本项目导航与应用框架', '',
552
550
  `- 导航类型:${NAVIGATION_TYPES[navigationConfig(plan).navigationType].label}`,
553
551
  `- 导航明暗:${navigationToneLabel(navigationStyle.tone)}`,
554
552
  '- 导航背景:`--pod-shell-theme-bg-color`(最终值见 tokens)',
555
- `- 选择依据:${text(navigationStyle.selectionReason)}`, '',
556
- 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]])), '',
557
557
  ...(plan.pages?.customPageDetails || []).map(page => [
558
558
  `#### ${text(page.name)}的导航`, '', mdTable(['项目', '方案'], pageNavigationRows(page, plan)),
559
559
  ].join('\n')),
@@ -619,7 +619,7 @@ function renderProjectApplications(plan, tokens, color, chapter) {
619
619
  '## 5. 项目应用与调整规则', '',
620
620
  themeImplementationRules(chapter), '',
621
621
  '### 本项目实现边界', '',
622
- '本项目显式 Token 与页面决定优先于默认值;原生导航采用第二章列出的六项实际配色,自绘导航按本项目入口方案实施。业务内容、操作和数据来自 PRD,条件配方仅用于本项目已有内容。',
622
+ '本项目显式 Token 与页面决定优先于默认值;原生导航采用第二章的导航设计和完整导航 token,自绘导航按本项目入口方案实施。导航、应用框架、表单、自定义页面与详情保持统一风格。业务内容、操作和数据来自 PRD,条件配方用于本项目已有内容。',
623
623
  `已确认约束:${text(plan.visualStyle?.constraints, '无额外约束')}。`, '',
624
624
  '### 配色与语义', '',
625
625
  `本项目主色为 ${text(color.primaryColorName, '')} ${color.primaryColor};来源为 ${text(color.source, 'AI 推断')}。${text(color.usage, '用于主要品牌操作和少量焦点,独立数据色和平台状态色保持各自语义。')}`, '',
@@ -719,7 +719,13 @@ function renderDesign(plan, options = {}) {
719
719
  if (typeof explicitTokens !== 'object' || Array.isArray(explicitTokens)) {
720
720
  throw new CliError('visualStyle.tokens 必须是 CSS token 对象', { code: 'DESIGN_PLAN_INVALID_TOKENS' });
721
721
  }
722
- 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 };
723
729
  for (const [token, value] of Object.entries(overrides)) {
724
730
  if (!/^--[\w-]+$/.test(token) || /^--color-brand/.test(token) || typeof value !== 'string') {
725
731
  throw new CliError(`无效视觉 token:${token};品牌色由 primaryColor 推导`, { code: 'DESIGN_PLAN_INVALID_TOKENS' });
@@ -744,8 +750,15 @@ function renderDesign(plan, options = {}) {
744
750
  chapters[1] += `\n\n### 自主设计决策\n\n${decision.composition}\n\n${decision.typography}\n\n${decision.material}`;
745
751
  chapters[2] += `\n\n### 表单布局决策\n\n${decision.formLayout}`;
746
752
  }
747
- chapters[1] = chapters[1].replace(/^(### 2\.2 应用导航\n)/m,
748
- (_, heading) => `${heading}\n${renderNavigationDesign(plan, navigationStyle, tokens)}\n\n`);
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
+ });
749
762
  const themeGuidance = fs.readFileSync(THEME_CONSISTENCY_PATH, 'utf8');
750
763
  for (const section of ['指标卡与按钮配色', '分组标题与分割线']) {
751
764
  const guidance = themeGuidance.match(new RegExp(`(?:^|\\n)(## ${section}\\n[\\s\\S]*?)(?=\\n## |$)`))?.[1];
@@ -825,8 +838,9 @@ function renderHtml(plan, sections) {
825
838
 
826
839
  // Draft sections share the final PRD renderer; implementation details are filled at final validation.
827
840
  function renderDraftPrd(plan) {
841
+ const { navigationStyle } = visualSelections(plan);
828
842
  return renderPrd(plan, {
829
- appConfig: { ...navigationConfig(plan), navTheme: plan.visualStyle?.forUser?.navigationStyle?.tone },
843
+ appConfig: { ...navigationConfig(plan), navTheme: navigationStyle.tone },
830
844
  pageNavigation: [], sampleDataPlan: [],
831
845
  pages: (plan.pages?.customPageDetails || []).map(page => ({ pageSpecHandoff: page.pageSpecHandoff || {} })),
832
846
  interactionStates: {}, resourceBlueprint: [], resourceCreationOrder: [], pageImplementationOrder: [],
@@ -920,6 +934,9 @@ function materializeUnlocked(inputPath, options = {}) {
920
934
  const readMs = performance.now() - start;
921
935
  const mergeStart = performance.now();
922
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
+ }
923
940
  if (merging && (!options.businessFile || !options.visualFile)) {
924
941
  throw new CliError('--business-file 与 --visual-file 需同时提供', { code: 'DESIGN_PLAN_PARTS_REQUIRED' });
925
942
  }
@@ -928,7 +945,7 @@ function materializeUnlocked(inputPath, options = {}) {
928
945
  }
929
946
  if (merging && options.fromPreview) { throw new CliError('选择一种规划来源', { code: 'DESIGN_PLAN_PART_CONFLICT' }); }
930
947
  const merged = options.fromPreview ? require('./preview').finalizePreview(source, path.join(path.dirname(input), 'preview/.state.json'))
931
- : merging ? mergeParts(source, options.businessFile, options.visualFile) : source;
948
+ : merging ? mergeParts(source, options.businessFile, options.visualFile, { input, rebaseParts: options.rebaseParts }) : source;
932
949
  const updatesSource = merging || options.fromPreview;
933
950
  const mergeMs = performance.now() - mergeStart;
934
951
  const previewDir = path.join(path.dirname(input), 'preview');
@@ -953,7 +970,9 @@ function materializeUnlocked(inputPath, options = {}) {
953
970
  }
954
971
  const timings = { readMs, ...(merging ? { mergeMs } : {}), ...artifacts.timings, writeMs: performance.now() - writeStart, totalMs: performance.now() - start };
955
972
  for (const key of Object.keys(timings)) {timings[key] = Math.round(timings[key] * 100) / 100;}
956
- 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 } : {}) };
957
976
  }
958
977
 
959
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