@linhey/rhine-design 0.11.1 → 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 (78) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/CONTRIBUTING.md +2 -0
  3. package/README.md +9 -4
  4. package/components/Actions.tsx +1 -0
  5. package/components/AppShell.tsx +3 -2
  6. package/components/COMMON_COMPONENTS.md +5 -1
  7. package/components/Chat.tsx +160 -0
  8. package/components/Navigation.tsx +15 -4
  9. package/components/Overlays.tsx +37 -7
  10. package/components/README.md +11 -1
  11. package/components/chat.css +32 -0
  12. package/components/components.tsx +19 -9
  13. package/components/dialog-settings.css +54 -1
  14. package/components/extensions.css +4 -0
  15. package/components/index.ts +8 -3
  16. package/components/patterns.css +4 -0
  17. package/components/styles.css +11 -2
  18. package/components/theme-arknights.css +4 -2
  19. package/docs/AGENT_CHAT.md +76 -0
  20. package/docs/AGENT_CHAT_VERIFICATION.md +42 -0
  21. package/docs/APPLICATION_COMPONENTS.md +2 -0
  22. package/docs/BUTTON_ACTIONS.md +32 -0
  23. package/docs/BUTTON_ACTIONS_VERIFICATION.md +14 -0
  24. package/docs/COMPACT_ACCOUNT_PATTERNS.md +16 -0
  25. package/docs/COMPACT_SELECTION_VERIFICATION.md +15 -0
  26. package/docs/FLOATING_OVERLAYS.md +4 -0
  27. package/docs/INTERACTION_PATTERNS.md +4 -0
  28. package/docs/SETTINGS_COMPOSITION_VERIFICATION.md +12 -0
  29. package/docs/SETTINGS_DIALOG.md +42 -4
  30. package/docs/SIDE_NAV_ACTIONS.md +9 -0
  31. package/docs/SINGLE_SEARCH.md +19 -0
  32. package/docs/SINGLE_SEARCH_VERIFICATION.md +15 -0
  33. package/docs/SUMMARY_TABS.md +7 -1
  34. package/docs/TRAY_USAGE.md +34 -0
  35. package/docs/TRAY_USAGE_VERIFICATION.md +13 -0
  36. package/docs/assets/chat/after.png +0 -0
  37. package/docs/assets/chat/arknights.png +0 -0
  38. package/docs/assets/chat/before.png +0 -0
  39. package/docs/assets/chat/neo-brutalism.png +0 -0
  40. package/package.json +1 -1
  41. package/preview/ButtonActionsGallery.tsx +25 -0
  42. package/preview/ChatGallery.tsx +98 -0
  43. package/preview/CompactSelectionGallery.tsx +76 -0
  44. package/preview/GalleryShell.tsx +2 -2
  45. package/preview/NavActionsGallery.tsx +24 -0
  46. package/preview/SettingsCompositionGallery.tsx +38 -0
  47. package/preview/SettingsDialogGallery.tsx +54 -14
  48. package/preview/SingleSearchGallery.tsx +50 -0
  49. package/preview/SummaryTabsGallery.tsx +7 -3
  50. package/preview/TrayUsageGallery.tsx +82 -0
  51. package/preview/chat-gallery.css +1 -0
  52. package/preview/compact-selection.css +9 -0
  53. package/preview/main.tsx +15 -1
  54. package/preview/packageInfo.ts +1 -1
  55. package/preview/settings-dialog-gallery.css +5 -0
  56. package/preview/tray-usage.css +4 -0
  57. package/scripts/check-button-actions.js +34 -0
  58. package/scripts/check-compact-selection.js +40 -0
  59. package/scripts/check-dialog-scroll-boundaries.js +41 -0
  60. package/scripts/check-nav-actions.js +41 -0
  61. package/scripts/check-settings-bounded.js +49 -0
  62. package/scripts/check-settings-composition.js +50 -0
  63. package/scripts/check-settings-interactions.js +93 -0
  64. package/scripts/check-settings-theme.js +1 -5
  65. package/scripts/check-single-search.js +43 -0
  66. package/scripts/check-tray-usage.js +68 -0
  67. package/scripts/prepare-settings-consumer.mjs +21 -0
  68. package/skills/rhine-design/SKILL.md +2 -0
  69. package/skills/rhine-design/references/API_MAP.md +11 -0
  70. package/skills/rhine-design/references/TASK_MAP.md +1 -0
  71. package/skills/rhine-design/references/integration.md +1 -1
  72. package/tests/Chat.test.tsx +84 -0
  73. package/tests/CompactSelection.test.tsx +47 -0
  74. package/tests/NavActions.test.tsx +29 -0
  75. package/tests/SettingsDialog.test.tsx +102 -9
  76. package/tests/SingleSearch.test.tsx +42 -0
  77. package/tests/SkillInstallation.test.tsx +77 -0
  78. package/tests/TrayUsage.test.tsx +70 -0
