diff --git a/docs/design-system.html b/docs/design-system.html new file mode 100644 index 000000000..f7754c690 --- /dev/null +++ b/docs/design-system.html @@ -0,0 +1,1143 @@ + + + + +CC Switch 设计系统 + + + + + + + + +
+ + + +
+ +
+ + +
+
+
Claude Code · Codex · Gemini 供应商切换器
+

CC Switch 设计系统

+

桌面端(Tauri + React)的视觉与交互规范。令牌直接取自 src/index.css 与 tailwind.config.cjs: + 中性黑白骨架、主题色 #F97315 只落在操作色上, + 直连 / 路由 / 叠加三种模式各有一套颜色,13px 正文、6 / 10 / 14 三档圆角,明暗双主题一一对应。

+
+ Tauri 2 · React 18 + Tailwind 3 · Radix · shadcn/ui + lucide 图标 + 主题色 #F97315 +
+
+ + +
+
基础 / 01
+

色彩

+

骨架是中性灰,彩色只表达「能操作」和「是什么状态」。 + 主题色只有一个:橙 #F97315,它占的是 --action-* 和 --ring 这组槽位—— + 一个视图只有一个实心橙按钮。模式色、语义色各管各的,不和主题色混用。

+ +

主题色 · 操作色

+
+
+ --action-bg + #F97315 +
+
+
50 · soft#FFF1E6
+
400 · 深色文字#FB923C
+
500 · 主题色#F97315
+
600 · hover#EA620A
+
700 · 浅色文字#C2410C
+
+
+ + + 已勾选 + 文字链接 + +
+
+

