@linhey/rhine-design 0.11.0 → 0.11.2

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 (87) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/README.md +9 -4
  4. package/components/Actions.tsx +1 -0
  5. package/components/AdvancedForms.tsx +13 -5
  6. package/components/AppShell.tsx +3 -2
  7. package/components/COMMON_COMPONENTS.md +5 -1
  8. package/components/Chat.tsx +160 -0
  9. package/components/Forms.tsx +66 -6
  10. package/components/Navigation.tsx +15 -4
  11. package/components/Overlays.tsx +37 -7
  12. package/components/README.md +11 -1
  13. package/components/chat.css +32 -0
  14. package/components/common.css +2 -0
  15. package/components/components.tsx +19 -9
  16. package/components/dialog-settings.css +54 -1
  17. package/components/extensions.css +4 -0
  18. package/components/index.ts +8 -3
  19. package/components/patterns.css +4 -0
  20. package/components/styles.css +11 -2
  21. package/components/theme-arknights.css +4 -2
  22. package/components/useFloatingScale.ts +7 -3
  23. package/docs/AGENT_CHAT.md +76 -0
  24. package/docs/AGENT_CHAT_VERIFICATION.md +42 -0
  25. package/docs/APPLICATION_COMPONENTS.md +4 -2
  26. package/docs/BUTTON_ACTIONS.md +32 -0
  27. package/docs/BUTTON_ACTIONS_VERIFICATION.md +14 -0
  28. package/docs/COMPACT_ACCOUNT_PATTERNS.md +16 -0
  29. package/docs/COMPACT_SELECTION_VERIFICATION.md +15 -0
  30. package/docs/FLOATING_OVERLAYS.md +20 -0
  31. package/docs/INTERACTION_PATTERNS.md +4 -0
  32. package/docs/SETTINGS_COMPOSITION_VERIFICATION.md +12 -0
  33. package/docs/SETTINGS_DIALOG.md +42 -4
  34. package/docs/SIDE_NAV_ACTIONS.md +9 -0
  35. package/docs/SINGLE_SEARCH.md +19 -0
  36. package/docs/SINGLE_SEARCH_VERIFICATION.md +15 -0
  37. package/docs/SUMMARY_TABS.md +7 -1
  38. package/docs/TRAY_USAGE.md +34 -0
  39. package/docs/TRAY_USAGE_VERIFICATION.md +13 -0
  40. package/docs/assets/chat/after.png +0 -0
  41. package/docs/assets/chat/arknights.png +0 -0
  42. package/docs/assets/chat/before.png +0 -0
  43. package/docs/assets/chat/neo-brutalism.png +0 -0
  44. package/package.json +1 -1
  45. package/preview/ButtonActionsGallery.tsx +25 -0
  46. package/preview/ChatGallery.tsx +98 -0
  47. package/preview/CompactSelectionGallery.tsx +76 -0
  48. package/preview/DenseFilterControlsGallery.tsx +5 -2
  49. package/preview/GalleryShell.tsx +2 -2
  50. package/preview/NavActionsGallery.tsx +24 -0
  51. package/preview/SettingsCompositionGallery.tsx +38 -0
  52. package/preview/SettingsDialogGallery.tsx +54 -14
  53. package/preview/SingleSearchGallery.tsx +50 -0
  54. package/preview/SummaryTabsGallery.tsx +7 -3
  55. package/preview/TrayUsageGallery.tsx +82 -0
  56. package/preview/chat-gallery.css +1 -0
  57. package/preview/compact-selection.css +9 -0
  58. package/preview/main.tsx +15 -1
  59. package/preview/packageInfo.ts +1 -1
  60. package/preview/settings-dialog-gallery.css +5 -0
  61. package/preview/tray-usage.css +4 -0
  62. package/scripts/check-button-actions.js +34 -0
  63. package/scripts/check-combobox-modal-focus.js +22 -0
  64. package/scripts/check-combobox-zoom.js +109 -0
  65. package/scripts/check-compact-selection.js +40 -0
  66. package/scripts/check-dialog-scroll-boundaries.js +41 -0
  67. package/scripts/check-multiselect-completion.js +46 -0
  68. package/scripts/check-nav-actions.js +41 -0
  69. package/scripts/check-settings-bounded.js +49 -0
  70. package/scripts/check-settings-composition.js +50 -0
  71. package/scripts/check-settings-interactions.js +93 -0
  72. package/scripts/check-settings-theme.js +1 -5
  73. package/scripts/check-single-search.js +43 -0
  74. package/scripts/check-tray-usage.js +68 -0
  75. package/scripts/prepare-settings-consumer.mjs +21 -0
  76. package/skills/rhine-design/SKILL.md +2 -0
  77. package/skills/rhine-design/references/API_MAP.md +11 -0
  78. package/skills/rhine-design/references/TASK_MAP.md +1 -0
  79. package/skills/rhine-design/references/integration.md +1 -1
  80. package/tests/Chat.test.tsx +84 -0
  81. package/tests/CompactSelection.test.tsx +47 -0
  82. package/tests/FormParity.test.tsx +104 -2
  83. package/tests/NavActions.test.tsx +29 -0
  84. package/tests/SettingsDialog.test.tsx +102 -9
  85. package/tests/SingleSearch.test.tsx +42 -0
  86. package/tests/SkillInstallation.test.tsx +77 -0
  87. package/tests/TrayUsage.test.tsx +70 -0
