openyida 2026.9.8-1 → 2026.9.9

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 (44) hide show
  1. package/lib/app/services/canvas-page-schema-builder.js +3 -3
  2. package/lib/core/sample.js +13 -1
  3. package/lib/design-plan/materialize.js +6 -10
  4. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +107 -23
  5. package/lib/samples/openyida-scaffold/canvas-nav/content.jsx +42 -0
  6. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  7. package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +84 -17
  8. package/lib/samples/yida-rechart/trend-combo.canvas.jsx +97 -18
  9. package/package.json +1 -1
  10. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +15 -3
  11. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +4 -1
  12. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +27 -7
  13. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +2 -41
  14. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +93 -65
  15. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +89 -0
  16. package/yida-skills/skills/yida-canvas-custom-page/references/component-library-guide.md +2 -2
  17. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +11 -3
  18. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +6 -2
  19. package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +122 -0
  20. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +102 -0
  21. package/yida-skills/skills/yida-canvas-table-form/SKILL.md +2 -2
  22. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +2 -4
  23. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +2 -2
  24. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +3 -3
  25. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +3 -3
  26. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +4 -4
  27. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +3 -3
  28. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +7 -7
  29. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +2 -2
  30. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +4 -4
  31. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +3 -3
  32. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +5 -5
  33. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +2 -2
  34. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +4 -4
  35. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +2 -2
  36. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +2 -2
  37. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +4 -4
  38. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +4 -4
  39. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +5 -5
  40. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +2 -2
  41. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +3 -3
  42. package/yida-skills/skills/yida-design/workflow/output-design.md +1 -1
  43. package/yida-skills/skills/yida-nav-shell/SKILL.md +7 -2
  44. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +84 -3
@@ -530,8 +530,8 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
530
530
  componentName: 'Page',
531
531
  id: nextNodeId(),
532
532
  props: {
533
- contentBgColor: 'var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)))',
534
- pageStyle: { backgroundColor: 'var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)))' },
533
+ contentBgColor: 'var(--pod-page-bg-color, var(--color-white, #fff))',
534
+ pageStyle: { backgroundColor: 'var(--pod-page-bg-color, var(--color-white, #fff))' },
535
535
  contentMargin: '0',
536
536
  contentPadding: '0',
537
537
  showTitle: false,
@@ -539,7 +539,7 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
539
539
  templateVersion: '1.0.0',
540
540
  contentMarginMobile: '0',
541
541
  className: 'page_' + nextSuffix(),
542
- contentBgColorMobile: 'var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)))',
542
+ contentBgColorMobile: 'var(--pod-page-bg-color, var(--color-white, #fff))',
543
543
  },
544
544
  condition: true,
