openyida 2026.9.19 → 2026.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +3 -3
  2. package/lib/app/application-style.js +67 -0
  3. package/lib/app/create-form/args.js +37 -12
  4. package/lib/app/create-form/batch.js +32 -5
  5. package/lib/app/create-form.js +94 -57
  6. package/lib/app/form-field-validator.js +0 -36
  7. package/lib/app/services/form-compiler.js +0 -2
  8. package/lib/app/theme-from-design.js +2 -1
  9. package/lib/core/command-manifest.js +51 -7
  10. package/lib/core/locales/en.js +3 -3
  11. package/lib/core/locales/zh.js +3 -3
  12. package/lib/core/sample.js +20 -1
  13. package/lib/design/document.js +1 -0
  14. package/lib/design-plan/init.js +5 -2
  15. package/lib/design-plan/materialize.js +23 -2
  16. package/lib/design-plan/validate.js +10 -0
  17. package/lib/design-plan/visual-policy.js +11 -0
  18. package/package.json +1 -1
  19. package/yida-skills/references/official-example-schema-patterns.md +1 -1
  20. package/yida-skills/skills/yida-app/SKILL.md +3 -0
  21. package/yida-skills/skills/yida-create-form-page/SKILL.md +34 -43
  22. package/yida-skills/skills/yida-create-form-page/references/advanced-form-modes.md +5 -5
  23. package/yida-skills/skills/yida-create-form-page/references/association-form-field.md +4 -4
  24. package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +2 -2
  25. package/yida-skills/skills/yida-create-form-page/references/field-definition-guide.md +4 -16
  26. package/yida-skills/skills/yida-create-form-page/references/form-field-properties.md +14 -37
  27. package/yida-skills/skills/yida-design/SKILL.md +3 -3
  28. package/yida-skills/skills/yida-design/references/application-style-library.md +78 -0
  29. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +3 -2
  30. package/yida-skills/skills/yida-design/references/native-form-styles.md +208 -0
  31. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +48 -1
  32. package/yida-skills/skills/yida-design/references/theme/application-style-recipes.css +58 -0
  33. package/yida-skills/skills/yida-design/references/theme-selection.md +5 -5
  34. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +23 -6
  35. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -1
  36. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +2 -0
  37. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +1 -1
  38. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +12 -11
  39. package/yida-skills/skills/yida-design/templates/application-styles.json +398 -0
  40. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +607 -0
  41. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +219 -0
  42. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/form-layout.json +15 -0
  43. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +607 -0
  44. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +219 -0
  45. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/form-layout.json +14 -0
  46. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +607 -0
  47. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +219 -0
  48. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/form-layout.json +13 -0
  49. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +607 -0
  50. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +219 -0
  51. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/form-layout.json +14 -0
  52. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +607 -0
  53. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +219 -0
  54. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/form-layout.json +14 -0
  55. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +607 -0
  56. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +219 -0
  57. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/form-layout.json +14 -0
  58. package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +607 -0
  59. package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +219 -0
  60. package/yida-skills/skills/yida-design/templates/design-themes/app-line/form-layout.json +13 -0
  61. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +607 -0
  62. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +219 -0
  63. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/form-layout.json +14 -0
  64. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +607 -0
  65. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +219 -0
  66. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/form-layout.json +13 -0
  67. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +607 -0
  68. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +219 -0
  69. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/form-layout.json +13 -0
  70. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +607 -0
  71. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +219 -0
  72. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/form-layout.json +14 -0
  73. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +607 -0
  74. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +219 -0
  75. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/form-layout.json +14 -0
  76. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +607 -0
  77. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +219 -0
  78. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/form-layout.json +14 -0
  79. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +607 -0
  80. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +219 -0
  81. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/form-layout.json +14 -0
  82. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +607 -0
  83. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +219 -0
  84. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/form-layout.json +14 -0
  85. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +607 -0
  86. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +219 -0
  87. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/form-layout.json +14 -0
  88. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +607 -0
  89. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +219 -0
  90. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/form-layout.json +13 -0
  91. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +607 -0
  92. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +219 -0
  93. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/form-layout.json +14 -0
  94. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline.md +4 -0
  95. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines.md +4 -0
  96. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels.md +4 -0
  97. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +526 -0
  98. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +191 -0
  99. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/form-layout.json +13 -0
  100. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid.md +4 -0
  101. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands.md +4 -0
  102. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks.md +4 -0
  103. package/yida-skills/skills/yida-design/templates/design-themes/index.json +276 -1
  104. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels.md +4 -0
  105. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm.md +4 -0
  106. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone.md +4 -0
  107. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces.md +4 -0
  108. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm.md +4 -0
  109. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band.md +4 -0
  110. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels.md +4 -0
  111. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels.md +4 -0
  112. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels.md +4 -0
  113. package/yida-skills/skills/yida-design/workflow/output-design.md +5 -2
  114. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +9 -3
package/README.md CHANGED
@@ -183,7 +183,7 @@ openyida get-schema APP_XXX FORM_XXX --compact --resolve-fields "Customer Name,S
183
183
  openyida get-schema APP_XXX --all --output-dir .cache/schemas
