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