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.
- package/README.md +1 -1
- package/lib/app/application-style.js +3 -1
- package/lib/app/theme-from-design.js +38 -13
- package/lib/app/update-app.js +63 -13
- package/lib/core/command-manifest.js +32 -20
- package/lib/core/locales/en.js +17 -0
- package/lib/core/locales/zh.js +17 -0
- package/lib/core/sample.js +17 -14
- package/lib/design-plan/confirmation.js +13 -0
- package/lib/design-plan/design-plan.js +44 -2
- package/lib/design-plan/entry-navigation.js +10 -2
- package/lib/design-plan/init.js +11 -7
- package/lib/design-plan/materialize.js +62 -43
- package/lib/design-plan/normalize.js +13 -14
- package/lib/design-plan/parallel.js +62 -13
- package/lib/design-plan/patch.js +21 -7
- package/lib/design-plan/rebase.js +60 -0
- package/lib/design-plan/themes.js +16 -1
- package/lib/design-plan/validate.js +6 -1
- package/lib/design-plan/visual-policy.js +18 -4
- package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +3 -3
- package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +6 -4
- package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +5 -5
- package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -2
- package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +5 -4
- package/package.json +1 -1
- package/yida-skills/SKILL.md +2 -2
- package/yida-skills/references/yida-api.md +4 -0
- package/yida-skills/skills/yida-app/SKILL.md +12 -4
- package/yida-skills/skills/yida-app/references/entry-navigation.md +11 -9
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +1 -1
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +11 -4
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +3 -3
- package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +2 -2
- package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +3 -3
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +3 -2
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +2 -2
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +1 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +1 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +140 -1
- package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +1 -1
- package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +1 -1
- package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +7 -0
- package/yida-skills/skills/yida-create-form-page/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/SKILL.md +5 -3
- package/yida-skills/skills/yida-design/references/application-style-library.md +22 -7
- package/yida-skills/skills/yida-design/references/application-theme-consistency.md +45 -5
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +1 -1
- package/yida-skills/skills/yida-design/references/native-form-styles.md +1 -1
- package/yida-skills/skills/yida-design/references/navigation-decision.md +6 -4
- package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +62 -0
- package/yida-skills/skills/yida-design/references/theme-selection.md +10 -9
- package/yida-skills/skills/yida-design/references/visual-decision-engine.md +1 -1
- package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +35 -9
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +13 -3
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +4 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +4 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-design.md +3 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +15 -17
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +2 -0
- package/yida-skills/skills/yida-design/templates/design-themes/README.md +15 -10
- package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +69 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +69 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +244 -103
- package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +223 -82
- package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +225 -84
- package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +69 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +244 -103
- package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +69 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +244 -103
- package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +236 -89
- package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +74 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +223 -82
- package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +240 -89
- package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +76 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +69 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +68 -13
- package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +254 -107
- package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +74 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +233 -88
- package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +73 -14
- package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +224 -83
- package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +69 -14
- package/yida-skills/skills/yida-design/templates/design-themes/basic-tokens.json +2 -1
- package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +690 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{dark-inset-hairline.md → dark-inset-hairline/design.md} +103 -58
- package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +691 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-fine-lines.md → dark-rail-fine-lines/design.md} +102 -50
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/app_theme.css +696 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-signal-panels.md → dark-rail-signal-panels/design.md} +62 -12
- package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +278 -115
- package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +57 -10
- package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/app_theme.css +692 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{graphite-bevel-grid.md → graphite-bevel-grid/design.md} +60 -10
- package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/app_theme.css +691 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{hairline-shared-bands.md → hairline-shared-bands/design.md} +62 -12
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +691 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{hairline-soft-blocks.md → hairline-soft-blocks/design.md} +57 -9
- package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/index.json +141 -48
- package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +701 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{ink-glow-soft-panels.md → ink-glow-soft-panels/design.md} +58 -8
- package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +691 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{inset-frame-pixel-rhythm.md → inset-frame-pixel-rhythm/design.md} +59 -9
- package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +698 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{outlined-texture-duotone.md → outlined-texture-duotone/design.md} +62 -12
- package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +691 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{soft-inset-surfaces.md → soft-inset-surfaces/design.md} +59 -11
- package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +690 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{soft-outline-rhythm.md → soft-outline-rhythm/design.md} +60 -26
- package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +698 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{soft-rail-bold-band.md → soft-rail-bold-band/design.md} +99 -49
- package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/app_theme.css +692 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{soft-spectral-panels.md → soft-spectral-panels/design.md} +63 -13
- package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +711 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{warm-canvas-contrast-panels.md → warm-canvas-contrast-panels/design.md} +60 -10
- package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +692 -0
- package/yida-skills/skills/yida-design/templates/design-themes/{warm-rail-muted-panels.md → warm-rail-muted-panels/design.md} +98 -50
- package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/form-layout.json +13 -0
- package/yida-skills/skills/yida-design/templates/navigation-styles.json +769 -0
- package/yida-skills/skills/yida-design/workflow/output-design.md +5 -5
- package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +5 -3
- package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +1 -1
- package/yida-skills/skills/yida-form-permission/SKILL.md +6 -0
- package/yida-skills/skills/yida-nav-shell/SKILL.md +1 -1
- package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +6 -0
- package/yida-skills/skills/yida-page-config/SKILL.md +9 -0
- package/yida-skills/skills/yida-prd/SKILL.md +1 -0
- package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
- package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +1 -1
- package/yida-skills/skills/yida-requirement-analysis/references/experience-groups.md +45 -9
- package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +5 -5
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +17 -4
- package/yida-skills/skills-index.json +2 -2
package/lib/core/sample.js
CHANGED
|
@@ -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
|
|
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
|
-
* 解析
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
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) &&
|
|
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 };
|
package/lib/design-plan/init.js
CHANGED
|
@@ -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:
|
|
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
|
-
'
|
|
237
|
-
'入口方案沿用 execution.entryRecommendation
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
`-
|
|
556
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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,
|
|
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'
|
|
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
|
|