openyida 2026.10.21-2 → 2026.10.22

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 (37) hide show
  1. package/README.md +3 -1
  2. package/lib/app/canvas-compile.js +8 -0
  3. package/lib/app/page-linter.js +10 -0
  4. package/lib/app/responsive-layout-guard.js +82 -0
  5. package/lib/core/command-manifest.js +6 -4
  6. package/lib/core/locales/en.js +34 -1
  7. package/lib/core/locales/zh.js +34 -1
  8. package/lib/core/sample.js +3 -0
  9. package/lib/feedback/feedback.js +2 -1
  10. package/lib/feedback/github-setup.js +97 -0
  11. package/lib/feedback/github.js +17 -8
  12. package/lib/feedback/report.js +35 -15
  13. package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +1 -1
  14. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  15. package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -1
  16. package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +36 -24
  17. package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +1 -1
  18. package/lib/samples/openyida-scaffold/canvas-responsive-layout.jsx +104 -0
  19. package/package.json +1 -1
  20. package/yida-skills/SKILL.md +2 -2
  21. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -1
  22. package/yida-skills/skills/yida-custom-page/SKILL.md +6 -2
  23. package/yida-skills/skills/yida-density/SKILL.md +6 -1
  24. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  25. package/yida-skills/skills/yida-design/references/responsive-layout.md +69 -22
  26. package/yida-skills/skills/yida-design/references/task-driven-visual-design.md +2 -0
  27. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -0
  28. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
  29. package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
  30. package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +2 -0
  31. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +10 -10
  32. package/yida-skills/skills/yida-feedback/SKILL.md +22 -5
  33. package/yida-skills/skills/yida-feedback/references/github-contribution.md +14 -8
  34. package/yida-skills/skills/yida-feedback/references/github-setup.md +74 -0
  35. package/yida-skills/skills/yida-feedback/references/report-input.md +10 -2
  36. package/yida-skills/skills/yida-feedback/references/user-interaction.md +19 -2
  37. package/yida-skills/skills/yida-prd/SKILL.md +1 -0
package/README.md CHANGED
@@ -446,7 +446,9 @@ openyida feedback report --input summary.json --output-dir feedback-draft-001
446
446
 
447
447
  Your AI prepares the summary from observed evidence and selected reproduction screenshots. PRs include before/after test comparisons. Before anything is published, review the final text and images, confirm sensitive information, and remove anything you do not want to share. See the [report input format](yida-skills/skills/yida-feedback/references/report-input.md). Review business information before sharing. With a GitHub account, authorize your AI to submit an Issue or contribute a verified public OpenYida fix through your personal Fork. Without an account, create a GitHub account first. If that is not possible, share the report in the AI开发者群 (AI developer group).
448
448
 
449
- When you choose to submit an OpenYida Issue, the feedback Skill can check your signed-in GitHub account with `openyida feedback github status --json`. Star and a personal Fork are optional: `feedback github star --confirm` and `feedback github fork --confirm` run only after you choose them. Existing actions are reused. This check is separate from installation and automatic updates; neither action is required to submit an Issue.
449
+ To configure a submission tool, your AI can first run `openyida feedback github setup --json` to check gh and the current account. After you approve the installation plan, it can run `openyida feedback github setup --install --confirm --json`, using existing Homebrew on macOS/Linux or WinGet on Windows. Without a supported manager, official instructions or the web remain available. Browser authorization is a separate step performed by you; installation does not log you in, request passwords or tokens, or replace public-content approval. Resume the original draft after setup. See the [account and tool guide](yida-skills/skills/yida-feedback/references/github-setup.md).
450
+
451
+ When preparing a PR that needs a personal Fork, or when you ask how to support the project, the feedback Skill can inspect account support state with `openyida feedback github status --json`. Star and a personal Fork are optional: `feedback github star --confirm` and `feedback github fork --confirm` run only after you choose them. Existing actions are reused. This check is separate from installation and automatic updates; neither action is required to submit an Issue.
450
452
 
451
453
  <!-- OPENYIDA_COMMANDS_START -->
452
454
  <!-- This section is generated by `npm run docs:commands`. Do not edit command rows by hand. -->
@@ -55,6 +55,7 @@ const { assertCanvasThemeStructure } = require('./canvas-theme-guard');
55
55
  const { assertCanvasIconExports } = require('./canvas-icon-guard');
56
56
  const { assertCanvasNavigationStructure } = require('./canvas-navigation-guard');
57
57
  const { assertCanvasNavigationPaths } = require('./canvas-path-guard');
58
+ const { findResponsiveLayoutIssues } = require('./responsive-layout-guard');
58
59
 