@@ -0,0 +1,32 @@
1
+ # 动作语义与按钮尺寸(#46,未发布)
2
+
3
+ variant表达操作层级及后果,尺寸表达场景强调程度。动作名和必要后果必须用文字说明,不只靠颜色。库不根据“退出”“删除”等字符串自动选色,不增加业务按钮组件。
4
+
5
+ | 动作 | 推荐 | 后果与确认规则 |
6
+ | --- | --- | --- |
7
+ | 保存、提交、继续等当前主任务 | primary | 同一区域通常一个主操作;输入/后果校验由宿主处理 |
8
+ | 导航、详情、更多 | secondary;低频轻量入口可quiet | 有明确点击边界时优先secondary,跳转仍用链接语义 |
9
+ | 取消、返回 | secondary或quiet | 与primary主操作区分;会丢失草稿时说明具体后果并按需确认 |
10
+ | 退出登录 | 独立会话设置区可primary+large;与其他主任务并列时secondary | 可重新登录,默认不视为不可逆删除,不因强调而改danger,不强制确认;未保存草稿或持续任务受影响时明确后果并按需确认 |
11
+ | 断开可重新连接的连接 | secondary;本节唯一主任务可primary | 说明是否中断当前工作;仅有不可恢复损失时升级危险层级 |
12
+ | 停止任务 | 通常secondary | 可以重启/保留结果时保持中性;会永久丢弃工作时danger并说明后果 |
13
+ | 重置 | secondary | 预览或可恢复重置保持中性;清空不可恢复配置时danger和明确确认 |
14
+ | 可恢复移除 | secondary | 提供撤销/恢复路径及期限;不要仅因“移除”自动染红 |
15
+ | 不可逆删除 | danger | 清楚写删除对象、影响和不可恢复性,使用ConfirmDialog等明确确认;只在服务器确认后反馈成功 |
16
+
17
+ 反例:只为醒目把退出登录统一设danger;看到danger就机械新增确认;所有按钮都primary;用颜色替代“不可恢复”的文字。真实展台 `#button-actions` 并排比较退出/断开/删除,全部使用合成操作,不改变真实会话或数据。
18
+
19
+ ## 标准与大尺寸
20
+
21
+ `Button.size?: 'standard' | 'large'` 默认standard,保留现有行为。此能力不在冻结0.11.0制品中。原有variant、busy、disabled、contentAlign及原生button属性不变。
22
+
23
+ | 尺寸 | 字号/行高/字重 | 最小高度 | 上下/左右内距 | 场景 |
24
+ | --- | --- | --- | --- | --- |
25
+ | standard | 14/22/label | 44px | 10px/16px | 表单、卡片动作和常规工具栏 |
26
+ | large | 16/24/label | 52px | 12px/24px | 独立设置动作、需要更醒目和易点击的主入口 |
27
+
28
+ 最小高度不是固定高度:主题边框可能自然增高,长标签按可用宽度换行,字号不缩小、内容不裁切。默认按内容宽度;独立窄表单需要铺满时在Button原生style设置width:'100%',不要把一组不相关动作全拉满。容器约束可用宽度,按钮max-width为100%,不会以全局固定宽度绑定业务场景。
29
+
30
+ busy沿用原内容占位与spinner,尺寸不跳变,保留焦点且拒绝重复点击;aria-disabled与事件防重同时生效。不同动作可用不同variant,但同组相同层级尺寸应保持配对。大尺寸也使用原主题hover/active/focus/disabled规则,不额外改颜色、圆角或动效;系统和手动减少动态沿用库规则。
31
+
32
+ 本轮可读性复核修正战术主题danger:深色文字沿用主题accent-ink,默认警戒红底,hover使用较深红底;默认/hover文字对比均高于4.5:1。其他危险状态语义和主题配色不变。
@@ -0,0 +1,14 @@
1
+ # #46 验收 · 2026-10-27(Asia/Shanghai)
2
+
3
+ 运行源码 `5eaf02d1debc71fd81bec15a6271597d314d440d`,基于main `0813833`。Node24 / React19.3.1 / Vite8.3.4;实际macOS Chrome155。合成操作,不修改真实会话/连接/记录。生产目录逐文件清单摘要SHA-256为 `f564d71b2c97c3ac841cb4f716175a4a29f47937a70b8290f2fd62e3745c5544`,清单及原始证据在忽略的 `output/playwright/`。
4
+
5
+ - PASS:42文件311测试,类型、主展台/两评审示例构建、CSS/SVG和包入口检查。最后焦点内环配对为CSS调整,之后类型/生产构建再次通过,最终head另由CI完整检查。
6
+ - PASS:三主题×400/520/880视口×100/200% CSS zoom,18组,54条动作比较;standard14/22/至少44、large16/24/至少52与内距配对,长标签自然增长且无容器溢出。default/disabled/busy保持尺寸;Enter/Space实际激活与防重、焦点保留;删除明确确认与Escape取消。
7
+ - PASS:独立浏览器设置页原生200%,CSS zoom=1、devicePixelRatio2、实际CSS视口400/520/880,九组27条动作比较;不以CSS zoom代替原生缩放。
8
+ - PASS:真实hover/active/focus-visible;三主题默认和hover文字背景对比均不低于4.5:1,焦点同时认可outline与战术主题内环。正常/系统减少动态/手动减少动态三主题共九组,标准/大尺寸长标签busy逐帧宽高不变。共享max-width后紧凑选择动作仍保持64/112实例宽度。
9
+ - PASS:截图比较退出、断开和删除的三主题/两尺寸;退出不自动danger或强制确认,删除文字与具体后果可读。原有主题形态与动效保留。
10
+ - FAIL(保留):战术主题danger原浅色文字/警戒红默认对比约2.77:1;改用accent-ink深色并调整hover红底后,默认与hover均通过。亮色主/危险按钮焦点内环改为深色,secondary/quiet保留浅色内环。
11
+ - FAIL(保留):首次脚本只检测outline,误将战术主题inset box-shadow焦点判失败;按实际主题实现修正测量并复验。未把该脚本失败当作组件失焦或抹除原记录。
12
+ - N/A:npm发布、消费升级、真实身份/删除/服务端请求;不修改冻结0.11.0制品。
13
+
14
+ 可复用检查:按钮可读性要测实际状态的前景/背景,不能根据danger语义推定对比合格;裁切主题可能使用内环,不能只检测outline。标准与大尺寸都需要长标签和busy逐帧验证,命中下限不是固定高度。
@@ -70,3 +70,19 @@
70
70
  正常880px橙趣样机的事实区高104px;这是当前样机测量,不把不同宿主截图作为严格前后对照。
71
71
 
72
72
  ![紧凑账号摘要与凭证操作,橙趣主题合成样机](assets/compact-account-neo.png)