184
184
  ```
185
185
 
186
- Form definitions support 19 business field types plus verified presentation/layout components. Prefer `Divider` for section titles and `ColumnContainer` (mapped to `ColumnsLayout` + `Column`) for multi-column layout; use `GroupContainer` / `PageSection` only when an actual grouping container is needed.
186
+ Native forms use a component tree: business fields capture data, tabs switch content, action groups trigger business actions, images establish visual focus, status areas provide feedback, and `Divider` plus `ColumnContainer` organize hierarchy and columns. `create-form` field definitions create 19 business field types plus `Divider` and `ColumnContainer` (mapped to `ColumnsLayout` + `Column`), while updates retain other existing components.
187
187
 
188
188
  ### Custom Page Development
189
189
 
@@ -415,7 +415,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
415
415
  | Command | Description |
416
416
  |---------|-------------|
417
417
  | `openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]` | Create forms concurrently by dependency |
418
- | `openyida create-form create <appType> "<formTitle>" <fieldsJsonFile> [--icon auto\|<iconName>] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a form page |
418
+ | `openyida create-form create <appType> "<formTitle>" <fieldsJsonFile> [--layout single\|double\|card\|section] [--theme default\|compact\|comfortable] [--label-align top\|left\|right] [--icon auto\|<iconName>] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a form page |
419
419
  | `openyida create-form icons [--json]` | List available form navigation icons |
420
420
  | `openyida create-form validate-fields <fieldsJsonOrFile> [--json]` | Validate form field JSON locally |
421
421
  | `openyida create-form update <appType> <formUuid> (<changesJsonOrFile> \| --data-file <changesJsonOrFile>) [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
@@ -523,7 +523,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
523
523
  | `openyida a2a <serve\|agent-card> [options]` | Start local read-only A2A adapter or print Agent Card |
524
524
  | `openyida bridge start [--token <pair-token>] [--port 6736] [--origin https://demo.aliwork.com] [--open\|--no-open]` | Start OpenYida local web bridge service |
525
525
  | `openyida copy [--force]` | Copy project working directory |
526
- | `openyida sample [--list] [<skill> <name>] [--output <file>] [--var KEY=VALUE ...] [--design-file <design.md>]` | Output code samples/templates |
526
+ | `openyida sample [--list] [<skill> <name>] [--output <file-or-directory>] [--var KEY=VALUE ...] [--design-file <design.md>] [--style-id <themeId\|free-creative>]` | Output code samples/templates |
527
527
  | `openyida doctor [--fix]` | Environment diagnostics & auto-fix |
528
528
  | `openyida db-seq-fix [--fix]` | Detect and repair PostgreSQL sequence drift |
529
529
  | `openyida formula evaluate <formula\|file> [--schema file]` | Static-check Yida formula syntax and field refs |
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { CliError } = require('../core/cli-error');
6
+ const { t } = require('../core/i18n');
7
+ const { DESIGN_SKILL_ROOT, loadThemeIndex } = require('../design-plan/themes');
8
+ const START = '/* OPENYIDA APPLICATION STYLE RECIPES START';
9
+ const END = '/* OPENYIDA APPLICATION STYLE RECIPES END */';
10
+ const CREATIVE_TOKENS = [
11
+ '--pod-page-bg-color', '--pod-card-bg-color', '--color-text1-4',
12
+ '--form-element-medium-corner', '--form-element-medium-height',
13
+ '--pod-page-footer-bg-color', '--pod-sticky-footer-box-shadow',
14
+ '--oyd-content-width', '--oyd-content-padding', '--oyd-field-gap',
15
+ '--oyd-heading-font', '--oyd-heading-size', '--oyd-rule-style',
16
+ ];
17
+
18
+ function validateCreativeDirection(direction, tokens) {
19
+ const missing = ['businessRationale', 'composition', 'typography', 'material', 'formLayout']
20
+ .filter(key => typeof direction?.[key] !== 'string' || !direction[key].trim());
21
+ missing.push(...CREATIVE_TOKENS.filter(key => typeof tokens?.[key] !== 'string' || !tokens[key].trim()));
22
+ if (missing.length) {
23
+ throw new CliError(t('design_document.invalid', 'creativeDirection', missing.join(', ')), {
24
+ code: 'APPLICATION_STYLE_CREATIVE_INCOMPLETE', details: { missing },
25
+ });
26
+ }
27
+ }
28
+
29
+ function applyApplicationStyle(css, metadata, tokens) {
30
+ const style = metadata.applicationStyle;
31
+ if (style && (style.recipe !== 'application-style-v1' || !['template', 'creative'].includes(style.mode))) {
32
+ throw new CliError(t('design_document.invalid', 'applicationStyle', 'APPLICATION_STYLE_INVALID'), { code: 'APPLICATION_STYLE_INVALID' });
33
+ }
34
+ if (style?.mode === 'creative') {validateCreativeDirection(metadata.creativeDirection, tokens);}
35
+ const first = css.indexOf(START);
36
+ const last = css.indexOf(END);
37
+ if (first >= 0 && last < first) {
38
+ throw new CliError(t('design_document.invalid', 'app_theme.css', 'APPLICATION_STYLE_CSS_INVALID'), { code: 'APPLICATION_STYLE_CSS_INVALID' });
39
+ }
40
+ const recipe = style
41
+ ? fs.readFileSync(path.join(DESIGN_SKILL_ROOT, 'references/theme/application-style-recipes.css'), 'utf8').trim()
42
+ : '';
43
+ // Only the managed recipe block is replaced. User-authored rules stay intact.
44
+ if (first >= 0) {return css.slice(0, first) + recipe + css.slice(last + END.length);}
45
+ return recipe ? `${css.trimEnd()}\n\n${recipe}\n` : css;
46
+ }
47
+
48
+ function exportApplicationStyle(id, outputDirectory) {
49
+ const theme = loadThemeIndex().themes.find(item => item.themeId === id && item.collection === 'application-styles');
50
+ if (!theme) {throw new CliError(t('design_document.invalid', id || '--style-id', 'APPLICATION_STYLE_UNKNOWN'), { code: 'APPLICATION_STYLE_UNKNOWN' });}
51
+ const directory = path.resolve(outputDirectory);
52
+ const assets = [['design.md', theme.templatePath], ['app_theme.css', theme.cssTemplatePath], ['form-layout.json', theme.formLayoutPath]];
53
+ const outputs = assets.map(([name, relative]) => {
54
+ const source = path.resolve(DESIGN_SKILL_ROOT, relative);
55
+ const target = path.join(directory, name);
56
+ const root = path.resolve(DESIGN_SKILL_ROOT, 'templates/design-themes') + path.sep;
57
+ if (!source.startsWith(root) || fs.existsSync(target)) {
58
+ throw new CliError(t('design_document.invalid', target, 'APPLICATION_STYLE_OUTPUT_CONFLICT'), { code: 'APPLICATION_STYLE_OUTPUT_CONFLICT' });
59
+ }
60
+ return { source, target, content: fs.readFileSync(source, 'utf8') };
61
+ });
62
+ fs.mkdirSync(directory, { recursive: true });
63
+ for (const item of outputs) {fs.writeFileSync(item.target, item.content, { flag: 'wx' });}
64
+ return { success: true, mode: theme.mode, themeId: id, authoringRequired: true, outputs: outputs.map(item => item.target) };
65
+ }
66
+
67
+ module.exports = { applyApplicationStyle, validateCreativeDirection, CREATIVE_TOKENS, exportApplicationStyle };
@@ -2,6 +2,10 @@
2
2
 
3
3
  const { detectDeprecatedCreateFormInvocation } = require('../../core/command-contract');
4
4
 
5
+ const CREATE_FORM_LAYOUTS = new Set(['single', '1', 'double', '2', 'card', 'section']);
6
+ const CREATE_FORM_THEMES = new Set(['default', 'compact', 'comfortable']);
7
+ const CREATE_FORM_LABEL_ALIGNS = new Set(['top', 'left', 'right']);
8
+
5
9
  function parseMaybeJsonValue(value) {
6
10
  if (typeof value !== 'string') {
7
11
  return value;
@@ -94,24 +98,38 @@ function createParseArgs(dependencies) {
94
98
  const args = [...rawArgs];
95
99
 
96
100
  for (let i = 0; i < args.length; i++) {
97
- if (args[i] === '--layout' && i + 1 < args.length) {
98
- options.layout = args[i + 1];
101
+ if (args[i] === '--layout') {
102
+ const value = args[i + 1];
103
+ if (!value || value.startsWith('--') || !CREATE_FORM_LAYOUTS.has(value)) {
104
+ throwCreateFormError('--layout must be one of: single, double, card, section', 'CREATE_FORM_INVALID_ARGUMENTS');
105
+ }
106
+ options.layout = value;
99
107
  args.splice(i, 2);
100
108
  i--;
101
- } else if (args[i] === '--theme' && i + 1 < args.length) {
102
- options.theme = args[i + 1];
109
+ } else if (args[i] === '--theme') {
110
+ const value = args[i + 1];
111
+ if (!value || value.startsWith('--') || !CREATE_FORM_THEMES.has(value)) {
112
+ throwCreateFormError('--theme must be one of: default, compact, comfortable', 'CREATE_FORM_INVALID_ARGUMENTS');
113
+ }
114
+ options.theme = value;
103
115
  args.splice(i, 2);
104
116
  i--;
105
- } else if (args[i] === '--label-align' && i + 1 < args.length) {
106
- options.labelAlign = args[i + 1];
117
+ } else if (args[i] === '--label-align') {
118
+ const value = args[i + 1];
119
+ if (!value || value.startsWith('--') || !CREATE_FORM_LABEL_ALIGNS.has(value)) {
120
+ throwCreateFormError('--label-align must be one of: top, left, right', 'CREATE_FORM_INVALID_ARGUMENTS');
121
+ }
122
+ options.labelAlign = value;
107
123
  args.splice(i, 2);
108
124
  i--;
109
- } else if (args[i] === '--icon' && i + 1 < args.length) {
110
- options.icon = args[i + 1];
125
+ } else if (args[i] === '--icon') {
126
+ const value = args[i + 1];
127
+ if (!value || value.startsWith('--')) {
128
+ throwCreateFormError('--icon requires a value', 'CREATE_FORM_INVALID_ARGUMENTS');
129
+ }
130
+ options.icon = value;
111
131
  args.splice(i, 2);
112
132
  i--;
113
- } else if (args[i] === '--icon') {
114
- throwCreateFormError('--icon requires a value', 'CREATE_FORM_INVALID_ARGUMENTS');
115
133
  } else if (args[i] === '--data-file') {
116
134
  const next = args[i + 1];
117
135
  if (dataFileInput !== undefined || !next || next.startsWith('--')) {
@@ -120,8 +138,12 @@ function createParseArgs(dependencies) {
120
138
  dataFileInput = next;
121
139
  args.splice(i, 2);
122
140
  i--;
123
- } else if ((args[i] === '--locale' || args[i] === '--content-locale' || args[i] === '--lang') && i + 1 < args.length) {
124
- options.contentLocale = args[i + 1];
141
+ } else if (args[i] === '--locale' || args[i] === '--content-locale' || args[i] === '--lang') {
142
+ const value = args[i + 1];
143
+ if (!value || value.startsWith('--')) {
144
+ throwCreateFormError(`${args[i]} requires a value`, 'CREATE_FORM_INVALID_ARGUMENTS');
145
+ }
146
+ options.contentLocale = value;
125
147
  if (!normalizeYidaLocale(options.contentLocale)) {
126
148
  error(`Unsupported locale: ${options.contentLocale}`);
127
149
  throwCreateFormError(`Unsupported locale: ${options.contentLocale}`, 'CREATE_FORM_INVALID_ARGUMENTS');
@@ -361,4 +383,7 @@ module.exports = {
361
383
  createParseArgs,
362
384
  parseInlineValidationOptions,
363
385
  parseMaybeJsonValue,
386
+ CREATE_FORM_LAYOUTS,
387
+ CREATE_FORM_THEMES,
388
+ CREATE_FORM_LABEL_ALIGNS,
364
389
  };
@@ -9,10 +9,15 @@ const { t } = require('../../core/i18n');
9
9
  const { usage, hint } = require('../../core/chalk');
10
10
  const { assertNoEmojiInText, assertNoEmojiInValue } = require('../../core/no-emoji-guard');
11
11
  const { normalizeYidaLocale } = require('../../core/yida-i18n');
12
+ const {
13
+ CREATE_FORM_LAYOUTS,
14
+ CREATE_FORM_THEMES,
15
+ CREATE_FORM_LABEL_ALIGNS,
16
+ } = require('./args');
12
17
  const { FORM_NAV_ICONS, resolveFormNavIcon } = require('./nav-icon');
13
18
 
14
19
  const BATCH_USAGE = 'openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]';
15
- const BATCH_PLAN_EXAMPLE = '{"forms":[{"key":"customer","title":"客户","fieldsFile":"customer-fields.json"},{"key":"order","title":"订单","fieldsFile":"order-fields.json","dependsOn":["customer"]}]}';
20
+ const BATCH_PLAN_EXAMPLE = '{"forms":[{"key":"customer","title":"客户","fieldsFile":"customer-fields.json","layout":"section","theme":"comfortable","labelAlign":"left"},{"key":"order","title":"订单","fieldsFile":"order-fields.json","dependsOn":["customer"]}]}';
16
21
 
17
22
  function invalid(reason) {
18
23
  throw new CliError(t('create_form.batch_invalid'), { code: 'FORM_BATCH_INVALID', details: { reason } });
@@ -46,6 +51,20 @@ function validateStaticDefinition(form, fields) {
46
51
  locale: form.locale,
47
52
  });
48
53
  }
54
+ for (const [key, values] of [
55
+ ['layout', CREATE_FORM_LAYOUTS],
56
+ ['theme', CREATE_FORM_THEMES],
57
+ ['labelAlign', CREATE_FORM_LABEL_ALIGNS],
58
+ ]) {
59
+ if (form[key] !== undefined && !values.has(form[key])) {
60
+ invalid({
61
+ formKey: form.key,
62
+ errorCode: 'CREATE_FORM_INVALID_ARGUMENTS',
63
+ errorMsg: `Unsupported ${key}: ${form[key]}`,
64
+ [key]: form[key],
65
+ });
66
+ }
67
+ }
49
68
  }
50
69
 
51
70
  function parseArgs(args) {
@@ -126,9 +145,9 @@ function loadPlan(file, options = {}) {
126
145
  if (!form || !/^[a-zA-Z][\w-]*$/.test(form.key || '') || typeof form.title !== 'string' || !form.title.trim() ||
127
146
  (form.fields === undefined) === (form.fieldsFile === undefined) ||
128
147
  (form.dependsOn !== undefined && (!Array.isArray(form.dependsOn) || form.dependsOn.some(key => typeof key !== 'string')))) { invalid('form'); }
129
- if (Object.keys(form).some(key => !['key', 'title', 'fields', 'fieldsFile', 'dependsOn', 'formUuid', 'icon', 'locale'].includes(key))) { invalid(form.key); }
148
+ if (Object.keys(form).some(key => !['key', 'title', 'fields', 'fieldsFile', 'dependsOn', 'formUuid', 'layout', 'theme', 'labelAlign', 'icon', 'locale'].includes(key))) { invalid(form.key); }
130
149
  if (form.formUuid !== undefined && (typeof form.formUuid !== 'string' || !form.formUuid.startsWith('FORM'))) { invalid(form.key); }
131
- for (const key of ['icon', 'locale']) { if (form[key] !== undefined && typeof form[key] !== 'string') { invalid(key); } }
150
+ for (const key of ['layout', 'theme', 'labelAlign', 'icon', 'locale']) { if (form[key] !== undefined && typeof form[key] !== 'string') { invalid(key); } }
132
151
  const rawFields = form.fieldsFile === undefined ? form.fields : JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), form.fieldsFile), 'utf8'));
133
152
  const fields = normalizeAssociationReferences(rawFields, options.appType);
134
153
  validateStaticDefinition(form, fields);
@@ -192,7 +211,7 @@ function expectedReadbackFields(fields) {
192
211
  expected.push(...expectedReadbackFields(field.children));
193
212
  continue;
194
213
  }
195
- if (['Divider', 'RichText', 'PageSection', 'GroupContainer'].includes(field.type)) { continue; }
214
+ if (['Divider', 'RichText'].includes(field.type)) { continue; }
196
215
  if (typeof field.label === 'string' && field.label.trim()) {
197
216
  expected.push({ label: field.label.trim(), componentName: field.type });
198
217
  }
@@ -361,7 +380,15 @@ async function run(args, dependencies = {}) {
361
380
  const resolvedFields = mapReferences(form.fields, resolve);
362
381
  if (!formUuid) {
363
382
  const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(resolvedFields), '--no-open'];
364
- for (const key of ['icon', 'locale']) { if (form[key]) { argv.push(`--${key}`, form[key]); } }
383
+ for (const [key, option] of Object.entries({
384
+ layout: '--layout',
385
+ theme: '--theme',
386
+ labelAlign: '--label-align',
387
+ icon: '--icon',
388
+ locale: '--locale',
389
+ })) {
390
+ if (form[key]) { argv.push(option, form[key]); }
391
+ }
365
392
  try {
366
393
  const created = await call(argv);
367
394
  if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
@@ -288,13 +288,9 @@ const FORM_PRESENTATION_TYPE_ALIAS = {
288
288
  columnContainer: 'ColumnsLayout',
289
289
  Column: 'Column',
290
290
  column: 'Column',
291
- GroupContainer: 'PageSection',
292
- groupContainer: 'PageSection',
293
- PageSection: 'PageSection',
294
- pageSection: 'PageSection',
295
291
  };
296
292
 
297
- const FORM_PRESENTATION_COMPONENTS = ['Divider', 'ColumnsLayout', 'Column', 'PageSection'];
293
+ const FORM_PRESENTATION_COMPONENTS = ['Divider', 'ColumnsLayout', 'Column'];
298
294
 
299
295
  const COMPONENT_ALIAS_META = Symbol('openyida.componentAlias');
300
296
 
@@ -521,13 +517,6 @@ function normalizeI18nValue(value, fallback) {
521
517
  return i18n(text, text, text);
522
518
  }
523
519
 
524
- function normalizeStyleChoice(value, fallback) {
525
- if (value && typeof value === 'object') {
526
- return value;
527
- }
528
- return { value: value || fallback };
529
- }
530
-
531
520
  function buildSchemaNode(componentName, props, children, options) {
532
521
  const node = {
533
522
  componentName: componentName,
@@ -596,41 +585,6 @@ function buildDividerComponent(field) {
596
585
  return buildSchemaNode('Divider', props, undefined, { fieldId: props.fieldId || fieldId });
597
586
  }
598
587
 
599
- function buildPageSectionComponent(field) {
600
- const fieldId = field.fieldId || field.id || generateFieldId('PageSection');
601
- const props = mergeExtraProps({
602
- fieldId,
603
- behavior: field.behavior || 'NORMAL',
604
- visibility: field.visibility || ['PC', 'MOBILE'],
605
- showHeader: field.showHeader !== undefined ? !!field.showHeader : true,
606
- title: normalizeI18nValue(field.title !== undefined ? field.title : field.label, '分组'),
607
- tooltip: normalizeI18nValue(field.tooltip || field.tips, ''),
608
- showHeadDivider: field.showHeadDivider !== undefined ? !!field.showHeadDivider : true,
609
- sectionHeaderStyle: field.sectionHeaderStyle || 'origin',
610
- sectionHeaderBgColor: field.sectionHeaderBgColor || '#0089ff',
611
- sectionHeaderTitleColor: field.sectionHeaderTitleColor || '#171A1D',
612
- pcStyle: normalizeStyleChoice(field.pcStyle, 'origin'),
613
- showBorder: !!field.showBorder,
614
- withMargin: !!field.withMargin,
615
- withPadding: field.withPadding !== undefined ? !!field.withPadding : true,
616
- mobileStyle: normalizeStyleChoice(field.mobileStyle, 'origin'),
617
- showBorderMobile: !!field.showBorderMobile,
618
- withMarginMobile: !!field.withMarginMobile,
619
- withPaddingMobile: !!field.withPaddingMobile,
620
- }, field);
621
-
622
- if (field.newIcon !== undefined) {
623
- props.newIcon = field.newIcon;
624
- }
625
-
626
- return buildSchemaNode(
627
- 'PageSection',
628
- props,
629
- buildFormNodeComponents(field.children || []),
630
- { fieldId: props.fieldId || fieldId }
631
- );
632
- }
633
-
634
588
  function countLayoutColumns(layout) {
635
589
  const parts = String(layout || '12').split(':').filter(Boolean);
636
590
  return parts.length || 1;
@@ -684,9 +638,6 @@ function buildFormNodeComponent(field) {
684
638
  if (componentName === 'Divider') {
685
639
  return buildDividerComponent(field);
686
640
  }
687
- if (componentName === 'PageSection') {
688
- return buildPageSectionComponent(field);
689
- }
690
641
  if (componentName === 'ColumnsLayout') {
691
642
  return buildColumnsLayoutComponent(field);
692
643
  }
@@ -1734,7 +1685,6 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1734
1685
  labelWeightMobile: 'normal',
1735
1686
  },
1736
1687
  condition: true,
1737
- css: 'body{background-color:transparent}',
1738
1688
  methods: {
1739
1689
  __initMethods__: {
1740
1690
  type: 'js',
@@ -1944,7 +1894,6 @@ function buildEmptyFormSchema() {
1944
1894
  contentBgColorMobile: 'white',
1945
1895
  },
1946
1896
  condition: true,
1947
- css: 'body{background-color:transparent}',
1948
1897
  methods: {
1949
1898
  __initMethods__: {
1950
1899
  type: 'js',
@@ -2609,6 +2558,84 @@ function findFieldByIdOrLabelDeep(fields, identifier) {
2609
2558
  return null;
2610
2559
  }
2611
2560
 
2561
+ function normalizeFormInjectionKey(key) {
2562
+ return String(key || '').replace(/[^a-z0-9]/gi, '').toLowerCase();
2563
+ }
2564
+
2565
+ function isRawFormInjectionKey(key) {
2566
+ const normalized = normalizeFormInjectionKey(key);
2567
+ return normalized === 'srcdoc'
2568
+ || normalized === 'stylesheet'
2569
+ || normalized === 'stylesheets'
2570
+ || normalized === 'styletext'
2571
+ || normalized === 'styletag'
2572
+ || normalized.includes('innerhtml')
2573
+ || normalized.includes('outerhtml')
2574
+ || normalized.includes('dangerouslysetinnerhtml')
2575
+ || normalized.startsWith('css')
2576
+ || normalized.endsWith('css')
2577
+ || normalized.startsWith('html')
2578
+ || normalized.endsWith('html');
2579
+ }
2580
+
2581
+ function isRawMarkupComponent(componentName) {
2582
+ return ['html', 'iframe'].includes(String(componentName || '').toLowerCase());
2583
+ }
2584
+
2585
+ function assertNoFormInjectionValue(value, location) {
2586
+ if (Array.isArray(value)) {
2587
+ value.forEach(function (item, index) {
2588
+ assertNoFormInjectionValue(item, location + '[' + index + ']');
2589
+ });
2590
+ return;
2591
+ }
2592
+ if (!value || typeof value !== 'object') {
2593
+ return;
2594
+ }
2595
+ Object.keys(value).forEach(function (key) {
2596
+ if (isRawFormInjectionKey(key)) {
2597
+ throw new Error('表单补丁不支持 CSS/HTML 注入: ' + location + '.' + key);
2598
+ }
2599
+ if (normalizeFormInjectionKey(key) === 'componentname' && isRawMarkupComponent(value[key])) {
2600
+ throw new Error('表单补丁不支持 CSS/HTML 注入组件: ' + value[key]);
2601
+ }
2602
+ assertNoFormInjectionValue(value[key], location + '.' + key);
2603
+ });
2604
+ }
2605
+
2606
+ function assertNoFormInjectionPath(pointer, value) {
2607
+ if (!pointer) {
2608
+ return;
2609
+ }
2610
+ const segments = splitJsonPointer(pointer);
2611
+ const blockedSegment = segments.find(isRawFormInjectionKey);
2612
+ if (blockedSegment) {
2613
+ throw new Error('表单补丁不支持 CSS/HTML 注入路径: ' + pointer);
2614
+ }
2615
+ const lastSegment = normalizeFormInjectionKey(segments[segments.length - 1]);
2616
+ if (lastSegment === 'componentname' && isRawMarkupComponent(value)) {
2617
+ throw new Error('表单补丁不支持 CSS/HTML 注入组件: ' + value);
2618
+ }
2619
+ }
2620
+
2621
+ function assertNoFormInjectionSource(source, location) {
2622
+ if (typeof source !== 'string' || source.length === 0) {
2623
+ return;
2624
+ }
2625
+ const forbiddenPatterns = [
2626
+ /\b(?:innerHTML|outerHTML|insertAdjacentHTML|dangerouslySetInnerHTML|srcdoc|cssText|insertRule|adoptedStyleSheets)\b/i,
2627
+ /\bdocument\s*\.\s*(?:write|writeln)\s*\(/i,
2628
+ /\bcreateElement\s*\(\s*['"`](?:style|script|iframe|link)['"`]\s*\)/i,
2629
+ /\bsetAttribute\s*\(\s*['"`](?:style|srcdoc)['"`]/i,
2630
+ /\.\s*style\s*(?:\.|\[)/i,
2631
+ /<\s*(?:style|script|iframe|html|body)\b/i,
2632
+ /openyida:theme|yida-global-theme/i,
2633
+ ];
2634
+ if (forbiddenPatterns.some(function (pattern) { return pattern.test(source); })) {
2635
+ throw new Error('表单动作不支持 CSS/HTML 注入: ' + location);
2636
+ }
2637
+ }
2638
+
2612
2639
  function resolveJsonPointerParent(target, pointer) {
2613
2640
  const segments = splitJsonPointer(pointer);
2614
2641
  if (segments.length === 0) {
@@ -2764,6 +2791,7 @@ function applyAtomicFieldAction(schema, formContainer, operation) {
2764
2791
  const actions = ensureActionModule(schema);
2765
2792
  const source = readOptionalSource(operation, 'source', 'sourceFile');
2766
2793
  if (source !== undefined) {
2794
+ assertNoFormInjectionSource(source, 'field-action ' + actionName);
2767
2795
  if (!extractExportedActionFunctionNames(source).includes(actionName)) {
2768
2796
  throw createCreateFormError(
2769
2797
  t('create_form.action_source_export_missing', actionName),
@@ -2779,12 +2807,14 @@ function applyAtomicFieldAction(schema, formContainer, operation) {
2779
2807
  }
2780
2808
  const blockStart = '/* openyida:field-action:start:' + actionName + ' */';
2781
2809
  const blockEnd = '/* openyida:field-action:end:' + actionName + ' */';
2782
- actions.module.source = upsertGeneratedSourceBlockWithBounds(
2810
+ const nextActionSource = upsertGeneratedSourceBlockWithBounds(
2783
2811
  actions.module.source || '',
2784
2812
  source,
2785
2813
  blockStart,
2786
2814
  blockEnd
2787
2815
  );
2816
+ assertNoFormInjectionSource(nextActionSource, 'field-action module');
2817
+ actions.module.source = nextActionSource;
2788
2818
  actions.module.compiled = compileActionSource(actions.module.source);
2789
2819
  actions.type = actions.type || 'FUNCTION';
2790
2820
  } else if (!extractExportedActionFunctionNames(actions.module.source || '').includes(actionName)) {
@@ -2829,6 +2859,14 @@ function applySchemaPatchOperations(schema, operations) {
2829
2859
  throw new Error('补丁[' + index + '] 缺少 action/op');
2830
2860
  }
2831
2861
 
2862
+ assertNoFormInjectionPath(operation.path, operation.value);
2863
+ if (Object.prototype.hasOwnProperty.call(operation, 'value')) {
2864
+ assertNoFormInjectionValue(operation.value, '补丁[' + index + '].value');
2865
+ }
2866
+ if (operation.props !== undefined) {
2867
+ assertNoFormInjectionValue(operation.props, '补丁[' + index + '].props');
2868
+ }
2869
+
2832
2870
  if (['add', 'replace', 'remove'].includes(action)) {
2833
2871
  applyJsonPointerOperation(schema, operation);
2834
2872
  applied.push({ action, path: operation.path });
@@ -2888,6 +2926,8 @@ function applySchemaPatchOperations(schema, operations) {
2888
2926
  const source = readOptionalSource(operation, 'source', 'sourceFile');
2889
2927
  const compiled = readOptionalSource(operation, 'compiled', 'compiledFile')
2890
2928
  || (source ? compileActionSource(source) : undefined);
2929
+ assertNoFormInjectionSource(source, 'actions-module source');
2930
+ assertNoFormInjectionSource(compiled, 'actions-module compiled');
2891
2931
  if (source !== undefined) {
2892
2932
  actions.module.source = source;
2893
2933
  }
@@ -4351,10 +4391,6 @@ function applyFieldChanges(component, changes) {
4351
4391
  props.tips = i18n(changes.tips);
4352
4392
  }
4353
4393
 
4354
- if (changes.tooltip !== undefined && component.componentName === 'PageSection') {
4355
- props.tooltip = i18n(changes.tooltip);
4356
- }
4357
-
4358
4394
  // ── 特殊处理:required(操作 validation 数组)
4359
4395
  if (changes.required !== undefined && !isFormPresentationComponent(component.componentName)) {
4360
4396
  if (changes.required) {
@@ -6518,6 +6554,7 @@ module.exports = {
6518
6554
  parseArgs,
6519
6555
  _private: {
6520
6556
  buildFormSchema,
6557
+ buildEmptyFormSchema,
6521
6558
  buildFormNodeComponent,
6522
6559
  countDataFieldDefinitions,
6523
6560
  collectComponentNames,
@@ -58,10 +58,6 @@ const PRESENTATION_TYPE_ALIAS = Object.freeze({
58
58
  columnContainer: 'ColumnContainer',
59
59
  Column: 'Column',
60
60
  column: 'Column',
61
- GroupContainer: 'GroupContainer',
62
- groupContainer: 'GroupContainer',
63
- PageSection: 'PageSection',
64
- pageSection: 'PageSection',
65
61
  });
66
62
 
67
63
  const BUSINESS_FIELD_TYPES = Object.freeze([
@@ -90,8 +86,6 @@ const PRESENTATION_FIELD_TYPES = Object.freeze([
90
86
  'Divider',
91
87
  'ColumnContainer',
92
88
  'Column',
93
- 'GroupContainer',
94
- 'PageSection',
95
89
  ]);
96
90
 
97
91
  const OPTION_FIELD_TYPES = Object.freeze([
@@ -372,11 +366,6 @@ function validateFieldDefinition(field, path, diagnostics, options) {
372
366
  return;
373
367
  }
374
368
 
375
- if (type === 'GroupContainer' || type === 'PageSection') {
376
- validateOneDimensionalChildren(field, path, diagnostics, type);
377
- return;
378
- }
379
-
380
369
  if (type === 'TableField') {
381
370
  validateTableField(field, path, diagnostics);
382
371
  }
@@ -496,31 +485,6 @@ function validateColumnContainer(field, path, diagnostics) {
496
485
  });
497
486
  }
498
487
 
499
- function validateOneDimensionalChildren(field, path, diagnostics, type) {
500
- if (!validateChildrenPropertyShape(field, path, diagnostics, 'FieldDefinition[]')) {
501
- return;
502
- }
503
- if (field.children === undefined || field.children === null) {
504
- return;
505
- }
506
- field.children.forEach(function (child, childIndex) {
507
- const childPath = path + '.children[' + childIndex + ']';
508
- if (Array.isArray(child)) {
509
- pushDiagnostic(
510
- diagnostics,
511
- 'INVALID_' + type.toUpperCase() + '_CHILDREN_DEPTH',
512
- childPath,
513
- 'FieldDefinition object',
514
- 'array',
515
- type + '.children must be a one-dimensional field array.',
516
- 'Remove extra array nesting from ' + type + '.children.'
517
- );
518
- return;
519
- }
520
- validateFieldDefinition(child, childPath, diagnostics);
521
- });
522
- }
523
-
524
488
  function validateTableField(field, path, diagnostics) {
525
489
  if (!validateChildrenPropertyShape(field, path, diagnostics, 'FieldDefinition[]')) {
526
490
  return;
@@ -1638,7 +1638,6 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, t
1638
1638
  labelWeightMobile: 'normal',
1639
1639
  },
1640
1640
  condition: true,
1641
- css: 'body{background-color:transparent}',
1642
1641
  methods: {
1643
1642
  __initMethods__: {
1644
1643
  type: 'js',
@@ -1844,7 +1843,6 @@ function buildEmptyFormSchema() {
1844
1843
  contentBgColorMobile: 'white',
1845
1844
  },
1846
1845
  condition: true,
1847
- css: 'body{background-color:transparent}',
1848
1846
  methods: {
1849
1847
  __initMethods__: {
1850
1848
  type: 'js',
@@ -133,7 +133,7 @@ function applyDesignTokens(template, markdown, previousMarkdown) {
133
133
  const changed = Object.fromEntries(Object.entries(tokens).filter(([name, value]) => !previous || previous[name] !== value));
134
134
  if (!Object.keys(changed).length && tone === previousTone) {
135
135
  extractThemeColor(template);
136
- return template;
136
+ return require('./application-style').applyApplicationStyle(template, metadata, tokens);
137
137
  }
138
138
  const root = /^:root\s*\{([\s\S]*?)^\}/m.exec(template);
139
139
  if (!root) {throw new Error('应用主题模板缺少 :root');}
@@ -157,6 +157,7 @@ function applyDesignTokens(template, markdown, previousMarkdown) {
157
157
  }
158
158
  let css = `${template.slice(0, root.index)}:root {${body}}${tail}`;
159
159
  if (tone) {css = applyNavigationTokens(css, tokens, changed, tone, previousTone);}
160
+ css = require('./application-style').applyApplicationStyle(css, metadata, tokens);
160
161
  validateThemeCssContent(css);
161
162
  extractThemeColor(css);
162
163
  return css;