@@ -1,4 +1,4 @@
1
- .rhine-dialog--settings { width:min(960px,calc(100% - 32px)); max-height:calc(100% - 32px); container-type:inline-size; }
1
+ .rhine-dialog.rhine-dialog--settings { width:min(960px,calc(100% - 32px)); max-height:calc(100% - 32px); container-type:inline-size; }
2
2
  .rhine-dialog__settings-grid { display:grid; grid-template-columns:minmax(0,1fr); gap:24px; }
3
3
  .rhine-dialog__navigation { min-width:0; padding-bottom:16px; border-bottom:1px solid var(--rhine-line); }
4
4
  .rhine-dialog__settings-content { min-width:0; overflow-wrap:anywhere; }
@@ -7,3 +7,56 @@
7
7
  .rhine-dialog__settings-grid { grid-template-columns:minmax(160px,1fr) minmax(0,3fr); }
8
8
  .rhine-dialog__navigation { padding:0 24px 0 0; border-bottom:0; border-right:1px solid var(--rhine-line); }
9
9
  }
10
+
11
+ /* Opt-in stable outer frame; footer never participates in content scrolling. */
12
+ .rhine-dialog.rhine-dialog--bounded { display:flex; flex-direction:column; gap:0; height:var(--rhine-dialog-height,auto); max-height:calc(100% - 32px); overflow:hidden; }
13
+ .rhine-dialog.rhine-dialog--bounded.rhine-dialog--split { width:min(960px,calc(100% - 32px)); }
14
+ .rhine-dialog__scroll { min-height:0; min-width:0; overflow:auto; flex:1 1 auto; display:grid; grid-template-columns:minmax(0,1fr); }
15
+ .rhine-dialog--bounded .rhine-dialog__main { min-height:0; }
16
+ .rhine-dialog--bounded .rhine-dialog__head { position:sticky; top:0; z-index:1; background:var(--rhine-overlay-bg); }
17
+ .rhine-dialog--bounded .rhine-dialog__head>.rhine-icon-button { position:static; flex:none; }
18
+ .rhine-dialog--bounded .rhine-dialog__side { padding-top:0; }
19
+ .rhine-dialog__footer { flex:none; min-width:0; padding-top:var(--rhine-space-4); margin-top:var(--rhine-space-4); border-top:1px solid var(--rhine-line); }
20
+ @media(min-width:721px) {
21
+ .rhine-dialog--bounded.rhine-dialog--split .rhine-dialog__scroll { grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr); gap:24px; overflow:hidden; }
22
+ .rhine-dialog--bounded.rhine-dialog--split .rhine-dialog__main,.rhine-dialog--bounded.rhine-dialog--split .rhine-dialog__side { overflow:auto; min-height:0; }
23
+ }
24
+
25
+ /* A bounded single-pane/settings dialog keeps its head outside both scroll areas.
26
+ Keep the legacy auxiliary sidePanel layout independent. */
27
+ .rhine-dialog--bounded:not(.rhine-dialog--split) .rhine-dialog__scroll { overflow:hidden; }
28
+ .rhine-dialog--bounded:not(.rhine-dialog--split) .rhine-dialog__main { display:flex; flex-direction:column; overflow:hidden; }
29
+ .rhine-dialog--bounded:not(.rhine-dialog--split) .rhine-dialog__head { position:static; flex:none; }
30
+ .rhine-dialog--bounded:not(.rhine-dialog--split) .rhine-dialog__main > p { flex:none; }
31
+ .rhine-dialog--bounded:not(.rhine-dialog--split) .rhine-dialog__body { min-height:0; overflow:auto; overscroll-behavior:contain; }
32
+ .rhine-dialog--bounded.rhine-dialog--settings .rhine-dialog__body { display:flex; flex:1; overflow:hidden; }
33
+ .rhine-dialog--bounded .rhine-dialog__settings-grid { flex:1; min-height:0; grid-template-rows:minmax(64px,1fr) minmax(0,3fr); gap:12px; }
34
+ .rhine-dialog--bounded .rhine-dialog__navigation { min-height:0; max-height:112px; overflow:auto; overscroll-behavior:contain; padding-bottom:0; }
35
+ .rhine-dialog--bounded .rhine-dialog__settings-content { min-height:0; overflow:auto; overscroll-behavior:contain; scrollbar-gutter:stable; }
36
+ .rhine-dialog__footer { display:flex; flex-wrap:wrap; align-items:center; justify-content:flex-end; gap:8px; max-height:var(--rhine-dialog-footer-limit,40%); overflow:auto; overscroll-behavior:contain; }
37
+ .rhine-dialog__footer > * { max-width:100%; }
38
+ @container (min-width:560px) {
39
+ .rhine-dialog--bounded .rhine-dialog__settings-grid { grid-template-rows:minmax(0,1fr); gap:24px; }
40
+ .rhine-dialog--bounded .rhine-dialog__navigation { max-height:none; padding-bottom:0; }
41
+ }
42
+ @container (max-width:559px) {
43
+ .rhine-dialog--bounded .rhine-dialog__body { margin-top:8px; }
44
+ .rhine-dialog--bounded .rhine-dialog__navigation { max-height:64px; }
45
+ .rhine-dialog--bounded .rhine-dialog__settings-grid { gap:8px; }
46
+ .rhine-dialog__footer { padding-top:8px; margin-top:8px; }
47
+ }
48
+ /* Root padding follows the available viewport; descendants use the real container. */
49
+ @media (max-width:560px) {
50
+ .rhine-dialog.rhine-dialog--bounded { padding:12px; }
51
+ }
52
+ @container (max-width:300px) {
53
+ .rhine-dialog__footer { gap:2px; }
54
+ .rhine-dialog__footer > .rhine-button { padding-inline:2px; }
55
+ }
56
+
57
+ .rhine-dialog.rhine-dialog--settings.rhine-dialog--bounded { padding-block:12px; }
58
+ /* Keep theme focus rings and offset shadows inside the footer's scroll bounds. */
59
+ .rhine-dialog__footer { padding-inline:4px; padding-bottom:4px; }
60
+ @media (max-width:560px) {
61
+ .rhine-dialog.rhine-dialog--settings.rhine-dialog--bounded { padding-inline:8px; }
62
+ }
@@ -54,3 +54,7 @@
54
54
  .rhine-table__depth { display:block; white-space:nowrap; color:var(--rhine-muted); font-size:12px; }