对比度:浅色主题实心按钮白字 2.8:1,按钮文字统一用 600 字重补偿;橙色当文字用时一律换 --action-text(浅 #C2410C 5.2:1 / 深 #FB923C 7.5:1)。深色主题实心按钮压暗为 #EA580C + 白字(3.6:1),焦点描边用更沉的 #D77A3C。

+ +

中性面与文字

+
+
bg-app
#FFFFFF / #1C1C1E
内容区底
+
bg-sidebar
#F5F5F7 / #161618
侧栏
+
bg-subtle
#EFEFF2 / #26262A
hover、分段轨道、通知
+
bg-selected
#E4E4EA / #313136
侧栏当前项
+
bg-card · surface
#FFFFFF / #2E2E33
卡片、弹层、输入
+
border-strong
#CFCFD6 / #4A4A52
描边按钮、hover 卡片
+
Aa
text-1 · fg-1
#18181B / #F4F4F5
标题、正文
+
Aa
text-2 · fg-2
#52525B / #D0D0D8
次要文字、图标
+
Aa
text-3 · fg-3
#65656E / #B5B5BD
占位、辅助说明
+
Tip
inverse
#18181B / #F4F4F5
HoverTip 胶囊
+
+ +

模式色

+
+
+
直连 direct
+
fill#0A84FF
+
text#0063C7
+
soft#EAF3FF
+
solid#0066CC
+
+
+
路由 route
+
fill#0D9488
+
text#06726B
+
soft#E7F6F4
+
solid#0F766E
+
+
+
叠加 stack
+
fill#7C3AED
+
text#6D28D9
+
soft#F2ECFE
+
solid#6D28D9
+
+
+

模式色只出现在:模式 tab、侧栏模式标签、当前生效的供应商卡片、模式切换的主按钮。其他地方不用。

+ +

语义色

+
+
正常
success
#16A34A · text #007634
健康、额度充足、已连接
+
注意
warning
#D97706 · text #A64A00
额度将尽、需重新登录
+
失败
danger
#DC2626 · text #B91C1C
请求失败、删除
+
chart-1
#0891B2 / #38BDF8
图表主序列
+
chart-2
#DB2777 / #F472B6
图表对比序列
+
+

橙 vs 琥珀:主题橙和 warning 琥珀色相接近。警告永远是「soft 底 + 三角图标 + 文字」,从不单独用一块橙/琥珀色表达状态;橙色只出现在能点的东西上。

+
+ + +
+
基础 / 02
+

字体

+

系统字体栈(SF Pro / 苹方 / Segoe UI / 微软雅黑),不加载网络字体。正文 13px,桌面工具的密度; + 字号只用下面 8 个角色(text-badge … text-metric),数字列用等宽数字。

+
+
metric24 / 32 · 600$12.48 · 1,284,302
+
page18 / 26 · 600Claude Code
+
title16 / 24 · 600编辑供应商
+
section15 / 22 · 600路由设置
+
strong14 / 20 · 500Anthropic 官方
+
body13 / 20 · 400切换后 Claude Code 下一次请求就走新的供应商,不用重启终端。
+
caption12 / 18 · 400api.anthropic.com · 上次使用 3 分钟前
+
badge11 / 16 · 500路由
+
mono12 · ui-monospacesk-ant-•••• · claude-sonnet-5-5 · 200 OK
+
+
+ + +
+
基础 / 03
+

间距 · 圆角

+

4px 基础刻度。页面左右留白 24px,卡片之间 8px,卡片内 12px。控件高度只有三档:28(卡片、对话框里)、32(页头、输入框)、36(分段控件、页签)。

+
+
+
1 · 4图标与文字
+
1.5 · 6按钮内图标间距
+
2 · 8卡片列表间距
+
3 · 12卡片内边距
+
4 · 16模式行元素间距
+
5 · 20对话框内边距
+
6 · 24页面左右留白
+
+
+
+
control6px · 按钮 / 输入 / 侧栏行
+
panel10px · 卡片 / 分段 / 通知
+
dialog14px · 对话框 / 命令面板
+
full徽章 / 开关 / HoverTip
+
+
+
+

嵌套圆角:内圆角 = 外圆角 − 内衬。分段控件轨道 10、内衬 3 → 选中块 7。

+
+ + +
+
基础 / 04
+

层级 · 阴影

+

平面靠描边分层,阴影只给「浮起来」的东西。深色主题的 shadow-sm 带一圈 8% 白描边,代替看不见的投影。

+
+
shadow-v7-sm分段选中块 · 卡片 hover
+
shadow-v7-md菜单 · 说明卡 · HoverTip · 拖拽中
+
shadow-v7-lg对话框 · 抽屉 · 命令面板
+
+

z 轴:对话框 40 → 嵌套 50 → 确认 60 → 命令面板 80 → Tooltip 130 → 说明卡 120(与 Tooltip 互斥,不会同时出现)。

+
+ + +
+
基础 / 05
+

动效

+

快、短、只改颜色和位置。默认 150ms;菜单 / 弹层 / 提示淡入 + 轻微放大(tailwindcss-animate 的 animate-in fade-in-0 zoom-in-95),关闭时反向;分段选中块、页签下划线 200ms 滑动;打开菜单的触发器按下不缩放;按下按钮缩到 scale .96;系统开了「减少动态效果」时 framer-motion 位移全部跳过; + 切换明暗主题期间暂停所有过渡(.theme-switching),避免整页闪一下。

+
+
active · scale .96
+
悬停卡片
描边 + 阴影 150ms
hover · border / shadow
+
开关 · 150ms
+
HoverTip · 0 延迟
+
+
+ + +
+
组件 / 01 · src/components/ui/button.tsx
+

按钮

+

一个视图只有一个实心按钮。其余用描边 neutral(卡片上的「切换」「设为默认」)或透明 quiet(页头次要操作)。 + 模式色实心按钮只用于「切换到这个模式」。

+
+
solid · 主题色
+
neutral
+
quiet
+
destructive
+
neutral · disabled
+
+

模式色按钮

+
+ + + +
+

尺寸

+
+
compact · 28 · 卡片 / 对话框
+
regular · 32 · 页头 / 模式切换
+
icon-compact · 28 见方
+
+
+ + +
+
组件 / 02 · src/components/ui/hover-tip.tsx
+

图标按钮 · HoverTip

+

工具栏、卡片操作、模式行右侧一律用纯图标按钮,文字放进 HoverTip:鼠标移上去立即出现深色胶囊,说清楚这一下会做什么。 + 按钮本身仍要 aria-label。

+
+ + + + + + + + +
+
+

规格

胶囊圆角 · inverse 底 · 12px / 500 · 内边距 6×12 · 距触发元素 6px · shadow-md · 默认在下方,碰边自动翻转。延迟 0,相邻按钮之间移动不再等待。

+

写法

<HoverTip content="切换到路由"><button aria-label="路由"…/></HoverTip>。内容写动作(动词开头)或「名字 + 状态」,一句话,不放链接和按钮。

+
+

HoverTip 与 HelpTip 分工:HoverTip 只报「这个按钮是什么 / 做什么」;规则和术语解释放 HelpTip 的「?」说明卡(见通知条一节)。

+
+ + +
+
组件 / 03
+

输入

+

高 32、圆角 6、border-strong 描边;聚焦时描边和 3px 光晕换主题色。标签在上,说明在下,出错时说明换成红字,不弹 toast。密钥、URL、模型名用等宽。

+
+
只在 CC Switch 里显示
+
保存在本机配置文件里
+
要以 http:// 或 https:// 开头
+
+
+
+
+
+ + +
+
组件 / 04 · switch.tsx · checkbox.tsx
+

开关 · 勾选

+

开关轨道 30×18(表格里成排用 26×16),开 = 主题色、圆点反色;点击区域用伪元素扩到 38×28。成排、不该抢眼的开关用 tone="neutral"(开 = 深灰)。

+
+
default · 开
+
default · 关
+
sm · 26×16
+
tone=neutral
+
disabled · 45%
+
+
+ 同步到 Claude Desktop + 写入 settings.json + 按队列 + 按延迟 +
+
+ + +
+
组件 / 05 · segmented-control.tsx · page-tabs.tsx
+

分段控件 · 页签

+

分段控件是按钮组(aria-pressed),用在模式和页内筛选;下划线页签换整块内容。两者不混用。分段选中块是白底 + 小阴影,不用主题色。 + 切换有动画:选中块和页签下划线都是单独的一块,点哪项就 200ms 滑过去(src/components/ui/sliding-indicator.tsx);页面刚打开时不播,系统开了「减少动态效果」时直接跳。

+
+
+
+ +
+ md · 36 +
+
+
+ +
+ sm · 28 +
+
+
+
+ + +
+ PageTabs · 36 高 · 下划线用 text-1,不用主题色 +
+
+ + +
+
组件 / 06
+

徽章

+

高 20、全圆角、11px / 500。用 soft 底 + text 色,不用实心彩色块。一张卡片上最多两个。

+
+ 官方 + 直连 + 路由 · 第 1 位 + 叠加 + 正常 · 312ms + 额度 8% + 连接失败 + 新 +
+
+ + +
+
组件 / 07 · providers/ProviderCard.tsx
+

供应商卡片

+

最小高 58、圆角 10。普通卡片白底细描边,hover 描边加深 + 小阴影,拖拽手柄和右侧操作只在 hover / 聚焦时出现。右侧操作全部是图标按钮(主操作 → 排序 → 编辑 → ⋯),名字由 HoverTip 报; + 有禁用原因时改挂说明卡报原因,不叠两层提示。状态(使用中 / 路由中)是文字不是按钮,照旧写字。 + 当前生效的那张用所在模式的 soft 底 + fill 描边。

+
+
+ + +
+
Anthropic 官方 路由 · 第 1 位
+
api.anthropic.com · claude-sonnet-5-5
+
+ 312ms + 路由中 +
+ + + + +
+
+
+ + +
+
OpenRouter
+
openrouter.ai/api · 上次使用 2 天前
+
+
+ + + +
+
+
+ + +
+
Kimi
+
⚠ 额度剩 8% · api.moonshot.cn
+
+
+ + + +
+
+
+

主操作图标对照(ProviderCardActions.tsx · BUTTON_ICON)

+
+ 切换 + 回到直连并使用 + 路由到此 / 从这家开始 + 加入 / 移出队列 + 开始叠加 + 添加 / 移除 +
+

设为默认 = 星标、启用 / 停用 = 电源;没对上的 key 回退为圆点图标,新加按钮时在 BUTTON_ICON 里补一条。

+

供应商图标框固定白底深色图标,深色主题里也不反色——单色品牌图标继承浅色文字会看不见。

+
+ + +
+
组件 / 08 · notice.tsx · help-tip.tsx
+

通知条 · 说明卡

+

通知条圆角 10、左侧 16px 图标、soft 底,不用左边框强调色。只有一行时图标、文字、按钮整行上下居中;有第二行时图标对齐第一行。应用页顶部的通知槽按严重程度从上到下排。

+
+
路由服务没能启动
端口 15721 被占用。请求现在直连默认供应商。
+
有 1 个账号需要重新登录
Codex · ChatGPT Plus 的令牌已过期
+
路由生效中 · 按队列
第 1 位不可用时自动换下一位
+
Claude Code 需要重开终端才能读到新的环境变量
+
+

说明卡 HelpTip(「?」)

+
+ 故障转移 + + 什么是故障转移当前供应商连续失败时,按队列自动换到下一家,恢复后再切回来。 + + 悬停 300ms 打开 · 移开 200ms 关闭 · 点一下固定 · 只放规则和术语,不放按钮 +
+
+ + +
+
组件 / 09
+

菜单 · 对话框 · Toast

+
+
+ +
+
+
+ +
+
+
+
已回到直连:Codex 现在直接连接官方。重启 Codex 后生效。
+
保存失败:配置文件被占用
+ +
+

Toast 是白卡片(深色主题为深灰)、底部居中、2 秒消失;状态只看图标颜色——成功绿勾、失败红、警告琥珀、提示灰;带关闭按钮时一律在右上角(RTL 镜像到左上)。对话框圆角 14、遮罩 --overlay、shadow-lg。确认危险操作时,按钮写具体动作(「删除」),不写「确定」。会丢东西、要重启这类后果写在对话框或 toast 里,不进说明卡。

+
+ + +
+
组件 / 10 · usage/
+

数据展示

+
+
今日花费
$12.48
昨天 $9.10
+
请求数
1,284
失败 6
+
输入 tokens
3.2M
缓存命中 71%
+
Codex 额度
62%
+
+
+ + + + + + + +
时间应用供应商模型状态输入输出耗时花费
14:02:31Claude CodeAnthropic 官方claude-sonnet-5-520018,2041,0324.1s$0.071
14:01:58CodexOpenRoutergpt-5.1-codex2009,8804122.3s$0.018
14:01:12Claude CodeKimikimi-k3429——0.4s—
+
+

数字列右对齐、tabular-nums;状态码、模型名、时间用等宽;额度条 >30% 绿、10–30% 琥珀、<10% 红。

+
+ + +
+
CC Switch / 01 · providers/mode/ModeTabs.tsx
+

连接模式

+

每个应用有三种连接方式:直连(直接写配置文件)、 + 路由(本地代理按队列 / 故障转移)、叠加(多家模型合并)。 + 模式 tab 保留图标 + 文字、不挂提示;同一行右侧的故障转移、路由设置是纯图标 + HoverTip。

+
+
+
+ + + +
+ + 三种模式的区别直连:直接写入应用配置。
路由:经本地代理转发,可故障转移。
叠加:把多家的模型合并给一个应用。
+ 路由生效中 · 按队列 · Anthropic 官方 + + +
+
+
+

看 ≠ 生效

白底凸起 = 正在看的那段;模式色文字 + 圆点 = 生效中的那段。两者可以不同。

+

切换要确认

看另一个模式时,底部出现模式色实心按钮「改为路由」,点了才生效。

+

颜色只在这几处

模式 tab、侧栏模式标签、当前卡片、切换按钮。别处出现模式名用中性文字。

+
+
+ + + + + +
+
规约 / 01
+

交互原则

+
+

一个主操作

每个视图只有一个实心主题色按钮;卡片上一律描边或图标。

+

图标 + HoverTip

重复出现的操作(编辑、复制、删除、设置、模式)只放图标,悬停立即显示操作内容,键盘聚焦同样显示。

+

状态写在原处

额度、延迟、需要登录写在卡片副行;后果(丢失、需重启)写在按钮、确认框或 toast,不藏进说明卡。

+

明暗一致

所有颜色走 token;浅色 / 深色两套值一一对应,切换时暂停过渡。

+

键盘可达

焦点环 2px 主题色;⌘K 打开命令面板;Escape 关闭浮层;禁用按钮用 aria-disabled 保留悬停说明原因。

+

文案

动词开头、说结果(「已切换到 X」),不写「操作成功」;按钮写具体动作,不写「确定」。

+
+
+ +
+
规约 / 02
+

Do / Don't

+
+
Do

主题橙只放在能点的东西上:主按钮、开关、勾选、焦点、链接。

+
Don't

用橙色表达「注意 / 警告」状态,或给分段控件选中块、页签下划线上橙色。

+
Do

卡片操作、工具栏用纯图标按钮,配 HoverTip + aria-label;模式 tab 这种「选哪一个」的控件保留文字。

+
Don't

图标按钮只靠 title 属性提示——要等一秒多才出现,样式也和系统不一致。

+
Do

当前生效的供应商卡片用模式 soft 底 + fill 描边。

+
Don't

给当前卡片再加主题色描边或实心彩色块,和模式色打架。

+
Do

数字列右对齐、等宽数字;模型名、状态码、密钥用 mono。

+
Don't

新增 token 以外的颜色或字号(text-[15px]、#ff8800)。

+
+
+ + +
+
+ +
+ + + + diff --git a/package.json b/package.json index 6bbad8cc2..985d642d5 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "postcss": "^8.4.49", "prettier": "^3.6.2", "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.3.0", "vite": "^7.3.0", "vitest": "^2.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39971d031..78877d449 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,9 @@ importers: tailwindcss: specifier: ^3.4.17 version: 3.4.18 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.18) typescript: specifier: ^5.3.0 version: 5.9.2 @@ -1355,67 +1358,56 @@ packages: resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.46.2': resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.46.2': resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.46.2': resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.46.2': resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.46.2': resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.46.2': resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.46.2': resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.46.2': resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.46.2': resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.46.2': resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.46.2': resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} @@ -1481,35 +1473,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.8.1': resolution: {integrity: sha512-VK/zwBzQY9SfyK7RSrxlIRQLJyhyssoByYWPK/FJMre8SV/y8zZ071cTQNG9dPWM1f+onI1WPTleG+TBUq/0Gw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.8.1': resolution: {integrity: sha512-bFw3zK6xkyurDR5kw2QgiU6YFlFNrfgtli3wRdTRv8zSVLZMQ2iZ8keYnd57vpvsbZ9PusFPYAMS7Fkzkf9I4g==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.8.1': resolution: {integrity: sha512-zOnFX+Rppuz0UVVSeCi67lMet8le+yT4UIiQ6t/QYGtpoWO/D4GpMoVYehJlR14klNXrC2CRxT9b3BUWTCEBwA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.8.1': resolution: {integrity: sha512-gLy6eisaeOTC6NQirs3a0XZNCVT/i7JPYHkXx6ArH6+Kb9IU8ogthTY4MQoYbkWmdOp3ijKX+RT1dD3IZURrEg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.8.1': resolution: {integrity: sha512-ciZ93Dm847zFDqRyc1e0YRiu/cdWne1bMhvifcZOibbyqSKB9o+b95Y5axMtXqR4Wsd2mHiC5TE+MVF3NDsdEw==} @@ -2268,28 +2255,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -2755,6 +2738,11 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + tailwindcss@3.4.18: resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} engines: {node: '>=14.0.0'} @@ -5505,6 +5493,10 @@ snapshots: tailwind-merge@3.3.1: {} + tailwindcss-animate@1.0.7(tailwindcss@3.4.18): + dependencies: + tailwindcss: 3.4.18 + tailwindcss@3.4.18: dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 65734edee..f0bf2de33 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1337,9 +1337,15 @@ pub fn run() { let settings = crate::settings::get_settings(); if let Some(window) = app.get_webview_window("main") { // 在窗口首次显示前同步装饰状态,避免前端加载后再切换导致标题栏闪烁 - // 仅 Linux 生效:解决 Wayland 下系统窗口按钮不可用的问题 + // Linux:由设置决定(解决 Wayland 下系统窗口按钮不可用的问题) #[cfg(target_os = "linux")] let _ = window.set_decorations(!settings.use_app_window_controls); + // Windows:一律去掉系统标题栏,用页头里的应用内窗口按钮;保留阴影和边缘缩放 + #[cfg(target_os = "windows")] + { + let _ = window.set_decorations(false); + let _ = window.set_shadow(true); + } if settings.silent_startup { // 静默启动模式:保持窗口隐藏 let _ = window.hide(); diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index d3733a4ee..1b57423ec 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -6,9 +6,16 @@ "label": "main", "title": "CC Switch", "titleBarStyle": "Visible", + "decorations": false, + "shadow": true, "visible": false, - "minWidth": 900, - "minHeight": 600 + "width": 1100, + "height": 650, + "minWidth": 1000, + "minHeight": 640, + "resizable": true, + "fullscreen": false, + "center": true } ] } diff --git a/src/App.tsx b/src/App.tsx index 773b467c7..62ffe0ad9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { invoke } from "@tauri-apps/api/core"; import { useQueryClient } from "@tanstack/react-query"; import { ExternalLink, KeyRound, MoreHorizontal, Plus } from "lucide-react"; @@ -37,7 +37,7 @@ import { } from "@/utils/errorUtils"; import { isTextEditableTarget } from "@/utils/domUtils"; import { deepClone } from "@/utils/deepClone"; -import { isLinux } from "@/lib/platform"; +import { isLinux, isWindows } from "@/lib/platform"; import { APP_STORAGE_KEY, appPageBelongsTo, @@ -51,7 +51,6 @@ import { } from "@/lib/navigation"; import { Sidebar } from "@/components/shell/Sidebar"; import { NewLayoutNotice } from "@/components/shell/NewLayoutNotice"; -import { CommandPalette } from "@/components/shell/CommandPalette"; import { AppPageHeader, WindowControlsContext, @@ -79,13 +78,13 @@ import type { StartupAttachFailure } from "@/types/proxy"; import UsageScriptModal from "@/components/UsageScriptModal"; import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel"; import PromptPanel from "@/components/prompts/PromptPanel"; -import { PromptEntryButton } from "@/components/prompts/PromptEntryButton"; import { PROMPT_APP_IDS } from "@/lib/query/prompts"; import UnifiedSkillsPanel from "@/components/skills/UnifiedSkillsPanel"; import { DeepLinkImportDialog } from "@/components/DeepLinkImportDialog"; import { FirstRunNoticeDialog } from "@/components/FirstRunNoticeDialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { HelpTip } from "@/components/ui/help-tip"; import { PageTabs } from "@/components/ui/page-tabs"; import { @@ -165,8 +164,9 @@ function App() { }, [currentView]); const { data: settingsData } = useSettingsQuery(); + // Windows 一律去掉系统标题栏,用页头里的应用内窗口按钮;Linux 由设置决定 const useAppWindowControls = - isLinux() && (settingsData?.useAppWindowControls ?? false); + isWindows() || (isLinux() && (settingsData?.useAppWindowControls ?? false)); const visibleApps = useMemo( () => ({ ...DEFAULT_VISIBLE_APPS, @@ -634,7 +634,6 @@ function App() { openSettingsRef.current = openSettings; const toggleSidebarRef = useRef(toggleSidebar); toggleSidebarRef.current = toggleSidebar; - const [paletteOpen, setPaletteOpen] = useState(false); const [usageAppFilter, setUsageAppFilter] = useState("all"); useEffect(() => { @@ -652,12 +651,6 @@ function App() { return; } - if (mod && event.key.toLowerCase() === "k") { - event.preventDefault(); - setPaletteOpen((open) => !open); - return; - } - if (event.key !== "Escape" || event.defaultPrevented) return; if (document.body.style.overflow === "hidden") return; if (managementBusyRef.current) return; @@ -1017,19 +1010,20 @@ function App() { const appMenu = ( - - - + + + + + openPage("sessions")}> {t("appPage.viewSessions")} @@ -1087,10 +1081,6 @@ function App() { (settingsData?.showProfileSwitcher ?? true) && ( )} - openPage("prompts")} - /> {activeApp === "hermes" && ( - diff --git a/src/components/apps/AppsPage.tsx b/src/components/apps/AppsPage.tsx index bc5ea7c42..63c4544cf 100644 --- a/src/components/apps/AppsPage.tsx +++ b/src/components/apps/AppsPage.tsx @@ -23,6 +23,7 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { HelpTip } from "@/components/ui/help-tip"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Dialog, DialogContent, @@ -205,17 +206,18 @@ export function AppsPage() { : t("appsPage.checkUpdates")} - - - + + + + + = ({ } return ( - + + + ); })} diff --git a/src/components/common/AppToggleGroup.tsx b/src/components/common/AppToggleGroup.tsx index fb098d12b..34364f051 100644 --- a/src/components/common/AppToggleGroup.tsx +++ b/src/components/common/AppToggleGroup.tsx @@ -1,9 +1,5 @@ import React from "react"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { HoverTip } from "@/components/ui/hover-tip"; import type { AppId } from "@/lib/api/types"; import { APP_IDS, APP_ICON_MAP } from "@/config/appConfig"; @@ -26,28 +22,24 @@ export const AppToggleGroup: React.FC = ({ const { label, icon, activeClass } = APP_ICON_MAP[app]; const enabled = apps[app]; return ( - - - - - -

- {label} - {enabled ? " ✓" : ""} -

-
-
+ + + ); })} diff --git a/src/components/common/FullScreenPanel.tsx b/src/components/common/FullScreenPanel.tsx index c56917e58..af49c4fd8 100644 --- a/src/components/common/FullScreenPanel.tsx +++ b/src/components/common/FullScreenPanel.tsx @@ -4,6 +4,7 @@ import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; import { ChevronLeft } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { AppPageHeader } from "@/components/shell/AppPageHeader"; import { isTextEditableTarget } from "@/utils/domUtils"; import { cn } from "@/lib/utils"; @@ -147,16 +148,18 @@ export const FullScreenPanel: React.FC = ({ subtitle={subtitle} actions={actions} leading={ - + + + } /> diff --git a/src/components/common/ManagementListSearch.tsx b/src/components/common/ManagementListSearch.tsx index f3879c92d..df4a7148b 100644 --- a/src/components/common/ManagementListSearch.tsx +++ b/src/components/common/ManagementListSearch.tsx @@ -1,5 +1,4 @@ -import { Search, X } from "lucide-react"; -import { Input } from "@/components/ui/input"; +import { SearchField } from "@/components/ui/search-field"; import { cn } from "@/lib/utils"; interface ManagementListSearchProps { @@ -21,35 +20,13 @@ export function ManagementListSearch({ className, }: ManagementListSearchProps) { return ( -
-
+ ); } diff --git a/src/components/env/EnvWarningBanner.tsx b/src/components/env/EnvWarningBanner.tsx index 6939248fb..eb829fbbc 100644 --- a/src/components/env/EnvWarningBanner.tsx +++ b/src/components/env/EnvWarningBanner.tsx @@ -2,10 +2,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, ChevronDown, ChevronUp, X, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Checkbox } from "@/components/ui/checkbox"; import type { EnvConflict } from "@/types/env"; import { deleteEnvVars } from "@/lib/api/env"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Dialog, DialogContent, @@ -147,14 +148,17 @@ export function EnvWarningBanner({ )} - + + + diff --git a/src/components/hermes/HermesMemoryPanel.tsx b/src/components/hermes/HermesMemoryPanel.tsx index 9b5e8d1b1..2707b593e 100644 --- a/src/components/hermes/HermesMemoryPanel.tsx +++ b/src/components/hermes/HermesMemoryPanel.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { AlertTriangle, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; @@ -16,6 +16,7 @@ import { } from "@/hooks/useHermes"; import { shortenHomePath } from "@/components/sessions/utils"; import type { HermesMemoryKind } from "@/types"; +import { fieldClass } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { hermesMemoryDrafts, @@ -206,7 +207,10 @@ const MemoryTabPane: React.FC = ({ } placeholder={loaded ? t(meta.placeholderKey) : t("prompts.loading")} spellCheck={false} - className="min-h-[200px] w-full flex-1 resize-none rounded-panel border border-border bg-surface px-3.5 py-3 font-mono text-body leading-[21px] text-fg-1 placeholder:text-fg-3 focus-visible:border-border-strong focus-visible:outline-none disabled:opacity-60" + className={cn( + fieldClass, + "min-h-[200px] flex-1 resize-none px-3.5 py-3 font-mono leading-[21px]", + )} /> )} diff --git a/src/components/mcp/AppMatrix.tsx b/src/components/mcp/AppMatrix.tsx index 7631c35c3..752130a43 100644 --- a/src/components/mcp/AppMatrix.tsx +++ b/src/components/mcp/AppMatrix.tsx @@ -1,12 +1,14 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; -import { AlertTriangle, Check, Search, X } from "lucide-react"; -import { toast } from "sonner"; +import { AlertTriangle, Check } from "lucide-react"; +import { toast } from "@/lib/toast"; import * as PopoverPrimitive from "@radix-ui/react-popover"; import type { AppId } from "@/lib/api/types"; import { Button } from "@/components/ui/button"; import { HelpTip } from "@/components/ui/help-tip"; +import { HoverTip } from "@/components/ui/hover-tip"; import { AppGlyph, APP_DISPLAY_NAME } from "@/components/shell/AppGlyph"; +import { SearchField } from "@/components/ui/search-field"; import { cn } from "@/lib/utils"; /** @@ -111,37 +113,46 @@ export function MatrixCell({ const tracking = useColumnTracking(app); return ( - + + + ); } @@ -212,38 +223,39 @@ export function MatrixColumnHeader({ data-column={app} {...tracking} > - - - + + + {enabledCount} + + + + {showName && ( // 名字已在列头的无障碍名称里,这里只给眼睛看
@@ -360,48 +372,27 @@ export function MatrixSearch({ }: MatrixSearchProps) { const { t } = useTranslation(); return ( -
-
+ ); } diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index dc0e7d1a6..29adb9b49 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Check, Eye, EyeOff, Plus, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { HelpTip } from "@/components/ui/help-tip"; @@ -13,6 +13,7 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { HoverTip } from "@/components/ui/hover-tip"; import { AppGlyph, APP_DISPLAY_NAME } from "@/components/shell/AppGlyph"; import type { McpServer, McpServerSpec } from "@/types"; import type { McpAppId } from "@/config/appConfig"; @@ -535,45 +536,51 @@ const McpFormModal: React.FC = ({ ) } /> - - + + + + +
); })} @@ -624,18 +631,21 @@ const McpFormModal: React.FC = ({ className="flex flex-wrap gap-2" > {mcpPresets.map((preset) => ( - + + ))} @@ -974,24 +984,28 @@ const McpFormModal: React.FC = ({ }) } /> - + + + ))} = ({ const dialogOpen = drawer !== null || deleteId !== null || importReport !== null; const interactionBlocked = writePending || mutationPending || dialogOpen; + // 外观上的禁用晚 300ms 才出现:点一个格子写得很快时不让整页按钮闪一下变灰。 + // 写入本身仍由写锁(writeLockRef / interactionBlocked)拦着。 + const controlsDisabled = useDelayedFlag(interactionBlocked); React.useEffect(() => { onInteractionBlockedChange?.(interactionBlocked); @@ -556,7 +561,7 @@ const UnifiedMcpPanel: React.FC = ({ type="button" variant="quiet" size="regular" - disabled={interactionBlocked} + disabled={controlsDisabled} onClick={() => void handleImport()} > @@ -566,28 +571,29 @@ const UnifiedMcpPanel: React.FC = ({ type="button" variant="solid" size="regular" - disabled={interactionBlocked} + disabled={controlsDisabled} onClick={openAdd} > {t("mcpPage.add")} - - - + + + + + void handleResyncAll()} > {t("mcpPage.resync")} @@ -655,7 +661,7 @@ const UnifiedMcpPanel: React.FC = ({ type="button" variant="neutral" size="compact" - disabled={interactionBlocked} + disabled={controlsDisabled} onClick={() => void retryApp(app)} > {t("common.retry")} @@ -707,7 +713,7 @@ const UnifiedMcpPanel: React.FC = ({ type="button" variant="neutral" size="regular" - disabled={interactionBlocked} + disabled={controlsDisabled} onClick={() => void handleImport()} > @@ -717,7 +723,7 @@ const UnifiedMcpPanel: React.FC = ({ type="button" variant="neutral" size="regular" - disabled={interactionBlocked} + disabled={controlsDisabled} onClick={openAdd} > @@ -758,7 +764,7 @@ const UnifiedMcpPanel: React.FC = ({ scopeFailed={scopeFailed} scopeKind={scope.kind} noun={noun} - disabled={interactionBlocked} + disabled={controlsDisabled} onEnableRest={() => void handleBulk(app, true)} onDisableAll={() => void handleBulk(app, false)} /> @@ -849,7 +855,7 @@ const UnifiedMcpPanel: React.FC = ({ id, apps: failedNames, })} - disabled={interactionBlocked} + disabled={controlsDisabled} onClick={(event) => { event.stopPropagation(); const app = rowFailedApps[0]; @@ -880,7 +886,7 @@ const UnifiedMcpPanel: React.FC = ({ key={app} app={app} state={state} - disabled={interactionBlocked} + disabled={controlsDisabled} label={t(`appMatrix.cell.${state}`, { name: id, app: APP_DISPLAY_NAME[app], @@ -896,38 +902,40 @@ const UnifiedMcpPanel: React.FC = ({ className="flex w-16 shrink-0 justify-end gap-1" onClick={(event) => event.stopPropagation()} > - + + + - - - + + + + + ({ onValueChange: (value: T) => void; label: string; }) { + const indicator = useSlidingIndicator( + '[aria-pressed="true"]', + value, + ); return (
+ {items.map((item) => { const pressed = item.value === value; return ( @@ -211,8 +230,8 @@ export function MiniSegmented({ aria-pressed={pressed} onClick={() => onValueChange(item.value)} className={cn( - "h-6 rounded-[5px] px-2.5 text-caption font-medium text-fg-2 transition-colors", - pressed && "bg-surface font-semibold text-fg-1 shadow-v7-sm", + "relative h-6 rounded-[5px] px-2.5 text-caption font-medium text-fg-2 transition-colors", + pressed && "font-semibold text-fg-1", )} > {item.label} diff --git a/src/components/openclaw/AgentsDefaultsPanel.tsx b/src/components/openclaw/AgentsDefaultsPanel.tsx index b4ef3ae0b..3423b1a42 100644 --- a/src/components/openclaw/AgentsDefaultsPanel.tsx +++ b/src/components/openclaw/AgentsDefaultsPanel.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Save, Plus, Trash2, TriangleAlert } from "lucide-react"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { useOpenClawAgentsDefaults, useSaveOpenClawAgentsDefaults, } from "@/hooks/useOpenClaw"; import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -303,14 +304,17 @@ const AgentsDefaultsPanel: React.FC = () => { ))} - + + +
); })} diff --git a/src/components/openclaw/EnvPanel.tsx b/src/components/openclaw/EnvPanel.tsx index 54ee5366a..4c60ac2ee 100644 --- a/src/components/openclaw/EnvPanel.tsx +++ b/src/components/openclaw/EnvPanel.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Save } from "lucide-react"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { useOpenClawEnv, useSaveOpenClawEnv } from "@/hooks/useOpenClaw"; import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; diff --git a/src/components/openclaw/ToolsPanel.tsx b/src/components/openclaw/ToolsPanel.tsx index b48e3e1c3..28c90cd09 100644 --- a/src/components/openclaw/ToolsPanel.tsx +++ b/src/components/openclaw/ToolsPanel.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Plus, Trash2, Save, TriangleAlert } from "lucide-react"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { useOpenClawTools, useSaveOpenClawTools } from "@/hooks/useOpenClaw"; import { extractErrorMessage } from "@/utils/errorUtils"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -199,14 +200,17 @@ const ToolsPanel: React.FC = () => { placeholder={t("openclaw.tools.patternPlaceholder")} className="font-mono text-xs" /> - + + + ))} + + + ))} - + + + + + + ) : ( <> {profile.name} - - + + + + + + )} diff --git a/src/components/prompts/PiNativePromptResources.tsx b/src/components/prompts/PiNativePromptResources.tsx index a73feb464..ea373413d 100644 --- a/src/components/prompts/PiNativePromptResources.tsx +++ b/src/components/prompts/PiNativePromptResources.tsx @@ -16,7 +16,7 @@ import { RefreshCw, } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { Button } from "@/components/ui/button"; import { HelpTip } from "@/components/ui/help-tip"; @@ -33,6 +33,7 @@ import { SheetDescription, SheetTitle, } from "@/components/ui/sheet"; +import { HoverTip } from "@/components/ui/hover-tip"; import { promptsApi, type PiPromptFileKind, @@ -953,35 +954,37 @@ export const PiPromptTemplates = forwardRef< ) : null}
- + + + - - - + + + + + {APP_DISPLAY_NAME[app]} diff --git a/src/components/prompts/PromptEntryButton.tsx b/src/components/prompts/PromptEntryButton.tsx deleted file mode 100644 index 71133fefb..000000000 --- a/src/components/prompts/PromptEntryButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ChevronRight } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@/components/ui/button"; -import type { AppId } from "@/lib/api"; -import { - enabledPromptOf, - promptAppOf, - usePromptListQuery, -} from "@/lib/query/prompts"; - -/** - * 应用页页头的「提示词:<启用中的那条> ›」(Main.dc.html 第 37–40 行):既显示状态,也一键去提示词页。 - * 只在支持提示词的应用页出现;Claude Desktop 显示 Claude Code 的那条。 - */ -export function PromptEntryButton({ - app, - onOpen, -}: { - app: AppId; - onOpen: () => void; -}) { - const { t } = useTranslation(); - const promptApp = promptAppOf(app); - const { data } = usePromptListQuery(promptApp); - if (!promptApp) return null; - - const name = enabledPromptOf(data)?.name ?? t("prompts.entryNone"); - const label = t("prompts.entryLabel", { name }); - - return ( - - ); -} diff --git a/src/components/prompts/PromptFormPanel.tsx b/src/components/prompts/PromptFormPanel.tsx index 6031e3617..9c31064d7 100644 --- a/src/components/prompts/PromptFormPanel.tsx +++ b/src/components/prompts/PromptFormPanel.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/sheet"; import { APP_DISPLAY_NAME } from "@/components/shell/AppGlyph"; import type { Prompt, AppId } from "@/lib/api"; +import { fieldClass } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { MCODE_PROMPT_LIMIT, @@ -30,8 +31,8 @@ interface PromptFormPanelProps { onDelete?: (id: string) => void; } -export const promptFieldClass = - "w-full rounded-[8px] border border-border-strong bg-surface px-2.5 text-body text-fg-1 outline-none placeholder:text-fg-3 focus-visible:ring-1 focus-visible:ring-ring aria-[invalid=true]:border-danger disabled:opacity-60"; +/** 和 Input / Textarea 同一套外观(ui/input.tsx 的 fieldClass) */ +export const promptFieldClass = fieldClass; export function FieldError({ id, children }: { id: string; children: string }) { return ( diff --git a/src/components/prompts/PromptListItem.tsx b/src/components/prompts/PromptListItem.tsx index 4ff42843f..b301e9cf9 100644 --- a/src/components/prompts/PromptListItem.tsx +++ b/src/components/prompts/PromptListItem.tsx @@ -9,6 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { HoverTip } from "@/components/ui/hover-tip"; import type { Prompt } from "@/lib/api"; import { cn } from "@/lib/utils"; import { promptMenuContentClass, promptMenuItemClass } from "./PromptPageFrame"; @@ -100,30 +101,35 @@ const PromptListItem: React.FC = ({
- + + + - - - + + + + + ) : null} - - - + + + + + 0; return (
-
diff --git a/src/components/providers/ProviderCardActions.tsx b/src/components/providers/ProviderCardActions.tsx index 40cb37431..4dacb3908 100644 --- a/src/components/providers/ProviderCardActions.tsx +++ b/src/components/providers/ProviderCardActions.tsx @@ -1,14 +1,29 @@ +import type * as React from "react"; import { useTranslation } from "react-i18next"; import { ArrowDown, + ArrowLeftRight, ArrowUp, ChevronDown, + CircleDot, + Layers, + ListMinus, + ListPlus, Loader2, + Minus, MoreHorizontal, Pencil, + Plug, + Plus, + Power, + PowerOff, + Route, + Star, + type LucideIcon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DisabledReason } from "@/components/ui/help-tip"; +import { HoverTip } from "@/components/ui/hover-tip"; import { DropdownMenu, DropdownMenuContent, @@ -28,6 +43,23 @@ const DOT: Record = { muted: "bg-fg-3", }; +/** 主操作位只放图标,名字由 HoverTip 悬停即显(key 见 presentation.ts)。 */ +const BUTTON_ICON: Record = { + switch: ArrowLeftRight, + exitAndUse: Plug, + routeHere: Route, + startRouteFrom: Route, + queueAdd: ListPlus, + queueRemove: ListMinus, + setDefault: Star, + startStackFrom: Layers, + add: Plus, + remove: Minus, + enable: Power, + use: Power, + disable: PowerOff, +}; + interface ProviderCardActionsProps { providerName: string; presentation: CardPresentation; @@ -43,6 +75,7 @@ interface ProviderCardActionsProps { /** * 卡片右侧(v7):主操作位(状态文字或按钮)→ 上移 / 下移(故障转移队列)→ 编辑 → ⋯。 * 次要操作都在 ⋯ 里:复制、检测连通、配置用量查询、打开终端、删除。 + * 按钮一律纯图标 + HoverTip;有禁用原因时改由 DisabledReason 的说明卡报原因。 */ export function ProviderCardActions({ providerName, @@ -60,7 +93,7 @@ export function ProviderCardActions({ return (
-
+
{status && ( - - + + - - + +
)} - + - + - - - + + + + + {onDuplicate && ( @@ -168,16 +214,45 @@ export function ProviderCardActions({ ); } +/** 有禁用原因时只挂说明卡(报原因),否则挂 HoverTip(报名字),两者不叠。 */ +function IconAction({ + tip, + disabledReason, + children, +}: { + tip: string; + disabledReason?: string; + children: React.ReactElement; +}) { + if (disabledReason) { + return ( + + {children} + + ); + } + return {children}; +} + function PrimaryButton({ button }: { button: CardButton }) { + const Icon = BUTTON_ICON[button.key] ?? CircleDot; + const icon = ; if (button.menu && !button.disabledReason) { return ( - - - + + + + + - - + ); } diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index eb346cd35..ba8095c8a 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -17,7 +17,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Search, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import type { OpenClawProviderConfig, Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { providersApi } from "@/lib/api/providers"; @@ -349,7 +349,7 @@ export function ProviderList({ if (isLoading) { return ( -
+
{[0, 1, 2].map((index) => (
+
{piStateErrorNotice} ); - // 宽窗口下卡片列表限宽(max-w-content = 1040px),靠左和页头左边缘对齐 + // 卡片列表铺满主区域,和页头同宽 return ( -
+
{piStateErrorNotice} {isSearchOpen && ( diff --git a/src/components/providers/forms/ApiKeyInput.tsx b/src/components/providers/forms/ApiKeyInput.tsx index e62d9605c..bafb6fc70 100644 --- a/src/components/providers/forms/ApiKeyInput.tsx +++ b/src/components/providers/forms/ApiKeyInput.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { Eye, EyeOff } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { fieldClass } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { REQUIRED_LABEL } from "./BasicFormFields"; @@ -55,10 +56,9 @@ const ApiKeyInput: React.FC = ({ aria-required={required || undefined} autoComplete="off" className={cn( - "h-8 w-full rounded-control border px-3 pe-10 text-body transition-colors", - disabled - ? "cursor-not-allowed border-border bg-subtle text-fg-3" - : "border-border-strong bg-surface text-fg-1 focus:outline-none focus:ring-2 focus:ring-ring", + fieldClass, + "h-8 pe-10", + disabled && "border-border bg-subtle text-fg-3 opacity-100", )} /> {!disabled && value && ( diff --git a/src/components/providers/forms/BasicFormFields.tsx b/src/components/providers/forms/BasicFormFields.tsx index b067567ce..3f35c8103 100644 --- a/src/components/providers/forms/BasicFormFields.tsx +++ b/src/components/providers/forms/BasicFormFields.tsx @@ -10,6 +10,7 @@ import { } from "@/components/ui/form"; import { ImeSafeInput } from "@/components/ui/ime-safe-input"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { ArrowLeft } from "lucide-react"; import { Dialog, @@ -57,29 +58,32 @@ export function BasicFormFields({ const iconButton = ( - - - + + + + + - + + +
))}
diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index 139e19fbe..03a5ab0b1 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -6,7 +6,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Checkbox } from "@/components/ui/checkbox"; import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; diff --git a/src/components/providers/forms/ClaudeStackModelsField.tsx b/src/components/providers/forms/ClaudeStackModelsField.tsx index 475b66c2e..c8ced8571 100644 --- a/src/components/providers/forms/ClaudeStackModelsField.tsx +++ b/src/components/providers/forms/ClaudeStackModelsField.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import { Download, Loader2, Plus, Star, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Checkbox } from "@/components/ui/checkbox"; import { FormLabel } from "@/components/ui/form"; import { ImeSafeInput } from "@/components/ui/ime-safe-input"; @@ -225,25 +226,26 @@ export function ClaudeStackModelsField({ key={row.rowId} className="grid grid-cols-1 gap-2 md:grid-cols-[36px_1fr_minmax(0,1fr)_64px_36px]" > - + + + @@ -296,20 +298,22 @@ export function ClaudeStackModelsField({ /> {t("providerForm.modelOneMLabel", { defaultValue: "1M" })} - + + +
); })} diff --git a/src/components/providers/forms/CodexConfigSections.tsx b/src/components/providers/forms/CodexConfigSections.tsx index 73900e9e6..f3ad87e93 100644 --- a/src/components/providers/forms/CodexConfigSections.tsx +++ b/src/components/providers/forms/CodexConfigSections.tsx @@ -16,6 +16,8 @@ import { setCodexRemoteCompaction, setCodexTopLevelInt, } from "@/utils/providerConfigUtils"; +import { fieldClass } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; interface CodexAuthSectionProps { value: string; @@ -259,7 +261,7 @@ export const CodexConfigSection: React.FC = ({ type="checkbox" checked={remoteCompactionEnabled} onChange={(e) => handleRemoteCompactionToggle(e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("codexConfig.enableRemoteCompaction")} @@ -280,7 +282,7 @@ export const CodexConfigSection: React.FC = ({ type="checkbox" checked={toggleStates.contextWindow1M} onChange={(e) => handleContextWindowToggle(e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("codexConfig.contextWindow1M")} @@ -294,7 +296,7 @@ export const CodexConfigSection: React.FC = ({ defaultValue={toggleStates.compactLimit} disabled={!toggleStates.contextWindow1M} onChange={(e) => handleCompactLimitChange(e.target.value)} - className="w-28 h-7 px-2 text-sm rounded border border-border bg-surface text-fg-1 disabled:opacity-50 disabled:cursor-not-allowed" + className={cn(fieldClass, "h-7 w-28 px-2")} />
diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index 9fd06cc0a..eff9e0a0e 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; @@ -29,7 +30,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Check, ChevronDown, @@ -1137,23 +1138,24 @@ export function CodexFormFields({ defaultValue: "设为默认模型", }); return ( - + + + ); }; @@ -1274,16 +1276,18 @@ export function CodexFormFields({ }) } /> - + + +
))}
@@ -1560,21 +1564,23 @@ export function CodexFormFields({ } className="flex-1" /> - + + + {defaultModelSuggestions.length > 0 && ( handleToggle("hideAttribution", e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("claudeConfig.hideAttribution")} @@ -228,7 +228,7 @@ export function CommonConfigEditor({ type="checkbox" checked={toggleStates.teammates} onChange={(e) => handleToggle("teammates", e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("claudeConfig.enableTeammates")} @@ -237,7 +237,7 @@ export function CommonConfigEditor({ type="checkbox" checked={toggleStates.enableToolSearch} onChange={(e) => handleToggle("enableToolSearch", e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("claudeConfig.enableToolSearch")} @@ -246,7 +246,7 @@ export function CommonConfigEditor({ type="checkbox" checked={toggleStates.effortMax} onChange={(e) => handleToggle("effortMax", e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("claudeConfig.effortMax")} @@ -257,7 +257,7 @@ export function CommonConfigEditor({ onChange={(e) => handleToggle("disableAutoUpgrade", e.target.checked) } - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("claudeConfig.disableAutoUpgrade")} @@ -266,7 +266,7 @@ export function CommonConfigEditor({ type="checkbox" checked={toggleStates.disableArtifact} onChange={(e) => handleToggle("disableArtifact", e.target.checked)} - className="w-4 h-4 text-fg-1 bg-surface border-border rounded focus:ring-border focus:ring-2" + className="ui-checkbox" /> {t("claudeConfig.disableArtifact")} diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx index 93715f8c3..13f88586c 100644 --- a/src/components/providers/forms/EndpointSpeedTest.tsx +++ b/src/components/providers/forms/EndpointSpeedTest.tsx @@ -501,7 +501,7 @@ const EndpointSpeedTest: React.FC = ({ onChange={(event) => { onAutoSelectChange(event.target.checked); }} - className="h-3.5 w-3.5 rounded border-border bg-surface text-primary focus:ring-2 focus:ring-primary/20" + className="ui-checkbox" /> {t("endpointTest.autoSelect")} @@ -509,8 +509,9 @@ const EndpointSpeedTest: React.FC = ({ type="button" onClick={runSpeedTest} disabled={isTesting || !hasEndpoints} - size="sm" - className="h-7 w-24 gap-1.5 text-xs bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-60" + variant="neutral" + size="compact" + className="w-24 gap-1.5" > {isTesting ? ( <> diff --git a/src/components/providers/forms/GeminiFormFields.tsx b/src/components/providers/forms/GeminiFormFields.tsx index 35f9ca5d2..846c271b3 100644 --- a/src/components/providers/forms/GeminiFormFields.tsx +++ b/src/components/providers/forms/GeminiFormFields.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { FormLabel } from "@/components/ui/form"; import { Download, Info, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import EndpointSpeedTest from "./EndpointSpeedTest"; import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared"; import { diff --git a/src/components/providers/forms/GrokBuildProviderForm.tsx b/src/components/providers/forms/GrokBuildProviderForm.tsx index f08670112..3d4bcccd1 100644 --- a/src/components/providers/forms/GrokBuildProviderForm.tsx +++ b/src/components/providers/forms/GrokBuildProviderForm.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Button } from "@/components/ui/button"; import { Form, diff --git a/src/components/providers/forms/HermesFormFields.tsx b/src/components/providers/forms/HermesFormFields.tsx index 2c3eec241..bb53eb6bf 100644 --- a/src/components/providers/forms/HermesFormFields.tsx +++ b/src/components/providers/forms/HermesFormFields.tsx @@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { ImeSafeInput } from "@/components/ui/ime-safe-input"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Select, SelectContent, @@ -12,7 +13,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Download, Plus, Trash2, ChevronRight, Loader2 } from "lucide-react"; import { ApiKeySection, ModelDropdown } from "./shared"; import { @@ -399,18 +400,24 @@ export function HermesFormFields({ })} className="min-w-0 flex-1" /> - + +
{isExpanded && ( diff --git a/src/components/providers/forms/InactiveFieldsPanel.tsx b/src/components/providers/forms/InactiveFieldsPanel.tsx index 4f1350eea..03d52ec1a 100644 --- a/src/components/providers/forms/InactiveFieldsPanel.tsx +++ b/src/components/providers/forms/InactiveFieldsPanel.tsx @@ -1,4 +1,4 @@ -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import type { ProviderEditorInactiveField } from "@/lib/api/providers"; /** 点一个字段做什么:加入上方的编辑框,或复制它的值(可以照抄的 TOML)。 */ diff --git a/src/components/providers/forms/OmoFormFields.tsx b/src/components/providers/forms/OmoFormFields.tsx index 9df315b19..95248bb6a 100644 --- a/src/components/providers/forms/OmoFormFields.tsx +++ b/src/components/providers/forms/OmoFormFields.tsx @@ -4,6 +4,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Textarea } from "@/components/ui/textarea"; import { Select, @@ -39,7 +40,7 @@ import { X, } from "lucide-react"; import { cn } from "@/lib/utils"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { useReadOmoLocalFile, useReadOmoSlimLocalFile } from "@/lib/query/omo"; import { OMO_BUILTIN_AGENTS, @@ -931,16 +932,22 @@ export function OmoFormFields({ {renderVariantSelect(currentModel, currentVariant, (value) => handleVariantChange(key, value, store, setter), )} - + +
{isExpanded && renderAdvancedEditor({ @@ -1017,30 +1024,39 @@ export function OmoFormFields({ if (!item.key) return; handleVariantChange(item.key, value, store, setter); })} - - + + + + +
{isExpanded && item.key && diff --git a/src/components/providers/forms/OpenClawFormFields.tsx b/src/components/providers/forms/OpenClawFormFields.tsx index d59dca8f6..3d7f4a644 100644 --- a/src/components/providers/forms/OpenClawFormFields.tsx +++ b/src/components/providers/forms/OpenClawFormFields.tsx @@ -4,6 +4,7 @@ import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ImeSafeInput } from "@/components/ui/ime-safe-input"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Switch } from "@/components/ui/switch"; import { Select, @@ -12,7 +13,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Download, Plus, Trash2, ChevronRight, Loader2 } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { ApiKeySection, ModelDropdown } from "./shared"; @@ -413,18 +414,24 @@ export function OpenClawFormFields({ })} className="min-w-0 flex-1" /> - + + {isExpanded && ( diff --git a/src/components/providers/forms/OpenCodeFormFields.tsx b/src/components/providers/forms/OpenCodeFormFields.tsx index 6c37597e0..a2ca0606a 100644 --- a/src/components/providers/forms/OpenCodeFormFields.tsx +++ b/src/components/providers/forms/OpenCodeFormFields.tsx @@ -4,6 +4,7 @@ import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ImeSafeInput } from "@/components/ui/ime-safe-input"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Select, SelectContent, @@ -11,7 +12,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Download, Plus, Trash2, ChevronRight, Loader2 } from "lucide-react"; import { ApiKeySection, ModelDropdown } from "./shared"; import { @@ -691,15 +692,18 @@ export function OpenCodeFormFields({ })} className="flex-1" /> - + + + ))} @@ -815,15 +819,18 @@ export function OpenCodeFormFields({ })} className="flex-1" /> - + + + {/* Expanded model details */} @@ -952,17 +959,20 @@ export function OpenCodeFormFields({ )} className="flex-1" /> - + + + ), ) @@ -1037,17 +1047,20 @@ export function OpenCodeFormFields({ )} className="flex-1" /> - + + + ), ) diff --git a/src/components/providers/forms/PiProviderForm.tsx b/src/components/providers/forms/PiProviderForm.tsx index c599ad394..32ab9b917 100644 --- a/src/components/providers/forms/PiProviderForm.tsx +++ b/src/components/providers/forms/PiProviderForm.tsx @@ -10,8 +10,9 @@ import { } from "lucide-react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { toast } from "sonner"; +import { toast } from "@/lib/toast"; import { Button } from "@/components/ui/button"; +import { HoverTip } from "@/components/ui/hover-tip"; import { Form, FormField, @@ -1716,16 +1717,18 @@ export function PiProviderForm({ required={!isEdit || model.hasName} className="min-w-0 flex-1" /> - + + + {isExpanded && ( @@ -1985,7 +1988,7 @@ export function PiProviderForm({ "default", ) } - className="h-4 w-4 accent-primary" + className="ui-radio" /> {t( "pi.form.thinkingLevelFollowDefault", @@ -2007,7 +2010,7 @@ export function PiProviderForm({ "value", ) } - className="h-4 w-4 accent-primary" + className="ui-radio" />