545
545
  css: [
@@ -51,12 +51,14 @@ const SAMPLES = {
51
51
  'canvas-form-drawer': 'canvas-form-drawer.canvas.jsx',
52
52
  'form-open-container': 'canvas-form-drawer.canvas.jsx',
53
53
  'canvas-dialog': 'canvas-dialog.canvas.jsx',
54
+ 'canvas-theme': 'canvas-form-drawer.canvas.jsx',
54
55
  'canvas-nav-side': 'canvas-nav/side.jsx',
55
56
  'canvas-nav-top': 'canvas-nav/top.jsx',
56
57
  'canvas-nav-mixed': 'canvas-nav/mixed.jsx',
57
58
  'canvas-nav-dock': 'canvas-nav/dock.jsx',
58
59
  'canvas-nav-tabs': 'canvas-nav/tabs.jsx',
59
60
  'canvas-nav-data': 'canvas-nav/data.jsx',
61
+ 'canvas-nav-content': 'canvas-nav/content.jsx',
60
62
  },
61
63
  'yida-design': {
62
64
  'app-theme': 'app-custom-theme-template.css',
@@ -91,12 +93,15 @@ const SAMPLE_OUTPUT_FILENAMES = {
91
93
  'openyida-page-template/canvas-nav-dock': 'canvas-nav-dock.jsx',
92
94
  'openyida-page-template/canvas-nav-tabs': 'canvas-nav-tabs.jsx',
93
95
  'openyida-page-template/canvas-nav-data': 'canvas-nav-data.jsx',
96
+ 'openyida-page-template/canvas-nav-content': 'canvas-nav-content.jsx',
94
97
  'openyida-page-template/form-open-container': 'form-open-container.jsx',
95
98
  'openyida-page-template/canvas-dialog': 'canvas-dialog.jsx',
99
+ 'openyida-page-template/canvas-theme': 'canvas-theme.jsx',
96
100
  'yida-design/app-theme': 'app-theme.css',
97
101
  };
98
102
 
99
103
  const SAMPLE_HINTS = {
104
+ 'openyida-page-template/canvas-nav-content': '合并 CanvasNavigationContent;顶部导航传 navigation,工作台传 children,原生页传 iframeSrc 和 title;宿主已分配高度时显式传 height。',
100
105
  'openyida-page-template/canvas-nav-data': '向 loadCanvasNavigation 传入 PRD 规划的 items,通常工作台在首位;接口只过滤不可见入口,保留规划顺序与分组。',
101
106
  'openyida-page-template/form-open-container': '将 import、抽屉组件和辅助函数合并到现有 Canvas 页面;表单使用 useYidaFormOpen,其他内容使用 CanvasDrawer。',
102
107
  'yida-design/app-theme': '已复制应用主题模板;请由 yida-design 按 design.md 定点修改需要变化的 token。',
@@ -251,7 +256,7 @@ async function run(args) {
251
256
  const defaultOutputPath = path.join(process.cwd(), '.cache', 'samples', outputFilename);
252
257
  const { outputPath, variables, designFile } = parseOptions(rest, defaultOutputPath);
253
258
  const isNavSample = skill === 'openyida-page-template' && name.startsWith('canvas-nav-');
254
- const prefixPath = isNavSample && !['canvas-nav-tabs', 'canvas-nav-data'].includes(name) ? path.join(path.dirname(sourcePath), 'shared.jsx') : null;
259
+ const prefixPath = isNavSample && !['canvas-nav-tabs', 'canvas-nav-data', 'canvas-nav-content'].includes(name) ? path.join(path.dirname(sourcePath), 'shared.jsx') : null;
255
260
  const sidebarPath = isNavSample && ['canvas-nav-side', 'canvas-nav-mixed'].includes(name) ? path.join(path.dirname(sourcePath), 'sidebar.jsx') : null;
256
261
  if ([sourcePath, prefixPath, sidebarPath, designFile].filter(Boolean).some(file => path.resolve(outputPath) === path.resolve(file))) {
257
262
  throw new CliError('输出路径不能覆盖模板或 design.md', { code: 'SAMPLE_OUTPUT_CONFLICT' });
@@ -289,6 +294,13 @@ async function run(args) {
289
294
  } else if (prefixPath) {
290
295
  const content = [prefixPath, sidebarPath, sourcePath].filter(Boolean).map(file => fs.readFileSync(file, 'utf8')).join('\n');
291
296
  fs.writeFileSync(outputPath, applyTemplateVariables(content, variables), 'utf8');
297
+ } else if (sampleKey === 'openyida-page-template/canvas-theme') {
298
+ const sourceContent = fs.readFileSync(sourcePath, 'utf8');
299
+ const fragment = sourceContent.match(/\/\/ @openyida-canvas-theme:start\r?\n([\s\S]*?)\/\/ @openyida-canvas-theme:end/);
300
+ if (!fragment) {
301
+ throw new CliError('Canvas theme fragment missing', { code: 'SAMPLE_FRAGMENT_MISSING' });
302
+ }
303
+ fs.writeFileSync(outputPath, "import React from 'react';\n\n" + fragment[1], 'utf8');
292
304
  } else if (sampleKey === 'openyida-page-template/form-open-container') {
293
305
  const sourceContent = fs.readFileSync(sourcePath, 'utf8');
294
306
  const fragment = sourceContent.match(/\/\/ @openyida-form-drawer:start\r?\n([\s\S]*?)\/\/ @openyida-form-drawer:end/);
@@ -491,8 +491,7 @@ function deriveSurfacePalette(primaryColor, surfaceTone, template) {
491
491
  const darkSurface = surface && [1, 3, 5].every(index => parseInt(surface.slice(index, index + 2), 16) < 128);
492
492
  if (surfaceTone !== 'brand-tinted' || darkSurface) {return {};}
493
493
  return {
494
- // 品牌浅底属于 Shell;原生页面保留独立表面,Canvas 透明时透出 Shell。
495
- // 不覆盖上方按导航归属生成的 --oyd-page-background。
494
+ // 品牌氛围属于 Shell;原生页面和 Canvas 共用页面底色,不受导航归属影响。
496
495
  '--pod-shell-bg-color-light': mixHex(primaryColor, [255, 255, 255], 0.94),
497
496
  '--pod-card-bg-color': 'var(--color-white, #fff)',
498
497
  '--color-fill1-1': mixHex(primaryColor, [255, 255, 255], 0.97),
@@ -684,12 +683,9 @@ function renderDesign(plan) {
684
683
  for (const [token, value] of Object.entries(derived)) {
685
684
  output = replaceTokenValue(output, token, value);
686
685
  }
687
- // 页面背景先遵循导航形态;项目显式 token 覆盖在下方应用,始终优先。
688
- // 深色设计保留其明确的暗色画布,普通平台导航默认跟随应用页面背景。
689
- if (navigationConfig(plan).hideAppNav === 'y' && !selected.themeId.startsWith('dark-')) {
690
- output = replaceTokenValue(output, '--oyd-page-background', 'transparent');
691
- } else if (!selected.themeId.startsWith('dark-')) {
692
- output = replaceTokenValue(output, '--oyd-page-background', 'var(--pod-page-bg-color, var(--color-white, #fff))');
686
+ // 页面底色统一使用平台 token;导航形态不改变底色,项目显式设计仍优先。
687
+ if (!selected.themeId.startsWith('dark-')) {
688
+ output = replaceTokenValue(output, '--pod-page-bg-color', 'var(--color-white, #fff)');
693
689
  }
694
690
  const explicitTokens = visual.tokens || {};
695
691
  if (typeof explicitTokens !== 'object' || Array.isArray(explicitTokens)) {
@@ -718,9 +714,9 @@ function renderDesign(plan) {
718
714
  output = injectAfterFrontmatter(output, [
719
715
  '## 项目配色适配(优先于模板默认配色)', '',
720
716
  `用户选择的 ${text(color.primaryColorName, primaryColor)} 用于整体浅色氛围。Shell 浅色背景、填充和边界已按同一主色派生,项目显式 token 覆盖优先;最终取值以本文 tokens 为准。`,
721
- '背景分层:--pod-shell-bg-color-light 承载 Shell 浅色氛围;--pod-page-bg-color 保留原生表单/数据管理的独立底色(浅色主题通常为白色);--oyd-page-background 仅控制自定义页。隐藏应用导航时自定义页默认透明,显示平台导航时默认跟随原生页面底色;品牌浅底不得覆盖这项导航默认值。仅有明确的页面背景设计时覆盖 --oyd-page-background,不要因此改写 --pod-page-bg-color。',
717
+ '背景分层:Shell 承载外层氛围;原生页面与自定义页统一消费 --pod-page-bg-color,卡片与面板消费 --pod-card-bg-color。导航隐藏不自动改为透明;渐变、纹理、图片属于页面局部装饰层。明确的应用背景设计通过 app-theme.css 配置平台 token。',
722
718
  '模板中“固定无彩画布”“中性灰必须 R=G=B”“禁止主题色进入背景”等默认限制在本项目不适用;保留布局、圆角、阴影与组件结构。不要为了保留模板灰底,把用户选择的浅彩导航改白。',
723
- '页面背景消费 --oyd-page-background,卡片消费 --pod-card-bg-color(回退 --color-white),边界消费 --pod-card-border;近白页面与白卡必须保留可见细边框,不能只靠阴影分层。导航、表头、嵌套区域与选中态消费同一组品牌/填充 token。',
719
+ '页面背景消费 --pod-page-bg-color,卡片消费 --pod-card-bg-color(回退 --color-white),边界消费 --pod-card-border;近白页面与白卡必须保留可见细边框,不能只靠阴影分层。导航、表头、嵌套区域与选中态消费同一组品牌/填充 token。',
724
720
  '正文与标题使用 --color-text1-4,表头与 placeholder 使用 --color-text1-10,辅助说明使用 --color-text1-3;保持中性文字明度层级,不把整页文字染成主色。成功、警告、错误沿用应用语义色。',
725
721
  'antd 的主色、容器、浮层、文字、边框、填充及语义色都须映射应用 token;浅彩氛围不能仅出现在导航和主按钮。页面实现前先读本节,再读取下面的结构与组件规则。', '',
726
722
  ].join('\n'));
@@ -64,6 +64,7 @@ function buildYidaFormUrl(request, currentAppType) {
64
64
  /** 抽屉外壳默认跟随应用主题,background 可为当前抽屉指定颜色、渐变或 CSS token。 */
65
65
  function CanvasDrawer({
66
66
  open, title, onClose, onOpenInNewWindow, extra, children,
67
+ contentMode = 'content',
67
68
  background = 'var(--pod-shell-theme-bg-color, var(--drawer-bg, var(--color-brand1-1, #f4f6ff)))',
68
69
  }) {
69
70
  const [fullScreen, setFullScreen] = useState(false);
@@ -120,6 +121,7 @@ function CanvasDrawer({
120
121
  .openyida-form-drawer.ant-drawer .ant-drawer-title {
121
122
  color: inherit;
122
123
  font-size: inherit;
124
+ font-weight: inherit;
123
125
  }
124
126
  .openyida-form-drawer .oy-drawer-actions {
125
127
  display: flex;
@@ -147,6 +149,12 @@ function CanvasDrawer({
147
149
  outline: 2px solid var(--color-brand1-6, #1677ff);
148
150
  outline-offset: 2px;
149
151
  }
152
+ .openyida-form-drawer .oy-drawer-frame {
153
+ position: relative;
154
+ flex: 1 1 0;
155
+ min-height: 0;
156
+ overflow: hidden;
157
+ }
150
158
  .openyida-form-drawer .oy-drawer-card {
151
159
  height: 100%;
152
160
  min-height: 0;
@@ -213,13 +221,22 @@ function CanvasDrawer({
213
221
  overflow: 'hidden',
214
222
  },
215
223
  header: {
216
- background: 'var(--drawer-title-bg-color, var(--pod-page-header-bg-color, transparent))',
224
+ background: 'transparent',
225
+ boxSizing: 'border-box',
226
+ height: 'var(--pod-nav-platform-header-height, 48px)',
227
+ minHeight: 'var(--pod-nav-platform-header-height, 48px)',
228
+ flex: '0 0 var(--pod-nav-platform-header-height, 48px)',
217
229
  color: 'var(--drawer-title-color, var(--pod-page-header-text-color, var(--color-text1-4, #1f2329)))',
218
- fontSize: 'var(--drawer-title-font-size, 16px)',
219
- padding: 'var(--drawer-title-padding-top, 12px) var(--drawer-title-padding-left-right, 20px) var(--drawer-title-padding-bottom, 12px)',
230
+ fontSize: 'var(--pod-page-title-font-size, 16px)',
231
+ fontWeight: 'var(--pod-page-title-font-weight, 500)',
232
+ padding: '0 var(--drawer-title-padding-left-right, 20px)',
220
233
  borderBottom: 'var(--drawer-title-border-width, 0px) solid var(--drawer-title-border-color, var(--drawer-border-color, transparent))',
221
234
  },
222
- body: { padding: '0 8px 8px', minHeight: 0, overflow: 'hidden' },
235
+ body: {
236
+ display: 'flex', flexDirection: 'column', flex: '1 1 0',
237
+ padding: contentMode === 'iframe' ? 0 : '0 8px 8px',
238
+ minHeight: 0, overflow: 'hidden',
239
+ },
223
240
  }}
224
241
  >
225
242
  {!fullScreen ? (
@@ -250,12 +267,13 @@ function CanvasDrawer({
250
267
  }}
251
268
  />
252
269
  ) : null}
253
- <div className="oy-drawer-card">{children}</div>
270
+ <div className={contentMode === 'iframe' ? 'oy-drawer-frame' : 'oy-drawer-card'}>{children}</div>
254
271
  </Drawer>
255
272
  </>
256
273
  );
257
274
  }
258
275
 
276
+ /** 原生提交页和详情页自带页面布局,外层只提供 iframe 视口,不再套内容卡片。 */
259
277
  function FormOpenContainer({ request, currentAppType, onClose, onAfterClose }) {
260
278
  const iframeSrc = useMemo(() => buildYidaFormUrl(request, currentAppType), [request, currentAppType]);
261
279
  const title = request && request.title ? request.title : '表单';
@@ -263,6 +281,7 @@ function FormOpenContainer({ request, currentAppType, onClose, onAfterClose }) {
263
281
  <CanvasDrawer
264
282
  title={title}
265
283
  open={!!request}
284
+ contentMode="iframe"
266
285
  background={request?.background}
267
286
  onOpenInNewWindow={iframeSrc ? () => window.open(iframeSrc, '_blank', 'noopener,noreferrer') : undefined}
268
287
  onClose={() => {
@@ -270,8 +289,8 @@ function FormOpenContainer({ request, currentAppType, onClose, onAfterClose }) {
270
289
  if (typeof onAfterClose === 'function') onAfterClose();
271
290
  }}
272
291
  >
273
- {/* antd body 的百分比高度链失效时,保留视口高度兜底,避免 iframe 回落到默认 150px。 */}
274
- {iframeSrc ? <iframe title={title} src={iframeSrc} style={{ width: '100%', height: '100%', minHeight: 'calc(100vh - 56px)', border: 0, display: 'block' }} /> : null}
292
+ {/* iframe 填满 flex 分配的剩余空间,只由内页滚动,不按视口猜测标题高度。 */}
293
+ {iframeSrc ? <iframe title={title} src={iframeSrc} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', minHeight: 0, border: 0, display: 'block' }} /> : null}
275
294
  </CanvasDrawer>
276
295
  );
277
296
  }
@@ -346,11 +365,6 @@ function resolveConfigValue(value, fallback) {
346
365
  return isUnresolvedTemplate(value) ? fallback : String(value);
347
366
  }
348
367
 
349
- function readThemeColor(name, fallback) {
350
- if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return fallback;
351
- return window.getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
352
- }
353
-
354
368
  function normalizeRows(rows) {
355
369
  const source = Array.isArray(rows) && rows.length ? rows : SAMPLE_ROWS;
356
370
  return source.map((row, index) => ({
@@ -367,7 +381,85 @@ function normalizeRows(rows) {
367
381
  }));
368
382
  }
369
383
 
384
+ // @openyida-canvas-theme:start
385
+ // Standalone samples keep this fragment identical; see canvas-theme.test.js.
386
+ function readCanvasTheme(root, fallback) {
387
+ const roles = {
388
+ colorPrimary: '--color-brand1-6', colorLink: '--color-brand1-6',
389
+ colorPrimaryHover: '--color-brand1-5', colorPrimaryActive: '--color-brand1-9',
390
+ colorBgLayout: ['--pod-page-bg-color', '--color-white'],
391
+ colorBgContainer: ['--pod-card-bg-color', '--color-white'],
392
+ colorBgElevated: ['--pod-card-bg-color', '--color-white'], colorText: '--color-text1-4',
393
+ colorTextHeading: '--color-text1-4', colorTextSecondary: '--color-text1-3',
394
+ colorTextDescription: '--color-text1-3', colorTextPlaceholder: '--color-text1-10',
395
+ colorBorder: '--color-line1-2', colorBorderSecondary: '--color-line1-1',
396
+ colorFillAlter: '--color-fill1-1', colorFillSecondary: '--color-fill1-2',
397
+ };
398
+ const token = { ...fallback };
399
+ if (!root) return token;
400
+ const doc = root.ownerDocument;
401
+ const view = doc.defaultView;
402
+ const scope = view.getComputedStyle(root);
403
+ const probe = doc.createElement('span');
404
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;';
405
+ root.appendChild(probe);
406
+ try {
407
+ Object.keys(roles).forEach((role) => {
408
+ const names = Array.isArray(roles[role]) ? roles[role] : [roles[role]];
409
+ const value = names.map((name) => scope.getPropertyValue(name).trim()).find(Boolean) || '';
410
+ if (!value || !view.CSS.supports('color', value)) return;
411
+ probe.style.color = value;
412
+ const color = view.getComputedStyle(probe).color;
413
+ if (color) token[role] = color;
414
+ });
415
+ } finally {
416
+ probe.remove();
417
+ }
418
+ return token;
419
+ }
420
+
421
+ function useCanvasTheme(fallback) {
422
+ const rootRef = React.useRef(null);
423
+ const fallbackRef = React.useRef(fallback);
424
+ const [token, setToken] = React.useState(fallback);
425
+ React.useLayoutEffect(() => {
426
+ const root = rootRef.current;
427
+ if (!root) return undefined;
428
+ const doc = root.ownerDocument;
429
+ const view = doc.defaultView;
430
+ let frame;
431
+ const refresh = () => {
432
+ const next = readCanvasTheme(root, fallbackRef.current);
433
+ setToken((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
434
+ };
435
+ const schedule = () => {
436
+ view.cancelAnimationFrame(frame);
437
+ frame = view.requestAnimationFrame(refresh);
438
+ };
439
+ refresh();
440
+ // Observe only theme-bearing ancestors and stylesheet changes, not business DOM.
441
+ const observer = new view.MutationObserver(schedule);
442
+ for (let node = root; node; node = node.parentElement) {
443
+ observer.observe(node, { attributes: true });
444
+ }
445
+ if (doc.head) observer.observe(doc.head, { subtree: true, childList: true, characterData: true, attributes: true });
446
+ doc.addEventListener('load', schedule, true);
447
+ view.addEventListener('resize', schedule);
448
+ view.addEventListener('openyida:theme-change', schedule);
449
+ return () => {
450
+ observer.disconnect();
451
+ view.cancelAnimationFrame(frame);
452
+ doc.removeEventListener('load', schedule, true);
453
+ view.removeEventListener('resize', schedule);
454
+ view.removeEventListener('openyida:theme-change', schedule);
455
+ };
456
+ }, []);
457
+ return { rootRef, token };
458
+ }
459
+ // @openyida-canvas-theme:end
460
+
370
461
  function YidaComp(props) {
462
+ const { rootRef, token } = useCanvasTheme({ colorPrimary: "#1677ff", borderRadius: 9 });
371
463
  const rawAppType = props && props.appType ? props.appType : RAW_APP_TYPE;
372
464
  const rawFormUuid = props && props.formUuid ? props.formUuid : RAW_FORM_UUID;
373
465
  const appType = resolveConfigValue(rawAppType, 'APP_XXX');
@@ -461,15 +553,7 @@ function YidaComp(props) {
461
553
 
462
554
  return (
463
555
  <ConfigProvider
464
- theme={{
465
- token: {
466
- colorPrimary: readThemeColor('--color-brand1-6', '#1677FF'),
467
- colorInfo: readThemeColor('--color-brand1-6', '#1677FF'),
468
- colorText: readThemeColor('--color-text1-4', '#1F2329'),
469
- colorBgLayout: readThemeColor('--color-fill1-1', '#F5F6F7'),
470
- borderRadius: 8,
471
- },
472
- }}
556
+ theme={{ token }}
473
557
  getPopupContainer={(triggerNode) => (triggerNode && triggerNode.parentElement) || document.body}
474
558
  >
475
559
  <style>{`
@@ -478,7 +562,7 @@ function YidaComp(props) {
478
562
  min-height: 100vh;
479
563
  box-sizing: border-box;
480
564
  padding: 24px;
481
- background: var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)));
565
+ background: var(--pod-page-bg-color, var(--color-white, #fff));
482
566
  color: var(--color-text1-4);
483
567
  }
484
568
  .oys-shell {
@@ -536,7 +620,7 @@ function YidaComp(props) {
536
620
  }
537
621
  `}</style>
538
622
 
539
- <div className="oys-page">
623
+ <div ref={rootRef} className="oys-page">
540
624
  <div className="oys-shell">
541
625
  <div className="oys-header">
542
626
  <div>
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+
3
+ // 顶部导航与内容的高度链;侧栏布局可在侧栏右侧已分配高度的区域使用 height="100%"。
4
+ // 只管理布局:菜单权限、URL/hash、离开未保存表单的确认由调用方处理。
5
+ // 合并到页面时保留组件,合并重复 import;不要额外包裹滚动卡片。
6
+ function CanvasNavigationContent({
7
+ navigation,
8
+ children,
9
+ iframeSrc,
10
+ title = '页面内容',
11
+ contentKey,
12
+ height = '100dvh',
13
+ maxWidth = 1280,
14
+ gutter = 'clamp(12px, 2vw, 24px)',
15
+ radius = 16,
16
+ background = 'transparent',
17
+ }) {
18
+ const embedded = Boolean(iframeSrc);
19
+ return (
20
+ <div className="openyida-nav-layout" style={{
21
+ height, minHeight: 0, minWidth: 0, display: 'flex', flexDirection: 'column',
22
+ overflow: 'hidden', background,
23
+ }}>
24
+ {navigation != null && <div style={{ flexShrink: 0 }}>{navigation}</div>}
25
+ <main className="openyida-nav-main" aria-label={title} style={{
26
+ display: 'flex', flexDirection: 'column', flex: '1 1 0', minHeight: 0,
27
+ minWidth: 0, overflow: 'hidden', padding: gutter, boxSizing: 'border-box',
28
+ }}>
29
+ <div key={contentKey} className="openyida-nav-content" style={{
30
+ position: 'relative', flex: '1 1 0', minHeight: 0, minWidth: 0,
31
+ width: '100%', maxWidth, margin: '0 auto', borderRadius: radius,
32
+ overflow: embedded ? 'hidden' : 'auto',
33
+ }}>
34
+ {embedded ? <iframe title={title} src={iframeSrc} style={{
35
+ position: 'absolute', inset: 0, width: '100%', height: '100%',
36
+ display: 'block', border: 0,
37
+ }} /> : children}
38
+ </div>
39
+ </main>
40
+ </div>
41
+ );
42
+ }
@@ -4,7 +4,7 @@ import React from 'react';
4
4
  // 这是可选实现示例;导航外观按 design.md 设计,已有 UI 无需替换成示例默认样式。
5
5
  // 选中菜单标识当前页面,main 用 aria-label 命名,children 直接承载业务内容。
6
6
  const CANVAS_NAV_CSS = `
7
- .oy-canvas-nav { display: flow-root; min-width: 0; min-height: 100vh; background: var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff))); }
7
+ .oy-canvas-nav { display: flow-root; min-width: 0; min-height: 100vh; background: var(--pod-page-bg-color, var(--color-white, #fff)); }
8
8
  .oy-canvas-nav .oy-nav-surface {
9
9
  background: var(--pod-shell-theme-bg-color, var(--pod-nav-popup-bg-color, var(--color-white, #fff)));
10
10
  color: var(--pod-nav-item-text-color, var(--color-text1-4, #1f2329));
@@ -89,11 +89,6 @@ function isFieldMapReady(fieldMap) {
89
89
  );
90
90
  }
91
91
 
92
- function readThemeColor(name, fallback) {
93
- if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return fallback;
94
- return window.getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
95
- }
96
-
97
92
  function toPayload(row, fieldMap) {
98
93
  return {
99
94
  [fieldMap.name]: String(row.name).trim(),
@@ -115,7 +110,86 @@ function getBridgeState(props) {
115
110
  return { ready: true, reason: '', writeBridge, fieldMap };
116
111
  }
117
112
 
113
+ // @openyida-canvas-theme:start
114
+ // Standalone samples keep this fragment identical; see canvas-theme.test.js.
115
+ function readCanvasTheme(root, fallback) {
116
+ const roles = {
117
+ colorPrimary: '--color-brand1-6', colorLink: '--color-brand1-6',
118
+ colorPrimaryHover: '--color-brand1-5', colorPrimaryActive: '--color-brand1-9',
119
+ colorBgLayout: ['--pod-page-bg-color', '--color-white'],
120
+ colorBgContainer: ['--pod-card-bg-color', '--color-white'],
121
+ colorBgElevated: ['--pod-card-bg-color', '--color-white'], colorText: '--color-text1-4',
122
+ colorTextHeading: '--color-text1-4', colorTextSecondary: '--color-text1-3',
123
+ colorTextDescription: '--color-text1-3', colorTextPlaceholder: '--color-text1-10',
124
+ colorBorder: '--color-line1-2', colorBorderSecondary: '--color-line1-1',
125
+ colorFillAlter: '--color-fill1-1', colorFillSecondary: '--color-fill1-2',
126
+ };
127
+ const token = { ...fallback };
128
+ if (!root) return token;
129
+ const doc = root.ownerDocument;
130
+ const view = doc.defaultView;
131
+ const scope = view.getComputedStyle(root);
132
+ const probe = doc.createElement('span');
133
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;';
134
+ root.appendChild(probe);
135
+ try {
136
+ Object.keys(roles).forEach((role) => {
137
+ const names = Array.isArray(roles[role]) ? roles[role] : [roles[role]];
138
+ const value = names.map((name) => scope.getPropertyValue(name).trim()).find(Boolean) || '';
139
+ if (!value || !view.CSS.supports('color', value)) return;
140
+ probe.style.color = value;
141
+ const color = view.getComputedStyle(probe).color;
142
+ if (color) token[role] = color;
143
+ });
144
+ } finally {
145
+ probe.remove();
146
+ }
147
+ return token;
148
+ }
149
+
150
+ function useCanvasTheme(fallback) {
151
+ const rootRef = React.useRef(null);
152
+ const fallbackRef = React.useRef(fallback);
153
+ const [token, setToken] = React.useState(fallback);
154
+ React.useLayoutEffect(() => {
155
+ const root = rootRef.current;
156
+ if (!root) return undefined;
157
+ const doc = root.ownerDocument;
158
+ const view = doc.defaultView;
159
+ let frame;
160
+ const refresh = () => {
161
+ const next = readCanvasTheme(root, fallbackRef.current);
162
+ setToken((previous) => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
163
+ };
164
+ const schedule = () => {
165
+ view.cancelAnimationFrame(frame);
166
+ frame = view.requestAnimationFrame(refresh);
167
+ };
168
+ refresh();
169
+ // Observe only theme-bearing ancestors and stylesheet changes, not business DOM.
170
+ const observer = new view.MutationObserver(schedule);
171
+ for (let node = root; node; node = node.parentElement) {
172
+ observer.observe(node, { attributes: true });
173
+ }
174
+ if (doc.head) observer.observe(doc.head, { subtree: true, childList: true, characterData: true, attributes: true });
175
+ doc.addEventListener('load', schedule, true);
176
+ view.addEventListener('resize', schedule);
177
+ view.addEventListener('openyida:theme-change', schedule);
178
+ return () => {
179
+ observer.disconnect();
180
+ view.cancelAnimationFrame(frame);
181
+ doc.removeEventListener('load', schedule, true);
182
+ view.removeEventListener('resize', schedule);
183
+ view.removeEventListener('openyida:theme-change', schedule);
184
+ };
185
+ }, []);
186
+ return { rootRef, token };
187
+ }
188
+ // @openyida-canvas-theme:end
189
+
118
190
  function YidaComp(props) {
191
+ const [modal, modalContextHolder] = Modal.useModal();
192
+ const { rootRef, token } = useCanvasTheme({ colorPrimary: "#1677ff", borderRadius: 9 });
119
193
  const formUuid = props && props.formUuid ? props.formUuid : 'sample-unbound';
120
194
  const draftKey = 'openyida_canvas_table_form_draft_' + formUuid;
121
195
  const [rows, setRows] = useState(() => loadDraft(draftKey));
@@ -262,7 +336,7 @@ function YidaComp(props) {
262
336
  }
263
337
  if (!validated.length) return;
264
338
 
265
- Modal.confirm({
339
+ modal.confirm({
266
340
  title: '确认批量提交',
267
341
  content: '将提交 ' + validated.length + ' 行数据,首行事项为“' + validated[0].name + '”。提交后成功行不会重复写入。',
268
342
  okText: '确认提交',
@@ -386,15 +460,7 @@ function YidaComp(props) {
386
460
 
387
461
  return (
388
462
  <ConfigProvider
389
- theme={{
390
- token: {
391
- colorPrimary: readThemeColor('--color-brand1-6', '#1677FF'),
392
- colorInfo: readThemeColor('--color-brand1-6', '#1677FF'),
393
- borderRadius: 9,
394
- colorText: readThemeColor('--color-text1-4', '#1F2329'),
395
- colorBgLayout: readThemeColor('--color-fill1-1', '#F5F6F7'),
396
- },
397
- }}
463
+ theme={{ token }}
398
464
  >
399
465
  <style>{`
400
466
  .canvas-table-page {
@@ -402,7 +468,7 @@ function YidaComp(props) {
402
468
  min-height: 100vh;
403
469
  box-sizing: border-box;
404
470
  padding: 28px;
405
- background: var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)));
471
+ background: var(--pod-page-bg-color, var(--color-white, #fff));
406
472
  }
407
473
  .canvas-table-shell { max-width: 1320px; margin: 0 auto; }
408
474
  .canvas-table-header {
@@ -437,7 +503,8 @@ function YidaComp(props) {
437
503
  }
438
504
  `}</style>
439
505
 
440
- <div className="canvas-table-page">
506
+ <div ref={rootRef} className="canvas-table-page">
507
+ {modalContextHolder}
441
508
  <div className="canvas-table-shell">
442
509
  <div className="canvas-table-header">
443
510
  <div>