openyida 2026.9.20 → 2026.9.22-1

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 (172) hide show
  1. package/README.md +3 -1
  2. package/bin/yida.js +6 -0
  3. package/lib/app/app-entry.js +118 -0
  4. package/lib/app/app-list.js +1 -0
  5. package/lib/app/application-style.js +3 -1
  6. package/lib/app/canvas-theme-guard.js +78 -8
  7. package/lib/app/fix-theme.js +82 -0
  8. package/lib/app/publish.js +67 -3
  9. package/lib/app/theme-from-design.js +38 -13
  10. package/lib/app/update-app.js +63 -13
  11. package/lib/core/command-manifest.js +42 -20
  12. package/lib/core/locales/en.js +40 -2
  13. package/lib/core/locales/zh.js +40 -2
  14. package/lib/core/sample.js +17 -14
  15. package/lib/design/document.js +5 -5
  16. package/lib/design-plan/confirmation.js +13 -0
  17. package/lib/design-plan/design-plan.js +44 -2
  18. package/lib/design-plan/entry-navigation.js +10 -2
  19. package/lib/design-plan/init.js +11 -7
  20. package/lib/design-plan/materialize.js +65 -56
  21. package/lib/design-plan/normalize.js +13 -14
  22. package/lib/design-plan/parallel.js +62 -13
  23. package/lib/design-plan/patch.js +21 -7
  24. package/lib/design-plan/preview.js +4 -1
  25. package/lib/design-plan/rebase.js +69 -0
  26. package/lib/design-plan/themes.js +16 -1
  27. package/lib/design-plan/validate.js +8 -3
  28. package/lib/design-plan/visual-policy.js +18 -4
  29. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +3 -3
  30. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +6 -4
  31. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +5 -5
  32. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -2
  33. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +1 -1
  34. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +5 -4
  35. package/package.json +1 -1
  36. package/yida-skills/SKILL.md +2 -2
  37. package/yida-skills/references/yida-api.md +4 -0
  38. package/yida-skills/skills/yida-app/SKILL.md +12 -4
  39. package/yida-skills/skills/yida-app/references/entry-navigation.md +11 -9
  40. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +1 -1
  41. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +11 -4
  42. package/yida-skills/skills/yida-app/workflow/step-2-design.md +3 -3
  43. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +2 -2
  44. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +3 -3
  45. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +23 -2
  46. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +3 -2
  47. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +1 -1
  48. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +1 -1
  49. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +140 -1
  50. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +1 -1
  51. package/yida-skills/skills/yida-canvas-custom-page/references/waf-safe-authoring.md +45 -0
  52. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +1 -1
  53. package/yida-skills/skills/yida-canvas-data-binding/SKILL.md +7 -0
  54. package/yida-skills/skills/yida-create-form-page/SKILL.md +1 -1
  55. package/yida-skills/skills/yida-design/SKILL.md +5 -3
  56. package/yida-skills/skills/yida-design/references/application-style-library.md +22 -7
  57. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +45 -5
  58. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +1 -1
  59. package/yida-skills/skills/yida-design/references/native-form-styles.md +1 -1
  60. package/yida-skills/skills/yida-design/references/navigation-decision.md +6 -4
  61. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +62 -0
  62. package/yida-skills/skills/yida-design/references/theme-selection.md +10 -9
  63. package/yida-skills/skills/yida-design/references/visual-decision-engine.md +1 -1
  64. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +35 -9
  65. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +13 -3
  66. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +4 -2
  67. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +4 -2
  68. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-design.md +3 -1
  69. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +15 -17
  70. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +2 -0
  71. package/yida-skills/skills/yida-design/templates/design-themes/README.md +15 -10
  72. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/app_theme.css +224 -83
  73. package/yida-skills/skills/yida-design/templates/design-themes/app-amber/design.md +69 -14
  74. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +224 -83
  75. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +69 -14
  76. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +244 -103
  77. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +68 -13
  78. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +223 -82
  79. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +68 -13
  80. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +225 -84
  81. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +69 -14
  82. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +244 -103
  83. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +68 -13
  84. package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +224 -83
  85. package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +69 -14
  86. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +244 -103
  87. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +68 -13
  88. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +236 -89
  89. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +74 -14
  90. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +224 -83
  91. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +68 -13
  92. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +223 -82
  93. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +68 -13
  94. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +224 -83
  95. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +68 -13
  96. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +240 -89
  97. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +76 -14
  98. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +224 -83
  99. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +69 -14
  100. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +224 -83
  101. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +68 -13
  102. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +254 -107
  103. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +74 -14
  104. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +233 -88
  105. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +73 -14
  106. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +224 -83
  107. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +69 -14
  108. package/yida-skills/skills/yida-design/templates/design-themes/basic-tokens.json +2 -1
  109. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +690 -0
  110. package/yida-skills/skills/yida-design/templates/design-themes/{dark-inset-hairline.md → dark-inset-hairline/design.md} +103 -58
  111. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/form-layout.json +13 -0
  112. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +691 -0
  113. package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-fine-lines.md → dark-rail-fine-lines/design.md} +102 -50
  114. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/form-layout.json +13 -0
  115. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/app_theme.css +696 -0
  116. package/yida-skills/skills/yida-design/templates/design-themes/{dark-rail-signal-panels.md → dark-rail-signal-panels/design.md} +62 -12
  117. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-signal-panels/form-layout.json +13 -0
  118. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +278 -115
  119. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +57 -10
  120. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/app_theme.css +692 -0
  121. package/yida-skills/skills/yida-design/templates/design-themes/{graphite-bevel-grid.md → graphite-bevel-grid/design.md} +60 -10
  122. package/yida-skills/skills/yida-design/templates/design-themes/graphite-bevel-grid/form-layout.json +13 -0
  123. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/app_theme.css +691 -0
  124. package/yida-skills/skills/yida-design/templates/design-themes/{hairline-shared-bands.md → hairline-shared-bands/design.md} +62 -12
  125. package/yida-skills/skills/yida-design/templates/design-themes/hairline-shared-bands/form-layout.json +13 -0
  126. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +691 -0
  127. package/yida-skills/skills/yida-design/templates/design-themes/{hairline-soft-blocks.md → hairline-soft-blocks/design.md} +57 -9
  128. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/form-layout.json +13 -0
  129. package/yida-skills/skills/yida-design/templates/design-themes/index.json +141 -48
  130. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +701 -0
  131. package/yida-skills/skills/yida-design/templates/design-themes/{ink-glow-soft-panels.md → ink-glow-soft-panels/design.md} +58 -8
  132. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/form-layout.json +13 -0
  133. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +691 -0
  134. package/yida-skills/skills/yida-design/templates/design-themes/{inset-frame-pixel-rhythm.md → inset-frame-pixel-rhythm/design.md} +59 -9
  135. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/form-layout.json +13 -0
  136. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +698 -0
  137. package/yida-skills/skills/yida-design/templates/design-themes/{outlined-texture-duotone.md → outlined-texture-duotone/design.md} +62 -12
  138. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/form-layout.json +13 -0
  139. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +691 -0
  140. package/yida-skills/skills/yida-design/templates/design-themes/{soft-inset-surfaces.md → soft-inset-surfaces/design.md} +59 -11
  141. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/form-layout.json +13 -0
  142. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +690 -0
  143. package/yida-skills/skills/yida-design/templates/design-themes/{soft-outline-rhythm.md → soft-outline-rhythm/design.md} +60 -26
  144. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/form-layout.json +13 -0
  145. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +698 -0
  146. package/yida-skills/skills/yida-design/templates/design-themes/{soft-rail-bold-band.md → soft-rail-bold-band/design.md} +99 -49
  147. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/form-layout.json +13 -0
  148. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/app_theme.css +692 -0
  149. package/yida-skills/skills/yida-design/templates/design-themes/{soft-spectral-panels.md → soft-spectral-panels/design.md} +63 -13
  150. package/yida-skills/skills/yida-design/templates/design-themes/soft-spectral-panels/form-layout.json +13 -0
  151. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +711 -0
  152. package/yida-skills/skills/yida-design/templates/design-themes/{warm-canvas-contrast-panels.md → warm-canvas-contrast-panels/design.md} +60 -10
  153. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/form-layout.json +13 -0
  154. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +692 -0
  155. package/yida-skills/skills/yida-design/templates/design-themes/{warm-rail-muted-panels.md → warm-rail-muted-panels/design.md} +98 -50
  156. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/form-layout.json +13 -0
  157. package/yida-skills/skills/yida-design/templates/navigation-styles.json +769 -0
  158. package/yida-skills/skills/yida-design/workflow/output-design.md +30 -5
  159. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +5 -3
  160. package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +1 -1
  161. package/yida-skills/skills/yida-form-permission/SKILL.md +6 -0
  162. package/yida-skills/skills/yida-nav-shell/SKILL.md +1 -1
  163. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +6 -0
  164. package/yida-skills/skills/yida-page-config/SKILL.md +9 -0
  165. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
  166. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  167. package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +1 -1
  168. package/yida-skills/skills/yida-publish-page/SKILL.md +1 -0
  169. package/yida-skills/skills/yida-requirement-analysis/references/experience-groups.md +45 -9
  170. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +5 -5
  171. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +17 -4
  172. package/yida-skills/skills-index.json +2 -2