55
55
  .rhine-table .rhine-check label { min-height:44px; }
56
56
  .rhine-pagination .rhine-select { width:auto; }
57
+
58
+ /* Direct action slots: keep hidden restore controls mounted without an empty bar. */
59
+ .rhine-shell__content-header[data-collapse-empty]:empty,
60
+ .rhine-shell__content-header[data-collapse-empty]:has(> [hidden]):not(:has(> :not([hidden]))) { display:none; }
@@ -37,14 +37,19 @@ export type { DataColumn, DataTableProps, TableSort, PaginationProps } from './D
37
37
  export { Checkbox, RadioGroup, Switch, Textarea, SearchField, Combobox } from './Forms';
38
38
  export type { ComboboxProps } from './Forms';
39
39
  export { Dialog, Drawer, ConfirmDialog, Popover, Menu, Tooltip } from './Overlays';
40
- export type { DialogProps, MenuItem } from './Overlays';
40
+ export type { DialogProps, PopoverProps, MenuItem } from './Overlays';
41
41
  export { Toast } from './Toast';
42
42
  export { SideNav, Breadcrumbs, Steps } from './Navigation';
43
- export type { NavItem, NavGroup, SideNavProps, BreadcrumbItem, BreadcrumbsProps, StepItem, StepsProps } from './Navigation';
43
+ export type { NavAction, NavItem, NavGroup, SideNavProps, BreadcrumbItem, BreadcrumbsProps, StepItem, StepsProps } from './Navigation';
44
44
  export { Fieldset, ErrorSummary } from './FormWorkflow';
45
45
  export type { FieldsetProps, FieldError, ErrorSummaryProps } from './FormWorkflow';
46
46
 
47
47
  export { Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState } from './components';
48
- export type { ButtonProps, SurfaceProps, Tone, Choice, TabItem } from './components';
48
+ export type { ButtonProps, TabsProps, SurfaceProps, Tone, Choice, TabItem } from './components';
49
49
 
50
50
  export { ThemePicker, ThemePreview, type ThemePickerProps, type ThemePickerOption, type ThemePreviewProps } from './ThemePicker';
51
+
52
+ export { ChatThread, ChatCard } from './Chat';
53
+ export type { ChatThreadProps, ChatCardProps, ChatEntry, ChatMessage, ChatEvent, ChatMember, ChatStatus } from './Chat';
54
+
55
+ export type { TextFieldProps } from './components';
@@ -37,3 +37,7 @@
37
37
  .rhine-dialog.rhine-drawer { inset-block:0; inset-inline-end:0; inset-inline-start:auto; width:min(440px,100%); max-height:100dvh; margin:0; transform:none; border-radius:0; animation:rhine-drawer-enter var(--rhine-duration-overlay) var(--rhine-ease-standard); }
38
38
  @keyframes rhine-drawer-enter { from { opacity:0; transform:translateX(3px); } to { opacity:1; transform:translateX(0); } }
39
39
  @keyframes rhine-pattern-enter { from { opacity:.65; transform:translateY(3px); } to { opacity:1; transform:translateY(0); } }