59
60
  /**
60
61
  * 依赖 → window 别名白名单,用于把 import 改写到运行时全局对象。
@@ -977,6 +978,13 @@ function compileCanvasLocal(source, options = {}) {
977
978
  assertCanvasNavigationPaths(source, options);
978
979
  assertInlineCssStructure(source, options);
979
980
  const warnings = [];
981
+ findResponsiveLayoutIssues(source).forEach((issue) => {
982
+ warnings.push({
983
+ line: issue.line,
984
+ rule: issue.rule,
985
+ message: t(issue.messageKey, ...(issue.params || [])),
986
+ });
987
+ });
980
988
  assertCanvasThemeStructure(source, { ...options, onThemeWarning(message, details) {
981
989
  warnings.push({ ...details, message });
982
990
  if (options.onThemeWarning) { options.onThemeWarning(message, details); }
@@ -9,6 +9,7 @@ const {
9
9
  const { findBareCjkJsxTextIdentifiers } = require('./jsx-text-guard');
10
10
  const { findDirectFormOpenIssues } = require('./form-open-guard');
11
11
  const { findFormDetailLinkIssues } = require('./form-detail-link-guard');
12
+ const { findResponsiveLayoutIssues } = require('./responsive-layout-guard');
12
13
  const Babel = require('@babel/standalone');
13
14
 
14
15
  const THEN_CALLBACK_LINE_LIMIT = 50;
@@ -1051,6 +1052,15 @@ function lintYidaSource(sourceCode, filePath) {
1051
1052
  findFormDetailLinkIssues(sourceCode, { parserOptions: PARSER_OPTIONS }).forEach((issue) => {
1052
1053
  pushIssue(errors, issue.line, 'form-detail-link', t('publish.lint_form_detail_link'), disableMap);
1053
1054
  });
1055
+ findResponsiveLayoutIssues(sourceCode).forEach((issue) => {
1056
+ pushIssue(
1057
+ warnings,
1058
+ issue.line,
1059
+ issue.rule,
1060
+ t(issue.messageKey, ...(issue.params || [])),
1061
+ disableMap
1062
+ );
1063
+ });
1054
1064
  return { errors, warnings };
1055
1065
  }
1056
1066
 
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const FIXED_WIDTH_THRESHOLD = 480;
4
+
5
+ function getLine(source, index) {
6
+ return source.slice(0, index).split('\n').length;
7
+ }
8
+
9
+ function lineHasOptOut(source, index, marker) {
10
+ const lineStart = source.lastIndexOf('\n', index) + 1;
11
+ const lineEnd = source.indexOf('\n', index);
12
+ const line = source.slice(lineStart, lineEnd < 0 ? source.length : lineEnd);
13
+ return line.includes(marker);
14
+ }
15
+
16
+ /**
17
+ * Find high-confidence responsive layout risks shared by Canvas and legacy JSX pages.
18
+ * The checks stay advisory because viewport-owned overlays and fixed-size visualizations
19
+ * can be intentional; authors can add the documented line marker after reviewing one.
20
+ * @param {string} source
21
+ * @returns {Array<{line: number, rule: string, messageKey: string, params?: Array<unknown>}>>}
22
+ */
23
+ function findResponsiveLayoutIssues(source) {
24
+ const text = String(source || '');
25
+ const issues = [];
26
+
27
+ const observerMatch = /new\s+(?:window\.)?ResizeObserver\s*\(/.exec(text);
28
+ if (observerMatch && !/\.\s*(?:disconnect|unobserve)\s*\(/.test(text)) {
29
+ issues.push({
30
+ line: getLine(text, observerMatch.index),
31
+ rule: 'resize-observer-cleanup',
32
+ messageKey: 'publish.lint_resize_observer_cleanup',
33
+ });
34
+ }
35
+
36
+ const viewportMatch = /(?:window\s*\.\s*innerWidth|matchMedia\s*\(\s*['"`]\s*\([^'"`]*(?:min|max)-width)/.exec(text);
37
+ const hasLayoutBranch = /(?:@media|grid-template-columns|gridTemplateColumns|flex-direction|flexDirection|\bmobile\b|\bisMobile\b)/.test(text);
38
+ const hasContainerSignal = /(?:@container|container-type|containerType|ResizeObserver|getBoundingClientRect\s*\(\s*\)\s*\.\s*width|\.\s*clientWidth\b)/.test(text);
39
+ if (
40
+ viewportMatch && hasLayoutBranch && !hasContainerSignal &&
41
+ !text.includes('openyida-responsive-viewport-ok')
42
+ ) {
43
+ issues.push({
44
+ line: getLine(text, viewportMatch.index),
45
+ rule: 'responsive-viewport-only',
46
+ messageKey: 'publish.lint_responsive_viewport_only',
47
+ });
48
+ }
49
+
50
+ const fixedWidthPattern = /(?:^|[\s{;,])width\s*:\s*['"]?(\d{3,})px?['"]?(?=\s*[,;}])/gm;
51
+ let fixedWidthMatch;
52
+ while ((fixedWidthMatch = fixedWidthPattern.exec(text)) !== null) {
53
+ const width = Number(fixedWidthMatch[1]);
54
+ if (
55
+ width >= FIXED_WIDTH_THRESHOLD &&
56
+ !lineHasOptOut(text, fixedWidthMatch.index, 'openyida-responsive-fixed-width-ok')
57
+ ) {
58
+ issues.push({
59
+ line: getLine(text, fixedWidthMatch.index),
60
+ rule: 'responsive-fixed-width',
61
+ messageKey: 'publish.lint_responsive_fixed_width',
62
+ params: [width],
63
+ });
64
+ }
65
+ }
66
+
67
+ const scaleMatch = /(?:transform\s*:\s*[^;\n}]*\bscale\s*\(|\bzoom\s*:)/.exec(text);
68
+ if (scaleMatch && !text.includes('openyida-responsive-scale-ok')) {
69
+ issues.push({
70
+ line: getLine(text, scaleMatch.index),
71
+ rule: 'responsive-page-scale',
72
+ messageKey: 'publish.lint_responsive_page_scale',
73
+ });
74
+ }
75
+
76
+ return issues;
77
+ }
78
+
79
+ module.exports = {
80
+ FIXED_WIDTH_THRESHOLD,
81
+ findResponsiveLayoutIssues,
82
+ };
@@ -648,7 +648,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
648
648
  mutates_yida: true,
649
649
  mutates_local: true,
650
650
  read_actions: ['url', 'status', 'github status'],
651
- mutating_actions: ['setup', 'dismiss', 'report', 'github star', 'github fork'],
651
+ mutating_actions: ['setup', 'dismiss', 'report', 'github setup --install --confirm', 'github star', 'github fork'],
652
652
  })],
653
653
  ['i18n', sideEffect('mixed', {
654
654
  mutates_yida: true,
@@ -2058,11 +2058,13 @@ const COMMAND_GROUPS = [
2058
2058
  requiresLogin: false,
2059
2059
  }),
2060
2060
  command('feedback', ['feedback'], 'feedback <report|github|setup|url|dismiss|status> [options]', 'help.cmd_feedback', { managedRun: { scope: 'actions', actions: { report: { scope: 'local' } } },
2061
- permission: actionDependentPermission({ preauthorized_actions: ['report', 'url', 'status', 'dismiss', 'github status'], ask_actions: ['setup', 'github star', 'github fork'] }),
2061
+ permission: actionDependentPermission({ preauthorized_actions: ['report', 'url', 'status', 'dismiss', 'github status'], ask_actions: ['setup', 'github setup', 'github setup --install --confirm', 'github star', 'github fork'] }),
2062
2062
  requiresLogin: false,
2063
2063
  output: 'text|json',
2064
- examples: ['openyida feedback report --input summary.json --output-dir feedback-draft-001'],
2065
- notes: ['report prepares local review drafts only; no login, source checkout, update check or network submission. github status [--json] checks the authenticated github.com user, Star and personal Fork through an existing gh installation; it performs no writes. github star|fork --confirm [--json] requires the user to explicitly choose that account action. Missing gh/login, unknown status and declined actions never block Issue submission. Already applied actions are reused; writes are single-attempt with readback. No auto-update/install hook, Issue submission, clone or git remote modification. Managed runs permit only offline report preparation.'],
2064
+ examples: ['openyida feedback report --input summary.json --output-dir feedback-draft-001', 'openyida feedback github setup --json', 'openyida feedback github setup --install --confirm --json'],
2065
+ // Keep setup conservative in permission metadata: --install changes the local machine.
2066
+ // The parser distinguishes read-only setup from a separately confirmed installation.
2067
+ notes: ['github setup [--json] detects gh and verifies the current account without installing or logging in. github setup --install --confirm performs one user-approved installation via existing Homebrew (macOS/Linux) or WinGet (Windows), then checks gh again. No manager bootstrap, source changes, automatic retry, upgrade, or automatic login; unsupported environments use official instructions or the web. Browser authorization requires separate consent and public submission still requires content review. report prepares local review drafts only; no login, source checkout, update check or network submission. github status [--json] checks the authenticated github.com user, Star and personal Fork through an existing gh installation; it performs no writes. github star|fork --confirm [--json] requires the user to explicitly choose that account action. Missing gh/login, unknown status and declined actions never block Issue submission. Already applied actions are reused; writes are single-attempt with readback. No auto-update/install hook, Issue submission, clone or git remote modification. Managed runs permit only offline report preparation.'],
2066
2068
  }),
2067
2069
  command('batch', ['batch'], 'batch <file>|--commands "cmd1 ; cmd2" [--stop-on-error] [--json]', 'help.cmd_batch', { managedRun: { scope: 'denied', reason: 'scope_not_audited' },
2068
2070
  output: 'text|json',
@@ -1534,6 +1534,10 @@ Examples:
1534
1534
  lint_setstate_non_timestamp: 'this.setState writes a non-timestamp field. Custom page business state should live in _customState and be updated via forceUpdate()/setCustomState(); this.setState should only carry the timestamp contract field',
1535
1535
  lint_self_binding_missing: 'renderJsx uses this without declaring var self = this;. Add var self = this; at the top of renderJsx and use self inside closures/callbacks to avoid losing this',
1536
1536
  lint_echarts_dom_ready: 'echarts.init is not wrapped in setTimeout; the container may not be mounted yet so the chart may not render. Use setTimeout(function(){ /* echarts.init */ }, 300)',
1537
+ lint_resize_observer_cleanup: 'ResizeObserver has no disconnect()/unobserve() cleanup and may keep measuring after unmount. Release it from the effect or lifecycle cleanup.',
1538
+ lint_responsive_viewport_only: 'Layout changes only from viewport width, so a narrow desktop sidebar, drawer, or iframe may not degrade. Measure the content container with @container or a cleaned-up ResizeObserver. Add openyida-responsive-viewport-ok only for intentional viewport-owned behavior.',
1539
+ lint_responsive_fixed_width: 'A fixed width of %spx may overflow a narrow container. Prefer max-width, minmax(0, 1fr), or local scrolling. Add openyida-responsive-fixed-width-ok only for an intentional fixed canvas.',
1540
+ lint_responsive_page_scale: 'Whole-page scale/zoom was detected. Responsive layouts should reflow content while preserving readable text and touch targets. Add openyida-responsive-scale-ok only for an intentionally scaled local visualization.',
1537
1541
  lint_yida_api_catch: 'this.utils.yida API call has no detected .catch(); add error handling and toast the user',
1538
1542
  lint_echarts_legacy_map_china: 'ECharts 5 no longer supports echarts/map/js/china.js. Load DataV GeoJSON and call echarts.registerMap("china", geoJson) instead',
1539
1543
  lint_echarts_rich_label_formatter: 'ECharts label.formatter returning rich text templates is unstable in Yida custom pages; prefer plain formatter strings or precomputed label text',
@@ -2440,6 +2444,26 @@ Object.assign(module.exports.help, {'cmd_formula_contracts': 'Show formula forma
2440
2444
  Object.assign(module.exports.formula_v2, {'parallel_formula': 'Use conditions/rules or all:true for parallel branches; expression is currently supported only for route branches.'});
2441
2445
 
2442
2446
  module.exports.feedback_report = {
2447
+ body: {
2448
+ summary: 'Your goal',
2449
+ actual: 'What happened',
2450
+ expected: 'Expected result',
2451
+ incident: 'Environment when the problem occurred',
2452
+ command: 'Related operation',
2453
+ steps: 'How to reproduce',
2454
+ impact: 'Impact',
2455
+ evidence: 'Observed evidence',
2456
+ attempts: 'Attempts and results',
2457
+ cause: 'Cause and confidence',
2458
+ cause_evidence: 'Evidence for assessment',
2459
+ next: 'Next verification',
2460
+ screenshots: 'Screenshots',
2461
+ review_image: 'Review image before sharing',
2462
+ limitations: 'Test limitations',
2463
+ generator: 'Report generator environment (not the incident environment)',
2464
+ unknown: 'Unknown / not verified',
2465
+ draft: 'Local draft — not submitted. Review text and screenshots for customer data, credentials and private information before sharing.',
2466
+ },
2443
2467
  comparison: 'Test pass/fail requires a command, actual result and evidence.',
2444
2468
  screenshots: 'Choose at most five PNG/JPEG/WebP screenshots of at most 10 MiB each.',
2445
2469
  text: 'Feedback text fields must be strings of at most 12000 characters.',
@@ -2527,7 +2551,16 @@ module.exports.task_center = {
2527
2551
  };
2528
2552
 
2529
2553
  module.exports.feedback_github = {
2530
- usage: 'Usage: openyida feedback github <status|star|fork> [--confirm] [--json]',
2554
+ setup_inspect_installation: 'Installation is unverified. Check gh --version, package-manager state or reopen the terminal before deciding to retry; keep the draft.',
2555
+ setup_check_execution: 'gh could not run. Check execution permissions or the local environment; do not reinstall or switch accounts automatically.',
2556
+ setup_confirm_install: 'GitHub CLI can be installed. Explain the plan and obtain consent before openyida feedback github setup --install --confirm --json; the web option remains available.',
2557
+ setup_manual_install: 'Automatic installation is unavailable. Use the official instructions, an existing connector or the web; keep the draft.',
2558
+ setup_ready: 'GitHub CLI and the current account are verified. Resume the draft; confirm public content and required permissions before submission.',
2559
+ setup_browser_login: 'GitHub CLI is available and login is required. With consent, guide browser authorization, then rerun setup --json to verify.',
2560
+ setup_check_account: 'GitHub CLI is available, but the account is unverified. Check network, authorization or rate limits; do not infer a missing account or reinstall.',
2561
+ setup_consent: 'Use --install --confirm only after consent to install GitHub CLI and required dependencies in the package manager default location. Windows accepts WinGet source/package agreements and may need user-approved system permissions. No automatic manager installation, privilege escalation or login.',
2562
+ setup_privacy: 'Installation is not consent to publish. The user authorizes in the browser; never request passwords, codes or tokens. If gh cannot store credentials securely, use the web or a secure connector. Keep account and tool diagnostics out of feedback reports.',
2563
+ usage: 'Usage: openyida feedback github setup [--install --confirm] [--json]; openyida feedback github <status|star|fork> [--confirm] [--json]',
2531
2564
  confirmation: 'Star or Fork requires the user to explicitly choose that action before --confirm. Neither is required to submit an Issue.',
2532
2565
  failed: 'The GitHub action could not be verified. Inspect its status before retrying; this does not block Issue submission.',
2533
2566
  };
@@ -1531,6 +1531,10 @@ openyida - 宜搭命令行工具
1531
1531
  lint_setstate_non_timestamp: '检测到 this.setState 写入非 timestamp 字段。业务态应写入 _customState 并调用 forceUpdate()/setCustomState() 触发重渲染;this.setState 仅用于 timestamp 契约字段',
1532
1532
  lint_self_binding_missing: 'renderJsx 内使用了 this 但未声明 var self = this;。请在 renderJsx 开头加入 var self = this;,闭包/回调内改用 self,避免 this 指向丢失',
1533
1533
  lint_echarts_dom_ready: 'echarts.init 未包裹在 setTimeout 中,容器可能尚未挂载导致图表不渲染。建议 setTimeout(function(){ /* echarts.init */ }, 300)',
1534
+ lint_resize_observer_cleanup: 'ResizeObserver 缺少 disconnect()/unobserve() 清理,页面卸载后可能继续测量。请在 effect 或生命周期清理函数中释放 observer。',
1535
+ lint_responsive_viewport_only: '布局只依据视口宽度变化,桌面侧栏、抽屉或 iframe 窄容器可能无法降级。请用 @container 或带清理的 ResizeObserver 测量实际内容容器;若确实是视口级行为,添加 openyida-responsive-viewport-ok 说明。',
1536
+ lint_responsive_fixed_width: '检测到固定宽度 %spx,窄容器可能产生整页横向溢出。请改用 max-width、minmax(0, 1fr) 或局部滚动;确有固定画布需求时添加 openyida-responsive-fixed-width-ok 说明。',
1537
+ lint_responsive_page_scale: '检测到 scale/zoom 整体缩放。响应式布局应重排内容并保留可读字号和点击区域;仅局部可视化确需缩放时添加 openyida-responsive-scale-ok 说明。',
1534
1538
  lint_yida_api_catch: 'this.utils.yida API 调用未检测到 .catch(),请补充错误处理并 toast 给用户',
1535
1539
  lint_echarts_legacy_map_china: 'ECharts 5 已废弃 echarts/map/js/china.js,请加载 DataV GeoJSON 后调用 echarts.registerMap("china", geoJson)',
1536
1540
  lint_echarts_rich_label_formatter: 'ECharts label.formatter 返回 rich text 模板在宜搭自定义页面环境不稳定,建议使用普通 formatter 字符串或预先拼好的标签文本',
@@ -2394,6 +2398,26 @@ Object.assign(module.exports.help, {'cmd_formula_contracts': '查看公式保存
2394
2398
  Object.assign(module.exports.formula_v2, {'parallel_formula': '并行分支使用 conditions/rules 或 all:true;expression 目前只适用于 route 分支。'});
2395
2399
 
2396
2400
  module.exports.feedback_report = {
2401
+ body: {
2402
+ summary: '想完成什么',
2403
+ actual: '实际发生了什么',
2404
+ expected: '希望看到什么结果',
2405
+ incident: '问题发生时的环境',
2406
+ command: '相关操作',
2407
+ steps: '怎样重现',
2408
+ impact: '影响',
2409
+ evidence: '已观察的证据',
2410
+ attempts: '尝试过的方法与结果',
2411
+ cause: '可能的原因及确认程度',
2412
+ cause_evidence: '判断依据',
2413
+ next: '下一步验证',
2414
+ screenshots: '问题截图',
2415
+ review_image: '公开前请检查图片',
2416
+ limitations: '尚未验证的范围',
2417
+ generator: '报告生成环境(不代表问题发生时)',
2418
+ unknown: '未知 / 尚未确认',
2419
+ draft: '本地草稿,尚未提交。请检查文字和截图,确认没有客户数据、凭证或内部信息后再公开。',
2420
+ },
2397
2421
  comparison: '测试通过或失败须提供真实命令、实际结果及证据。',
2398
2422
  screenshots: '截图须为明确选择的 PNG/JPEG/WebP,每张至多 10 MiB,最多 5 张。',
2399
2423
  text: '反馈文本字段须为不超过 12000 字符的字符串。',
@@ -2481,7 +2505,16 @@ module.exports.task_center = {
2481
2505
  };
2482
2506
 
2483
2507
  module.exports.feedback_github = {
2484
- usage: '用法:openyida feedback github <status|star|fork> [--confirm] [--json]',
2508
+ setup_inspect_installation: '安装结果尚未核实。先检查 gh --version、包管理器状态或重新打开终端,再决定是否重试;草稿仍保留。',
2509
+ setup_check_execution: 'gh 暂时无法运行。请检查执行权限或本机环境;不要重复安装或更换账号。',
2510
+ setup_confirm_install: '可安装 GitHub CLI。先说明安装计划并取得用户同意,再执行 openyida feedback github setup --install --confirm --json;也可使用网页。',
2511
+ setup_manual_install: '当前没有可用的自动安装方式。可查看官方安装说明、使用已有连接器或网页,草稿仍保留。',
2512
+ setup_ready: 'GitHub CLI 和当前账号已验证。继续原草稿,提交前仍需核对公开内容和所需权限。',
2513
+ setup_browser_login: 'GitHub CLI 已可用,需要登录。用户同意后引导浏览器授权,完成后再次运行 setup --json 核实。',
2514
+ setup_check_account: 'GitHub CLI 已可用,但当前账号尚未核实。先检查网络、授权或限流;不要据此判断用户没有账号或要求重新安装。',
2515
+ setup_consent: '仅在用户同意安装 GitHub CLI 及其必要依赖后使用 --install --confirm。使用包管理器默认位置;Windows 安装会接受 WinGet 来源和软件协议,可能需要用户批准系统权限。不自动安装包管理器、提权或登录。',
2516
+ setup_privacy: '安装不代表同意公开提交。浏览器授权由用户完成;不索取密码、验证码或 Token。若 gh 无法安全保存凭证,改用网页或安全连接器;账号和工具诊断不写入反馈报告。',
2517
+ usage: '用法:openyida feedback github setup [--install --confirm] [--json]; openyida feedback github <status|star|fork> [--confirm] [--json]',
2485
2518
  confirmation: 'Star 或 Fork 需要用户明确选择后再传入 --confirm;提交 Issue 不要求这两项操作。',
2486
2519
  failed: 'GitHub 操作未能核实完成,请先查询状态;这不影响提交 Issue。',
2487
2520
  };
@@ -60,6 +60,7 @@ const SAMPLES = {
60
60
  'canvas-theme': 'canvas-theme-provider.template.jsx',
61
61
  'canvas-navigation': 'canvas-navigation.jsx',
62
62
  'canvas-view-state': 'canvas-view-state.jsx',
63
+ 'canvas-responsive-layout': 'canvas-responsive-layout.jsx',
63
64
  'canvas-admin-entry': 'canvas-admin-entry.jsx',
64
65
  'canvas-nav-side': 'canvas-nav/side.jsx',
65
66
  'canvas-nav-top': 'canvas-nav/top.jsx',
@@ -109,6 +110,7 @@ const SAMPLE_OUTPUT_FILENAMES = {
109
110
  'openyida-page-template/form-open-container': 'form-open-container.jsx',
110
111
  'openyida-page-template/canvas-dialog': 'canvas-dialog.jsx',
111
112
  'openyida-page-template/canvas-theme': 'canvas-theme.jsx',
113
+ 'openyida-page-template/canvas-responsive-layout': 'canvas-responsive-layout.jsx',
112
114
  'yida-design/app-theme': 'app-theme.css',
113
115
  };
114
116
 
@@ -116,6 +118,7 @@ const SAMPLE_HINTS = {
116
118
  'openyida-page-template/canvas-admin-entry': '已包含统一跳转 helper;在 CanvasThemeProvider 内渲染 CanvasAdminWorkbenchButton,填写当前 appType 与真实 workbenchFormUuid。身份读取当前应用 loginUser.isAppAdmin,未知时隐藏;默认不跳开发后台或猜测管理首页。',
117
119
  'openyida-page-template/canvas-navigation': '先从资源回读填写 appType、目标类型与页面 ID;本页 Tab 用 selectView,独立前台用 custom,管理入口用 page,表单入口接 openForm;按钮和链接共用 buildCanvasPageUrl。',
118
120
  'openyida-page-template/canvas-theme': '以 CanvasThemeProvider 包住业务组件;Tabs 用于内容切换,Segmented/Radio 用于筛选,Button 用于动作。默认轻底选中态与可读文字,controls 可供自绘控件复用;避免内层固定色覆盖。保留内置加载遮罩防闪边样式;旧页面需更新 Provider 并重新发布。',
121
+ 'openyida-page-template/canvas-responsive-layout': '合并 useContainerLayout 与 ResponsiveLayout;按实际内容容器重排筛选栏和主辅面板,宽表仅局部滚动,图表按 data-chart-width resize。业务状态放在布局壳之上,断点切换不得修改 key 或重挂载。',
119
122
  'openyida-page-template/canvas-nav-top': '顶部默认贴顶通栏;长页先合并 canvas-nav-content,CanvasNav 传 headerOnly,有首屏背景时传 overlay;悬浮外观仅显式 floating=true。',
120
123
  'openyida-page-template/canvas-nav-content': '合并 CanvasNavigationContent;长页用 layout=document,CanvasNav 传 headerOnly/overlay;默认 sticky,受宿主限制时用 navigationPosition=fixed。工作区用 workspace,宿主已分配高度时传 height。',
121
124
  'openyida-page-template/canvas-nav-data': '页内展示用 mode=local(key/viewKey/targetType=local),不请求平台菜单;platform 需真实资源 ID;independent 或声明 access 时必须接入真实 resolveAccess。保留 PRD 顺序与分组。',
@@ -51,6 +51,7 @@ function printUsage() {
51
51
  openyida feedback setup <appType> [--name "OpenYida 体验反馈"] [--path /o/openyida-feedback] [--open|--no-open]
52
52
  openyida feedback setup --create-app [--app-name "OpenYida 体验反馈"] [--name "OpenYida 体验反馈"]
53
53
  openyida feedback report --input <summary.json> --output-dir <new-directory>
54
+ openyida feedback github setup [--install --confirm] [--json]
54
55
  openyida feedback github <status|star|fork> [--confirm] [--json]
55
56
  openyida feedback url [--tool Codex] [--model gpt-5] [--command publish] [--diagnostics "..."]
56
57
  openyida feedback dismiss --today|--forever|--reset
@@ -58,7 +59,7 @@ function printUsage() {
58
59
 
59
60
  说明:
60
61
  ${t('feedback_report.help')}
61
- setup 会在宜搭创建一个公开反馈表单,并写入本地 .cache/openyida/feedback/config.json。
62
+ feedback setup 会在宜搭创建一个公开反馈表单,并写入本地 .cache/openyida/feedback/config.json。
62
63
  url 只生成带脱敏元数据的填写链接,不提交任何网络请求。
63
64
  dismiss/status 只读写本地提醒状态。
64
65
  `);
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const { t } = require('../core/i18n');
5
+
6
+ const INSTALL_URL = 'https://github.com/cli/cli#installation';
7
+ const LOGIN_COMMAND = 'gh auth login --hostname github.com --web --skip-ssh-key';
8
+
9
+ /** Fixed argv only. Capture diagnostics privately, including package-manager failures. */
10
+ function invoke(binary, args, options, installing = false) {
11
+ try {
12
+ return (options.spawnSyncFn || spawnSync)(binary, args, {
13
+ shell: false, encoding: 'utf8', timeout: installing ? 300000 : 15000,
14
+ maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
15
+ env: { ...(options.env || process.env), GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1',
16
+ GIT_TERMINAL_PROMPT: '0', HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1',
17
+ HOMEBREW_NO_INSTALL_UPGRADE: '1', HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: '1', HOMEBREW_NO_SUDO: '1',
18
+ ...(installing ? { HOMEBREW_NO_ASK: '1' } : {}) },
19
+ });
20
+ } catch { return { status: null }; }
21
+ }
22
+
23
+ function toolState(options) {
24
+ const result = invoke('gh', ['--version'], options);
25
+ if (result.error?.code === 'ENOENT') { return { state: 'missing', version: null }; }
26
+ const version = String(result.stdout || '').match(/^gh version (\d+\.\d+\.\d+)\b/)?.[1];
27
+ return result.status === 0 && !result.error && version
28
+ ? { state: 'available', version } : { state: 'unverified', version: null };
29
+ }
30
+
31
+ function installPlan(options) {
32
+ const platform = options.platform || process.platform;
33
+ const binary = platform === 'win32' ? 'winget' : ['darwin', 'linux'].includes(platform) ? 'brew' : null;
34
+ if (!binary) { return { available: false, reason: 'unsupported_platform', url: INSTALL_URL }; }
35
+ const probe = invoke(binary, ['--version'], options);
36
+ if (probe.error || probe.status !== 0) {
37
+ return { available: false, reason: probe.error?.code === 'ENOENT' ? 'manager_missing' : 'manager_unverified', url: INSTALL_URL };
38
+ }
39
+ const args = binary === 'brew' ? ['install', 'homebrew/core/gh']
40
+ : ['install', '--id', 'GitHub.cli', '--exact', '--source', 'winget', '--no-upgrade', '--disable-interactivity',
41
+ '--accept-source-agreements', '--accept-package-agreements'];
42
+ return { available: true, binary, args, url: INSTALL_URL, requiresConsent: true };
43
+ }
44
+
45
+ function accountState(options) {
46
+ const result = invoke('gh', ['api', '--hostname', 'github.com', '--method', 'GET', 'user'], options);
47
+ if (result.status === 4 && !result.error) { return { state: 'login_required', login: null }; }
48
+ let account;
49
+ try { account = JSON.parse(result.stdout); } catch { /* Unknown response stays unverified. */ }
50
+ if (result.status === 0 && !result.error && account?.type === 'User'
51
+ && /^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(account.login || '')) {
52
+ return { state: 'verified', login: account.login };
53
+ }
54
+ return { state: 'unverified', login: null };
55
+ }
56
+
57
+ /** Called only after the router validates consent and the managed-run boundary. */
58
+ function setup(parsed, options) {
59
+ let tool = toolState(options);
60
+ const installation = { attempted: false, state: 'not_requested', plan: null };
61
+ if (tool.state === 'missing') {
62
+ installation.plan = installPlan(options);
63
+ if (parsed.install) {
64
+ installation.state = 'unavailable';
65
+ if (installation.plan.available) {
66
+ installation.attempted = true;
67
+ const result = invoke(installation.plan.binary, installation.plan.args, options, true);
68
+ tool = toolState(options);
69
+ installation.state = result.status === 0 && !result.error && tool.state === 'available'
70
+ ? 'verified' : 'unverified';
71
+ }
72
+ }
73
+ } else if (parsed.install) {
74
+ installation.state = tool.state === 'available' ? 'already_available' : 'unavailable';
75
+ }
76
+ const account = tool.state === 'available' ? accountState(options) : { state: 'not_checked', login: null };
77
+ const ready = tool.state === 'available' && account.state === 'verified';
78
+ const nextStep = installation.state === 'unverified' ? 'inspect_installation'
79
+ : tool.state === 'unverified' ? 'check_execution'
80
+ : tool.state === 'missing' ? (installation.plan?.available ? 'confirm_install' : 'manual_install')
81
+ : ready ? 'ready' : account.state === 'login_required' ? 'browser_login' : 'check_account';
82
+ return {
83
+ success: !parsed.install || ['verified', 'already_available'].includes(installation.state),
84
+ action: 'setup', tool, account, ready, installation, nextStep,
85
+ message: t(`feedback_github.setup_${nextStep}`),
86
+ consentNotice: t('feedback_github.setup_consent'),
87
+ privacyNotice: t('feedback_github.setup_privacy'),
88
+ authorization: { performed: false, requiresSeparateConsent: true,
89
+ command: tool.state === 'available' && !ready ? LOGIN_COMMAND : null,
90
+ url: 'https://cli.github.com/manual/gh_auth_login' },
91
+ links: { install: INSTALL_URL, signup: 'https://github.com/signup', issue: 'https://github.com/openyida/openyida/issues/new' },
92
+ submissionPerformed: false, publicConfirmationRequired: true,
93
+ inspectBeforeRetry: installation.state === 'unverified',
94
+ };
95
+ }
96
+
97
+ module.exports = { setup };
@@ -11,15 +11,18 @@ const links = Object.freeze({ repository: `https://github.com/${REPOSITORY}`,
11
11
  /** Parse explicit account actions; submitting an Issue never implies Star or Fork consent. */
12
12
  function parseArgs(args = []) {
13
13
  const [action, ...flags] = args;
14
- if (!['status', 'star', 'fork'].includes(action) || new Set(flags).size !== flags.length
15
- || flags.some(flag => !['--json', '--confirm'].includes(flag))
16
- || (action === 'status' && flags.includes('--confirm'))) {
14
+ const install = flags.includes('--install');
15
+ if (!['status', 'star', 'fork', 'setup'].includes(action) || new Set(flags).size !== flags.length
16
+ || flags.some(flag => !['--json', '--confirm', '--install'].includes(flag))
17
+ || (install && action !== 'setup')
18
+ || (action === 'status' && flags.includes('--confirm'))
19
+ || (action === 'setup' && !install && flags.includes('--confirm'))) {
17
20
  throw new CliError(t('feedback_github.usage'), { code: 'FEEDBACK_GITHUB_USAGE' });
18
21
  }
19
- if (action !== 'status' && !flags.includes('--confirm')) {
20
- throw new CliError(t('feedback_github.confirmation'), { code: 'FEEDBACK_GITHUB_CONFIRMATION_REQUIRED' });
22
+ if ((['star', 'fork'].includes(action) || install) && !flags.includes('--confirm')) {
23
+ throw new CliError(t(install ? 'feedback_github.setup_consent' : 'feedback_github.confirmation'), { code: 'FEEDBACK_GITHUB_CONFIRMATION_REQUIRED' });
21
24
  }
22
- return { action, confirm: flags.includes('--confirm') };
25
+ return { action, confirm: flags.includes('--confirm'), ...(action === 'setup' ? { install } : {}) };
23
26
  }
24
27
 
25
28
  /** Run gh against github.com only. Never expose its raw output or credential diagnostics. */
@@ -90,10 +93,12 @@ function inspect(options) {
90
93
 
91
94
  /** Execute one explicitly authorized action, then read it back. No retry, clone, remote change or Issue submission. */
92
95
  function execute(parsed, options = {}) {
93
- const { action } = parseArgs([parsed.action, ...(parsed.confirm === true ? ['--confirm'] : [])]);
96
+ const validated = parseArgs([parsed.action, ...(parsed.confirm === true ? ['--confirm'] : []), ...(parsed.install === true ? ['--install'] : [])]);
97
+ const { action } = validated;
94
98
  if ((options.env || process.env).OPENYIDA_MANAGED_RUN === '1') {
95
99
  throw new CliError(t('feedback_github.failed'), { code: 'FEEDBACK_GITHUB_MANAGED_DENIED' });
96
100
  }
101
+ if (action === 'setup') { return require('./github-setup').setup(validated, options); }
97
102
  const result = inspect(options);
98
103
  if (action === 'status') { return result; }
99
104
  const fail = (stage, mutationPerformed = false) => {
@@ -124,7 +129,11 @@ function execute(parsed, options = {}) {
124
129
  /** CLI entrypoint. The confirmation flag attests to the user's explicit choice of this action. */
125
130
  function run(args = []) {
126
131
  const parsed = parseArgs(args);
127
- console.log(JSON.stringify(execute(parsed), null, 2));
132
+ const result = execute(parsed);
133
+ if (result.success === false) {
134
+ throw new CliError(result.message, { code: 'FEEDBACK_GITHUB_SETUP_FAILED', details: result });
135
+ }
136
+ console.log(JSON.stringify(result, null, 2));
128
137
  }
129
138
 
130
139
  module.exports = { parseArgs, execute, run };
@@ -13,6 +13,15 @@ const MAX_INPUT_BYTES = 1024 * 1024;
13
13
  const REPOSITORY = 'openyida/openyida';
14
14
  const TEXT_FIELDS = ['title', 'summary', 'command', 'expected', 'actual', 'impact', 'tool', 'model'];
15
15
  const LIST_FIELDS = ['steps', 'evidence', 'attempts'];
16
+ const INCIDENT_FIELDS = ['openyida', 'node', 'platform', 'arch', 'installation', 'browser', 'deployment', 'organizationContext'];
17
+
18
+ function incidentEnvironment(value) {
19
+ if (value === undefined) {value = {};}
20
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {throw invalid('object');}
21
+ // Only explicit incident facts belong here. The reporting process may run
22
+ // after an upgrade or on another machine; never infer incident versions.
23
+ return Object.fromEntries(INCIDENT_FIELDS.map(key => [key, sanitizeText(value[key])]));
24
+ }
16
25
 
17
26
  function comparisonRun(value) {
18
27
  if (value === undefined) {return { status: 'not_run', evidence: [] };}
@@ -98,6 +107,8 @@ function buildReport(input) {
98
107
  throw invalid('confirmed');
99
108
  }
100
109
  report.screenshots = [];
110
+ report.incidentEnvironment = incidentEnvironment(input.incidentEnvironment);
111
+ // Retain this output contract for existing consumers: generator context only.
101
112
  report.environment = {
102
113
  openyida: require('../../package.json').version,
103
114
  node: process.version,
@@ -114,17 +125,28 @@ function buildReport(input) {
114
125
 
115
126
  // Escape markup so copied diagnostics cannot inject images, links or HTML in an issue.
116
127
  function markdownText(value) {
117
- return String(value || 'Unknown / not verified').replace(/[\\`*_{}[\]<>()#!|~]/g, '\\$&');
128
+ return String(value || t('feedback_report.body.unknown')).replace(/[\\`*_{}[\]<>()#!|~]/g, '\\$&');
118
129
  }
119
130
 
120
131
  function renderMarkdown(report) {
132
+ const label = key => t('feedback_report.body.' + key);
133
+ const incident = report.incidentEnvironment || {};
134
+ const environmentLines = Object.entries({
135
+ openyida: incident.openyida, node: incident.node, platform: incident.platform, arch: incident.arch,
136
+ installation: incident.installation, tool: report.tool, model: report.model,
137
+ ...Object.fromEntries(['browser', 'deployment', 'organizationContext']
138
+ .filter(key => incident[key]).map(key => [key, incident[key]])),
139
+ }).map(([key, value]) => `${key}: ${value || label('unknown')}`);
121
140
  const sections = [
122
- ['Summary', report.summary], ['Command scenario', report.command],
123
- ['Steps to reproduce', report.steps], ['Expected', report.expected], ['Actual', report.actual],
124
- ['Impact', report.impact], ['Evidence (observed)', report.evidence], ['Attempts and results', report.attempts],
125
- ['Root cause assessment', `${report.cause.category} / ${report.cause.status}\n${report.cause.summary || 'Not established'}`],
126
- ['Evidence for assessment', report.cause.evidence], ['Next verification', report.cause.nextSteps],
127
- ['Reproduction screenshots', report.screenshots.map(item => `${item.file}: ${item.description || 'Review image before sharing'}`)],
141
+ [label('summary'), report.summary || label('unknown')],
142
+ [label('actual'), report.actual], [label('expected'), report.expected || label('unknown')],
143
+ [label('incident'), environmentLines],
144
+ [label('command'), report.command], [label('steps'), report.steps],
145
+ [label('impact'), report.impact], [label('evidence'), report.evidence], [label('attempts'), report.attempts],
146
+ ...(report.cause.summary || report.cause.status !== 'unknown' || report.cause.category !== 'unknown'
147
+ ? [[label('cause'), `${report.cause.category} / ${report.cause.status}\n${report.cause.summary}`]] : []),
148
+ [label('cause_evidence'), report.cause.evidence], [label('next'), report.cause.nextSteps],
149
+ [label('screenshots'), report.screenshots.map(item => `${item.file}: ${item.description || label('review_image')}`)],
128
150
  ...(['before', 'after', 'regression'].some(key => report.comparison[key].status !== 'not_run') || report.comparison.limitations.length ? ['before', 'after', 'regression'] : []).map(key => [
129
151
  `Test comparison: ${key}`, [
130
152
  `Status: ${report.comparison[key].status}`,
@@ -134,19 +156,17 @@ function renderMarkdown(report) {
134
156
  ...(report.comparison[key].evidence || []),
135
157
  ],
136
158
  ]),
137
- ...(report.comparison.limitations.length ? [['Test limitations', report.comparison.limitations]] : []),
138
- ];
159
+ ...(report.comparison.limitations.length ? [[label('limitations'), report.comparison.limitations]] : []),
160
+ [label('generator'), Object.entries(report.environment).map(([key, value]) => `${key}: ${value}`)],
161
+ ].filter(([, value]) => Array.isArray(value) ? value.length : Boolean(value));
139
162
  return [
140
163
  `# ${markdownText(report.title)}`, '',
141
- 'Local draft — not submitted. Review business data and private repository information before sharing.', '',
142
- `Fingerprint: ${report.fingerprint}`, `Observed occurrences: ${report.occurrenceCount || 'Unknown'}`, '',
143
- '## Environment', '',
144
- ...Object.entries({ ...report.environment, tool: report.tool, model: report.model })
145
- .map(([key, value]) => `- ${key}: ${markdownText(value)}`), '',
164
+ label('draft'), '',
146
165
  ...sections.flatMap(([heading, value]) => [
147
166
  `## ${heading}`, '',
148
- Array.isArray(value) ? (value.length ? value.map(item => `- ${markdownText(item)}`).join('\n') : 'Unknown / not verified') : markdownText(value), '',
167
+ Array.isArray(value) ? value.map(item => `- ${markdownText(item)}`).join('\n') : markdownText(value), '',
149
168
  ]),
169
+ `Fingerprint: ${report.fingerprint}`, `Observed occurrences: ${report.occurrenceCount || label('unknown')}`, '',
150
170
  ].join('\n');
151
171
  }
152
172
 
@@ -12,7 +12,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
12
12
  .oy-canvas-nav.oy-nav-mixed .oy-nav-sidebar { padding: var(--pod-nav-l-aside-padding, 8px); }
13
13
  .oy-nav-mixed .oy-nav-content { flex: 1; }
14
14
  .oy-nav-mixed .oy-nav-menu { display: flex; flex-direction: column; gap: var(--pod-nav-menu-gap, 8px); }
15
- @media (max-width: 767px) {
15
+ @container (max-width: 767px) {
16
16
  .oy-nav-mixed .oy-nav-groups { flex-basis: 100%; }
17
17
  .oy-nav-mixed .oy-nav-body { display: block; }
18
18
  .oy-nav-mixed .oy-nav-menu { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--pod-nav-sub-divider-color, var(--color-line1-1, #ddd)); }
@@ -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(--pod-page-bg-color, var(--color-white, #fff)); }
7
+ .oy-canvas-nav { container-type: inline-size; 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));
@@ -6,7 +6,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
6
6
  .oy-nav-side .oy-nav-content { flex: 1; }
7
7
  .oy-nav-side .oy-nav-brand { min-height: var(--pod-nav-platform-header-height, 48px); }
8
8
  .oy-nav-side .oy-nav-menu { display: flex; flex-direction: column; gap: var(--pod-nav-menu-gap, 8px); }
9
- @media (max-width: 767px) { .oy-nav-side { display: block; } }
9
+ @container (max-width: 767px) { .oy-nav-side { display: block; } }
10
10
  `}</style>
11
11
  <CanvasSidebar title={title} logo={logo} actions={actions} activeKey={activeKey} defaultWidth={sidebarWidth} defaultCollapsed={defaultCollapsed}>
12
12
  <nav aria-label="应用导航" className="oy-nav-menu">