73
+
74
+ ## 可选择对象与行内字段(#41,未发布)
75
+
76
+ 本节扩展只读摘要,真实组合为 `#compact-selection`。卡片左侧集中名称、类型和额度,分组标签与选择入口同一行;右侧详情/主动作组整体居中。参考12px内距、4px信息间距只用于该展台,既不固定卡片高度,也不作为全库默认。长名称与窄窗自然增高。
77
+
78
+ 同组当前/非当前卡片预留相同的空状态位,非当前不渲染隐藏的“当前项”文本。只在目标行显示busy,其余保持已确认事实;宿主同时用同步请求锁和禁用语义阻止重复写入,详情仍可访问。失败保留原选择,错误放在相关结果区,不插入改变卡片高度的新行。
79
+
80
+ 字段标签、选择值、箭头及相邻按钮按标准密度配对:标签14/22/label字重、值与动作14/22/label字重、箭头20px;标题16/24/heading字重保留层级,辅助类型12/18/400。标准控件44px最小命中,主题边框可使实际高度增长;标签按行盒视觉居中,不因次要就改为caption。展台含caption标签配标准按钮的反例,几何居中不能证明文字份量合理。本轮不新增32px视觉密度,避免未经验证的相邻命中重叠。
81
+
82
+ 行内复杂选择入口使用 `Button variant="secondary" contentAlign="between"`,名称左、箭头右,保留button/Dialog语义。`contentAlign`默认为center;between让现有内容span撑满可用宽度,无需宿主依赖内部选择器。原生style可以设置该实例宽度,长值换行,高度可增大;不覆盖主题边框/背景。只读/禁用、加载、未绑定与未知分别展示,不伪装原生select/combobox。
83
+
84
+ 同一动作列将实例宽度直接设置在实际Button,Menu包装不承担百分比宽度。展台详情64px、主动作/菜单112px只是对比参数;主动作primary,详情与更多secondary。不可用对象以状态和原因替代无意义禁用主按钮,保留详情入口。
85
+
86
+ 复杂选择采用“草稿预览+显式保存”:候选只更新预览,footer保存成功后更新确认值,关闭/Escape丢弃草稿;重开回到确认值。未改动不可保存;保存中同步防重、阻止关闭;失败保留确认值与草稿及原因,允许重试。成功以确认值回显和保存恢复不可用表达,不重复常驻“已保存”。即时提交是另一种契约,不给所有Select强加保存按钮。
87
+
88
+ Dialog的高度/footer契约见[稳定预览弹窗](SETTINGS_DIALOG.md#稳定预览与底部操作区41未发布)。不以填充假记录保持高度。后台刷新、数据时间和错误/过期语义仍由宿主保存,删除重复标题或机制说明不意味着删除这些事实。
@@ -0,0 +1,15 @@
1
+ # #41 验收 · 2026-10-27(Asia/Shanghai)
2
+
3
+ 最终运行候选 `90bd0c64006af97292e5331b92ba1cc37eacc5e7`,基于已含#42/#43/#45的main `85b8f2c`。Node24、React19.3.1、Vite8.3.4;浏览器实际UA为macOS Chrome155。合成数据,原始日志/截图/逐帧样本与生产文件摘要保存在忽略的 `output/playwright/`。
4
+
5
+ - PASS:42文件311测试、类型、主展台与两评审示例生产构建、CSS/SVG和包入口检查。保留主线Button原有aria-disabled防重,不覆盖其他任务修正。
6
+ - PASS:生产三主题、400/520/880视口与局部容器、100/200% CSS zoom共18组:当前/非当前等高,普通/菜单动作64/112实例宽度,长名称自然换行;真实requestAnimationFrame采样目标busy与状态切换全程,卡片高度不跳动,Tab保持原选中。
7
+ - PASS:Dialog两/四/末页一条、空组、加载、错误、候选预览、保存中/成功/失败全程逐帧采样,外框高度和Y位置保持;footer实际点击可达,Escape在保存中被拒绝,确认后保存不可用,无重复成功文案;关闭丢草稿、重开确认值与关闭归焦通过。
8
+ - PASS:独立Chrome配置通过设置页原生200%网页缩放,九组三主题/400/520/880 CSS视口;CSS zoom=1、devicePixelRatio=2,外部设置宽度为800/1040/1760,页面innerWidth实际为400/520/880。重复上述卡片、Dialog/保存、Tabs行为。
9
+ - PASS:另外九组正常/系统减少动态/手动减少动态,未选择/未绑定/未知/只读/加载,预览加载取消旧计时器,滚动后保存可达;单/双行摘要×content/fill,Home/End与状态变化不切页。宿主定时器在卸载清理,保存锁同步拒绝重复提交。
10
+ - PASS:实际截图比较标准14/22标签和值、16/24标题、相邻动作、caption反例;主题背景/边框不被宿主覆盖。状态位为空元素,不渲染隐藏的当前项文字;44px命中保持。
11
+ - FAIL(保留):首次split使用100vw,200% CSS zoom下footer超视口;改用可用容器百分比后实际点击和边界通过。橙趣状态徽标比空位高1px,改为足够的稳定状态位后全程通过。一次复验从about:blank取得null origin导致脚本失败,显式载入展台后重跑;这不是组件错误。
12
+ - FAIL(保留):首次卸载测试把Radix自身延后清理计时器计为宿主泄漏,改为明确检查宿主保存timer被clear;findLastIndex不受当前TS lib支持,改用findIndex。两项修正后完整检查通过。
13
+ - N/A:服务端请求、真实业务权限、npm发布和消费产品升级。height/footer限定Dialog,不扩展Drawer;实例560px、64/112px不成为全库默认。
14
+
15
+ 可复用边界:CSS zoom下100vw与固定元素可用宽度不可混用;稳定卡片状态位必须按三主题实际徽标高度检查,不能只量默认主题。原生网页缩放和CSS zoom分别留证;静态截图不代替逐帧状态切换。
@@ -12,4 +12,24 @@ Popover、Menu、Tooltip、StatusTimeline 与 TimelineLegend 的 Portal 保留
12
12
 
13
13
  真实展台 `#overlay-bounds` 使用独立 iframe,提供三主题、400/520/880px、100%/200%、文档/主题根/正文、四角锚点和四个优先方向。账号长文本、日期字段、取消/应用、菜单末项均为合成数据。
14
14
 
15
+ 0.11.1 补丁:Combobox/RemoteMultiSelect也接入同一useFloatingScale,修正0.11.0直接使用Radix Content遗漏坐标归一化的问题。`#compact-filters`增加弹窗内多选,核对400/520/880px与100/200% CSS zoom时列表边界、鼠标选择及Escape。旧0.11.0不包含此修复;产品查询、分页和取消仍由宿主负责。追踪 #16 / #40。回归脚本 `scripts/check-combobox-zoom.js` 使用当前 Playwright 页面 origin,需先打开该版本的构建展台;结果必须 `pass: true`,不能只检查 CLI 退出码。CSS zoom 证据不外推浏览器原生缩放。
16
+
15
17
  浏览器回归入口为 `scripts/check-overlay-bounds.js`,使用 Playwright CLI 的 `run-code --filename`。先运行 `npm run build && npx vite preview --host 127.0.0.1 --port 18480 --strictPort`;脚本固定使用 18480,并非 `npm run dev` 的默认端口。检查实际浮层和操作边界、内部滚动后的真实点击、焦点归还,以及打开期间动态缩放的输入节点与草稿保持;仅 `document.scrollWidth` 无法证明浮层没有越界。
18
+
19
+ 0.11.1 同时保留多选反选、明确完成操作和焦点归还。候选打开期间使用 Radix 的 `updatePositionStrategy="always"` 跟随锚点矩形变化:底层按帧比较矩形,有变化才重算,卸载时清理。此设置用于动态 CSS zoom;默认策略在 WebKit 的200%→100%检查出现 ResizeObserver 告警,改为该策略后定向三主题12组清零。一次将共享hook的ResizeObserver延后到rAF的试验无效,未保留该修改;不把告警无条件过滤掉。
20
+
21
+ 验证记录(2026-10-27):维护基线292项测试与全量构建/包清单通过。main 首次并行315项中中子星异步初始化用例一次未读到预期标签(loading),此前312项通过;保留首次日志,另以单worker复验,不据此推定根因。WebKit旧候选的36组边界/交互通过但有ResizeObserver告警,rAF试改无效并撤回;原0.11.0的400px动态缩放对照确认浮层越界,未把旧失败算作新版本验收。
22
+
23
+ 首个候选(发布前被标签×修复取代):Chromium 与 WebKit 各36组三主题×400/520/880px×100%/200% CSS zoom×页内/弹窗场景通过,包含打开时动态缩放、鼠标/键盘反选、空结果、长列表完成按钮、焦点/节点/草稿保留、打开/关闭两态清空后不展开,pageerror均0。main单worker315项、类型/全部构建/包清单复验通过;首次异步用例失败仍保留,未修改该用例或推定失败根因。浏览器回归使用 `scripts/check-combobox-zoom.js`,额外完成行为脚本为 `scripts/check-multiselect-completion.js`。
24
+
25
+ 标签移除收口:首个制品候选在发布前发现标签×也会因归焦重新展开,因此撤出发布,原制品与记录保留。RemoteMultiSelect 统一归焦策略:标签移除/清空收起,输入框空查询Backspace保留原状态;新单测覆盖原开/关两态、剩余值、查询、焦点与不提交。最终维护基线296项、main319项全量检查通过;发布制品和消费项目以最终registry摘要及#40回读为准。
26
+
27
+ 完成按钮的Tab路径由#57补齐:输入Tab进入、Shift+Tab返回、继续Tab沿输入框原后继前进;嵌套WebKit关闭时如停在Radix焦点保护节点才归焦恢复,普通外点关闭不抢焦点。浏览器断言按本机原生Tab顺序比对,不能假定各平台都将标签按钮纳入原生Tab。Combobox保留240px内容高度上限并按视口缩放约束,不把Radix可用高度变量反馈到候选列表尺寸;其他浮层默认策略保持原样。
28
+
29
+ 多选的标签和清空按钮属于同一交互区域:pointerdown/焦点转移不提前销毁浮层,等按钮click完成后收起。否则窄屏缩放时滚动布局可能在按下与抬起之间移动,让click落在公共祖先而丢失清空动作;浏览器用真实指针路径验证。
30
+
31
+ ## 可选可见Popover头部(#56,未发布)
32
+
33
+ `showHeader={false}` 省略可见标题和关闭按钮整行,但必填title继续命名浮层;默认true兼容现有内容。保持图标切换、外点/Escape关闭和键盘归焦。账号与用量的组合、稳定反馈及宿主状态职责见[托盘用量](TRAY_USAGE.md)。
34
+
35
+ 快速键盘退出嵌套下拉时,Radix在异步卸载回调后恢复父FocusScope;这一间隙用临时focusin保护维持最近的Modal内焦点,父约束恢复即清理,组件卸载也清理。快速连续键可能保持当前内部焦点,但不能落到背景;正常Tab循环随后恢复。check-combobox-modal-focus.js先断言即时包含关系,再单独检查恢复后的循环,不用预先延时掩盖失败。
@@ -149,3 +149,7 @@ function submit(event: FormEvent) {
149
149
  6. 分别检查内容刚好装下、仅溢出少量、明显超出窗口三种高度;确认滚动归属,末条详情不裁切、不遮挡分页。间距同时测可见边界和命中区,不只读gap配置。
150
150
 
151
151
  参考来源与取舍见 [GitHub 调研](./REFERENCE_RESEARCH.md)。
152
+
153
+ 紧凑选择卡片与草稿预览+显式保存见[紧凑组合](COMPACT_ACCOUNT_PATTERNS.md),其标准14/22密度与64/112实例动作宽度不应外推全库默认。
154
+
155
+ 退出登录/断开/停止/取消/重置/移除/删除的variant、确认和尺寸选择见[动作语义矩阵](BUTTON_ACTIONS.md);不要只因醒目就使用danger。
@@ -0,0 +1,12 @@
1
+ # 设置页组合验收(#53,未发布)
2
+
3
+ 2026-10-27,Node 24 / macOS / Chrome,合成更新数据;生产源码见本记录所属PR的最终head。生产入口 `?fixture=settings-composition`,脚本 `scripts/check-settings-composition.js` 从当前页面取origin。
4
+
5
+ - PASS:`npm run check`,42文件/319测试、类型、生产构建、评审示例及包清单。
6
+ - PASS:三主题 × 400/520/880 × 100%/200% CSS zoom,共18组;长文、四种更新状态、状态/按钮右边缘、隐藏包装折叠、有动作重新显示、侧栏键盘收起/恢复与焦点、窄窗Drawer/Escape及重置。
7
+ - PASS:隔离Chrome profile的网页缩放设置200%,CSS zoom保持1;400/520/880物理视口分别读回200/260/440 CSS px,三主题9组同样断言通过。脚本当前URL加`native=1`运行该矩阵;须先在浏览器设置原生200%,该参数本身不改变缩放。
8
+ - PASS:系统与手动减少动态下检查中spinner计算动画时长均0s;仍显示检查状态。页面异常0。
9
+ - FAIL(保留):首次脚本使用CLI沙箱不存在的URL构造器;改用字符串取origin后重跑。服务未启动时一次连接拒绝;非持久浏览器访问设置页关闭,切换独立持久profile后可设置并读回原生缩放。这些不算组件通过,也不据此推断其他环境。
10
+ - N/A:真实更新服务、原生桌面导航、npm发布与消费产品升级。
11
+
12
+ 生产主JS SHA-256 `948e14150240d83e06da598224ef2617e4d7c82002e426c3451a0d0c2486205f`;主CSS `355db57d7b8ece745fc6ced6ea2d2fda85afae3bb05e1008abd56c6b7000a4fc`。原始日志和截图保留在本地任务证据,不公开本机路径。
@@ -1,9 +1,12 @@
1
1
  # 设置导航 Dialog
2
2
 
3
- 自 0.8.0 提供,对应 #18。默认 Dialog、Drawer 和 `sidePanel` 用法保持兼容。
3
+ `navigation` 自 0.8.0 提供,对应 #18。下例的 `height/footer`、可省略 `description` 类型及固定头尾修复随 **0.11.2** 发布;使用前固定 `@linhey/rhine-design@0.11.2`,不要据未固定的展台源码推断安装包能力。默认 Dialog、Drawer 和 `sidePanel` 用法保持兼容。
4
4
 
5
5
  ```tsx
6
- <Dialog title="设置" description="管理工作区偏好" trigger={<Button>设置</Button>}
6
+ <Dialog title="设置" height={640} open={open} onOpenChange={setOpen} trigger={<Button>设置</Button>}
7
+ footer={<><Button variant="secondary" onClick={undo}>撤销</Button>
8
+ <Button variant="secondary" onClick={() => setOpen(false)}>关闭</Button>
9
+ <Button busy={saving} onClick={save}>保存</Button></>}
7
10
  navigation={<SideNav aria-label="设置导航" currentId={section} groups={groups}
8
11
  onNavigate={(item, event) => { event.preventDefault(); setSection(item.id); }} />}>
9
12
  <SettingsSection section={section} />
@@ -12,10 +15,45 @@
12
15
 
13
16
  `navigation?: ReactNode` 专门承载左侧导航,`children` 为任务内容;不与辅助 `sidePanel` 同时使用,两者同时提供会抛出明确错误。Drawer 不提供此插槽。
14
17
 
15
- 标题、说明和关闭动作在同一头部。内容实际宽度达到 560 CSS px 时分成约 1:3 两栏,否则导航在上、任务内容在下;容器查询会响应宿主宽度和 CSS zoom,而不是仅判断窗口宽度。长内容在弹窗内滚动,保留原字体和点击目标。
18
+ 标题和关闭动作在同一头部。`description?: ReactNode` 可真正省略(包括 undefined/null/false/空串);此时不生成说明节点、空白占位或 `aria-describedby`。仅在帮助决策时提供说明;不重复字段/导航已经表达的操作,也不加无决策必要的全局人数或规则摘要。错误、保存中、未保存和冲突反馈必须保留。内容实际宽度达到 560 CSS px 时分成约 1:3 两栏,否则导航在上、任务内容在下;容器查询会响应宿主宽度和 CSS zoom,而不是仅判断窗口宽度。自然高度模式的长内容在弹窗内滚动;显式有界模式如下。settings 根选择器的优先级高于基础 Dialog,不依赖生产 CSS 发出顺序;不让消费者用私有类覆盖宽度。保留原字体和点击目标。
16
19
 
17
20
  应用拥有栏目、数据、权限、请求、草稿、保存和取消。库不改变 URL、不自动保存、也不会因为异步数据变化而关闭弹窗。需要保留切换栏目期间的草稿时,将状态提升到 Dialog 的宿主;关闭后的重置也由宿主明确执行。不要给 Dialog 或 ThemeProvider 使用随栏目/主题变化的 key。
18
21
 
19
22
  嵌套编辑继续使用 Radix 的焦点陷阱和焦点归还;Dialog 在布局阶段按焦点所属弹窗接管 Escape,避免快速打开内层时旧监听误关外层:先关闭内层并归还其触发器,外层仍保持打开。`onCloseAutoFocus` 仍可处理触发器已被宿主删除的场景。
20
23
 
21
- 展台 `#settings-dialog` 包含账号草稿/异步保存/取消、长用户列表、嵌套编辑,以及项目加载/错误/空态。浏览器回归在 `scripts/check-settings-theme.js`,覆盖三主题、400/520/880/1440px、100%/200% CSS zoom、实际按钮边界、嵌套 Escape、焦点与宿主查询/URL 保持。
24
+ 展台 `#settings-dialog` / `?fixture=settings-dialog` 包含账号草稿、异步保存、撤销、失败/冲突、30行成员、异步栏目、长导航/长操作文案、嵌套搜索多选和编辑 Dialog,以及项目加载/错误/空态。短动作保留完整可访问名称;主题切换保持草稿与节点。宿主例允许保存中关闭,迟到成功只更新已提交值,不覆盖重新打开后的新编辑;计时器在卸载清理。
25
+
26
+ ## 稳定预览与底部操作区(#41,0.11.2)
27
+
28
+ `Dialog.height?: CSSProperties['height']` 显式指定实例期望高度,数字按CSS px解释,默认不指定、保持原内容自适应行为;实际高度受可用视口减32px限制。`footer?: ReactNode` 是独立操作区,不参与内容滚动;单独提供footer也启用有界滚动,不强迫固定高度。两者不能作为全库560px默认,560仅为展台比较参数。
29
+
30
+ 提供height或footer时,主体使用有界内部滚动。普通单栏和 navigation 模式的标题/关闭行位于正文滚动区外,footer固定在 Dialog 内部底部;均不使用绑定浏览器的 position:fixed。navigation 宽屏左右各自滚动,窄容器上下排列仍分别滚动:导航有界,正文保留剩余空间。正文 region可聚焦并用键盘滚动;无导航的有界纯文本正文也有唯一的命名region Tab入口。辅助 sidePanel 沿用 #41 的宽屏独立双栏滚动、窄屏共用滚动区及 sticky 标题策略,不把设置布局强加给它。根保持主题、焦点陷阱、Escape与onCloseAutoFocus,宿主仍拥有草稿和保存状态。短内容、末页、空组、未选择、预览加载/失败、保存中/成功/失败不改变同一视口的外框高度/位置;视口或缩放改变时可响应调整。
31
+
32
+ `#compact-selection` 显式保存组合保留预览侧栏,底部保存不与说明或内容混排。保存期间宿主通过受控onOpenChange拒绝关闭,使用请求锁防重,并在卸载清理计时器。height/footer本轮限定Dialog;Drawer与既有navigation/sidePanel默认行为保持。
33
+
34
+ ## 设置页组合(#53,未发布)
35
+
36
+ `Icon name="settings"` 使用20px滑杆几何,沿用1.5px线宽、currentColor、装饰性SVG;SideNav通过文字命名,独立IconButton必须提供label。省略号仍只表示更多操作。
37
+
38
+ 关于卡片用 `Inline` 可换行排列产品身份和更新 `Stack`:两列均设 `minWidth:0`,更新列 `alignItems:'flex-end'`、`textAlign:'end'`,长文 `overflowWrap:'anywhere'`。状态与按钮共享逻辑末端;检查中保留busy反馈,失败保留重试。更新请求和安装由宿主负责,不新增产品更新组件。
39
+
40
+ 仅含操作的 `AppShell` 显式设置 `collapseEmptyContentHeader`(默认false,保留既有头部),`contentHeader` 直接传Fragment,里面放 `AppShellToggle placement="content"` 与按实际权限/状态条件渲染的动作,不套空Inline/nav。共享样式在所有直接子元素均有hidden或没有元素时收起整个头部(含背景、内距和分隔线);不能以React节点或DOM节点存在代替可见动作。这个有限规则不检测任意深层包装或display:none;自定义组合须条件省略整个包装,文字标题应放入元素。隐藏的恢复控件保持挂载;侧栏收起或窄窗时恢复控件出现,头部随之恢复,焦点沿用AppShell。
41
+
42
+ 真实展台 `#settings-composition` / `?fixture=settings-composition` 包含长文、更新四态、可选顶部动作、侧栏与重置。此能力未进入npm版本,不修改0.11.1冻结制品。
43
+
44
+
45
+ ## 操作区与短屏(#18,0.11.2)
46
+
47
+ `footer` 默认是右对齐、可换行的 flex 操作区;直接传按钮或 Fragment,避免再套整宽、不换行的包装。设置展台使用「撤销 / 关闭 / 保存」,通过 aria-label 保留完整动作名。窄容器收紧间距和横向按钮内距,字号不变、目标至少44×44 CSS px;长可见文案可由宿主在窄容器替换为明确短标签。操作区预留焦点环/主题硬阴影空间。
48
+
49
+ 期望高度受视口限制;导航占用有界空间,不随正文滚动。极端长操作区自身可滚(指定高度时最高40%;自动高度时最高可用视口40%,按祖先CSS zoom换算),不采用裁剪遮盖按钮;调用方仍须对实际动作数量/文案/短屏测试,不能承诺任意内容在任意小窗口同时完整可见。规范矩阵400px、200% CSS zoom、648px短屏下,示例三个动作可同时完整显示且正文非零。基础短 Dialog不启用固定高度,Drawer没有height/footer/navigation。
50
+
51
+ ## 生产与固定制品回归
52
+
53
+ - `scripts/check-settings-bounded.js`:57组(三主题×400/520/880/1280×100/200%×900/648高,加390×680、889×480、1440×900的100%),量测正文和长导航滚动前后标题/关闭/footer及按钮rect、末项可达、非零正文、44px目标、横向/操作区溢出和列数。
54
+ - `scripts/check-settings-interactions.js`:24组主题/宽度/缩放的保存、撤销、失败/冲突、异步栏目、嵌套搜索多选/编辑、快速Tab/ShiftTab、Escape、归焦、宿主URL/查询/草稿及主题节点保留;系统/手动减少动态独立检查。
55
+ - `scripts/prepare-settings-consumer.mjs <tarball> <new-directory> [--late-css]`:从固定tarball全新安装、构建合成消费者。后置公共 `styles.css?late` 真实构建可重现旧基线480px覆盖,修复候选须在同一条件通过;普通导入构建也要检查。启动该目录的vite preview后,前两脚本使用当前页面origin。
56
+
57
+ 以上是CSS zoom与Chromium行为,不外推原生网页缩放、其他引擎或真实业务服务。正式版本发布、原消费者删除局部适配并复验仍是 #18 关闭条件;0.11.2 已包含本节设置 Dialog 修复;更换版本仍须重新固定制品并执行消费验证。
58
+
59
+ - `scripts/check-dialog-scroll-boundaries.js`:三主题×400/880×100/200%、648px短屏,验证无height的长footer有界且末项可达,以及纯文本有界正文的Tab/End滚动。自动高度footer监听视口及祖先zoom/主题变化,卸载时清理。
@@ -0,0 +1,9 @@
1
+ # 导航行操作(#45,未发布)
2
+
3
+ `NavItem.actions?: readonly NavAction[]` 为可选命令列表,复用 Menu 和常显 IconButton。每项提供 id、label、disabled、danger 和 `onSelect(trigger: HTMLButtonElement)`。缺省或空数组不渲染按钮、包装或空白;调用方过滤不可见动作,库不判断身份或权限。
4
+
5
+ 链接和菜单触发器处于同一 li 的兄弟分支,没有嵌套 button/link。长标题自然换行,尾部按钮保持44px命中;更多默认可见,不依赖 hover。导航与菜单分别进入 Tab 顺序,Enter/Space 打开菜单,方向键选择,Escape 关闭;菜单关闭由 Radix 归焦。
6
+
7
+ 菜单项打开受控 Dialog 时,宿主保存 `onSelect` 收到的 trigger。Dialog 的 `onCloseAutoFocus` 中 preventDefault,再聚焦该按钮。若宿主删除原行,需改为已存在的导航或内容入口。详情使用 Dialog 语义,不将路由挂在更多按钮上。真实组合见 `#nav-actions` / `preview/NavActionsGallery.tsx`。
8
+
9
+ 此能力未进入冻结的0.11.0制品;合并源码不代表发布。消费方继续固定已验证版本。
@@ -0,0 +1,19 @@
1
+ # 单输入搜索(#42,未发布)
2
+
3
+ 只有一个查询条件时直接组合 TextField,不增加 Surface、重复可见标题、默认帮助段落、搜索按钮或常驻重置行。需要提交按钮时使用 SearchField;多条件筛选仍使用 ListFilterBar,不绕过它的层级约束。
4
+
5
+ ```tsx
6
+ <TextField label="搜索条目" labelVisibility="hidden" type="search"
7
+ placeholder="搜索名称或关键词" value={query}
8
+ onChange={event => setQuery(event.target.value)} />
9
+ ```
10
+
11
+ `labelVisibility` 默认 `visible`,`hidden` 仅视觉隐藏关联 label。`label` 必须是非空文本,空白值会报错;placeholder 不是 accessible name。原生 input 属性继续透传,有效 aria-label / aria-labelledby 仍遵循原生优先级;空白 aria-label 不会遮蔽 label,宿主须确保 aria-labelledby 指向存在且非空的内容。`hint` / `error` 与外部 aria-describedby 合并,禁用和错误语义不变。`className` 仍落在字段容器,原生 style 落在 input。
12
+
13
+ 输入和 placeholder 采用正文 token(14px / 22px / 400),44px 最小命中;大尺寸可通过原生 style 的 minHeight、padding 配置,不能用标题字号制造“大搜索”。保持主题提供的背景、边框和焦点,不覆盖内部 CSS。
14
+
15
+ 默认只显示输入。查询中、无结果、错误及必要摘要放在关联结果区,使用稳定的 aria-live 容器、按需 aria-describedby;不能删掉失败反馈。没有查询时不播报重复说明。
16
+
17
+ 数据请求、防抖、IME、取消、过期响应保护和清空策略属于宿主。`#single-search` 合成展台在 composition 期间不查询,提交组合输入后等待250ms,再模拟500ms响应;Escape在非组合输入时清空并保持输入焦点,原生搜索清空按 change 处理。更改查询、禁用或卸载取消旧定时器;真实服务应同时用 AbortSignal 和请求序号防护。TextField不隐式提交、请求或过滤。
18
+
19
+ 验收区分 DOM composition 事件回归与真实操作系统输入法;前者不能证明中文候选窗行为。CSS zoom 与浏览器原生缩放分别记录。展台只用合成数据。
@@ -0,0 +1,15 @@
1
+ # #42 候选验收 · 2026-10-27(Asia/Shanghai)
2
+
3
+ 初始运行候选 `7607c5c`,基于 `fa2e6bc`;Node 24 / React 19.3.1 / Vite 8.3.4,macOS Chromium 155。所有内容为合成数据。证据位于忽略的 `output/playwright/`。
4
+
5
+ - PASS:38文件291测试;类型、主展台/两评审示例生产构建、CSS/SVG与包入口检查。
6
+ - PASS:最终生产目录,三主题 × 400/520/880局部容器 × 100/200% CSS zoom,共18组;14/22/400正文及placeholder、44px或46px主题命中、长查询、Escape清空/焦点、加载/无结果/错误/禁用、系统及手动减少动态。脚本 `scripts/check-single-search.js`。
7
+ - PASS:独立持久Chrome配置通过设置页选择原生200%,CSS zoom保持1、innerWidth600、devicePixelRatio4;三主题 × 三容器,共9组查询与清空/焦点/边界通过。并非每个容器都能在600 CSS px视口保持声明宽度,max-width按可用空间约束。
8
+ - PASS:Chromium CDP `Input.imeSetComposition` 与 `Input.insertText` 真实引擎输入路径,组合时不查询,提交“中文”后查询;定向测试另覆盖防抖、过期响应取消、禁用/卸载清理。
9
+ - PASS(用户人工验收):2026-10-27用户在最终本地展台实际使用系统中文输入法,确认选词期间不搜索、提交“中文”后出现结果、Escape清空且输入框保持焦点。此证据来自用户明确回复,区别于CDP引擎测试。
10
+ - FAIL(保留):首轮脚本误将Toolbar strong当heading,等待30秒超时;修正搜索框定位后18组PASS。一次隔离context不能打开Chrome设置页,改用独立持久profile后原生缩放PASS;未触碰其他会话浏览器。
11
+ - N/A:npm发布、真实服务和消费产品迁移。本候选不进入已冻结0.11.0制品;发布回合已结束,源码合并限制解除;npm登录不再阻塞本issue。
12
+
13
+ 后续对齐主线:在`791cf2c`重跑291项检查和18组生产矩阵PASS;在#45合并后再次合入最新main,待最终CI回读。系统输入法补证尝试:独立macOS Chrome148中通过CUA按键只能取得拉丁输入,未出现中文候选窗;输入菜单agent读取超时,未将这些尝试记为IME通过。后由用户在`be164aed16588b19ba20803d3d8459e7980f777a`构建的展台补齐真实系统输入法三项,全部PASS。
14
+
15
+ 最终集成候选`be164aed`基于main`2c2edaa`(含#43/#45),全量检查通过;本文后续提交仅更新人工证据,不改运行源码。浏览器构建制品对应该候选,数据全为合成。
@@ -35,7 +35,7 @@
35
35
  | 命中区域 | 最小高度44,自然增高 | 默认莱茵单行名称/状态、单行摘要70px(24+4+18+12×2);Badge或文字换行可增高 |
36
36
  | 底部线 | 普通1px,选中2px | 选中线覆盖该页签命中区宽度;状态徽标不替代选中线 |
37
37
 
38
- 带摘要的一组Tab等分可用宽度,均从顶部对齐;第一项与面板正文左缘对齐。容器保持至少必要的焦点外延空间,不加阴影、整块选中底色、额外卡片边框或圆角胶囊。
38
+ `layout="content"`(新默认)按内容宽度靠左排列,页签栏收缩到内容总宽度并受max-width约束;`layout="fill"`显式均分可用宽度,两种布局均从顶部对齐。status/description只决定文字结构,不决定铺满;第一项与面板正文左缘对齐。容器保持至少必要的焦点外延空间,不加阴影、整块选中底色、额外卡片边框或圆角胶囊。
39
39
 
40
40
  ## 状态、主题与动效
41
41
 
@@ -75,3 +75,9 @@
75
75
  独立展台 `/#summary-tabs` 提供局部320/520/自适应宽度、正常/加载/错误/禁用/长文本、尺寸说明和可编辑面板草稿。保留 `/#selection` 普通Tabs作对照;两个变体不能共用临时业务CSS。包内入口见[组件契约](../components/README.md),间距含义见[间距规范](../components/SPACING.md)。
76
76
 
77
77
  验收同时检查计算样式与实际像素:12/8/4/16/24、名称14/22/600、摘要12/18/400;三主题长文本、键盘、禁用跳过、焦点保留、隐藏面板、状态变化、系统/手动减少动态。局部无横向溢出不替代视觉检查。正式消费需固定包含此能力的版本或提交;本地候选展台不代表能力已经发布。
78
+
79
+ ## 单行摘要与显式布局(#41,未发布)
80
+
81
+ 身份已在内容区可读时,只传label和只读status,不传description,即名称+短状态的单行组合。`layout="content" | "fill"`与摘要存在与否独立,默认content;0.11.0摘要曾隐式铺满,升级后需维持原展示时显式传fill。文字过长自然换行,页签组可换行,不缩小字号、不裁剪,内容面板始终使用可用宽度。选中下划线跟随对应按钮,content下就是该页签内容宽度。
82
+
83
+ 真实展台 `#summary-tabs` 可切换单/双行与两种布局,`#compact-selection` 展示单行摘要与完整身份区。异步身份/状态更新不改变value或焦点,方向/Home/End及隐藏面板保留规则不变。
@@ -0,0 +1,34 @@
1
+ # 托盘用量与账号组合(#56,未发布)
2
+
3
+ 本规范采用issue #56在2026-10-27的两条最新评论,取代旧“管理/查看用量/打开应用为次级动作、切换留在主窗口”的建议;最新偏好同时移除可见标题/关闭整行。本轮不改版本,不代表npm 0.11.1冻结制品或消费应用已经包含新能力。
4
+
5
+ ## 信息顺序与动作
6
+
7
+ 顶部从当前Agent的明确账号Select开始,显示完整账号名、本周期剩余额度及周期/时区。高频账号切换在上下文内完成;纯数值和名称不伪装成按钮。底部保留今日预估消费及币种、请求数、待计价数和更新时间。不放管理、查看用量或打开应用按钮。共享成员各列周期剩余,不累加百分比;多个连接共用同一上游时明确其共享关系。
8
+
9
+ 未知额度显示未知/不可用,不传Progress的0值冒充。加载提供Skeleton,低额度有文字说明;未匹配连接不猜测余额。刷新失败可保留可信缓存但明确上次数据和时间;账号目录过期禁用切换。未登录仅显示登录提示,移除旧账号、用量和共享成员内容。
10
+
11
+ ## 通用Popover API
12
+
13
+ `Popover.showHeader?: boolean` 默认true,保留已有可见标题及关闭按钮。设false只移除该行;必填、非空的`title`仍作为dialog的aria-label。不要用空title消除重复标题。触发器必须是具名、可聚焦并接收ref的元素,如IconButton;图标切换、外点和Escape沿用Radix,键盘关闭归还触发器,外点不抢走用户点击的焦点。隐藏头部适用于内容已有明确起点的简洁详情,复杂任务继续使用有可见退出入口的Dialog。
14
+
15
+ ```tsx
16
+ <Popover title="账号与用量概览" showHeader={false} open={open} onOpenChange={setOpen}
17
+ trigger={<IconButton icon="chart" label="切换用量面板" />}>
18
+ <Stack>{/* Select / Metric / Progress / Notice / Skeleton */}</Stack>
19
+ </Popover>
20
+ ```
21
+
22
+ 只使用公共Popover的边界、滚动、缩放与主题能力,不覆盖其内部选择器、不另造产品托盘组件。浏览器浮层不是原生系统菜单或真实桌面托盘。
23
+
24
+ ## 宿主切换契约
25
+
26
+ 宿主保有已确认账号和待确认目标两个状态。立即锁住请求入口并禁用当前账号选择;请求完成前保留已确认账号和额度。只有确认成功才同时替换账号与额度;失败保留原值、允许同一目标重试。其他Agent和今日统计不跟随目标账号切换重置。加载、目录过期、连接处理中、未匹配和未登录不可切换;同步请求锁防止同一事件批次重复提交。
27
+
28
+ 为反馈保留稳定空间。示例让空闲、处理、失败加重试、成功四种反馈共同参与同一网格的固有尺寸,非当前状态visibility:hidden且aria-hidden,不能聚焦;无需截断长错误或写死单行高度。状态切换保持同一视口下反馈区和内容高度;视口、文字长度变化可自然重排。
29
+
30
+ 场景重置、身份失效、切换目录和卸载时取消模拟任务,并用递增请求标识拒绝迟到结果。实际请求取消、鉴权、目录校验、账号与额度的一致性由产品宿主实现;库不请求网络、不承诺业务事务。示例首次失败后可重试成功,仅为可复现测试。
31
+
32
+ ## 展台
33
+
34
+ `#tray-usage` 和 `?fixture=tray-usage` 默认显示图标与展开面板;重置恢复初始合成数据。提供九种数据场景、长账号、首次切换失败,第二Agent和底部统计用于核对隔离。共享12成员使用Popover内部滚动。验收脚本 `scripts/check-tray-usage.js` 使用当前生产origin;`native=1`只选择原生缩放矩阵,运行前仍须在独立Chrome设置真实200%网页缩放。
@@ -0,0 +1,13 @@
1
+ # 托盘用量组合验收(#56,未发布)
2
+
3
+ 2026-10-27,Node24 / macOS / Chrome,合成账号与用量;源码绑定本记录所属PR最终head。入口 `?fixture=tray-usage`,回归脚本 `scripts/check-tray-usage.js` 从当前页面取生产origin。未连接账号服务或原生托盘。
4
+
5
+ - PASS:`npm run check`,43文件/323测试,类型、生产构建、评审示例与包内容检查。新增4项行为测试覆盖header默认兼容及可访问名称、重复提交/确认前原值/失败重试/数据隔离、禁用条件/重置和退出登录拒绝迟到结果、未知与缓存标记。
6
+ - PASS:生产Chrome三主题 × 400/520/880 × 100%/200% CSS zoom,18组;九种数据场景、长名称、共享12成员和末项/底部可达、未知无百分比进度、过期禁用和缓存标记、未登录无旧数据。
7
+ - PASS:切换前、处理中、失败、成功四阶段的反馈区高度与内容scrollHeight一致;确认前选择值仍为原账号,失败保留,重试成功,第二Agent/今日统计文本不变。图标切换、外点、Escape、Enter重开、焦点返回、重置通过。浮层无可见标题/关闭行,dialog名称仍非空;页面异常0。
8
+ - PASS:独立Chrome profile的真实网页缩放200%,CSS zoom=1,400/520/880读回200/260/440 CSS px,三主题9组通过。`native=1`只选择矩阵,运行前须手动设置真实浏览器缩放;不是CSS或设备缩放模拟。
9
+ - PASS:系统reduce与系统no-preference下的手动reduced分别测试;Popover、Skeleton和设置示例spinner的计算动画时长均0s。回归脚本手动阶段明确撤销系统reduce,避免混淆两条路径。此项也补充#53的独立手动减少动态证据。
10
+ - FAIL(保留):首轮点击被面板覆盖的外部场景控件超时;第二轮外点关闭等待超时,未确认组件根因;后续点击被面板遮挡的标题再次超时。最终先Escape关闭以切场景、待160ms进场结束后点击可见页面边缘,全部矩阵通过,不删除旧日志或把重试推定为组件修复。
11
+ - N/A:真实账号目录/权限/切换事务、生产计费、原生系统托盘、npm发布和消费应用升级。浏览器行为不外推原生能力。
12
+
13
+ 生产主JS SHA-256 `c4bc41938a91450005d390625c231fff9ad0d53e5b91dcfde807fee36b4deb58`;CSS `5fd6e5023f2891b183dedad7f16b61bde46b5d585ea611d3fe21e00cbfdbf3ce`。截图、各次原始失败、矩阵JSON与制品保留于本地任务证据,公开记录不包含机器路径。
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linhey/rhine-design",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -0,0 +1,25 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Button, ConfirmDialog, Inline, Select, Stack, Surface, Text, Toolbar } from '../components';
3
+
4
+ export function ButtonActionsGallery() {
5
+ const [width,setWidth]=useState('520'),[state,setState]=useState('normal');
6
+ const [busy,setBusy]=useState<string|null>(null),[result,setResult]=useState('');
7
+ const lock=useRef(false),timer=useRef<ReturnType<typeof setTimeout>|undefined>(undefined);
8
+ useEffect(()=>()=>clearTimeout(timer.current),[]);
9
+ const run=(id:string)=>{if(lock.current)return;lock.current=true;setBusy(id);setResult('');timer.current=setTimeout(()=>{setBusy(null);lock.current=false;setResult(`${id}:合成操作完成,未改变真实会话。`);},700)};
10
+ return <section id="button-actions"><Stack gap={6}>
11
+ <Toolbar title="动作语义与尺寸" description="层级和后果决定配色;大尺寸强调独立操作,不自动表示危险。">
12
+ <Select label="按钮容器宽度" value={width} onChange={e=>setWidth(e.target.value)}>{['400','520','880'].map(v=><option key={v}>{v}</option>)}</Select>
13
+ <Select label="按钮状态" value={state} onChange={e=>setState(e.target.value)}><option value="normal">正常</option><option value="disabled">禁用</option><option value="busy">处理中</option><option value="long">长标签</option></Select>
14
+ </Toolbar>
15
+ <div style={{width:`${width}px`,maxWidth:'100%'}} data-button-actions><Stack gap={6}>
16
+ {(['standard','large'] as const).map(size=><Surface key={size}><Stack gap={3}><Text as="h3" variant="heading">{size==='standard'?'标准尺寸':'大尺寸 · 独立设置动作'}</Text><Inline gap={3}>
17
+ <Button size={size} variant="primary" disabled={state==='disabled'} busy={state==='busy'||busy===`${size}退出登录`} onClick={()=>run(`${size}退出登录`)}>{state==='long'?'退出当前合成工作区并保留已确认的设置':'退出登录'}</Button>
18
+ <Button size={size} variant="secondary" disabled={state==='disabled'} busy={state==='busy'||busy===`${size}断开连接`} onClick={()=>run(`${size}断开连接`)}>{state==='long'?'断开当前连接并保留可重新连接的配置':'断开连接'}</Button>
19
+ <ConfirmDialog trigger={<Button size={size} variant="danger" disabled={state==='disabled'} busy={state==='busy'}>{state==='long'?'永久删除选中的合成记录且无法恢复':'删除记录'}</Button>} title="删除合成记录" description="这条合成记录将无法恢复。这里只演示确认,不删除真实数据。" confirmLabel="确认删除" danger onConfirm={()=>setResult('删除:合成确认完成,未删除真实数据。')} />
20
+ </Inline><Text variant="caption">{size==='standard'?'14/22,至少44px,横内距16px':'16/24,至少52px,横内距24px'}。退出/断开可恢复,不自动使用危险色;删除明确提示不可恢复。</Text></Stack></Surface>)}
21
+ <Surface><Stack gap={3}><Text as="h3" variant="heading">窄表单的独立全宽动作</Text><Button size="large" style={{width:'100%'}} onClick={()=>run('全宽退出')} busy={busy==='全宽退出'}>退出登录</Button><Text variant="caption">使用原生width,长文自然换行;不要给所有工具栏动作铺满宽度。</Text></Stack></Surface>
22
+ <Text role="status">{result}</Text>
23
+ </Stack></div>
24
+ </Stack></section>;
25
+ }
@@ -0,0 +1,98 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Badge, Button, ChatCard, ChatThread, Checkbox, Inline, Select, Stack, Surface, Text, Toolbar, type ChatEntry, type ChatStatus } from '../components';
3
+ import './chat-gallery.css';
4
+
5
+ const members = [{ id: 'a', name: '成员 A' }, { id: 'b', name: '成员 B' }, { id: 'c', name: '成员 C' }];
6
+ const paragraph = '先保留原始证据,再比较两个版本的差异。长段落应完整换行,错误原因和未知状态不会被折叠隐藏;消息、引用与结构化结果各有清晰归属。';
7
+ function message(id: string, member: number, minute: number, content: ChatEntry['content'], day = '今天 · 10 月 27 日'): ChatEntry {
8
+ return { id, kind: 'message', member: members[member], day, time: `10:${String(minute).padStart(2, '0')}`, dateTime: `2026-10-${day.startsWith('昨天') ? '26' : '27'}T10:${String(minute).padStart(2, '0')}:00+08:00`, content };
9
+ }
10
+ const history = [
11
+ message('history-a', 0, 10, <p>这是更早的合成记录。资料清单包含版本说明、公开摘要和未确认项。</p>, '昨天 · 10 月 26 日'),
12
+ message('history-b', 1, 12, <><p>{paragraph}</p><p>{paragraph}</p><pre><code>{'const evidence = { source: "sample", verified: false };\n// 未确认不等于零,也不等于完成。'}</code></pre></>, '昨天 · 10 月 26 日'),
13
+ ];
14
+ const states: [ChatStatus, string][] = [
15
+ ['pending', '资料核对请求,等待成员回应。'], ['accepted', '成员 B 已接受;尚未执行。'],
16
+ ['declined', '另一个可选请求被成员 C 拒绝,本分支结束。'], ['blocked', '等待公开资料补全,不显示无限加载。'],
17
+ ['running', '正在比较已到达的资料。'], ['partial', '已找到两处差异,结论仍不完整。'],
18
+ ['failed', '来源样例暂不可用。保留部分结果,可由有权限的成员稍后重试。'],
19
+ ['unknown', '来源没有提供最终状态,不能推断成功。'], ['cancelled', '成员停止了该分支,已有内容保留。'],
20
+ ['expired', '请求超过有效期,后续操作由宿主决定。'], ['succeeded', '独立的摘要归档已确认完成。'],
21
+ ];
22
+
23
+ /** A host example using public exports only; all timers and transitions are synthetic host state. */
24
+ export function ChatGallery() {
25
+ const [scene, setScene] = useState('pair');
26
+ const [layout, setLayout] = useState('after');
27
+ const [width, setWidth] = useState('880');
28
+ const [request, setRequest] = useState<ChatStatus>('pending');
29
+ const [older, setOlder] = useState(false);
30
+ const [loading, setLoading] = useState(false);
31
+ const [historyError, setHistoryError] = useState('');
32
+ const [failHistory, setFailHistory] = useState(false);
33
+ const [streamState, setStreamState] = useState<ChatStatus>('unknown');
34
+ const [chunks, setChunks] = useState(0);
35
+ const [arrivals, setArrivals] = useState(0);
36
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
37
+ const running = streamState === 'running';
38
+ useEffect(() => {
39
+ if (!running) return;
40
+ const interval = setInterval(() => setChunks(value => value + 1), 220);
41
+ return () => clearInterval(interval);
42
+ }, [running]);
43
+ useEffect(() => { if (chunks >= 24 && running) setStreamState('partial'); }, [chunks, running]);
44
+ useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
45
+ function resetScene(next: string) {
46
+ if (timer.current) clearTimeout(timer.current);
47
+ setScene(next); setOlder(false); setLoading(false); setHistoryError(''); setChunks(0); setStreamState('unknown'); setArrivals(0); setRequest('pending');
48
+ }
49
+ const before = layout === 'before';
50
+ const requestCard = before ? <Surface><Stack gap={3}><Badge>待回应</Badge><Text as="h3" variant="heading">资料核对请求</Text><Text>比较版本并附上依据。</Text><Button variant="quiet">查看条款</Button></Stack></Surface>
51
+ : <ChatCard title="资料核对请求" status={request} summary="比较版本并附上依据。" actions={<><Button variant="quiet" aria-disabled={request !== 'pending'} onClick={() => { if (request === 'pending') setRequest('accepted'); }}>接受请求</Button><Button variant="quiet" aria-disabled={request !== 'pending'} onClick={() => { if (request === 'pending') setRequest('declined'); }}>拒绝请求</Button></>}>
52
+ <p>仅比较公开的合成资料,保留未知项;接受不代表运行完成。</p><a href="https://example.com">来源示例 · example.com</a>
53
+ </ChatCard>;
54
+ const pair: ChatEntry[] = [
55
+ message('a1', 0, 23, <p>需要一起核对资料变化。</p>),
56
+ message('a2', 0, 24, requestCard),
57
+ message('b1', 1, 25, <><p>先查看这段依据:</p><blockquote>“比较版本并附上依据。”<br /><a href="https://example.com">公开来源示例</a></blockquote><p>{paragraph}</p></>),
58
+ message('b2', 1, 26, <><pre><code>{'const result = {\n status: "partial",\n source: "synthetic-example-with-a-very-long-unbroken-identifier-0123456789"\n};'}</code></pre><p><strong>尚未确认:</strong>补全来源后再形成最终结论。</p></>),
59
+ message('a3', 0, 32, <ChatCard title="来源核验" status="failed" summary="公开样例暂不可用,已保留部分内容。请稍后重新核对。"><p>{paragraph.repeat(3)}</p></ChatCard>),
60
+ message('b3', 1, 33, <ChatCard title="结果摘要" status="unknown" summary="尚未收到最终确认,不将结束视为成功。"><p>完整产物由宿主在独立详情视图呈现;聊天内保留摘要和来源。</p></ChatCard>),
61
+ ];
62
+ const team: ChatEntry[] = [pair[0], pair[1], { id: 'event1', kind: 'event', day: '今天 · 10 月 27 日', content: '10:25 · 成员 C 加入记录(合成事件)' },
63
+ message('c1', 2, 25, <><p>我补充一份公开来源,成员身份没有左右主从之分。</p><blockquote>{paragraph}</blockquote></>), pair[2], pair[3], pair[4], pair[5]];
64
+ const observing = scene === 'observer';
65
+ const observation: ChatEntry[] = states.map(([status, summary], index) => message(`observation-${status}`, index % 3, index + 20,
66
+ <ChatCard title={`记录 ${index + 1} · 资料核对`} status={status} summary={summary}><p>{paragraph}</p><pre><code>{`{ "state": "${status}", "synthetic": true }`}</code></pre></ChatCard>));
67
+ const entries = [ ...(older ? history : []), ...(observing ? observation : scene === 'team' ? team : pair),
68
+ ...Array.from({ length: arrivals }, (_, i) => message(`arrival-${i}`, 2, 40, <p>新到达的合成消息 {i + 1}。上翻历史时不会强制回到这里。</p>)),
69
+ ...(chunks || running || streamState === 'cancelled' || streamState === 'failed' ? [message('stream', 1, 45,
70
+ <ChatCard title="合成流式记录" status={streamState} summary={<><p>{streamState === 'cancelled' ? '生成已取消;保留部分内容,未确认成功。' : streamState === 'failed' ? '合成连接中断;保留部分内容,未确认成功。' : '文字逐段到达,终态由宿主提供。'}</p><p>{paragraph.repeat(Math.ceil(chunks / 3)).slice(0, chunks * 25)}</p></>}><p>分片计数:{chunks}。仅用于合成演示,未调用模型。</p></ChatCard>)] : []),
71
+ ];
72
+ return <section id="agent-chat"><Stack gap={6}>
73
+ <Toolbar title="Agent Chat · 对话与记录" description="正文优先,成员平等;摘要紧随消息,阅读位置由读者掌握。">
74
+ <Select label="聊天场景" value={scene} onChange={e => resetScene(e.target.value)}><option value="pair">两成员连续对话</option><option value="team">三成员与事件混排</option><option value="observer">只读观察与历史</option></Select>
75
+ <Select label="聊天容器宽度" value={width} onChange={e => setWidth(e.target.value)}><option>400</option><option>520</option><option>880</option></Select>
76
+ <Select label="排版对照" value={layout} onChange={e => setLayout(e.target.value)}><option value="after">规范组合</option><option value="before">问题结构复现</option></Select>
77
+ </Toolbar>
78
+ <div className="chat-gallery-canvas" style={{ width: `${width}px` }}>
79
+ {before ? <Surface><Stack gap={6}>{entries.map(entry => <div key={entry.id}>{entry.kind === 'message' && <Inline gap={2}><Text>{entry.member.name}</Text><Text variant="caption">{entry.time}</Text></Inline>}{entry.content}</div>)}</Stack></Surface>
80
+ : <ChatThread key={scene} label="合成聊天消息" entries={entries} readOnly={observing} hasEarlier={!older} loadingEarlier={loading} historyError={historyError}
81
+ announcement={running ? '正在接收合成记录' : streamState === 'cancelled' ? '已取消,保留部分内容' : streamState === 'failed' ? '连接中断,未完成' : ''}
82
+ onLoadEarlier={() => {
83
+ setLoading(true); setHistoryError('');
84
+ timer.current = setTimeout(() => { setLoading(false); if (failHistory) setHistoryError('合成历史加载失败。取消下方失败模拟后,可重新加载。'); else setOlder(true); timer.current = null; }, 600);
85
+ }} />}
86
+ </div>
87
+ <Surface><Stack gap={3}><Text as="h2" variant="heading">合成数据演示控制</Text><Text tone="muted">此处只控制展台数据,不代表成员操作。上翻消息或打开详情后,播放新记录检查阅读位置。</Text>
88
+ <Inline gap={2}>
89
+ <Button variant="secondary" onClick={() => setArrivals(value => value + 1)}>到达一条合成消息</Button>
90
+ <Button variant="secondary" disabled={running} onClick={() => { setChunks(0); setStreamState('running'); }}>播放合成流式记录</Button>
91
+ <Button variant="quiet" aria-disabled={!running} onClick={() => { if (running) setStreamState('cancelled'); }}>取消演示生成</Button>
92
+ <Button variant="quiet" aria-disabled={!running} onClick={() => { if (running) setStreamState('failed'); }}>模拟连接失败</Button>
93
+ </Inline>
94
+ <Checkbox label="下次历史加载失败" checked={failHistory} onChange={event => setFailHistory(event.target.checked)} />
95
+ <Text variant="caption">离开本章节或切换场景会清理计时器;自然结束保留「部分结果」,不会自行宣称成功。</Text>
96
+ </Stack></Surface>
97
+ </Stack></section>;
98
+ }