package/README.md CHANGED
@@ -393,13 +393,15 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
393
393
 
394
394
  | Command | Description |
395
395
  |---------|-------------|
396
+ | `openyida app-entry get <appType> [--json]` | Read frontend and management runtime entries |
397
+ | `openyida app-entry set <appType> [--frontend <url>] [--management <url>] [--clear-frontend] [--clear-management] [--json]` | Register application entries with partial update and readback |
396
398
  | `openyida app-list [--type managed\|created] [--page N] [--size N]` | Page through apps I manage or created |
397
399
  | `openyida corp-efficiency [overview\|details\|detail\|groups\|notify] [options] [--open\|--no-open]` | Query enterprise efficiency overview and detail reports |
398
400
  | `openyida create-app "<name>"\|--name <name> [options] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a Yida app |
399
401
  | `openyida design-plan catalog [--json]` | List available planning themes and page patterns |
400
402
  | `openyida design-plan init <requirement-brief.json> [--theme-id <id>] [--output-dir <dir>] [--json]` | Initialize a plan draft from confirmed requirements |
401
403
  | `openyida design-plan preview <build-plan.json> --part-file <module.json> [--json]` | Update plan drafts by module |
402
- | `openyida design-plan materialize <build-plan.json> [--from-preview \| --business-file <json> --visual-file <json>] [--output-dir <dir>] [--check] [--json]` | Generate or update plan documents and theme with consistency checks |
404
+ | `openyida design-plan materialize <build-plan.json> [--from-preview \| --business-file <json> --visual-file <json>] [--output-dir <dir>] [--rebase-parts] [--check] [--json]` | Generate or update plan documents and theme with consistency checks |
403
405
  | `openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]` | Patch plan fields and optionally sync documents and theme |
404
406
  | `openyida update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--nav-theme light\|dark\|white\|gray] [--logo-source appIcon\|customImage] [--layout side\|top\|l_shape] [--hide-app-nav\|--show-app-nav]` | Update app info |
405
407
  | `openyida app-online <appType> [--to-ding-app-center] [--show-app-center]` | Enable a Yida app |
package/bin/yida.js CHANGED
@@ -733,6 +733,12 @@ async function main() {
733
733
  break;
734
734
  }
735
735
 
736
+ case 'app-entry': {
737
+ const { run } = require('../lib/app/app-entry');
738
+ await run(args);
739
+ break;
740
+ }
741
+
736
742
  case 'app-list': {
737
743
  const { run } = require('../lib/app/app-list');
738
744
  await run(args);
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { createAuthRef, createYidaClient, unwrapYidaResponse } = require('../core/yida-client');
4
+ const { t } = require('../core/i18n');
5
+ const { throwUsage } = require('../core/command-errors');
6
+
7
+ /** Keep complete runtime URLs; the server verifies tenant and resource ownership. */
8
+ function normalizeUrl(value, appType) {
9
+ const input = String(value || '').trim();
10
+ if (!input) { throw new Error(t('app_entry.invalid_url')); }
11
+ // eslint-disable-next-line no-control-regex -- reject control characters in persisted URLs
12
+ if (input.length > 2048 || /[\\\s\x00-\x1f\x7f]/.test(input)) {
13
+ throw new Error(t('app_entry.invalid_url'));
14
+ }
15
+ const match = /^https?:\/\/[^/?#]+(\/[^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i.exec(input);
16
+ if (!match) { throw new Error(t('app_entry.invalid_url')); }
17
+ const url = new URL(input);
18
+ const route = match[1];
19
+ const prefix = `/${appType}/`;
20
+ const parts = route.startsWith(prefix) ? route.slice(prefix.length).split('/') : [];
21
+ const short = /^\/(?:s\/[A-Za-z0-9_-]+|o\/[A-Za-z0-9_-]+(?:\/FORM-[A-Za-z0-9_-]+)?)$/.test(route);
22
+ const runtime = ['custom', 'workbench', 'submission', 'manage'].includes(parts[0]) &&
23
+ ((parts.length === 1 && parts[0] === 'workbench') ||
24
+ (parts.length === 2 && /^[A-Za-z0-9_-]+$/.test(parts[1])));
25
+ if (url.username || url.password || url.pathname !== route || (!short && !runtime)) {
26
+ throw new Error(t('app_entry.invalid_url'));
27
+ }
28
+ if (match[2] !== undefined) {
29
+ const seen = new Set();
30
+ for (const parameter of match[2].split('&')) {
31
+ const pair = /^(corpid|locale|viewUuid|processCode|hideLeftNav|noShowTopBottom)=([A-Za-z0-9_-]+)$/.exec(parameter);
32
+ if (!pair || seen.has(pair[1]) ||
33
+ (['hideLeftNav', 'noShowTopBottom'].includes(pair[1]) && !/^(true|false|y|n|0|1)$/.test(pair[2]))) {
34
+ throw new Error(t('app_entry.invalid_url'));
35
+ }
36
+ seen.add(pair[1]);
37
+ }
38
+ }
39
+ if (match[3] !== undefined && !/^[A-Za-z0-9_/-]+$/.test(match[3])) {
40
+ throw new Error(t('app_entry.invalid_url'));
41
+ }
42
+ return input;
43
+ }
44
+
45
+ /** Reject duplicate or ambiguous flags; omissions never clear stored entries. */
46
+ function parseArgs(args) {
47
+ const [action, appType, ...flags] = args;
48
+ if (!['get', 'set'].includes(action) || !/^[A-Za-z0-9_-]+$/.test(appType || '')) {
49
+ throwUsage(t('app_entry.usage'));
50
+ }
51
+ const values = {};
52
+ for (let index = 0; index < flags.length; index++) {
53
+ const flag = flags[index];
54
+ if (flag === '--json') {
55
+ continue;
56
+ }
57
+ const match = /^--(clear-)?(frontend|management)$/.exec(flag);
58
+ if (action !== 'set' || !match || Object.prototype.hasOwnProperty.call(values, match[2])) {
59
+ throwUsage(t('app_entry.usage'));
60
+ }
61
+ const value = match[1] ? null : flags[++index];
62
+ if (value !== null && (!value || value.startsWith('--'))) {
63
+ throwUsage(t('app_entry.usage'));
64
+ }
65
+ values[match[2]] = value;
66
+ }
67
+ if (action === 'set' && !Object.keys(values).length) {
68
+ throwUsage(t('app_entry.usage'));
69
+ }
70
+ return { action, appType, values };
71
+ }
72
+
73
+ /** Read the revision used for a conditional partial write. */
74
+ async function readEntries(client, appType) {
75
+ const result = unwrapYidaResponse(await client.get(`/${appType}/query/app/getAccessEntries.json`));
76
+ if (!result || typeof result.revision !== 'string' || !result.revision ||
77
+ !result.accessEntries || typeof result.accessEntries !== 'object' || Array.isArray(result.accessEntries)) {
78
+ throw new Error(t('app_entry.invalid_response'));
79
+ }
80
+ return result;
81
+ }
82
+
83
+ /** Persist only supplied entries and verify them through a separate read. */
84
+ async function run(args = []) {
85
+ if (args.includes('--help') || args.includes('-h')) {
86
+ console.log(t('app_entry.usage'));
87
+ return;
88
+ }
89
+ const { action, appType, values } = parseArgs(args);
90
+ const authRef = createAuthRef();
91
+ const client = createYidaClient({ authRef });
92
+ const patch = {};
93
+ for (const [key, value] of Object.entries(values)) {
94
+ patch[key] = value === null ? null : { url: normalizeUrl(value, appType) };
95
+ }
96
+ let result = await readEntries(client, appType);
97
+ if (action === 'set') {
98
+ unwrapYidaResponse(await client.postForm(`/${appType}/query/app/saveAccessEntries.json`, {
99
+ accessEntries: JSON.stringify(patch), revision: result.revision,
100
+ }));
101
+ result = await readEntries(client, appType);
102
+ for (const [key, entry] of Object.entries(patch)) {
103
+ if ((result.accessEntries[key]?.url || '') !== (entry?.url || '')) {
104
+ throw new Error(t('app_entry.readback_failed'));
105
+ }
106
+ }
107
+ }
108
+ const urls = {};
109
+ for (const key of ['frontend', 'management']) {
110
+ if (result.accessEntries[key]?.url) {
111
+ urls[key] = normalizeUrl(result.accessEntries[key].url, appType);
112
+ }
113
+ }
114
+ console.log(JSON.stringify({ appType, ...result, urls }, null, 2));
115
+ return result;
116
+ }
117
+
118
+ module.exports = { run, parseArgs, normalizeUrl };
@@ -84,6 +84,7 @@ function formatApp(app, baseUrl) {
84
84
  appName: (app.appName && app.appName.zh_CN) || '',
85
85
  appType: app.appType || '',
86
86
  systemLink: app.systemLink || '',
87
+ ...(app.accessEntries ? { accessEntries: app.accessEntries } : {}),
87
88
  ...buildApplicationEntryUrls({ appType: app.appType, systemLink: app.systemLink, baseUrl }),
88
89
  };
89
90
  }
@@ -13,6 +13,8 @@ const CREATIVE_TOKENS = [
13
13
  '--pod-page-footer-bg-color', '--pod-sticky-footer-box-shadow',
14
14
  '--oyd-content-width', '--oyd-content-padding', '--oyd-field-gap',
15
15
  '--oyd-heading-font', '--oyd-heading-size', '--oyd-rule-style',
16
+ '--pod-shell-theme-bg-color', '--pod-nav-item-text-color', '--pod-nav-item-text-hover-color',
17
+ '--pod-nav-item-text-selected-color', '--pod-nav-menu-bg-hover-color', '--pod-nav-menu-bg-selected-color',
16
18
  ];
17
19
 
18
20
  function validateCreativeDirection(direction, tokens) {
@@ -46,7 +48,7 @@ function applyApplicationStyle(css, metadata, tokens) {
46
48
  }
47
49
 
48
50
  function exportApplicationStyle(id, outputDirectory) {
49
- const theme = loadThemeIndex().themes.find(item => item.themeId === id && item.collection === 'application-styles');
51
+ const theme = loadThemeIndex().themes.find(item => item.themeId === id);
50
52
  if (!theme) {throw new CliError(t('design_document.invalid', id || '--style-id', 'APPLICATION_STYLE_UNKNOWN'), { code: 'APPLICATION_STYLE_UNKNOWN' });}
51
53
  const directory = path.resolve(outputDirectory);
52
54
  const assets = [['design.md', theme.templatePath], ['app_theme.css', theme.cssTemplatePath], ['form-layout.json', theme.formLayoutPath]];
@@ -2,8 +2,33 @@
2
2
 
3
3
  const Babel = require('@babel/standalone');
4
4
  const { CliError } = require('../core/cli-error');
5
+ const { warn } = require('../core/chalk');
5
6
  const { t } = require('../core/i18n');
6
7
 
8
+ // Whether provably fixed brand overrides should be downgraded from a hard
9
+ // failure to a warning. Mirrors OPENYIDA_CANVAS_ALLOW_UNSUPPORTED_IMPORTS so
10
+ // legacy pages can ship first and migrate to canvas-theme later.
11
+ function isTruthy(value) {
12
+ return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
13
+ }
14
+
15
+ function allowFixedBrand(options = {}, env = process.env) {
16
+ return options.allowFixedBrand === true || isTruthy(env.OPENYIDA_CANVAS_ALLOW_FIXED_BRAND);
17
+ }
18
+
19
+ // Whether fixed brand overrides should hard-block publishing. Off by default so
20
+ // users are never stuck: the guard warns and lets the app theme apply. Teams
21
+ // that want CI enforcement opt in via --strict-theme / OPENYIDA_CANVAS_STRICT_THEME.
22
+ function strictFixedBrand(options = {}, env = process.env) {
23
+ return options.strictBrand === true || isTruthy(env.OPENYIDA_CANVAS_STRICT_THEME);
24
+ }
25
+
26
+ function emitThemeWarning(options, message) {
27
+ if (typeof options.onThemeWarning === 'function') { options.onThemeWarning(message); return; }
28
+ warn(message);
29
+ }
30
+
31
+
7
32
  // Check the maintained theme contract without executing business code.
8
33
  // Dynamic component factories and runtime branches still need browser verification.
9
34
  function assertCanvasThemeStructure(source, options = {}) {
@@ -123,9 +148,12 @@ function assertCanvasThemeStructure(source, options = {}) {
123
148
  }
124
149
  }
125
150
 
126
- // Only reject provably fixed brand colors passed to antd's ConfigProvider.
127
- // Semantic colors, layout tokens and dynamic theme resolvers remain supported.
128
- function assertBrandOverrides(ast, traverse, options) {
151
+ // Collect every provably fixed brand color passed to antd's ConfigProvider.
152
+ // Semantic colors, layout tokens and dynamic theme resolvers are ignored.
153
+ // Each entry keeps the offending property's char offsets so the theme codemod
154
+ // can surgically remove it while the guard reuses the same detection to fail.
155
+ function collectBrandOverrides(ast, traverse) {
156
+ const violations = [];
129
157
  const resolve = (p, seen = new Set()) => {
130
158
  if (!p?.node || seen.has(p.node)) { return null; }
131
159
  seen.add(p.node);
@@ -147,10 +175,12 @@ function assertBrandOverrides(ast, traverse, options) {
147
175
  const color = val.isStringLiteral() ? val.node.value : val.node.quasis[0].value.cooked;
148
176
  if (!color) { return; }
149
177
  const line = val.node.loc?.start.line || 1;
150
- throw new CliError(t('publish.canvas_theme_fixed_brand', line), {
151
- code: 'OPENYIDA_CANVAS_THEME_FIXED_BRAND',
152
- details: { stage: 'canvas_compile', sourcePath: options.sourcePath || '', line, field, value: color,
153
- sample: 'openyida sample openyida-page-template canvas-theme' },
178
+ // Remove the override at its ConfigProvider usage site, not the resolved literal.
179
+ const property = p?.parentPath?.isObjectProperty() ? p.parentPath.node : null;
180
+ violations.push({
181
+ field, color, line,
182
+ start: property ? property.start : val.node.start,
183
+ end: property ? property.end : val.node.end,
154
184
  });
155
185
  };
156
186
  const tokens = p => properties(p).forEach(item => {
@@ -179,6 +209,46 @@ function assertBrandOverrides(ast, traverse, options) {
179
209
  }
180
210
  },
181
211
  });
212
+ return violations;
213
+ }
214
+
215
+ // Fixed brand colors passed to antd's ConfigProvider are relaxed by default:
216
+ // warn and continue so users are never blocked (the app theme still applies at
217
+ // runtime). --strict-theme / OPENYIDA_CANVAS_STRICT_THEME restores the hard fail
218
+ // for CI enforcement; --allow-fixed-brand keeps its explicit ship-as-is warning.
219
+ function assertBrandOverrides(ast, traverse, options) {
220
+ const explicitlyAllowed = allowFixedBrand(options);
221
+ const strict = strictFixedBrand(options);
222
+ for (const { field, color, line } of collectBrandOverrides(ast, traverse)) {
223
+ if (explicitlyAllowed) {
224
+ emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_allowed', line, field));
225
+ continue;
226
+ }
227
+ if (!strict) {
228
+ emitThemeWarning(options, t('publish.canvas_theme_fixed_brand_relaxed', line, field));
229
+ continue;
230
+ }
231
+ throw new CliError(t('publish.canvas_theme_fixed_brand', line), {
232
+ code: 'OPENYIDA_CANVAS_THEME_FIXED_BRAND',
233
+ details: { stage: 'canvas_compile', sourcePath: options.sourcePath || '', line, field, value: color,
234
+ retryable: false, retrySafe: true, sideEffectState: 'none',
235
+ nextAction: { type: 'edit_source_then_recheck' },
236
+ sample: 'openyida sample openyida-page-template canvas-theme' },
237
+ });
238
+ }
239
+ }
240
+
241
+ // Parse a canvas source and return removable fixed-brand overrides (with char
242
+ // offsets). Returns [] when the source has no theme markers or fails to parse,
243
+ // mirroring the guard's own gate so the codemod never acts on unrelated code.
244
+ function collectFixedBrandViolations(source) {
245
+ if (!/CanvasThemeProvider|useCanvasThemeContext|CanvasThemeContext|@canvas-theme-provider|antd/.test(source)) { return []; }
246
+ let ast;
247
+ try {
248
+ ast = Babel.packages.parser.parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
249
+ } catch { return []; }
250
+ const traverse = Babel.packages.traverse.default || Babel.packages.traverse;
251
+ return collectBrandOverrides(ast, traverse);
182
252
  }
183
253
 
184
- module.exports = { assertCanvasThemeStructure };
254
+ module.exports = { assertCanvasThemeStructure, collectFixedBrandViolations };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * fix-theme.js - Canvas 主题迁移 codemod(纯函数)
5
+ *
6
+ * 把写死在 antd `ConfigProvider` 里的品牌交互色覆盖(如 `colorPrimary: '#1677ff'`)
7
+ * 从源码中移除,交由外层应用主题(CanvasThemeProvider / 平台主题)解析。这是
8
+ * "un-pin" 迁移:移除硬编码覆盖后,品牌色由应用主题接管,正好满足主题守卫
9
+ * `OPENYIDA_CANVAS_THEME_FIXED_BRAND` 的要求。
10
+ *
11
+ * 本模块不做任何 IO,只接收源码字符串、返回改写后的源码,便于单测与复用。
12
+ */
13
+
14
+ const { collectFixedBrandViolations } = require('./canvas-theme-guard');
15
+
16
+ /**
17
+ * 计算某个对象属性的删除区间:优先连同其后的逗号一起删;若是最后一个属性,
18
+ * 则回退删除其前的逗号。属性独占一行时顺带清理该行缩进与换行,避免留下空行。
19
+ * @param {string} source
20
+ * @param {number} start - 属性节点起始偏移
21
+ * @param {number} end - 属性节点结束偏移
22
+ * @returns {{ start: number, end: number }}
23
+ */
24
+ function expandRemovalSpan(source, start, end) {
25
+ let removeStart = start;
26
+ let removeEnd = end;
27
+ const trailing = source.slice(removeEnd).match(/^[ \t]*,/);
28
+ if (trailing) {
29
+ removeEnd += trailing[0].length;
30
+ const lineStart = source.lastIndexOf('\n', removeStart - 1) + 1;
31
+ const indent = source.slice(lineStart, removeStart);
32
+ if (/^[ \t]*$/.test(indent)) {
33
+ removeStart = lineStart;
34
+ const newline = source.slice(removeEnd).match(/^[ \t]*\r?\n/);
35
+ if (newline) { removeEnd += newline[0].length; }
36
+ }
37
+ return { start: removeStart, end: removeEnd };
38
+ }
39
+ // Last property in its object: drop the preceding comma instead.
40
+ const leading = source.slice(0, removeStart).match(/,[ \t\r\n]*$/);
41
+ if (leading) { removeStart -= leading[0].length; }
42
+ return { start: removeStart, end: removeEnd };
43
+ }
44
+
45
+ /**
46
+ * 移除源码中写死的品牌色覆盖。改写按偏移从后往前应用,避免前面的偏移失效。
47
+ * @param {string} source - canvas 源码
48
+ * @returns {{ changed: boolean, output: string, violations: Array<{field:string,color:string,line:number}> }}
49
+ */
50
+ function transformFixedBrandTheme(source) {
51
+ const violations = collectFixedBrandViolations(source)
52
+ .filter(item => Number.isInteger(item.start) && Number.isInteger(item.end) && item.end > item.start);
53
+ if (!violations.length) {
54
+ return { changed: false, output: source, violations: [] };
55
+ }
56
+ const spans = violations
57
+ .map(item => expandRemovalSpan(source, item.start, item.end))
58
+ .sort((a, b) => a.start - b.start);
59
+
60
+ // Adjacent overrides can claim the same separating comma (one as trailing,
61
+ // the next as leading), so merge overlapping spans instead of dropping one.
62
+ const merged = [];
63
+ for (const span of spans) {
64
+ const last = merged[merged.length - 1];
65
+ if (last && span.start <= last.end) { last.end = Math.max(last.end, span.end); }
66
+ else { merged.push({ ...span }); }
67
+ }
68
+
69
+ let output = source;
70
+ // Apply from the back so earlier offsets stay valid.
71
+ for (let i = merged.length - 1; i >= 0; i -= 1) {
72
+ output = output.slice(0, merged[i].start) + output.slice(merged[i].end);
73
+ }
74
+
75
+ return {
76
+ changed: output !== source,
77
+ output,
78
+ violations: violations.map(({ field, color, line }) => ({ field, color, line })),
79
+ };
80
+ }
81
+
82
+ module.exports = { transformFixedBrandTheme, expandRemovalSpan };
@@ -99,9 +99,12 @@ function parseArgs(argv = process.argv.slice(2)) {
99
99
  const compat = args.includes('--compat') || args.includes('--modern');
100
100
  const force = args.includes('--force');
101
101
  const canvas = args.includes('--canvas');
102
+ const allowFixedBrand = args.includes('--allow-fixed-brand');
103
+ const fixTheme = args.includes('--fix-theme');
104
+ const strictTheme = args.includes('--strict-theme');
102
105
  const autoNavOrder = args.includes('--auto-nav-order');
103
106
  const help = args.includes('--help') || args.includes('-h');
104
- const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force' && arg !== '--canvas' && arg !== '--auto-nav-order' && arg !== '--json' && arg !== '--help' && arg !== '-h');
107
+ const filteredArgs = args.filter(arg => arg !== '--skip-lint' && arg !== '--health-check' && arg !== '--check' && arg !== '--compat' && arg !== '--modern' && arg !== '--force' && arg !== '--canvas' && arg !== '--allow-fixed-brand' && arg !== '--fix-theme' && arg !== '--strict-theme' && arg !== '--auto-nav-order' && arg !== '--json' && arg !== '--help' && arg !== '-h');
105
108
 
106
109
  if (help) {
107
110
  usage(t('publish.usage'), t('publish.example'));
@@ -122,6 +125,9 @@ function parseArgs(argv = process.argv.slice(2)) {
122
125
  compat,
123
126
  force,
124
127
  canvas,
128
+ allowFixedBrand,
129
+ fixTheme,
130
+ strictTheme,
125
131
  autoNavOrder,
126
132
  browserOpenMode: openOption.mode,
127
133
  };
@@ -146,6 +152,52 @@ function buildCanvasCompileCliError(errorObject, sourcePath) {
146
152
  });
147
153
  }
148
154
 
155
+ // Ask the human (via stdin) whether to migrate. Returns false unless both the
156
+ // input and the prompt's output stream are real TTYs, so agent/CI/piped runs
157
+ // never block on a question nobody can answer and must opt in with --fix-theme.
158
+ function canPromptInteractively() {
159
+ return Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY);
160
+ }
161
+
162
+ function confirmFixedBrandMigration() {
163
+ if (!canPromptInteractively()) { return Promise.resolve(false); }
164
+ const readline = require('readline');
165
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
166
+ return new Promise((resolve) => {
167
+ rl.question(`${t('publish.fix_theme_confirm')} `, (answer) => {
168
+ rl.close();
169
+ resolve(/^(y|yes)$/i.test(String(answer).trim()));
170
+ });
171
+ });
172
+ }
173
+
174
+ // Offer to un-pin fixed brand colors so the app theme takes over. Confirms with
175
+ // the user (or auto-applies when --fix-theme is set), rewrites the source file
176
+ // in place, and returns the migrated source; returns null when nothing changed
177
+ // or the user declined. Callers continue publishing regardless unless strict.
178
+ async function offerFixedBrandThemeMigration(sourcePath, source, { autoConfirm, strict } = {}) {
179
+ const { transformFixedBrandTheme } = require('./fix-theme');
180
+ const result = transformFixedBrandTheme(source);
181
+ if (!result.changed) { return null; }
182
+
183
+ warn(t('publish.fix_theme_detected', result.violations.length));
184
+ result.violations.forEach((item) => hint(t('publish.fix_theme_item', item.line, item.field, item.color)));
185
+
186
+ const proceed = autoConfirm === true || await confirmFixedBrandMigration();
187
+ if (!proceed) {
188
+ // Only nudge harder in strict mode; the relaxed path prints its own note.
189
+ if (strict) {
190
+ if (!canPromptInteractively()) { info(t('publish.fix_theme_flag_hint')); }
191
+ else { info(t('publish.fix_theme_skipped')); }
192
+ }
193
+ return null;
194
+ }
195
+
196
+ fs.writeFileSync(sourcePath, result.output);
197
+ success(t('publish.fix_theme_applied', sourcePath));
198
+ return result.output;
199
+ }
200
+
149
201
  function enableQuietForJson(argv = []) {
150
202
  if (!Array.isArray(argv) || !argv.includes('--json')) {
151
203
  return () => {};
@@ -614,7 +666,7 @@ async function runMain(argv) {
614
666
  if (options.help) {
615
667
  return options;
616
668
  }
617
- const { appType, formUuid, sourceFile, skipLint, healthCheck, compat, force, canvas: canvasFlag, autoNavOrder, browserOpenMode } = options;
669
+ const { appType, formUuid, sourceFile, skipLint, healthCheck, compat, force, canvas: canvasFlag, allowFixedBrand, fixTheme, strictTheme, autoNavOrder, browserOpenMode } = options;
618
670
 
619
671
  let sourcePath = path.resolve(sourceFile);
620
672
  if (!fs.existsSync(sourcePath)) {
@@ -644,8 +696,20 @@ async function runMain(argv) {
644
696
  // (JSX/TS → runtimeCode + importedModules),无需登录态,可在开浏览器前失败快。
645
697
  step(1, t('publish.step_canvas_compile'));
646
698
  info(t('publish.canvas_compiling'));
699
+ // Offer to un-pin fixed brand colors before compiling so the ask-human /
700
+ // --fix-theme migration still runs now that the guard no longer blocks by
701
+ // default. Skipped when --allow-fixed-brand explicitly ships as-is. In
702
+ // non-TTY runs the prompt declines instead of hanging, and the guard warns
703
+ // (or hard-fails under --strict-theme) rather than blocking the user.
704
+ if (!allowFixedBrand) {
705
+ const migratedSource = await offerFixedBrandThemeMigration(sourcePath, initialSourceCode, {
706
+ autoConfirm: fixTheme,
707
+ strict: strictTheme,
708
+ });
709
+ if (migratedSource !== null) { sourceCode = migratedSource; }
710
+ }
647
711
  try {
648
- canvasResult = await compileCanvas(initialSourceCode, { sourcePath });
712
+ canvasResult = await compileCanvas(sourceCode, { sourcePath, allowFixedBrand, strictBrand: strictTheme });
649
713
  } catch (canvasCompileError) {
650
714
  throw buildCanvasCompileCliError(canvasCompileError, sourcePath);
651
715
  }
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const { CliError } = require('../core/cli-error');
6
6
  const { REQUIRED_BRAND_SCALE_TOKENS, validateThemeCssContent, extractThemeColor } = require('./custom-theme');
7
7
  const { parseDesignDocument, extractDesignTokens } = require('../design/document');
8
- const NAVIGATION_TOKENS = [
8
+ const NAVIGATION_COLOR_TOKENS = [
9
9
  '--pod-shell-theme-bg-color', '--pod-nav-item-text-color', '--pod-nav-item-text-hover-color',
10
10
  '--pod-nav-item-text-selected-color', '--pod-nav-menu-bg-hover-color', '--pod-nav-menu-bg-selected-color',
11
11
  ];
@@ -48,7 +48,7 @@ function resolveDesignTokens(metadata) {
48
48
  }
49
49
 
50
50
  function navigationTone(metadata, tokens) {
51
- if (!NAVIGATION_TOKENS.every(name => tokens[name])) {return null;}
51
+ if (!NAVIGATION_COLOR_TOKENS.every(name => tokens[name])) {return null;}
52
52
  const tone = metadata.themeProfile?.navTheme;
53
53
  if (['light', 'dark'].includes(tone)) {return tone;}
54
54
  if (tone !== undefined) {throw new CliError('themeProfile.navTheme 必须是 light 或 dark', { code: 'DESIGN_THEME_NAVIGATION_INVALID' });}
@@ -91,7 +91,9 @@ function applyNavigationTokens(css, tokens, changed, tone, previousTone) {
91
91
  const source = fs.readFileSync(path.resolve(__dirname,
92
92
  '../../yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css'), 'utf8');
93
93
  const defaults = Object.assign({}, ...[...source.matchAll(/^:root\s*\{([^}]+)\}/gm)].map(match => declarationValues(match[1])));
94
- const navigationChanged = Object.fromEntries(NAVIGATION_TOKENS.filter(name => name in changed).map(name => [name, tokens[name]]));
94
+ const isNavigation = name => /^(--pod-nav-|--pod-shell-|--pod-page-header-)/.test(name);
95
+ const navigationNames = Object.keys(tokens).filter(isNavigation);
96
+ const navigationChanged = Object.fromEntries(navigationNames.filter(name => name in changed).map(name => [name, tokens[name]]));
95
97
  let output = css.replace(/(^:root\s*\{)([^}]+)(\})/gm,
96
98
  (block, start, body, close) => start + replaceDeclarations(body, navigationChanged) + close);
97
99
  for (const mode of ['light', 'dark']) {
@@ -102,19 +104,23 @@ function applyNavigationTokens(css, tokens, changed, tone, previousTone) {
102
104
  // A first application or a mode change establishes both palettes. Delta
103
105
  // updates otherwise leave unrelated, manually customized declarations alone.
104
106
  const resetMode = !previousTone || previousTone !== tone;
105
- const names = resetMode ? NAVIGATION_TOKENS : (mode === tone ? Object.keys(navigationChanged) : []);
106
- const values = Object.fromEntries(names.map(name => [name, mode === tone ? tokens[name] : modeDefaults[name]]));
107
+ // The selected marker is a mode-independent shape, not a navigation palette.
108
+ const names = (resetMode ? navigationNames : (mode === tone ? Object.keys(navigationChanged) : []))
109
+ .filter(name => name !== '--pod-nav-menu-item-selected-shadow');
110
+ const values = Object.fromEntries(names.map(name => [name, mode === tone ? tokens[name] : modeDefaults[name]])
111
+ .filter(([, value]) => value !== undefined));
107
112
  if (!Object.keys(values).length) {continue;}
113
+ const isForeground = name => name.startsWith('--pod-nav-') || name === '--pod-page-header-text-color';
114
+ const background = Object.fromEntries(Object.entries(values).filter(([name]) => !isForeground(name)));
108
115
  const shell = values['--pod-shell-theme-bg-color'];
109
- if (shell) {
110
- const background = {
111
- '--pod-shell-theme-bg-color': shell,
112
- '--pod-page-header-bg-color': mode === tone ? shell : modeDefaults['--pod-page-header-bg-color'],
113
- };
116
+ if (shell && !tokens['--pod-page-header-bg-color']) {
117
+ background['--pod-page-header-bg-color'] = mode === tone ? shell : modeDefaults['--pod-page-header-bg-color'];
118
+ }
119
+ if (Object.keys(background).length) {
114
120
  output = output.replace(new RegExp(`(\\.pod-premium\\.nav-${mode}\\s*\\{)([^}]*)(\\})`, 'g'),
115
121
  (block, start, body, close) => start + replaceDeclarations(body, background, true) + close);
116
122
  }
117
- const foregrounds = Object.fromEntries(Object.entries(values).filter(([name]) => name !== '--pod-shell-theme-bg-color'));
123
+ const foregrounds = Object.fromEntries(Object.entries(values).filter(([name]) => isForeground(name)));
118
124
  output = output.replace(new RegExp(`(\\.pod-premium\\.is-${mode}\\s*\\{)([^}]*)(\\})`, 'g'),
119
125
  (block, start, body, close) => start + replaceDeclarations(body, foregrounds, true) + close);
120
126
  }
@@ -122,11 +128,30 @@ function applyNavigationTokens(css, tokens, changed, tone, previousTone) {
122
128
  }
123
129
 
124
130
  function applyDesignTokens(template, markdown, previousMarkdown) {
125
- // Reject damaged templates or existing stylesheets before the scope rewrite.
126
- validateThemeCssContent(template);
127
131
  const metadata = designMetadata(markdown);
128
132
  const previousMetadata = previousMarkdown === markdown ? metadata : (previousMarkdown ? designMetadata(previousMarkdown) : null);
129
133
  const tokens = resolveDesignTokens(metadata);
134
+ // Bundled CSS is an authoring template. Fill only unresolved declarations from
135
+ // the completed design before validating and rewriting CSS scopes.
136
+ template = template.replace(/^([ \t]*)(--[\w-]+)(\s*:\s*)([^;\n]+);/gm,
137
+ (line, indent, name, separator, value) => {
138
+ if (!/\{\{PRIMARY_COLOR\}\}|<生成实际色值:/.test(value)) {return line;}
139
+ if (!tokens[name] || /\{\{|<生成实际色值:/.test(tokens[name])) {
140
+ throw new CliError(`design.md 缺少品牌 token:${name}`, { code: 'DESIGN_THEME_TOKENS_REQUIRED' });
141
+ }
142
+ return `${indent}${name}${separator}${tokens[name]};`;
143
+ });
144
+ // Reject damaged templates or existing stylesheets before the scope rewrite.
145
+ validateThemeCssContent(template);
146
+ // Older generated files have the selected-shadow rule only. Bring in the
147
+ // shared consumer once when a design opts into menu borders, retaining all
148
+ // existing project rules and declarations during incremental updates.
149
+ if (tokens['--pod-nav-menu-item-border'] && !template.includes('border: var(--pod-nav-menu-item-border,')) {
150
+ const source = fs.readFileSync(path.resolve(__dirname,
151
+ '../../yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css'), 'utf8');
152
+ const shapeRules = source.match(/\/\* openyida-navigation-shape:start \*\/[\s\S]*?\/\* openyida-navigation-shape:end \*\//)[0];
153
+ template += `\n${shapeRules}\n`;
154
+ }
130
155
  const previous = previousMetadata === metadata ? tokens : (previousMetadata ? resolveDesignTokens(previousMetadata) : null);
131
156
  const tone = navigationTone(metadata, tokens);
132
157
  const previousTone = previous ? navigationTone(previousMetadata, previous) : null;