@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.
- package/CHANGELOG.md +25 -1
- package/CONTRIBUTING.md +2 -0
- package/README.md +9 -4
- package/components/Actions.tsx +1 -0
- package/components/AppShell.tsx +3 -2
- package/components/COMMON_COMPONENTS.md +5 -1
- package/components/Chat.tsx +160 -0
- package/components/Navigation.tsx +15 -4
- package/components/Overlays.tsx +37 -7
- package/components/README.md +11 -1
- package/components/chat.css +32 -0
- package/components/components.tsx +19 -9
- package/components/dialog-settings.css +54 -1
- package/components/extensions.css +4 -0
- package/components/index.ts +8 -3
- package/components/patterns.css +4 -0
- package/components/styles.css +11 -2
- package/components/theme-arknights.css +4 -2
- package/docs/AGENT_CHAT.md +76 -0
- package/docs/AGENT_CHAT_VERIFICATION.md +42 -0
- package/docs/APPLICATION_COMPONENTS.md +2 -0
- package/docs/BUTTON_ACTIONS.md +32 -0
- package/docs/BUTTON_ACTIONS_VERIFICATION.md +14 -0
- package/docs/COMPACT_ACCOUNT_PATTERNS.md +16 -0
- package/docs/COMPACT_SELECTION_VERIFICATION.md +15 -0
- package/docs/FLOATING_OVERLAYS.md +4 -0
- package/docs/INTERACTION_PATTERNS.md +4 -0
- package/docs/SETTINGS_COMPOSITION_VERIFICATION.md +12 -0
- package/docs/SETTINGS_DIALOG.md +42 -4
- package/docs/SIDE_NAV_ACTIONS.md +9 -0
- package/docs/SINGLE_SEARCH.md +19 -0
- package/docs/SINGLE_SEARCH_VERIFICATION.md +15 -0
- package/docs/SUMMARY_TABS.md +7 -1
- package/docs/TRAY_USAGE.md +34 -0
- package/docs/TRAY_USAGE_VERIFICATION.md +13 -0
- package/docs/assets/chat/after.png +0 -0
- package/docs/assets/chat/arknights.png +0 -0
- package/docs/assets/chat/before.png +0 -0
- package/docs/assets/chat/neo-brutalism.png +0 -0
- package/package.json +1 -1
- package/preview/ButtonActionsGallery.tsx +25 -0
- package/preview/ChatGallery.tsx +98 -0
- package/preview/CompactSelectionGallery.tsx +76 -0
- package/preview/GalleryShell.tsx +2 -2
- package/preview/NavActionsGallery.tsx +24 -0
- package/preview/SettingsCompositionGallery.tsx +38 -0
- package/preview/SettingsDialogGallery.tsx +54 -14
- package/preview/SingleSearchGallery.tsx +50 -0
- package/preview/SummaryTabsGallery.tsx +7 -3
- package/preview/TrayUsageGallery.tsx +82 -0
- package/preview/chat-gallery.css +1 -0
- package/preview/compact-selection.css +9 -0
- package/preview/main.tsx +15 -1
- package/preview/packageInfo.ts +1 -1
- package/preview/settings-dialog-gallery.css +5 -0
- package/preview/tray-usage.css +4 -0
- package/scripts/check-button-actions.js +34 -0
- package/scripts/check-compact-selection.js +40 -0
- package/scripts/check-dialog-scroll-boundaries.js +41 -0
- package/scripts/check-nav-actions.js +41 -0
- package/scripts/check-settings-bounded.js +49 -0
- package/scripts/check-settings-composition.js +50 -0
- package/scripts/check-settings-interactions.js +93 -0
- package/scripts/check-settings-theme.js +1 -5
- package/scripts/check-single-search.js +43 -0
- package/scripts/check-tray-usage.js +68 -0
- package/scripts/prepare-settings-consumer.mjs +21 -0
- package/skills/rhine-design/SKILL.md +2 -0
- package/skills/rhine-design/references/API_MAP.md +11 -0
- package/skills/rhine-design/references/TASK_MAP.md +1 -0
- package/skills/rhine-design/references/integration.md +1 -1
- package/tests/Chat.test.tsx +84 -0
- package/tests/CompactSelection.test.tsx +47 -0
- package/tests/NavActions.test.tsx +29 -0
- package/tests/SettingsDialog.test.tsx +102 -9
- package/tests/SingleSearch.test.tsx +42 -0
- package/tests/SkillInstallation.test.tsx +77 -0
- package/tests/TrayUsage.test.tsx +70 -0
|
@@ -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
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Badge, Button, Dialog, Icon, Inline, Menu, Select, Stack, Surface, Tabs, Text, Toolbar } from '../components';
|
|
3
|
+
import './compact-selection.css';
|
|
4
|
+
|
|
5
|
+
export function CompactSelectionGallery() {
|
|
6
|
+
const [width, setWidth] = useState('880'), [scenario, setScenario] = useState('normal');
|
|
7
|
+
const [selected, setSelected] = useState('a'), [pending, setPending] = useState<string | null>(null), [error, setError] = useState('');
|
|
8
|
+
const [confirmed, setConfirmed] = useState('alpha'), [draft, setDraft] = useState('alpha');
|
|
9
|
+
const [dialogOwner, setDialogOwner] = useState('a');
|
|
10
|
+
const [open, setOpen] = useState(false), [saving, setSaving] = useState(false), [saveError, setSaveError] = useState('');
|
|
11
|
+
const [page, setPage] = useState('two'), [tab, setTab] = useState('one'), [layout, setLayout] = useState<'content' | 'fill'>('content');
|
|
12
|
+
const request = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
13
|
+
const saveRequest = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
14
|
+
const locked = useRef(false), saveLock = useRef(false);
|
|
15
|
+
useEffect(() => () => { clearTimeout(request.current); clearTimeout(saveRequest.current); }, []);
|
|
16
|
+
const choose = (id: string) => {
|
|
17
|
+
if (locked.current || id === selected) return;
|
|
18
|
+
locked.current = true; setPending(id); setError('');
|
|
19
|
+
request.current = setTimeout(() => {
|
|
20
|
+
if (scenario === 'error') setError('切换失败,保留最后确认项。'); else setSelected(id);
|
|
21
|
+
locked.current = false; setPending(null);
|
|
22
|
+
}, 700);
|
|
23
|
+
};
|
|
24
|
+
const changeOpen = (next: boolean) => {
|
|
25
|
+
if (saveLock.current) return;
|
|
26
|
+
setOpen(next); setDraft(confirmed); setSaveError(''); setPage('two');
|
|
27
|
+
};
|
|
28
|
+
const save = () => {
|
|
29
|
+
if (saveLock.current || draft === confirmed) return;
|
|
30
|
+
saveLock.current = true; setSaving(true); setSaveError('');
|
|
31
|
+
saveRequest.current = setTimeout(() => {
|
|
32
|
+
if (scenario === 'error') setSaveError('保存失败,请重试;确认值未改变。'); else setConfirmed(draft);
|
|
33
|
+
saveLock.current = false; setSaving(false);
|
|
34
|
+
}, 700);
|
|
35
|
+
};
|
|
36
|
+
const groupName = (value: string) => ({alpha:'示例组',beta:'联合研究组',none:'未绑定',unknown:'未知组'})[value] || value || '未选择';
|
|
37
|
+
useEffect(() => { if (page !== 'loading') return; const timer = setTimeout(() => setPage('two'), 600); return () => clearTimeout(timer); }, [page]);
|
|
38
|
+
const count = page === 'four' ? 4 : page === 'two' ? 2 : page === 'last' ? 1 : 0;
|
|
39
|
+
const items = [
|
|
40
|
+
{ id:'a', name: scenario === 'long' ? '跨区域联合研究与长期设计验证的合成对象名称' : '合成对象 A', menu:false },
|
|
41
|
+
{ id:'b', name: scenario === 'long' ? '跨区域联合研究与长期设计验证的合成备用对象' : '合成对象 B', menu:true },
|
|
42
|
+
];
|
|
43
|
+
return <section id="compact-selection"><Stack gap={6}>
|
|
44
|
+
<Toolbar title="紧凑选择与预览保存" description="正文配对、稳定状态位和动作列;选择先预览,保存后才确认。">
|
|
45
|
+
<Select label="组合宽度" value={width} onChange={e=>setWidth(e.target.value)}>{['400','520','880'].map(v=><option key={v}>{v}</option>)}</Select>
|
|
46
|
+
<Select label="组合场景" value={scenario} onChange={e=>setScenario(e.target.value)}><option value="normal">正常</option><option value="long">长名称与多行标签</option><option value="error">失败</option><option value="unbound">未绑定</option><option value="unknown">未知</option><option value="disabled">只读</option><option value="loading">加载</option></Select>
|
|
47
|
+
<Select label="页签布局" value={layout} onChange={e=>setLayout(e.target.value as 'content'|'fill')}><option value="content">内容宽度左对齐</option><option value="fill">均分铺满</option></Select>
|
|
48
|
+
</Toolbar>
|
|
49
|
+
<div className="ds-compact-selection" style={{width:`${width}px`,maxWidth:'100%'}}>
|
|
50
|
+
<Tabs label="选择摘要" layout={layout} value={tab} onValueChange={setTab} items={[
|
|
51
|
+
{value:'one',label:'工作区',status:<Badge>{pending?'处理中':'可用'}</Badge>,content:<Text>合成身份完整展示在这里,不在页签重复第二行。</Text>},
|
|
52
|
+
{value:'two',label:'备用区',status:<Badge>已停止</Badge>,content:<Text>备用区身份与说明。</Text>},
|
|
53
|
+
]}/>
|
|
54
|
+
<div className="ds-compact-selection__cards">{items.map(item=><Surface key={item.id} padding="none" className="ds-choice-card" data-card={item.id}>
|
|
55
|
+
<div className="ds-choice-card__info"><Inline gap={2}><Text as="h3" variant="heading">{item.name}</Text><span className="ds-choice-card__status">{selected===item.id?<Badge tone="success">当前项</Badge>:null}</span></Inline>
|
|
56
|
+
<Text variant="caption" tone="muted">研究类型 · 合成额度 120</Text>
|
|
57
|
+
<div className="ds-inline-choice"><Text variant="body">{scenario==='long'?'授权分组与跨区域研究范围':'授权分组'}</Text>
|
|
58
|
+
<Dialog trigger={<Button variant="secondary" contentAlign="between" disabled={scenario==='disabled'} busy={scenario==='loading'} aria-label={`选择 ${item.name} 的授权分组`} style={{width:180,maxWidth:'100%'}} onClick={()=>{}}>{scenario==='unbound'?'未绑定':scenario==='unknown'?'未知组':groupName(confirmed)}<Icon name="chevronDown" /></Button>}
|
|
59
|
+
title="分组选择" description="选择仅更新预览;保存后生效。" height={560} open={open && item.id===dialogOwner} onOpenChange={next=>{if(next) setDialogOwner(item.id);changeOpen(next);}}
|
|
60
|
+
footer={<Inline style={{justifyContent:'flex-end'}}><Button busy={saving} disabled={draft===confirmed || !draft} onClick={save}>保存</Button></Inline>}
|
|
61
|
+
sidePanel={<Stack gap={3}><Text as="h3" variant="heading">预览:{groupName(draft)}</Text><Text>已确认:{groupName(confirmed)}</Text><Select label="预览内容" value={page} onChange={e=>setPage(e.target.value)}><option value="two">两条</option><option value="four">四条</option><option value="last">末页一条</option><option value="empty">空组</option><option value="loading">异步加载</option><option value="error">读取错误</option></Select>
|
|
62
|
+
{count>0?Array.from({length:count},(_,i)=><Text key={i}>合成记录 {i+1}:用于比较候选内容。{'长详情用于验证内部滚动。'.repeat(5)}</Text>):<Text role="status">{page==='loading'?'正在读取预览…':page==='error'?'预览读取失败,请重试。':'暂无记录'}</Text>}</Stack>}>
|
|
63
|
+
<Stack gap={4}><Select label="候选分组" value={draft} disabled={saving} onChange={e=>setDraft(e.target.value)}><option value="">未选择</option><option value="alpha">示例组</option><option value="beta">联合研究组</option><option value="none">未绑定</option><option value="unknown">未知组</option></Select><Text role="status">{saving?'正在保存…':saveError}</Text></Stack>
|
|
64
|
+
</Dialog>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="ds-choice-card__actions"><Dialog trigger={<Button variant="secondary" style={{width:64}}>详情</Button>} title={`${item.name}详情`} description="只读详情在其他写入处理中仍可访问。"><Text>纯合成记录。</Text></Dialog>
|
|
68
|
+
{item.menu?<Menu label={`${item.name}操作`} trigger={<Button variant="secondary" style={{width:112}} disabled={!!pending && pending!==item.id} busy={pending===item.id}>切换操作</Button>} items={[{id:'choose',label:'设为当前项',disabled:!!pending||selected===item.id,onSelect:()=>choose(item.id)}]}/>:<Button style={{width:112}} busy={pending===item.id} disabled={selected===item.id||(!!pending&&pending!==item.id)} onClick={()=>choose(item.id)}>设为当前</Button>}
|
|
69
|
+
</div>
|
|
70
|
+
</Surface>)}</div>
|
|
71
|
+
<Text role="status">{error}</Text>
|
|
72
|
+
<Surface padding="none" className="ds-choice-card"><div><Text as="h3" variant="heading">永久不可用对象</Text><Text>已停用 · 无法切换,详情仍可访问。</Text></div><Dialog trigger={<Button variant="secondary" style={{width:64}}>详情</Button>} title="停用详情" description="原因保留可见。"><Text>合成停用原因。</Text></Dialog></Surface>
|
|
73
|
+
<Surface><Stack gap={2}><Text as="h3" variant="heading">密度反例(不推荐)</Text><Inline><Text variant="caption">授权分组</Text><Button variant="secondary">示例组<Icon name="chevronDown" /></Button><Button variant="secondary">详情</Button></Inline><Text variant="caption">caption 12/18 辅助标签搭配标准按钮,文字份量失配。上方推荐行采用14/22标签和值,标题16/24保留层级,命中均不小于44px。</Text></Stack></Surface>
|
|
74
|
+
</div>
|
|
75
|
+
</Stack></section>;
|
|
76
|
+
}
|
package/preview/GalleryShell.tsx
CHANGED
|
@@ -10,8 +10,8 @@ export const galleryGroups = [
|
|
|
10
10
|
{ label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例'], ['compact-filters', '紧凑与大量筛选'], ['checkbox-rows', '侧栏双行多选'], ['component-typography', '组件文字配对']] },
|
|
11
11
|
{ label: '应用能力', items: [['app-shell', '背景与多侧栏'], ['advanced-forms', '复杂表单'], ['advanced-data', '受控数据表'], ['media', '媒体卡片与查看器'], ['monitoring', '健康详情与图表']] },
|
|
12
12
|
{ label: '设计语言', items: [['foundation', '视觉基础'], ['backgrounds', '场景背景'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
|
|
13
|
-
{ label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['summary-tabs', '带摘要的页签'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
|
|
14
|
-
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['compact-account', '紧凑账号与凭证'], ['list-filter-bar', '列表筛选栏'], ['date-range-filter', '日期筛选组合'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
|
|
13
|
+
{ label: '通用组件', items: [['button-actions', '动作语义与尺寸'], ['actions', '按钮与输入'], ['selection', '选择与切换'], ['summary-tabs', '带摘要的页签'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['tray-usage', '托盘用量组合'], ['settings-composition', '设置页组合'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['nav-actions', '导航行操作'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
|
|
14
|
+
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['agent-chat', '聊天消息与记录'], ['compact-selection', '紧凑选择与保存'], ['compact-account', '紧凑账号与凭证'], ['single-search', '单输入搜索'], ['list-filter-bar', '列表筛选栏'], ['date-range-filter', '日期筛选组合'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
|
|
15
15
|
] as const;
|
|
16
16
|
export type GallerySection = typeof galleryGroups[number]['items'][number][0];
|
|
17
17
|
const entries = galleryGroups.flatMap(group => [...group.items]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { Dialog, Select, SideNav, Stack, Text, Toolbar } from '../components';
|
|
3
|
+
export function NavActionsGallery() {
|
|
4
|
+
const [width, setWidth] = useState('520');
|
|
5
|
+
const [long, setLong] = useState(false);
|
|
6
|
+
const [current, setCurrent] = useState('a');
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
const returnFocus = useRef<HTMLButtonElement | null>(null);
|
|
9
|
+
const label = long ? '跨区域联合研究项目与长期设计验证的合成工作区' : '合成工作区';
|
|
10
|
+
return <section id="nav-actions"><Stack gap={6}>
|
|
11
|
+
<Toolbar title="导航行操作" description="导航链接与更多操作互为兄弟,空动作不占位。">
|
|
12
|
+
<Select label="导航容器宽度" value={width} onChange={e => setWidth(e.target.value)}>{['400', '520', '880'].map(value => <option key={value}>{value}</option>)}</Select>
|
|
13
|
+
<Select label="导航标题" value={long ? 'long' : 'normal'} onChange={e => setLong(e.target.value === 'long')}><option value="normal">普通</option><option value="long">长标题</option></Select>
|
|
14
|
+
</Toolbar>
|
|
15
|
+
<div style={{ width: `${width}px`, maxWidth: '100%' }}><SideNav aria-label="行操作示例" currentId={current} onNavigate={(item, event) => { event.preventDefault(); setCurrent(item.id); }} groups={[{ label: '合成项目', items: [
|
|
16
|
+
{ id: 'a', label, href: '#nav-actions', icon: 'chart', actions: [{ id: 'details', label: '查看详情', onSelect: trigger => { returnFocus.current = trigger; setOpen(true); } }, { id: 'disabled', label: '暂不可用动作', disabled: true, onSelect: () => {} }] },
|
|
17
|
+
{ id: 'b', label: '没有动作的工作区', href: '#nav-actions', actions: [] },
|
|
18
|
+
] }]} /></div>
|
|
19
|
+
<Text role="status">当前导航:{current === 'a' ? label : '没有动作的工作区'}</Text>
|
|
20
|
+
<Dialog trigger={<button hidden type="button">受控详情入口</button>} title="工作区详情" description="由菜单打开的受控 Dialog。关闭后回到原行更多按钮。" open={open} onOpenChange={setOpen} onCloseAutoFocus={event => { event.preventDefault(); returnFocus.current?.focus(); }}>
|
|
21
|
+
<Text>纯合成内容;动作由调用方提供,不含权限判断。</Text>
|
|
22
|
+
</Dialog>
|
|
23
|
+
</Stack></section>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AppShell, AppShellToggle, Button, Checkbox, Inline, Select, SideNav, Stack, Surface, Text } from '../components';
|
|
3
|
+
import { ThemeProvider, type ThemeName } from '../components/theme';
|
|
4
|
+
|
|
5
|
+
export function SettingsCompositionGallery() {
|
|
6
|
+
const [open, setOpen] = useState(true), [state, setState] = useState('current');
|
|
7
|
+
const [long, setLong] = useState(false), [action, setAction] = useState(false);
|
|
8
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
9
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
10
|
+
const reset = () => { clearTimeout(timer.current); setOpen(true); setState('current'); setLong(false); setAction(false); };
|
|
11
|
+
const check = () => { clearTimeout(timer.current); setState('checking'); timer.current = setTimeout(() => setState('available'), 1200); };
|
|
12
|
+
return <Stack>
|
|
13
|
+
<Text as="h2" variant="section">设置页组合</Text><Text>未发布示例 · 合成更新数据,宿主负责真实检查与安装。</Text>
|
|
14
|
+
<Select label="更新状态" value={state} onChange={e => { clearTimeout(timer.current); setState(e.target.value); }}>
|
|
15
|
+
<option value="current">已是最新</option><option value="checking">检查中</option><option value="available">可安装</option><option value="error">检查失败</option>
|
|
16
|
+
</Select>
|
|
17
|
+
<Inline><Checkbox label="长产品名与状态" checked={long} onChange={e => setLong(e.target.checked)} /><Checkbox label="显示顶部动作" checked={action} onChange={e => setAction(e.target.checked)} /><Button variant="secondary" onClick={reset}>重置设置示例</Button></Inline>
|
|
18
|
+
<div style={{ height: 520 }}>
|
|
19
|
+
<AppShell label="设置组合工作区" layout="single" controls="inline" collapseEmptyContentHeader navigation={{ label: '设置导航', open, onOpenChange: setOpen, content: <Stack><Inline><Text>演示工作区</Text><AppShellToggle placement="panel" iconOnly /></Inline><SideNav currentId="settings" groups={[{ label: '偏好', items: [{ id: 'settings', href: '#settings-composition', label: '设置', icon: 'settings' }] }]} onNavigate={(_, event) => event.preventDefault()} /></Stack> }}
|
|
20
|
+
contentHeader={<><AppShellToggle placement="content" iconOnly />{action && <Button variant="quiet" onClick={() => setAction(false)}>完成顶部操作</Button>}</>}>
|
|
21
|
+
<Stack><Text as="h1" variant="heading">设置</Text><Surface>
|
|
22
|
+
<Inline style={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
|
23
|
+
<Stack gap={2} style={{ flex: '1 1 180px', minWidth: 0, overflowWrap: 'anywhere' }}><Text as="h2" variant="heading">{long ? 'Rhine Studio · 用于跨地区协作的超长产品身份与版本名称' : 'Rhine Studio'}</Text><Text tone="muted">演示版本 · 关于</Text></Stack>
|
|
24
|
+
<Stack gap={2} data-testid="update-area" style={{ flex: '1 1 200px', minWidth: 0, alignItems: 'flex-end', textAlign: 'end', overflowWrap: 'anywhere' }}>
|
|
25
|
+
<Text role="status">{state === 'checking' ? '正在检查更新,请稍候…' : state === 'error' ? '检查失败,请重试。' : state === 'available' ? '发现可安装的演示更新。' : '当前已是最新版本。'}{long && '此说明包含完整的更新渠道和网络检查信息,会随可用空间自然换行。'}</Text>
|
|
26
|
+
<Button variant="secondary" busy={state === 'checking'} busyLabel="正在检查更新" onClick={() => state === 'available' ? setState('current') : check()}>{state === 'error' ? '重试检查' : state === 'available' ? '安装演示更新' : '检查更新'}</Button>
|
|
27
|
+
</Stack>
|
|
28
|
+
</Inline>
|
|
29
|
+
</Surface></Stack>
|
|
30
|
+
</AppShell>
|
|
31
|
+
</div>
|
|
32
|
+
</Stack>;
|
|
33
|
+
}
|
|
34
|
+
export function SettingsCompositionFixture() {
|
|
35
|
+
const params = new URLSearchParams(location.search), zoom = Number(params.get('zoom')) || 1;
|
|
36
|
+
useEffect(() => { document.documentElement.style.zoom = String(zoom); return () => { document.documentElement.style.zoom = ''; }; }, [zoom]);
|
|
37
|
+
return <ThemeProvider theme={(params.get('theme') || 'rhine') as ThemeName} data-rhine-motion={params.get('motion') || 'system'}><div style={{ padding: 16 }}><SettingsCompositionGallery /></div></ThemeProvider>;
|
|
38
|
+
}
|
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Button, Dialog, EmptyState, Notice, Select, SideNav, Skeleton, Stack, Text, TextField,
|
|
3
|
-
import { ThemeProvider, type ThemeName } from '../components/theme';
|
|
2
|
+
import { Button, Checkbox, Dialog, Drawer, EmptyState, Notice, RemoteMultiSelect, Select, SideNav, Skeleton, Stack, Text, TextField, type Choice } from '../components';
|
|
3
|
+
import { ThemeProvider, themes, useTheme, type ThemeName } from '../components/theme';
|
|
4
|
+
import './settings-dialog-gallery.css';
|
|
4
5
|
|
|
6
|
+
const members: Choice[] = Array.from({ length: 30 }, (_, i) => ({ value: `member-${i}`, label: `合成成员 ${i + 1}` }));
|
|
5
7
|
export function SettingsDialogGallery() {
|
|
8
|
+
const { theme, setTheme } = useTheme();
|
|
6
9
|
const [section, setSection] = useState('account'), [state, setState] = useState('normal');
|
|
7
10
|
const [draft, setDraft] = useState('研究工作区'), [saved, setSaved] = useState('研究工作区');
|
|
8
11
|
const [busy, setBusy] = useState(false), [message, setMessage] = useState('');
|
|
9
|
-
const [open, setOpen] = useState(false);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
const [open, setOpen] = useState(false), [longNav, setLongNav] = useState(false), [longActions, setLongActions] = useState(false);
|
|
13
|
+
const [outcome, setOutcome] = useState('success'), [loadingSection, setLoadingSection] = useState(false);
|
|
14
|
+
const [query, setQuery] = useState(''), [selected, setSelected] = useState<Choice[]>([]);
|
|
15
|
+
const draftEdited = useRef(false), pending = useRef(false);
|
|
16
|
+
const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
17
|
+
const sectionTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
18
|
+
useEffect(() => () => { clearTimeout(saveTimer.current); clearTimeout(sectionTimer.current); }, []);
|
|
19
|
+
const save = () => {
|
|
20
|
+
if (pending.current) return;
|
|
21
|
+
pending.current = true;
|
|
22
|
+
const submitted = draft; draftEdited.current = false; setBusy(true); setMessage('');
|
|
23
|
+
saveTimer.current = setTimeout(() => {
|
|
24
|
+
if (outcome === 'success') { setSaved(submitted); if (!draftEdited.current) setDraft(submitted); setMessage('已保存演示草稿'); }
|
|
25
|
+
else setMessage(outcome === 'conflict' ? '内容已更新,请核对后重新保存' : '保存失败,草稿已保留');
|
|
26
|
+
pending.current = false; setBusy(false);
|
|
27
|
+
}, 500);
|
|
28
|
+
};
|
|
29
|
+
const navigate = (id: string) => {
|
|
30
|
+
clearTimeout(sectionTimer.current); setSection(id); setLoadingSection(id === 'async');
|
|
31
|
+
if (id === 'async') sectionTimer.current = setTimeout(() => setLoadingSection(false), 500);
|
|
32
|
+
};
|
|
33
|
+
const items = [{ id: 'account', label: '账号', href: '#account' }, { id: 'users', label: '用户管理与权限', href: '#users' }, { id: 'public', label: '公开项目', href: '#public' }, { id: 'async', label: '异步栏目', href: '#async' }, ...(longNav ? Array.from({ length: 20 }, (_, i) => ({ id: `extra-${i}`, label: `扩展设置 ${i + 1} · 长栏目名称`, href: `#extra-${i}` })) : [])];
|
|
34
|
+
return <Stack gap={4}><Text as="h2" variant="section">设置导航对话框</Text><Text>稳定窗口、独立正文滚动;使用合成数据。</Text>
|
|
13
35
|
<TextField label="宿主查询" defaultValue="保留这条查询" />
|
|
14
|
-
<
|
|
15
|
-
|
|
36
|
+
<Checkbox label="长导航" checked={longNav} onChange={event => setLongNav(event.target.checked)} />
|
|
37
|
+
<Checkbox label="长操作文案" checked={longActions} onChange={event => setLongActions(event.target.checked)} />
|
|
38
|
+
<Select label="保存结果" value={outcome} onChange={event => setOutcome(event.target.value)}><option value="success">成功</option><option value="error">失败</option><option value="conflict">冲突</option></Select>
|
|
39
|
+
<Dialog title="设置" height={640} trigger={<Button>打开设置</Button>} open={open} onOpenChange={next => { setOpen(next); if (next) { draftEdited.current = false; setDraft(saved); setMessage(''); } }}
|
|
40
|
+
footer={<>
|
|
41
|
+
<Button variant="secondary" aria-label="撤销全部未保存的修改" disabled={busy} onClick={() => { draftEdited.current = false; setDraft(saved); setMessage('已撤销未保存修改'); }}><span className={longActions ? 'rhine-settings-action-full' : undefined}>{longActions ? '撤销全部未保存的修改' : '撤销'}</span>{longActions && <span className="rhine-settings-action-short">撤销</span>}</Button>
|
|
42
|
+
<Button variant="secondary" aria-label="关闭设置" onClick={() => setOpen(false)}>关闭</Button>
|
|
43
|
+
<Button busy={busy} aria-label="保存当前工作区设置" onClick={save}><span className={longActions ? 'rhine-settings-action-full' : undefined}>{longActions ? '保存当前工作区设置' : '保存'}</span>{longActions && <span className="rhine-settings-action-short">保存</span>}</Button>
|
|
44
|
+
</>}
|
|
45
|
+
navigation={<SideNav aria-label="设置导航" currentId={section} groups={[{ label: '偏好与管理', items }]} onNavigate={(item, event) => { event.preventDefault(); navigate(item.id); }} />}>
|
|
16
46
|
<Stack gap={4}>
|
|
17
|
-
<
|
|
18
|
-
{section === 'account' && <><Text as="h3" variant="heading">账号</Text><TextField label="工作区名称" value={draft} onChange={event => { draftEdited.current = true; setDraft(event.target.value);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
47
|
+
{busy ? <Text role="status">正在保存</Text> : message ? <Notice tone={outcome !== 'success' && !message.startsWith('已撤销') ? 'error' : 'neutral'} title={message} /> : draft !== saved ? <Text role="status">有未保存的修改</Text> : null}
|
|
48
|
+
{section === 'account' && <><Text as="h3" variant="heading">账号</Text><TextField label="工作区名称" value={draft} onChange={event => { draftEdited.current = true; setDraft(event.target.value); setMessage(''); }} />
|
|
49
|
+
<Select label="界面主题" value={theme} onChange={event => setTheme(event.target.value as ThemeName)}>{themes.map(item => <option key={item.name} value={item.name}>{item.label}</option>)}</Select>
|
|
50
|
+
<RemoteMultiSelect label="授权成员" query={query} onQueryChange={setQuery} value={selected} onValueChange={setSelected} options={members.filter(item => item.label.includes(query))} maxVisibleTags={2} />
|
|
51
|
+
</>}
|
|
52
|
+
{section === 'users' && <><Text as="h3" variant="heading">用户管理</Text>{Array.from({length: 30}, (_, i) => <Stack key={i} gap={2}><Text>演示成员 {i + 1} · 长名称研究工作组</Text><Dialog trigger={<Button variant="secondary">编辑成员 {i + 1}</Button>} title={`编辑成员 ${i + 1}`}><TextField label="成员备注" defaultValue="合成备注" /></Dialog></Stack>)}</>}
|
|
53
|
+
{section === 'public' && <><Text as="h3" variant="heading">公开项目</Text><Select label="项目状态" value={state} onChange={event => setState(event.target.value)}><option value="normal">正常</option><option value="loading">加载</option><option value="error">失败</option><option value="empty">空态</option></Select>{state === 'loading' ? <Skeleton label="正在读取项目" /> : state === 'error' ? <Notice tone="error" title="项目读取失败" action={<Button onClick={() => setState('normal')}>重试</Button>} /> : state === 'empty' ? <EmptyState title="暂无公开项目" description="" /> : <TextField label="项目标题" defaultValue="公开研究计划" />}</>}
|
|
54
|
+
{section === 'async' && (loadingSection ? <Skeleton label="正在读取栏目" /> : <Text>异步栏目已就绪</Text>)}
|
|
55
|
+
{section.startsWith('extra-') && <Text>扩展栏目 {Number(section.slice(6)) + 1}</Text>}
|
|
22
56
|
</Stack>
|
|
23
57
|
</Dialog>
|
|
24
58
|
<Text>已保存名称:{saved}</Text>
|
|
59
|
+
<Drawer title="普通抽屉" trigger={<Button variant="secondary">普通抽屉示例</Button>}><TextField label="抽屉输入" /></Drawer>
|
|
60
|
+
<Dialog title="辅助分栏" trigger={<Button variant="secondary">辅助分栏示例</Button>} sidePanel={<Text>辅助内容</Text>}><TextField label="分栏输入" /></Dialog>
|
|
61
|
+
<Dialog title="有界普通表单" height={640} trigger={<Button variant="secondary">有界普通表单示例</Button>} footer={<Button>确认</Button>}><Stack>{Array.from({length:30}, (_,i)=><TextField key={i} label={`表单字段 ${i+1}`} />)}</Stack></Dialog>
|
|
62
|
+
<Dialog title="自动高度操作区" trigger={<Button variant="secondary">自动高度操作区示例</Button>} footer={<>{Array.from({length:20}, (_,i)=><Button key={i}>长操作 {i+1}</Button>)}</>}><Text>短正文仍然可读。</Text></Dialog>
|
|
63
|
+
<Dialog title="纯文本长正文" height={400} trigger={<Button variant="secondary">纯文本长正文示例</Button>} footer={<Button>阅读完成</Button>}><Stack>{Array.from({length:30}, (_,i)=><Text key={i}>合成说明第 {i+1} 段,仅用于键盘滚动验证。</Text>)}</Stack></Dialog>
|
|
64
|
+
<Dialog title="短对话框" trigger={<Button variant="secondary">普通短弹窗</Button>}><TextField label="简短输入" /></Dialog>
|
|
25
65
|
</Stack>;
|
|
26
66
|
}
|
|
27
67
|
|
|
@@ -29,5 +69,5 @@ export function SettingsDialogFixture() {
|
|
|
29
69
|
const params = new URLSearchParams(location.search);
|
|
30
70
|
const zoom = Number(params.get('zoom')) || 1;
|
|
31
71
|
useEffect(() => { document.documentElement.style.zoom = String(zoom); return () => { document.documentElement.style.zoom = ''; }; }, [zoom]);
|
|
32
|
-
return <ThemeProvider
|
|
72
|
+
return <ThemeProvider defaultTheme={(params.get('theme') || 'rhine') as ThemeName} data-rhine-motion={params.get('motion') || 'reduced'}><div style={{padding:16}}><SettingsDialogGallery /></div></ThemeProvider>;
|
|
33
73
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useId, useState } from 'react';
|
|
2
|
+
import { Select, Stack, Text, TextField, Toolbar } from '../components';
|
|
3
|
+
|
|
4
|
+
// Synthetic host: debounce, composition, cancellation and results belong here, not in TextField.
|
|
5
|
+
export function SingleSearchGallery() {
|
|
6
|
+
const [width, setWidth] = useState('520');
|
|
7
|
+
const [scenario, setScenario] = useState('normal');
|
|
8
|
+
const [query, setQuery] = useState('');
|
|
9
|
+
const [composing, setComposing] = useState(false);
|
|
10
|
+
const [feedback, setFeedback] = useState('');
|
|
11
|
+
const [failed, setFailed] = useState(false);
|
|
12
|
+
const feedbackId = useId();
|
|
13
|
+
const disabled = scenario === 'disabled';
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setFeedback(''); setFailed(false);
|
|
16
|
+
if (!query.trim() || composing || disabled) return;
|
|
17
|
+
let active = true;
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
let response: ReturnType<typeof setTimeout>;
|
|
20
|
+
const debounce = setTimeout(() => {
|
|
21
|
+
if (!active) return;
|
|
22
|
+
setFeedback('正在搜索…');
|
|
23
|
+
if (scenario === 'loading') return;
|
|
24
|
+
response = setTimeout(() => {
|
|
25
|
+
if (!active || controller.signal.aborted) return;
|
|
26
|
+
const error = scenario === 'error';
|
|
27
|
+
setFailed(error);
|
|
28
|
+
setFeedback(error ? '搜索失败,请修改查询后重试。' : scenario === 'empty' ? '没有匹配结果。' : `找到与“${query}”相关的 2 条合成结果。`);
|
|
29
|
+
}, 500);
|
|
30
|
+
}, 250);
|
|
31
|
+
return () => { active = false; controller.abort(); clearTimeout(debounce); clearTimeout(response); };
|
|
32
|
+
}, [query, composing, disabled, scenario]);
|
|
33
|
+
return <section id="single-search"><Stack gap={6}>
|
|
34
|
+
<Toolbar title="单输入搜索" description="只有一个搜索条件时,直接使用正文级输入;反馈按需放在结果区。">
|
|
35
|
+
<Select label="搜索容器宽度" value={width} onChange={e => setWidth(e.target.value)}>{['400', '520', '880'].map(value => <option key={value}>{value}</option>)}</Select>
|
|
36
|
+
<Select label="搜索场景" value={scenario} onChange={e => setScenario(e.target.value)}>
|
|
37
|
+
<option value="normal">正常</option><option value="loading">加载</option><option value="empty">无结果</option><option value="error">错误</option><option value="disabled">禁用</option><option value="long">长占位</option>
|
|
38
|
+
</Select>
|
|
39
|
+
</Toolbar>
|
|
40
|
+
<div style={{ width: `${width}px`, maxWidth: '100%' }} data-search-example>
|
|
41
|
+
<TextField label="搜索合成条目" labelVisibility="hidden" type="search" value={query} disabled={disabled}
|
|
42
|
+
placeholder={scenario === 'long' ? '搜索名称、描述或跨区域联合研究项目中的合成关键词' : '搜索名称或关键词'}
|
|
43
|
+
aria-describedby={feedback ? feedbackId : undefined} aria-invalid={failed || undefined}
|
|
44
|
+
onChange={e => setQuery(e.target.value)} onCompositionStart={() => setComposing(true)} onCompositionEnd={e => { setQuery(e.currentTarget.value); setComposing(false); }}
|
|
45
|
+
onKeyDown={e => { if (e.key === 'Escape' && !composing && !e.nativeEvent.isComposing) { e.preventDefault(); setQuery(''); } }} />
|
|
46
|
+
<div id={feedbackId} role="status" aria-live="polite" aria-atomic="true">{feedback && <Text tone={failed ? 'error' : 'muted'}>{feedback}</Text>}</div>
|
|
47
|
+
</div>
|
|
48
|
+
<Text variant="caption" tone="muted">输入后等待 250ms,再模拟 500ms 请求;组合输入期间不查询。Escape 清空并保持焦点,离开章节取消等待。</Text>
|
|
49
|
+
</Stack></section>;
|
|
50
|
+
}
|
|
@@ -2,12 +2,16 @@ import { useState } from 'react';
|
|
|
2
2
|
import { Badge, KeyValue, Select, Stack, Surface, Tabs, Text, TextField, Toolbar } from '../components';
|
|
3
3
|
|
|
4
4
|
export function SummaryTabsGallery() {
|
|
5
|
+
const [layout, setLayout] = useState<'content' | 'fill'>('content');
|
|
6
|
+
const [single, setSingle] = useState(false);
|
|
5
7
|
const [width, setWidth] = useState('520');
|
|
6
8
|
const [scenario, setScenario] = useState('normal');
|
|
7
9
|
const [selected, setSelected] = useState('primary');
|
|
8
10
|
const pending = scenario === 'loading', failed = scenario === 'error';
|
|
9
11
|
return <section id="summary-tabs"><Stack gap={6}>
|
|
10
12
|
<Toolbar title="带摘要的页签" description="名称与状态在同一组,身份摘要紧随;每个对象的状态始终可见。">
|
|
13
|
+
<Select label="摘要布局" value={layout} onChange={e=>setLayout(e.target.value as 'content'|'fill')}><option value="content">内容宽度左对齐</option><option value="fill">均分铺满</option></Select>
|
|
14
|
+
<Select label="摘要行数" value={single?'single':'double'} onChange={e=>setSingle(e.target.value==='single')}><option value="single">名称与短状态</option><option value="double">双行摘要</option></Select>
|
|
11
15
|
<Select label="局部容器宽度" value={width} onChange={event => setWidth(event.target.value)}>
|
|
12
16
|
<option value="320">320px · 窄容器</option><option value="520">520px · 标准</option><option value="100%">自适应</option>
|
|
13
17
|
</Select>
|
|
@@ -17,11 +21,11 @@ export function SummaryTabsGallery() {
|
|
|
17
21
|
</Toolbar>
|
|
18
22
|
<Surface><Stack gap={6}>
|
|
19
23
|
<div style={{width: width === '100%' ? width : `${width}px`, maxWidth:'100%'}}>
|
|
20
|
-
<Tabs label="摘要页签规范示例" value={selected} onValueChange={setSelected} items={[
|
|
21
|
-
{value:'primary',label:scenario === 'long' ? '跨部门联合研究工作区' : '主工作区',description:pending ? '尚未读取当前身份' : scenario === 'long' ? '研究项目 · 跨区域联合研发与长期设计验证团队' : '研发 · 研究项目',
|
|
24
|
+
<Tabs layout={layout} label="摘要页签规范示例" value={selected} onValueChange={setSelected} items={[
|
|
25
|
+
{value:'primary',label:scenario === 'long' ? '跨部门联合研究工作区' : '主工作区',description:single ? undefined : pending ? '尚未读取当前身份' : scenario === 'long' ? '研究项目 · 跨区域联合研发与长期设计验证团队' : '研发 · 研究项目',
|
|
22
26
|
status:<Badge tone={failed ? 'error' : pending ? 'neutral' : 'success'}>{failed ? '读取失败' : pending ? '读取中' : '同步中'}</Badge>,
|
|
23
27
|
content:<Stack gap={4}><Text>主工作区内容</Text><TextField label="主工作区草稿" placeholder="切换后保留已输入内容"/></Stack>},
|
|
24
|
-
{value:'team',label:'协作工作区',description:'协作 · 设计项目',disabled:scenario === 'disabled',status:<Badge>{scenario === 'disabled' ? '暂不可用' : '已停止'}</Badge>,
|
|
28
|
+
{value:'team',label:'协作工作区',description:single ? undefined : '协作 · 设计项目',disabled:scenario === 'disabled',status:<Badge>{scenario === 'disabled' ? '暂不可用' : '已停止'}</Badge>,
|
|
25
29
|
content:<Stack gap={4}><Text>协作工作区内容</Text><TextField label="协作工作区草稿" placeholder="与另一页签独立"/></Stack>},
|
|
26
30
|
]}/>
|
|
27
31
|
</div>
|