40
+
41
+ .rhine-side-nav__row--actions { display:flex; align-items:center; gap:var(--rhine-space-1); min-width:0; }
42
+ .rhine-side-nav__row--actions>a { flex:1; min-width:0; }
43
+ .rhine-side-nav__row--actions>.rhine-overlay-anchor { flex:none; }
@@ -8,6 +8,7 @@
8
8
  @import './orbit.css';
9
9
  @import './neutron.css';
10
10
  @import './charts.css';
11
+ @import './chat.css';
11
12
 
12
13
  .rhine-theme { color:var(--rhine-ink); background:var(--rhine-bg); font:var(--rhine-text-body)/var(--rhine-leading-body) var(--rhine-font); font-synthesis:none; }
13
14
  .rhine-theme[data-rhine-theme="rhine"] { color-scheme:light; }
@@ -15,7 +16,7 @@
15
16
  .rhine-theme [hidden] { display:none!important; }
16
17
  .rhine-theme :is(button,input,select,textarea,summary,a,[tabindex]):focus-visible { outline:2px solid var(--rhine-green); outline-offset:3px; }
17
18
  .rhine-sr-only { position:absolute!important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0; }
18
- .rhine-button { position:relative; display:inline-flex; align-items:center; justify-content:center; min-height:44px; min-width:44px; padding:10px 16px; border:1px solid var(--rhine-ink); border-radius:var(--rhine-radius-control); font:var(--rhine-weight-label) var(--rhine-text-body)/var(--rhine-leading-body) var(--rhine-font); background:var(--rhine-ink); color:var(--rhine-surface); cursor:pointer; overflow-wrap:anywhere; transition:background-color var(--rhine-duration-hover) var(--rhine-ease-standard),border-color var(--rhine-duration-hover) var(--rhine-ease-standard),transform var(--rhine-duration-press) var(--rhine-ease-standard); }
19
+ .rhine-button { max-width:100%; position:relative; display:inline-flex; align-items:center; justify-content:center; min-height:44px; min-width:44px; padding:10px 16px; border:1px solid var(--rhine-ink); border-radius:var(--rhine-radius-control); font:var(--rhine-weight-label) var(--rhine-text-body)/var(--rhine-leading-body) var(--rhine-font); background:var(--rhine-ink); color:var(--rhine-surface); cursor:pointer; overflow-wrap:anywhere; transition:background-color var(--rhine-duration-hover) var(--rhine-ease-standard),border-color var(--rhine-duration-hover) var(--rhine-ease-standard),transform var(--rhine-duration-press) var(--rhine-ease-standard); }
19
20
  .rhine-button:hover:not(:disabled):not([aria-disabled=true]) { background:var(--rhine-green); border-color:var(--rhine-green); }
20
21
  .rhine-button:active:not(:disabled):not([aria-disabled=true]) { transform:translateY(1px); }
21
22
  .rhine-button--secondary { background:var(--rhine-surface); color:var(--rhine-ink); border-color:var(--rhine-control); }
@@ -61,7 +62,7 @@
61
62
  .rhine-tabs__list button[aria-selected=true]::after { transform:scaleX(1); }
62
63
  .rhine-tabs__list button:disabled { color:var(--rhine-muted); opacity:.6; cursor:default; }
63
64
  .rhine-tabs__list--detailed { align-items:stretch; gap:var(--rhine-space-4); }
64
- .rhine-tabs__list--detailed>button { display:flex; flex-direction:column; align-items:stretch; flex:1 1 0; min-width:0; padding-block:var(--rhine-space-3); text-align:start; overflow-wrap:anywhere; }
65
+ .rhine-tabs__list--detailed>button { display:flex; flex-direction:column; align-items:stretch; min-width:0; padding-block:var(--rhine-space-3); text-align:start; overflow-wrap:anywhere; }
65
66
  .rhine-tabs__list--detailed>button:hover:not(:disabled) { color:var(--rhine-ink); }
66
67
  .rhine-tabs__title { display:flex; align-items:center; flex-wrap:wrap; gap:var(--rhine-space-2); font-weight:var(--rhine-weight-heading); }
67
68
  .rhine-tabs__status { display:inline-flex; }
@@ -133,3 +134,11 @@
133
134
  .rhine-theme *, .rhine-theme *::before, .rhine-theme *::after { animation:none!important; transition:none!important; }
134
135
  .rhine-theme .rhine-button:active { transform:none; }
135
136
  }
137
+
138
+ .rhine-button--between>span:first-child { display:flex; align-items:center; justify-content:space-between; gap:var(--rhine-space-2); width:100%; min-width:0; overflow-wrap:anywhere; text-align:start; }
139
+ .rhine-button--between>span:first-child>.rhine-icon { flex:none; }
140
+ .rhine-tabs__list--content { width:max-content; max-width:100%; }
141
+ .rhine-tabs__list--content>button { flex:0 1 auto; min-width:0; max-width:100%; overflow-wrap:anywhere; }
142
+ .rhine-tabs__list--fill>button { flex:1 1 0; min-width:0; overflow-wrap:anywhere; }
143
+
144
+ .rhine-button--large { font-size:var(--rhine-text-heading); line-height:var(--rhine-leading-heading); min-height:52px; padding:12px 24px; max-width:100%; }
@@ -130,8 +130,8 @@
130
130
  outline: none;
131
131
  box-shadow: inset 0 0 0 2px var(--rhine-ink);
132
132
  }
133
- .rhine-theme[data-rhine-theme="arknights"] .rhine-button--danger { background: var(--rhine-error); border-color: var(--rhine-error); color: var(--rhine-ink); }
134
- .rhine-theme[data-rhine-theme="arknights"] .rhine-button--danger:hover:not(:disabled):not([aria-disabled=true]) { background: #c93a32; border-color: #c93a32; }
133
+ .rhine-theme[data-rhine-theme="arknights"] .rhine-button--danger { background: var(--rhine-error); border-color: var(--rhine-error); color: var(--ak-accent-ink); }
134
+ .rhine-theme[data-rhine-theme="arknights"] .rhine-button--danger:hover:not(:disabled):not([aria-disabled=true]) { background: #e74b40; border-color: #e74b40; }
135
135
  .rhine-theme[data-rhine-theme="arknights"] .rhine-button:active:not(:disabled):not([aria-disabled=true]) { transform:none; }
136
136
 
137
137
  /* 图标按钮保持方形小切角 */
@@ -304,3 +304,5 @@
304
304
  .rhine-theme[data-rhine-theme="arknights"][data-rhine-motion=reduced] .rhine-button {
305
305
  transition: none;
306
306
  }
307
+
308
+ .rhine-theme[data-rhine-theme="arknights"] .rhine-button:focus-visible:not(.rhine-button--secondary):not(.rhine-button--quiet) { box-shadow:inset 0 0 0 2px var(--ak-accent-ink); }
@@ -0,0 +1,76 @@
1
+ # Agent Chat:消息、卡片与历史
2
+
3
+ 源码候选,关联 #43;不属于 npm 0.11.0。以包含本规范的精确源码或后续已验证版本接入。入口:`ChatThread`、`ChatCard`,展台 `#agent-chat`。不引入模型、聊天运行时、Markdown 解析器或业务流程。
4
+
5
+ ## 参考与采用决策(2026-10-27)
6
+
7
+ 本轮实际读取官方文档、代码示例、网页可访问性结构及可见演示。日期固定为 2026-10-27(Asia/Shanghai),这些页面没有统一发布版本;LibreChat 标注 Latest (dev)。以下是当日文档/演示证据,不声称审计安装后的完整产品。没有安装三个项目的依赖。
8
+
9
+ | 问题 | 官方证据与界面观察 | Rhine 决定与原因 |
10
+ | --- | --- | --- |
11
+ | 流式更新打断历史阅读 | [assistant-ui Thread](https://www.assistant-ui.com/docs/primitives/thread) 的 Viewport & Auto-Scroll 明确上翻暂停、展开 disclosure 暂停;LoadEarlier 保留可见消息和按钮焦点。示例展示消息视口与独立 composer。 | 采用锚点与暂停;用稳定消息 ID 保留位置。进一步要求显式「回到最新并跟随」才恢复,展开/新运行均不擅自恢复。无需运行时 Provider。 |
12
+ | 正文与卡片抢层级 | 旧 [Message](https://elements.ai-sdk.dev/components/message) 演示正文、代码及紧随内容的动作;[Tool](https://elements.ai-sdk.dev/components/tool) 演示折叠行及 Pending / Running / Error 等文字状态。本轮点击 Error 行确认展开状态变化,状态仍在摘要行。 | 采用正文无外框、结构化卡片摘要优先;错误和未知解释始终置于摘要。拒绝固定 user/assistant 左右角色。 |
13
+ | 依赖即将迁移 | 旧文档横幅写明 2026 年 11 月底弃用;实际跟随 [shadcn/ui 迁移入口](https://ui.shadcn.com/docs/ai-elements#migrating-from-ai-sdk-elements),Conversation 对应 Chat,Tool 对应 Tool/Trace,既有复制代码仍可用。[新 Chat](https://ui.shadcn.com/docs/ai-elements/base/chat) 采用 Message Scroller;[新 Tool](https://ui.shadcn.com/docs/ai-elements/base/tool) 保持状态与详情分层。 | 参考行为与组合,不锁定旧 AI Elements 包。没有复制其代码或迁移消费项目。新 Chat 的 person turn 顶部锚点适合问答;Rhine 多成员记录采用时间顺序和当前可见消息锚点。 |
14
+ | 完整产物挤占对话 | [LibreChat Artifacts](https://www.librechat.ai/docs/features/artifacts) 描述独立交互视图;Preview and Export 说明窄屏不展示独立窗口操作,并描述紧凑 Mermaid 内嵌卡片进入产物面板。页面有嵌入演示视频入口。 | 聊天只承载摘要、依据与必要详情。复杂产物由宿主提供独立详情入口,不把可执行 HTML 或完整编辑器塞入消息;本轮没有运行其模型、视频中的产品或产物执行器。 |
15
+
16
+ 本地采样目录为 `output/playwright/`:reference-assistant / reference-migration / reference-tool / reference-librechat。官方站点的第三方加载错误单列为外站环境现象,不据此推断组件失败。Rhine 前后对照使用同一组合成内容;「问题结构复现」重建 issue 中的重复元信息与大卡片结构,不冒充消费产品截图。
17
+
18
+ ## 消息骨架与密度
19
+
20
+ - 时间升序;`id` 唯一稳定。宿主提供成员 ID/名称、可选装饰头像、ISO 时间(含时区)及本地化时间/日期。头像不重复朗读姓名。
21
+ - 同成员、同日期、间隔不超过 5 分钟且中间无事件,合并可见元信息;每条仍保留可访问作者/时间。换成员、跨日、系统事件、较长间隔均重新开始组。无右侧「当前用户」特权。
22
+ - 正文用 body 14/22 与主题 regular 字重,不缩字;作者与时间 caption 12/18,作者用 label 字重。行宽上限 72ch;卡片 64ch,窄屏不设最小宽度。
23
+ - 组间 24px、同组消息 8px、元信息到正文 8px、段间 12px。日期以细线区隔,事件用次级文字,不伪装成员气泡。
24
+ - 普通正文无卡片外框。结构化内容复用 Surface、Badge、Disclosure,卡片内距 12px,标题/状态间距 8px。动作靠近对应正文或卡片尾部,使用 Button quiet/secondary,自然宽度并换行;保留 44px 命中范围。
25
+ - 文本、列表、代码、引用均完整换行。代码保留空白并可在长标识符处换行;复制由宿主使用原始数据。引用使用 blockquote + 可键盘访问的来源链接,宿主校验 URL 并提供可读名称。大型表格/产物另开详情,不默认裁切正文。
26
+
27
+ ## 卡片与状态所有权
28
+
29
+ `ChatCard({title, status, summary, children?, actions?, readOnly?})`:标题和状态同层,摘要始终显示;children 是可开合详情,错误原因/恢复建议不得只放在 children。支持请求、工具过程、等待依据、结果/产物与来源卡片,无业务种类枚举。详情不默认展开。
30
+
31
+ | status | 含义 |
32
+ | --- | --- |
33
+ | pending / accepted / declined | 待回应 / 已接受(未必运行)/ 已拒绝 |
34
+ | blocked / running | 等待依赖(无无限 spinner)/ 正在运行 |
35
+ | partial / succeeded | 部分结果(未完成)/ 宿主已确认完成 |
36
+ | failed / cancelled / expired / unknown | 失败 / 已取消 / 已过期 / 未知,分别保留事实与后续说明 |
37
+
38
+ 状态只有呈现映射,没有自动转换、重试、超时或权限推断。缺失/异常外部状态先由宿主规范化为 unknown;运行时兜底也显示未知,不显示成功。停止流、网络断开或动画结束都不能推导 succeeded。宿主取消请求、屏蔽迟到回调、卸载清理;部分结果继续可读。
39
+
40
+ `ChatThread readOnly` 通过上下文隐藏消息和 ChatCard 的 actions;卡片自身也可 readOnly。展开、来源访问、历史加载和恢复跟随属于阅读动作,仍可用。宿主不得将发送/接受/拒绝等操作藏进任意 content/summary/children 插槽;readOnly 不是权限沙箱。不发送已读回执、不登记观察者身份、不自动改变阅读状态。首版无输入框。
41
+
42
+ ## 滚动、焦点与历史
43
+
44
+ `ChatThread({label, entries, readOnly?, hasEarlier?, loadingEarlier?, historyError?, onLoadEarlier?, announcement?})`。
45
+
46
+ - 初次显示定位最新。跟随时新增消息、流式增长与图片尺寸变化保持底部;ResizeObserver 观察内容和视口,卸载 disconnect。
47
+ - 上翻、PageUp/Home、进入消息内控件或开合详情会暂停。暂停后新增/流式内容不夺取阅读位置或焦点;提供常驻恢复按钮。不会因新 run 或滚回底部意外恢复。
48
+ - 加载更早由宿主发起;宿主原样保留已有 ID 并 prepend。根据首个可见消息 ID 与相对视口偏移恢复,而非简单比较总 scrollHeight(其间可能还在流式增长)。日期/组边界变化也纳入定位。加载按钮在忙碌/无更多记录时保持 DOM 与焦点,用 aria-disabled 阻止重复调用;失败文字常显且可重试。
49
+ - 本地详情展开保留当前行锚点,使用既有 Disclosure 键盘/焦点和减少动态实现;不会主动移焦到新消息。历史定位由宿主先加载所需记录,再聚焦该内容中带稳定 DOM id 的链接/控件;focus capture 会暂停跟随。切换整个对话使用 `key={conversationId}` 重新挂载,重置阅读状态。
50
+ - 非虚拟列表适用于有界记录;大量历史的窗口化不在首版。移除当前锚点、任意重排或更换稳定 ID 不承诺保持阅读位置。异步媒体应声明尺寸,避免单条超长消息内部重排。
51
+ - 消息区是有名称、可聚焦 region + 有序列表,不把历史和每个流式字元放入 live log。announcement 仅供宿主低频播报「接收中/已取消/失败」等;不重复播报全部正文。
52
+ - 跟随/锚点调整均即时,无平滑位移。系统/手动减少动态继续保留明确文字状态。CSS zoom 验证与浏览器原生缩放分别记录,前者不替代后者。
53
+
54
+ ## 公开接入
55
+
56
+ 下面只用公开 API。分页、权限、格式化与内容安全由宿主负责。已发布版本是否包含 API 需核对实际导出。
57
+
58
+ ```tsx
59
+ import { ChatThread, ChatCard, Button, type ChatEntry } from '@linhey/rhine-design';
60
+
61
+ const entries: ChatEntry[] = [{
62
+ id: 'sample-request', kind: 'message', day: '2026 年 10 月 27 日',
63
+ member: { id: 'member-a', name: '成员 A' },
64
+ dateTime: '2026-10-27T10:23:00+08:00', time: '10:23',
65
+ content: <ChatCard title="资料核对" status="blocked"
66
+ summary="等待公开来源补全;当前没有运行中的任务。">
67
+ <p>保留已有依据,未知项尚待核对。</p>
68
+ </ChatCard>,
69
+ actions: <Button variant="quiet" onClick={() => { /* 宿主允许的操作 */ }}>查看来源目录</Button>,
70
+ }];
71
+ export function Observation() {
72
+ return <ChatThread label="资料核对记录" entries={entries} readOnly />;
73
+ }
74
+ ```
75
+
76
+ 完整可运行公开导出消费样例:`preview/ChatGallery.tsx`。三场景全部合成:两成员连续消息、三成员和事件、只读状态分支记录;演示控制在消息区之外,不执行模型或真实业务接口。`排版对照` 提供改造前后相同内容。验证结果见 [Agent Chat 验证](AGENT_CHAT_VERIFICATION.md)。
@@ -0,0 +1,42 @@
1
+ # Agent Chat 验证记录
2
+
3
+ 关联 #43;2026-10-27,Asia/Shanghai。纯合成数据,没有业务 API、模型调用或消费迁移。源码候选不包含于冻结的 0.11.0 发布制品;合并不代表 npm 发布。
4
+
5
+ ## 视觉对照
6
+
7
+ 同一合成资料核对对话,520px 容器。之前复现 issue 的重复作者/时间、标题和状态分层及宽动作;之后合并连续作者,正文轻量,状态紧随卡片标题,动作左对齐。截图显示各自开头,长记录通过滚动阅读;不是消费产品截图。
8
+
9
+ | 问题结构复现 | 规范组合 |
10
+ | --- | --- |
11
+ | ![改造前](assets/chat/before.png) | ![改造后](assets/chat/after.png) |
12
+
13
+ | 战术主题 · 多成员事件 | 橙趣主题 · 只读记录 |
14
+ | --- | --- |
15
+ | ![战术主题](assets/chat/arknights.png) | ![橙趣主题](assets/chat/neo-brutalism.png) |
16
+
17
+ ## 证据与范围
18
+
19
+ 运行实现修订 `e7806fcfb646309cc3f70a9d0670bbc4f5668737`(基线 `4d70e73`),Node 24.21.0、React 19.3.1、Vite 8.3.4;Chromium 155 / macOS 的独立 Playwright 会话。随后 rebase 到 `5d9eec1` 保留导航行操作,聊天运行文件内容不变;证据文档/截图提交不改变运行实现;最终 head、制品摘要和 CI 在 PR #51 中绑定。原始本地证据 `output/playwright/` 不直接公开(可能含本机工具路径);此文与 PR 仅保留脱敏摘要。
20
+
21
+ - PASS:Node 24 下 `npm run check`,302 项测试通过,包含 Vitest、类型、生产构建、CSS/SVG 产物检查、两个评审示例构建和包清单检查。
22
+ - PASS:三主题 × 400/520/880 容器设置 × 三场景 × 100%/200% **CSS zoom**,54 组量测与截图;另检查 400/520/880 浏览器视口。正文 14px / 22px,卡片内距 12px,正文无横向溢出。放大时容器按可用空间缩窄,不假称仍有 880px 可用宽度。
23
+ - PASS:普通/系统减少动态/手动减少动态,三主题下 200% CSS zoom 键盘 Enter 开合、焦点保留、完整详情可读。
24
+ - PASS:上翻后到达消息不跳底,加载历史保留同一消息和相对偏移(误差阈值 2px),加载按钮焦点保留;显式恢复跟随回到底部。真实可见流式正文增长时保持跟随;PageUp 暂停,流式增长同时 prepend 保留锚点。
25
+ - PASS:观察模式无接受/拒绝/发送/重试;错误和未知文字在折叠外可见;历史失败保留并可重试;取消/连接失败保留内容且无迟到更新;自然结束为部分结果。卸载断开 ResizeObserver、清理流式与历史计时器。
26
+ - PASS:公开包入口独立安装消费与生产构建、真实浏览器操作;268 个安装文件逐一核对相同;示例仅登记成员、内容、状态与允许动作。
27
+ - SKIPPED:浏览器原生网页缩放、屏幕阅读器真人朗读、其他浏览器引擎、超大记录虚拟化;不以 CSS zoom、DOM 语义或 Chromium 结果外推。#43 要求的 CSS zoom 与键盘矩阵已单列。
28
+ - N/A:业务权限、真实模型、后端分页、iOS、消费产品升级、npm 发布。
29
+
30
+ ## 保留的失败与修正
31
+
32
+ 1. 首次全量检查的测试辅助函数返回宽联合类型,导致 TypeScript 不接受消息 actions;收窄为 ChatMessage 后通过。单元测试通过不等于类型检查通过。
33
+ 2. 首次视觉检查发现 Surface 的无内距修饰覆盖 ChatCard 内距;提高 ChatCard 规则具体性后,54 组实际计算值均为 12px。
34
+ 3. 首次浏览器行为脚本用完整文本精确定位 summary,但其中还有装饰符号,导致 30 秒超时。改为有作用域的 summary 定位,保留原失败;没有将定位器问题记成组件修复。
35
+ 4. 实际流式交互发现仅比较末条 ID 会漏报插在流式末条之前的新消息。改为比较稳定 ID 集合、排除前插历史,补契约回归并重新验证。
36
+ 5. 官方参考页有第三方资源报错;本轮只把可见页面、实际展开和官方文档作为证据,没有宣称完整参考产品运行通过。
37
+
38
+ 6. 动作焦点回归发现 Button 覆盖宿主传入的 aria-disabled。补齐原生属性透传语义并阻止点击,保留忙碌/不可操作控件焦点;历史加载使用已有 busy API。
39
+
40
+ ## 新增可复用结论
41
+
42
+ 新消息不一定改变列表尾部 ID(例如消息插入到已有的流式记录之前)。新消息提示应比较稳定 ID,单独排除 prepend 的历史;滚动恢复也应绑定原消息与偏移,而非总高度差。该结论已通过组件回归与真实流式同时加载历史验证,不推断任意重排/虚拟化行为。
@@ -56,6 +56,8 @@ import { ThemeProvider } from '@linhey/rhine-design/theme';
56
56
  </AppShell>
57
57
  ```
58
58
 
59
+ 仅含动作的顶部可选 `collapseEmptyContentHeader`(未发布,默认false),直传恢复控件与条件动作,按hidden可见性折叠;具体结构限制和设置页组合见[设置页组合](SETTINGS_DIALOG.md#设置页组合53未发布)。
60
+
59
61
  ### 工作区导航栏(0.8.0)
60
62
 
61
63
  参考 [Ant Design Layout](https://ant.design/components/layout/) 的 Header / Sider / Content 分工与 [MUI App Bar](https://mui.com/material-ui/react-app-bar/) 的标题、导航和操作组合,工作区顶部承载当前位置与导航入口。用可选 `contentHeader` 将导航栏放在正文滚动区外,与侧栏并列;它不会随正文滚走,不需要宿主另写 sticky。既有 `header` 仍位于整个外壳顶部,不传新插槽时保持原有行为。
@@ -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分别留证;静态截图不代替逐帧状态切换。
@@ -28,4 +28,8 @@ Popover、Menu、Tooltip、StatusTimeline 与 TimelineLegend 的 Portal 保留
28
28
 
29
29
  多选的标签和清空按钮属于同一交互区域:pointerdown/焦点转移不提前销毁浮层,等按钮click完成后收起。否则窄屏缩放时滚动布局可能在按下与抬起之间移动,让click落在公共祖先而丢失清空动作;浏览器用真实指针路径验证。
30
30
 
31
+ ## 可选可见Popover头部(#56,未发布)
32
+
33
+ `showHeader={false}` 省略可见标题和关闭按钮整行,但必填title继续命名浮层;默认true兼容现有内容。保持图标切换、外点/Escape关闭和键盘归焦。账号与用量的组合、稳定反馈及宿主状态职责见[托盘用量](TRAY_USAGE.md)。
34
+
31
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及隐藏面板保留规则不变。