diff --git a/src/main/__tests__/agent-shells.test.ts b/src/main/__tests__/agent-shells.test.ts index 3d67216bc..9b10df961 100644 --- a/src/main/__tests__/agent-shells.test.ts +++ b/src/main/__tests__/agent-shells.test.ts @@ -250,3 +250,38 @@ describe('AgentShells · 收工', () => { expect(second.info.id).not.toBe(first.info.id) }) }) + +describe('AgentShells · 界面旁观', () => { + it('★ peek 不消费模型的缓冲 —— 界面看过一眼,BashOutput 照样读得到', async () => { + const shells = new AgentShells() + const { info, process } = adopt(shells) + process.emit('stdout', 'ready\n') + process.emit('stderr', 'warn\n') + await flush() + expect(shells.peek(info.id)?.output).toBe('ready\nwarn\n') + expect(shells.read(info.id).stdout).toBe('ready\n') + // 模型读走之后,界面的尾巴不跟着清空 + expect(shells.peek(info.id)?.output).toBe('ready\nwarn\n') + }) + + it('peek 认不出的 id 返回 undefined,不 throw —— 界面上那条可能刚被淘汰', () => { + expect(new AgentShells().peek('bash_404')).toBeUndefined() + }) + + it('开、杀、退出各通知一次;输出增长不通知', async () => { + const shells = new AgentShells() + const listener = vi.fn() + shells.onChange(listener) + const first = adopt(shells) + expect(listener).toHaveBeenCalledTimes(1) + first.process.emit('stdout', 'log line\n') + await flush() + expect(listener).toHaveBeenCalledTimes(1) + shells.kill(first.info.id) + expect(listener).toHaveBeenCalledTimes(2) + const second = adopt(shells, { callId: 'call_2' }) + second.process.finish(0) + await flush() + expect(listener).toHaveBeenCalledTimes(4) + }) +}) diff --git a/src/main/agent-shells.ts b/src/main/agent-shells.ts index 042a6c0ed..e44d1253e 100644 --- a/src/main/agent-shells.ts +++ b/src/main/agent-shells.ts @@ -43,6 +43,14 @@ interface BackgroundEntry { info: BackgroundShellInfo stdout: Stream stderr: Stream + /** + * 给界面看的输出尾巴(stdout 与 stderr 按到达顺序交错)。 + * + * ★ 与上面两条缓冲**分开记**:那两条是「读走即清空」,专为 `BashOutput` 的 + * 「两次读之间不重不漏」服务。界面若也去读它们,就会把模型还没读到的输出吃掉 —— + * 模型下一次 `BashOutput` 什么都拿不到,而它会据此断定「服务没有任何输出」。 + */ + tail: string process: AdoptableProcess /** 环境租约。★ 必须活到进程结束——SSH 连接空闲 60s 就会被回收,见 manager.ts。 */ release: () => void @@ -69,6 +77,9 @@ function append(stream: Stream, chunk: string): void { stream.dropped = true } +/** 尾巴只留最后这么多字符。界面上只看最后几十行,不需要更多。 */ +const TAIL_CHARS = 8 * 1024 + /** 读走并清空。`filter` 是按**行**过滤的正则,调用方已经筛过 ReDoS。 */ function take(stream: Stream, filter: RegExp | undefined): string { const text = stream.buffer @@ -87,6 +98,30 @@ export class AgentShells { * 而两边都不会报错。 */ private sequence = 0 + private readonly listeners = new Set<() => void>() + + /** + * 后台列表变了(新开、退出、被杀、被淘汰)。返回退订函数。 + * + * ★ 只说「变了」,不带增量:监听方(IPC 广播)每次发全量,丢过一次通知也能自己收敛。 + * 输出增长**不**算变化 —— 一个刷日志的 dev server 每秒几十次,全量广播会被它淹没。 + */ + onChange(listener: () => void): () => void { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } + + private changed(): void { + for (const listener of this.listeners) { + try { + listener() + } catch { + // 一个坏掉的监听方不能挡住进程状态本身的推进 + } + } + } // ─────────────────────────── 前台 ─────────────────────────── @@ -160,6 +195,7 @@ export class AgentShells { }, stdout: emptyStream(), stderr: emptyStream(), + tail: '', process: req.process, release: req.release } @@ -175,7 +211,11 @@ export class AgentShells { 代价:一条序列恰好被切在两个 data 事件之间时剥不干净(极少见, 残留是几个可见字符,不会影响后续行)。 */ - source.on('data', (chunk: string) => append(stream, stripAnsi(chunk))) + source.on('data', (chunk: string) => { + const text = stripAnsi(chunk) + append(stream, text) + entry.tail = (entry.tail + text).slice(-TAIL_CHARS) + }) // 进程被杀时管道会抛 EPIPE/ECONNRESET。它不是一个要上报的错误, // 而没有这个监听器的话 Node 会把它升级成 uncaughtException 打死主进程。 source.on('error', () => {}) @@ -195,8 +235,10 @@ export class AgentShells { } entry.info.endedAt = Date.now() this.evict() + this.changed() }) this.evict() + this.changed() return { ...entry.info } } @@ -229,6 +271,7 @@ export class AgentShells { entry.info.status = 'killed' entry.info.endedAt = Date.now() entry.process.kill() + this.changed() } return { ...entry.info } } @@ -237,6 +280,16 @@ export class AgentShells { return [...this.background.values()].map((entry) => ({ ...entry.info })) } + /** + * 界面看一眼输出尾巴。**不消费**模型那两条缓冲(理由见 `BackgroundEntry.tail`)。 + * 找不到时返回 `undefined` 而不是 throw:界面上那条可能刚好被淘汰,这不是错误。 + */ + peek(id: string): { info: BackgroundShellInfo; output: string } | undefined { + const entry = this.background.get(id) + if (entry === undefined) return undefined + return { info: { ...entry.info }, output: entry.tail } + } + /** * 应用退出 / 测试收尾:杀光所有后台进程。 * @@ -254,6 +307,7 @@ export class AgentShells { } this.background.clear() this.foreground.clear() + this.changed() } /** 只淘汰**已结束**的,从最早结束的那条开始。还在跑的一律不动。 */ diff --git a/src/main/index.ts b/src/main/index.ts index 16be67c61..2fb1c887d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -292,7 +292,7 @@ function createMainWindow( /* ★ **跟着用户的主题偏好走,不能写死。** 这个色是窗口在渲染层画出第一帧之前 露出来的底色 —— 写死深色的话,浅色用户看到的是「深色空块 → 界面出来 → 啪一下 - 变浅」。两个值取自 theme.css 里的 `--color-app`(深 #2a2d2b / 浅 #f2eee6), + 变浅」。两个值取自 theme.css 里的 `--color-app`(深 #252527 / 浅 #ececf0), 改那边记得同步这里。 ★ **不能用 `store.getSettings()`。** 建窗现在排在启动迁移闸门之前,而闸门 @@ -300,7 +300,16 @@ function createMainWindow( 随后真正的 `openDatabase()` 直接抛错,启动死在那里。 `peekThemePreference()` 自己开一个只读连接,不碰那个句柄。 */ - backgroundColor: resolveTheme(peekThemePreference(join(resolveDataRoot(), DB_FILENAME))) === 'light' ? '#f2eee6' : '#2a2d2b', + backgroundColor: resolveTheme(peekThemePreference(join(resolveDataRoot(), DB_FILENAME))) === 'light' ? '#ececf0' : '#252527', + /* + ★ 苹果风格(Liquid Glass):macOS 上窗口本身是毛玻璃,桌面壁纸透过来 —— + 渲染层在 `data-vibrancy` 下把窗口底和两块主面板改成半透明(见 theme.css 末尾)。 + vibrancy 只有在背景色全透明时才看得见,所以这里把上面那个底色覆盖掉; + 首帧不会闪,因为系统材质本身就跟随深浅外观。Windows/Linux 保持实色。 + */ + ...(process.platform === 'darwin' + ? { vibrancy: 'under-window' as const, visualEffectState: 'active' as const, backgroundColor: '#00000000' } + : {}), // Windows/Linux 的任务栏与窗口图标(macOS 忽略,那边走下面的 app.dock.setIcon) icon: nativeImage.createFromPath(appIconPath), // macOS:红绿灯嵌进侧边栏(方案 §8)。Windows/Linux:只去掉系统标题栏, diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 7160877d4..7b74b7530 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -65,6 +65,7 @@ import { store } from '../state/store' import { windows, type WindowContext } from '../window/registry' +import { agentShells } from '../agent-shells' import { refreshScheduler, runScheduledTaskNow } from '../scheduled/scheduler' import { scheduledWrites, setScheduledChangeListener } from '../scheduled/bridge' import { setSchedulerRefresh } from '../scheduled/refresh' @@ -604,6 +605,10 @@ const handlers: HandlerMap = { // ── Agent 的 shell:只停这一条命令,run 继续 ── 'shell:stopToolCall': (req, ctx) => stopToolCall(req, ctx), 'shell:backgroundToolCall': (req, ctx) => backgroundToolCall(req, ctx), + // ── 后台 shell:右上角活动面板。按 id 操作,与模型的 BashOutput / KillShell 同一张表 ── + 'shell:listBackground': ({ workspaceId }) => agentShells.list().filter((shell) => shell.workspaceId === workspaceId), + 'shell:peekBackground': ({ id }) => agentShells.peek(id) ?? null, + 'shell:killBackground': ({ id }) => agentShells.peek(id) === undefined ? null : agentShells.kill(id), 'agent:backgroundSubagent': (req, ctx) => backgroundSubagent(req, ctx), // ── 步骤 8:终端 ── 'terminal:create': (req, ctx) => terminalHost.create(req, ctx.sender), @@ -1078,6 +1083,8 @@ export function registerIpc(): void { `windows.emitToAll` —— 那个模块拖着 electron,所以由这一侧装进去。 */ setScheduledChangeListener((change) => windows.emitToAll('scheduled:changed', change)) + // 同一种接线:`agent-shells.ts` 不认识窗口,广播由这一侧装进去 + agentShells.onChange(() => windows.emitToAll('shell:backgroundChanged', { shells: agentShells.list() })) /* 导入服务的接线,和上面 `setMcpChangeListener` 是同一种: diff --git a/src/renderer/index.html b/src/renderer/index.html index 844246e28..93c50f4c4 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -1,5 +1,5 @@ - +
diff --git a/src/renderer/src/components/ProviderModelMenu.tsx b/src/renderer/src/components/ProviderModelMenu.tsx index 60921f880..ae6918799 100644 --- a/src/renderer/src/components/ProviderModelMenu.tsx +++ b/src/renderer/src/components/ProviderModelMenu.tsx @@ -60,7 +60,7 @@ const PANEL = cn( ) const ITEM = cn( - 'flex w-full cursor-default select-none items-center gap-2.5 rounded-[7px] px-2.5 py-[7px]', + 'flex w-full cursor-default select-none items-center gap-2.5 rounded-[8px] px-2.5 py-[7px]', 'text-left text-[13px] text-fg outline-none transition-colors', 'data-[highlighted]:bg-tint-strong data-[disabled]:opacity-40' ) diff --git a/src/renderer/src/components/arc/action-button/action-button.module.css b/src/renderer/src/components/arc/action-button/action-button.module.css index 704de76f0..8f1035ea3 100644 --- a/src/renderer/src/components/arc/action-button/action-button.module.css +++ b/src/renderer/src/components/arc/action-button/action-button.module.css @@ -1,5 +1,5 @@ -.button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; overflow: hidden; padding: 0 var(--space-5); border: 1px solid var(--foreground); border-radius: var(--radius-control); background: var(--foreground); color: var(--background); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); } -@media (hover: hover) and (pointer: fine) { .button:hover:not(:disabled) { box-shadow: var(--shadow-resting); } } +.button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; overflow: hidden; padding: 0 var(--space-5); border: 1px solid transparent; border-radius: var(--radius-control); background: linear-gradient(to bottom, rgb(255 255 255 / .1), rgb(255 255 255 / 0)) var(--accent); color: var(--accent-foreground); box-shadow: 0 0 0 .5px color-mix(in srgb, var(--accent), black 25%), 0 1px 1.5px rgb(0 0 0 / .12); font: inherit; font-size: 13px; font-weight: 500; line-height: var(--leading-body); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); } +@media (hover: hover) and (pointer: fine) { .button:hover:not(:disabled) { background-color: color-mix(in srgb, var(--accent), white 8%); } } .button:disabled { cursor: not-allowed; } .button:disabled:not([data-state="pending"]) { opacity: .7; } .button[data-state="pending"] { cursor: progress; } diff --git a/src/renderer/src/components/arc/button/button.module.css b/src/renderer/src/components/arc/button/button.module.css index a156b98b0..bcf7ee6d0 100644 --- a/src/renderer/src/components/arc/button/button.module.css +++ b/src/renderer/src/components/arc/button/button.module.css @@ -1,4 +1,4 @@ -.button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; gap: var(--space-2); border: 1px solid transparent; border-radius: var(--radius-control); padding: 0 var(--space-4); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } +.button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; gap: var(--space-2); border: 1px solid transparent; border-radius: var(--radius-control); padding: 0 var(--space-4); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } .button:disabled { cursor: not-allowed; opacity: .52; } .button[aria-busy="true"] { cursor: progress; } /* Anchors of floating layers never scale: Radix opens on pointerdown and would measure a shrunken trigger. */ @@ -10,27 +10,29 @@ .labelContent { display: inline-flex; flex: none; align-items: center; } .labelPhase { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); white-space: nowrap; } .labelPhase svg { flex: none; } -.primary { color: var(--background); background: var(--foreground); border-color: var(--foreground); } -.secondary { color: var(--foreground); background: var(--surface); border-color: var(--border); } +/* macOS 默认按钮:强调色实底 + 顶部一道极淡的高光渐变 */ +.primary { color: var(--accent-foreground); background: linear-gradient(to bottom, rgb(255 255 255 / .1), rgb(255 255 255 / 0)) var(--accent); border-color: transparent; box-shadow: 0 0 0 .5px color-mix(in srgb, var(--accent), black 25%), 0 1px 1.5px rgb(0 0 0 / .12); } +/* macOS 普通按钮:白底(深色下是一层亮灰) + 发丝收边 + 一点投影,没有实线描边 */ +.secondary { color: var(--foreground); background: var(--surface); border-color: var(--border-strong); } + .ghost { color: var(--text-secondary); background: transparent; border-color: transparent; } -.danger { color: var(--danger); background: var(--surface); border-color: var(--border); } +.danger { color: var(--danger); background: var(--surface); border-color: var(--border-strong); } /* A disabled outlined button keeps its shape and fades only its label, so it still reads as a button that is off, not as plain text. */ -.secondary:disabled, .danger:disabled { opacity: 1; color: var(--text-muted); background: var(--surface-muted); border-color: var(--border); } -.danger:disabled { color: color-mix(in oklab, var(--danger) 55%, var(--surface-muted)); } +.secondary:disabled, .danger:disabled { opacity: .55; color: var(--text-muted); } +.danger:disabled { color: color-mix(in oklab, var(--danger) 55%, var(--surface)); } .ghost:disabled { opacity: 1; color: var(--text-muted); } @media (hover: hover) and (pointer: fine) { - .primary:hover:not(:disabled) { opacity: .91; box-shadow: var(--shadow-resting); } - .secondary:hover:not(:disabled) { background: var(--surface-muted); box-shadow: var(--shadow-resting); } + .primary:hover:not(:disabled) { background-color: color-mix(in srgb, var(--accent), white 8%); } + .secondary:hover:not(:disabled), .danger:hover:not(:disabled) { background-color: color-mix(in srgb, var(--surface), var(--foreground) 4%); } .ghost:hover:not(:disabled) { color: var(--foreground); background: var(--surface-muted); } - .danger:hover:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); } } /* Color feedback on press, so touch and menu triggers (which never scale) still answer the finger. */ -.primary:active:not(:disabled) { opacity: .84; } -.secondary:active:not(:disabled), .ghost:active:not(:disabled) { color: var(--foreground); background: var(--surface-muted); } -.danger:active:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); } -.sm { min-height: var(--control-height-sm); padding-inline: var(--space-3); font-size: var(--text-sm); } -.md { min-height: var(--control-height-md); } -.lg { min-height: var(--control-height-lg); padding-inline: var(--space-5); font-size: var(--text-sm); } +.primary:active:not(:disabled) { background-color: color-mix(in srgb, var(--accent), black 14%); } +.secondary:active:not(:disabled), .danger:active:not(:disabled) { background-color: color-mix(in srgb, var(--surface), var(--foreground) 10%); } +.ghost:active:not(:disabled) { color: var(--foreground); background: color-mix(in srgb, var(--foreground) 10%, transparent); } +.sm { min-height: var(--control-height-sm); padding-inline: 12px; font-size: 12px; } +.md { min-height: var(--control-height-md); padding-inline: 14px; font-size: 13px; } +.lg { min-height: var(--control-height-lg); padding-inline: var(--space-5); font-size: 13px; } .loader { position: absolute; left: 50%; top: 50%; display: grid; width: var(--space-4); height: var(--space-4); margin: calc(var(--space-4) / -2); } .spinner { border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; } .loadingLabel { opacity: 0; } diff --git a/src/renderer/src/components/arc/context-menu/context-menu.module.css b/src/renderer/src/components/arc/context-menu/context-menu.module.css index 208c51caa..26e8cd727 100644 --- a/src/renderer/src/components/arc/context-menu/context-menu.module.css +++ b/src/renderer/src/components/arc/context-menu/context-menu.module.css @@ -1,13 +1,15 @@ .target { display: block; outline: none; } /* Motion scales the menu from the pointer (transform-origin is set inline) and fades it out faster than it arrives. */ -.menu { position: fixed; z-index: 100; min-width: min(13rem, calc(100vw - 16px)); max-width: calc(100vw - 16px); max-height: min(300px, calc(100vh - 16px)); overflow-y: auto; padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); outline: none; } -.highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: calc(var(--radius-panel) - 6px); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); } -.highlight[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); } -.item { position: relative; display: flex; width: 100%; min-height: 34px; align-items: center; gap: 9px; padding: 0 11px; border: 0; border-radius: calc(var(--radius-panel) - 6px); background: transparent; color: inherit; font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); line-height: var(--leading-body); text-align: left; cursor: pointer; outline: none; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-fast) var(--ease-standard); animation: context-item-in var(--duration-standard) var(--ease-enter) both; animation-delay: calc(min(var(--i, 0), 4) * 35ms); } +.menu { position: fixed; z-index: 100; min-width: min(13rem, calc(100vw - 16px)); max-width: calc(100vw - 16px); max-height: min(300px, calc(100vh - 16px)); overflow-y: auto; padding: 5px; border: 0; border-radius: 10px; background: var(--material-menu); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); color: var(--foreground); box-shadow: var(--shadow-floating); outline: none; } +.highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: 5px; background: var(--accent); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); } +.highlight[data-tone="danger"] { background: var(--danger); } +.item { position: relative; display: flex; width: 100%; min-height: 24px; align-items: center; gap: 8px; padding: 0 9px; border: 0; border-radius: 5px; background: transparent; color: inherit; font: inherit; font-size: 13px; letter-spacing: var(--tracking-body); line-height: var(--leading-body); text-align: left; cursor: pointer; outline: none; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-fast) var(--ease-standard); animation: context-item-in var(--duration-standard) var(--ease-enter) both; animation-delay: calc(min(var(--i, 0), 4) * 35ms); } /* The gliding highlight is the focus indicator, as in the dropdown menu. */ .item:focus-visible { outline: none; } +/* 选中行压在强调色上,文字和图标翻成白色 */ +.item:focus:not(:disabled), .item:focus:not(:disabled) .icon { color: var(--accent-foreground); } /* Rows press with a tint, not a scale. */ -.item:active:not(:disabled) { background: color-mix(in oklab, var(--foreground) 5%, transparent); } +.item:active:not(:disabled) { background: transparent; } .item:disabled { opacity: .45; cursor: default; } .item.destructive { color: var(--danger); } .icon { display: inline-flex; flex: none; width: 16px; align-items: center; justify-content: center; color: var(--text-secondary); } diff --git a/src/renderer/src/components/arc/dialog/dialog.module.css b/src/renderer/src/components/arc/dialog/dialog.module.css index ce024a9ff..94b58855e 100644 --- a/src/renderer/src/components/arc/dialog/dialog.module.css +++ b/src/renderer/src/components/arc/dialog/dialog.module.css @@ -1,6 +1,6 @@ -.overlay { position: fixed; inset: 0; z-index: 50; background: oklch(10% 0 0 / .46); backdrop-filter: blur(7px); } +.overlay { position: fixed; inset: 0; z-index: 50; background: rgb(0 0 0 / .28); } /* Centered with auto margins (like a native modal dialog) so transform stays free for the entrance spring. */ -.content { position: fixed; z-index: 51; inset: 0; width: min(calc(100vw - var(--space-8)), 440px); height: fit-content; max-height: calc(100vh - var(--space-8)); max-height: calc(100dvh - var(--space-8)); margin: auto; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-surface); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); } +.content { position: fixed; z-index: 51; inset: 0; width: min(calc(100vw - var(--space-8)), 440px); height: fit-content; max-height: calc(100vh - var(--space-8)); max-height: calc(100dvh - var(--space-8)); margin: auto; overflow: auto; border: 0; border-radius: var(--radius-surface); background: var(--material-sheet); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); color: var(--foreground); box-shadow: var(--shadow-floating); } /* Motion animates both layers. Keyframes only run when DialogContent sits under a bare Radix root. */ .keyframes.overlay[data-state="open"] { animation: overlay-in var(--duration-standard) var(--ease-enter) both; } .keyframes.overlay[data-state="closed"] { animation: overlay-out var(--duration-fast) var(--ease-standard) both; } @@ -9,23 +9,23 @@ .content:focus { outline: none; } /* While the layers leave, clicks pass through, so the trigger can reopen the dialog mid-exit. Radix sets pointer-events inline on the overlay. */ .overlay[data-state="closed"], .content[data-state="closed"] { pointer-events: none !important; } -.header { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-5); padding: var(--space-6); border-bottom: 1px solid var(--border); } -.title { margin: 0; font-family: var(--font-body); font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); line-height: var(--leading-body); } -.description { margin: var(--space-2) 0 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); } +.header { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-5); padding: var(--space-5) var(--space-5) var(--space-3); border-bottom: 0; } +.title { margin: 0; font-family: var(--font-body); font-size: 15px; font-weight: 600; letter-spacing: var(--tracking-body); line-height: var(--leading-body); } +.description { margin: var(--space-1) 0 0; color: var(--text-secondary); font-size: 13px; line-height: var(--leading-body); } .title, .description { position: relative; overflow-wrap: anywhere; } .swap { display: block; } /* Line height sits on the text itself, so a page-level heading reset cannot shrink the title and pull the close button off its line. */ .title .swap { line-height: var(--leading-body); } /* A compact icon button: quick press, spring release. The negative margin centres it on the title's first line. */ -.close { flex: 0 0 auto; margin-block: calc((var(--text-lg) * var(--leading-body) - var(--space-8)) / 2); display: grid; place-items: center; width: var(--space-8); height: var(--space-8); border: 1px solid var(--border); border-radius: var(--radius-control); background: color-mix(in oklab, var(--surface-muted) 50%, transparent); color: var(--text-secondary); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); } -@media (hover: hover) and (pointer: fine) { .close:hover { background: var(--surface-muted); color: var(--foreground); } } +.close { flex: 0 0 auto; margin-block: calc((15px * var(--leading-body) - 22px) / 2); display: grid; place-items: center; width: 22px; height: 22px; border: 0; border-radius: 999px; background: color-mix(in srgb, var(--foreground) 8%, transparent); color: var(--text-secondary); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); } +@media (hover: hover) and (pointer: fine) { .close:hover { background: color-mix(in srgb, var(--foreground) 14%, transparent); color: var(--foreground); } } .close:active { transform: scale(.96); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); } -.body { padding: var(--space-6); font-size: var(--text-sm); line-height: var(--leading-body); } +.body { padding: var(--space-2) var(--space-5) var(--space-5); font-size: 13px; line-height: var(--leading-body); } @keyframes overlay-in { from { opacity: 0; } } @keyframes overlay-out { to { opacity: 0; } } @keyframes content-fade-in { from { opacity: 0; } } -@keyframes content-rise-in { from { transform: translateY(8px) scale(.96); } } -@keyframes content-out { to { opacity: 0; transform: translateY(4px) scale(.98); } } +@keyframes content-rise-in { from { transform: scale(.94); } } +@keyframes content-out { to { opacity: 0; transform: scale(.97); } } @keyframes content-fade-out { to { opacity: 0; } } @media (prefers-reduced-motion: reduce) { .keyframes.overlay[data-state="open"], .keyframes.content[data-state="open"] { animation: overlay-in var(--duration-instant) var(--ease-standard) both; } diff --git a/src/renderer/src/components/arc/dropdown-menu/dropdown-menu.module.css b/src/renderer/src/components/arc/dropdown-menu/dropdown-menu.module.css index 5d6138668..b4b745b21 100644 --- a/src/renderer/src/components/arc/dropdown-menu/dropdown-menu.module.css +++ b/src/renderer/src/components/arc/dropdown-menu/dropdown-menu.module.css @@ -1,8 +1,8 @@ -.trigger { display: inline-flex; max-width: 100%; min-height: var(--control-height-sm); align-items: center; gap: var(--space-2); padding: 0 var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: background-color var(--duration-instant) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } -@media (hover: hover) and (pointer: fine) { .trigger:hover { border-color: var(--border-strong); background: var(--surface-muted); box-shadow: var(--shadow-resting); } } +.trigger { display: inline-flex; max-width: 100%; min-height: var(--control-height-sm); align-items: center; gap: var(--space-2); padding: 0 var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-control); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: background-color var(--duration-instant) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } +@media (hover: hover) and (pointer: fine) { .trigger:hover { background: color-mix(in srgb, var(--surface), var(--foreground) 4%); } } /* The trigger anchors the menu, so press feedback stays in color. A scaled rect would be measured on pointerdown and shift the menu. */ -.trigger[data-state="open"] { border-color: var(--border-strong); background: var(--surface-muted); box-shadow: none; } -.trigger:active { border-color: var(--border-strong); background: color-mix(in oklab, var(--surface-muted), var(--border) 55%); box-shadow: none; } +.trigger[data-state="open"] { background: color-mix(in srgb, var(--surface), var(--foreground) 8%); } +.trigger:active { background: color-mix(in srgb, var(--surface), var(--foreground) 10%); } .item:focus-visible { outline: none; } .triggerIcon { display: inline-flex; flex: none; color: var(--text-secondary); } .label { position: relative; display: inline-flex; min-width: 0; flex: 0 1 auto; overflow-x: clip; } @@ -12,7 +12,7 @@ .trigger[data-state="open"] .chevron { transform: rotate(180deg); } /* The menu grows from the trigger edge: offset toward the trigger, scale from the Radix origin, fade quickly. Transitions instead of keyframes, so a close that interrupts the open (or a reopen during the close) reverses from where it is. */ -.menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: 60; min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); } +.menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: 60; min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: 5px; border: 0; border-radius: 10px; background: var(--material-menu); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); } .menu[data-side="top"] { --menu-y: 5px; } .menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; } .menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; } @@ -22,11 +22,13 @@ } /* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */ .menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity 130ms var(--ease-standard), transform 130ms var(--ease-standard); animation: menu-exit 130ms linear both; pointer-events: none; } -.highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: calc(var(--radius-panel) - 6px); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); } -.highlight[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); } -.item { position: relative; display: flex; min-height: 36px; align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-panel) - 6px); color: var(--foreground); font-size: var(--text-sm); cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms); } -.icon { display: inline-flex; width: 17px; color: var(--text-secondary); } -.separator { height: 1px; margin: var(--space-1) calc(var(--space-1) * -1); background: var(--border-subtle); } +/* macOS 菜单的选中行:强调色实底 + 白字 */ +.highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: 5px; background: var(--accent); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); } +.highlight[data-tone="danger"] { background: var(--danger); } +.item { position: relative; display: flex; min-height: 24px; align-items: center; gap: 8px; padding: 0 9px; border-radius: 5px; color: var(--foreground); font-size: 13px; cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms); } +.icon { display: inline-flex; width: 16px; color: var(--text-secondary); } +.item[data-highlighted], .item[data-highlighted] .icon { color: var(--accent-foreground); } +.separator { height: 1px; margin: 5px 9px; background: var(--border); } .destructive { color: var(--danger); } .destructive .icon { color: var(--danger); } .item[data-disabled] { opacity: .45; cursor: default; } diff --git a/src/renderer/src/components/arc/empty-state/empty-state.module.css b/src/renderer/src/components/arc/empty-state/empty-state.module.css index 127e96770..02636551d 100644 --- a/src/renderer/src/components/arc/empty-state/empty-state.module.css +++ b/src/renderer/src/components/arc/empty-state/empty-state.module.css @@ -1,9 +1,9 @@ .root { display: flex; width: 100%; flex-direction: column; align-items: center; padding: clamp(var(--space-8), 8vw, var(--space-12)) var(--space-5); text-align: center; } -.icon { position: relative; display: grid; width: var(--space-12); height: var(--space-12); place-items: center; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface-muted); color: var(--text-secondary); animation: empty-state-icon var(--duration-considered) var(--ease-enter) both; } +.icon { position: relative; display: grid; width: var(--space-12); height: var(--space-12); place-items: center; border: 0; border-radius: 12px; background: color-mix(in srgb, var(--foreground) 6%, transparent); color: var(--text-secondary); animation: empty-state-icon var(--duration-considered) var(--ease-enter) both; } .glyph { display: grid; place-items: center; } .frame { width: 100%; max-width: 100%; } .copy { display: flow-root; } -.root h3 { position: relative; margin: var(--space-5) 0 0; color: var(--foreground); font-size: var(--text-base); font-weight: 500; line-height: var(--leading-body); } +.root h3 { position: relative; margin: var(--space-4) 0 0; color: var(--foreground); font-size: 15px; font-weight: 600; line-height: var(--leading-body); } .root p { position: relative; max-width: 18rem; margin: var(--space-2) auto 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); } /* Centered copy wraps into even lines, so a new description never leaves a single word hanging. */ .line { display: block; text-wrap: balance; } diff --git a/src/renderer/src/components/arc/input/input.module.css b/src/renderer/src/components/arc/input/input.module.css index fd02e4a42..0951be41d 100644 --- a/src/renderer/src/components/arc/input/input.module.css +++ b/src/renderer/src/components/arc/input/input.module.css @@ -1,11 +1,11 @@ .field { display: grid; min-width: 0; } .label { margin-bottom: var(--space-2); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); } /* Focus answers with the border colour alone (no rings or halos); the field itself never changes size. */ -.input { width: 100%; min-height: var(--control-height-md); padding: 0 var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-control); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); outline: none; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); } +.input { width: 100%; min-height: var(--control-height-md); padding: 0 var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding-inline: 14px; background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); outline: none; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } .input::placeholder { color: var(--text-muted); } -@media (hover: hover) and (pointer: fine) { .input:hover:not(:disabled, [aria-invalid="true"]) { border-color: var(--foreground); } } -.input:focus-visible { border-color: var(--foreground); } -.input[aria-invalid="true"] { border-color: var(--danger); } +@media (hover: hover) and (pointer: fine) { .input:hover:not(:disabled, [aria-invalid="true"]) { border-color: var(--border-strong); } } +.input:focus-visible { border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus-outline); } +.input[aria-invalid="true"] { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 30%, transparent); } .input:disabled { cursor: not-allowed; opacity: .5; background: var(--surface-muted); } /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */ .messageSlot { display: block; overflow: hidden; } diff --git a/src/renderer/src/components/arc/number-field/number-field.module.css b/src/renderer/src/components/arc/number-field/number-field.module.css index 14c840a67..9792e21e7 100644 --- a/src/renderer/src/components/arc/number-field/number-field.module.css +++ b/src/renderer/src/components/arc/number-field/number-field.module.css @@ -11,10 +11,10 @@ .label[data-scrub] { cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; } /* A hairline shell. Focus darkens the border only (the library draws no rings); a typed value past a limit tints it with the warning role instead, calm rather than alarming. Set --number-field-width to resize the shell. */ -.control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: 2px; border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); } +.control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: 2px; border: .5px solid var(--border-strong); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); } @media (hover: hover) and (pointer: fine) { .control:hover, .field:has(.label[data-scrub]:hover) .control { border-color: var(--border-strong); } } -.control[data-scrubbing] { border-color: var(--foreground); } -.control:has(input:focus) { border-color: var(--foreground); } +.control[data-scrubbing] { border-color: var(--accent); } +.control:has(input:focus) { border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus-outline); } .control[data-warn], .control[data-warn]:hover { border-color: color-mix(in oklab, var(--warning) 55%, var(--border)); background: color-mix(in oklab, var(--warning) 5%, var(--surface)); } .control[data-warn]:has(input:focus) { border-color: color-mix(in oklab, var(--warning) 80%, var(--border)); } .control[data-disabled] { opacity: .5; background: var(--surface-muted); } diff --git a/src/renderer/src/components/arc/progress/progress.module.css b/src/renderer/src/components/arc/progress/progress.module.css index 99829dc22..8348c565e 100644 --- a/src/renderer/src/components/arc/progress/progress.module.css +++ b/src/renderer/src/components/arc/progress/progress.module.css @@ -1 +1 @@ -.progress { width: 100%; color: var(--text-secondary); font-size: var(--text-sm); } .meta { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px; line-height: var(--leading-body); } .label { position: relative; min-width: 0; } .line { display: block; } .value { display: inline-flex; flex: none; align-items: center; gap: 4px; color: var(--text-muted); font-variant-numeric: tabular-nums; } .count { display: inline-grid; justify-items: end; } /* A hidden "100%" row reserves the widest count, so the check never shifts as the number gains a digit. */ .count::before { content: "100%"; height: 0; overflow: hidden; visibility: hidden; } .done { display: grid; place-items: center; color: var(--success); } .track { height: 7px; overflow: hidden; border: 1px solid var(--border); border-radius: 99px; background: var(--surface-muted); } .fill { display: block; width: 100%; height: 100%; border-radius: inherit; background: var(--accent); transition: background-color var(--duration-standard) var(--ease-standard); } .progress[data-complete] .fill { background: var(--success); transition-delay: 200ms; } @media (prefers-reduced-motion: reduce) { .fill, .progress[data-complete] .fill { transition: none; } } +.progress { width: 100%; color: var(--text-secondary); font-size: var(--text-sm); } .meta { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px; line-height: var(--leading-body); } .label { position: relative; min-width: 0; } .line { display: block; } .value { display: inline-flex; flex: none; align-items: center; gap: 4px; color: var(--text-muted); font-variant-numeric: tabular-nums; } .count { display: inline-grid; justify-items: end; } /* A hidden "100%" row reserves the widest count, so the check never shifts as the number gains a digit. */ .count::before { content: "100%"; height: 0; overflow: hidden; visibility: hidden; } .done { display: grid; place-items: center; color: var(--success); } .track { height: 6px; overflow: hidden; border: 0; border-radius: 99px; background: color-mix(in srgb, var(--foreground) 10%, transparent); } .fill { display: block; width: 100%; height: 100%; border-radius: inherit; background: var(--accent); transition: background-color var(--duration-standard) var(--ease-standard); } .progress[data-complete] .fill { background: var(--success); transition-delay: 200ms; } @media (prefers-reduced-motion: reduce) { .fill, .progress[data-complete] .fill { transition: none; } } diff --git a/src/renderer/src/components/arc/radio-cards/radio-cards.module.css b/src/renderer/src/components/arc/radio-cards/radio-cards.module.css index 7013105a1..cf25a9805 100644 --- a/src/renderer/src/components/arc/radio-cards/radio-cards.module.css +++ b/src/renderer/src/components/arc/radio-cards/radio-cards.module.css @@ -18,8 +18,8 @@ z-index: 1; top: 0; left: 0; - border: 1.5px solid var(--control-on); - border-radius: 20px; + border: 2px solid var(--control-on); + border-radius: 10px; pointer-events: none; } @@ -31,7 +31,7 @@ gap: 4px 12px; padding: 14px 16px; border: 1px solid var(--border); - border-radius: 20px; + border-radius: 10px; background: var(--surface); cursor: pointer; user-select: none; diff --git a/src/renderer/src/components/arc/search-field/search-field.module.css b/src/renderer/src/components/arc/search-field/search-field.module.css index ebcd097cd..4c2e56c62 100644 --- a/src/renderer/src/components/arc/search-field/search-field.module.css +++ b/src/renderer/src/components/arc/search-field/search-field.module.css @@ -1,9 +1,10 @@ .field { display: grid; gap: var(--space-2); min-width: 0; } .field label { font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); } /* Focus darkens the border; the shell never changes size. */ -.shell { display: flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-2); border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 var(--space-3); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); } -@media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: var(--foreground); } } -.shell:focus-within { border-color: var(--foreground); } +.shell { display: flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-2); /* NSSearchField:灰底圆角,聚焦时才变白并亮起焦点环 */ + border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 var(--space-3); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } +@media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: color-mix(in srgb, var(--foreground) 24%, transparent); } } +.shell:focus-within { border-color: var(--accent); background: var(--surface); box-shadow: 0 0 0 3px var(--focus-outline); } /* The glass wakes up with the field: muted at rest, full contrast while searching. */ .shell > svg { flex: 0 0 auto; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); } .shell:focus-within > svg, .shell[data-filled="true"] > svg { color: var(--foreground); } diff --git a/src/renderer/src/components/arc/segmented-control/segmented-control.module.css b/src/renderer/src/components/arc/segmented-control/segmented-control.module.css index 826a1d9d6..f855beb98 100644 --- a/src/renderer/src/components/arc/segmented-control/segmented-control.module.css +++ b/src/renderer/src/components/arc/segmented-control/segmented-control.module.css @@ -1,14 +1,15 @@ -.root { display: inline-flex; min-width: 0; max-width: 100%; flex-shrink: 1; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface-muted); } +.root { display: inline-flex; min-width: 0; max-width: 100%; flex-shrink: 1; border: 1px solid var(--border-strong); border-radius: 999px; background: var(--surface); } /* The track owns the stacking context so the gliding highlight passes under every label, not over earlier ones. */ -.track { --fade-start: 0px; --fade-end: 0px; isolation: isolate; display: flex; min-width: 0; gap: 2px; overflow-x: auto; overscroll-behavior-x: contain; padding: 3px; border-radius: inherit; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); } +.track { --fade-start: 0px; --fade-end: 0px; isolation: isolate; display: flex; min-width: 0; gap: 2px; overflow-x: auto; overscroll-behavior-x: contain; padding: 2px; border-radius: inherit; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); } .track::-webkit-scrollbar { display: none; } .track[data-fade-start] { --fade-start: 20px; } .track[data-fade-end] { --fade-end: 20px; } /* Items never scale or change weight; only the highlight travels and the label color follows it. */ -.button { position: relative; flex: 0 0 auto; min-height: var(--control-height-sm); border: 0; border-radius: calc(var(--radius-control) - 4px); padding: 0 12px; background: transparent; color: var(--text-muted); font: inherit; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); } +.button { position: relative; flex: 0 0 auto; min-height: 22px; border: 0; border-radius: 999px; padding: 0 12px; background: transparent; color: var(--text-secondary); font-size: 12px; font: inherit; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); } @media (hover: hover) and (pointer: fine) { .button:hover { color: var(--foreground); } } .button:active, .button[aria-pressed="true"] { color: var(--foreground); } -.selection { position: absolute; z-index: -1; inset: 0; border: 1px solid var(--border); border-radius: inherit; background: var(--surface); box-shadow: var(--shadow-resting); } +.selection { position: absolute; z-index: -1; inset: 0; border: 0; border-radius: inherit; background: var(--color-surface-input); box-shadow: 0 0 0 .5px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .12); } +:root[data-theme="dark"] .selection { background: color-mix(in srgb, var(--foreground) 24%, transparent); box-shadow: inset 0 .5px 0 rgb(255 255 255 / .1), 0 1px 2px rgb(0 0 0 / .3); } .label { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 6px; } @media (max-width: 380px) { .button { padding: 0 10px; } } @media (prefers-reduced-motion: reduce) { .button { transition: none; } } diff --git a/src/renderer/src/components/arc/select/select.module.css b/src/renderer/src/components/arc/select/select.module.css index 6da66f56c..9a79a7d0c 100644 --- a/src/renderer/src/components/arc/select/select.module.css +++ b/src/renderer/src/components/arc/select/select.module.css @@ -1,29 +1,31 @@ .field { display: grid; gap: var(--space-2); min-width: 0; } .field label { font-size: var(--text-sm); font-weight: 500; } /* The trigger anchors the floating menu. Radix opens on pointerdown and measures this box, so it never scales: press feedback is color only. */ -.trigger { position: relative; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); width: 100%; min-height: var(--control-height-md); border: 1px solid var(--border); border-radius: var(--radius-control); padding: 0 var(--space-3); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } -@media (hover: hover) and (pointer: fine) { .trigger:hover:not([data-disabled]) { border-color: var(--border-strong); background: var(--surface-muted); } } -.trigger:active:not([data-disabled]), .trigger[data-state="open"] { border-color: var(--border-strong); background: var(--surface-muted); } -.trigger:focus-visible { border-color: var(--border-strong); } +.trigger { position: relative; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); width: 100%; min-height: var(--control-height-md); border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 var(--space-3) 0 14px; background: var(--surface); color: var(--foreground); font: inherit; font-size: 13px; text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); } +@media (hover: hover) and (pointer: fine) { .trigger:hover:not([data-disabled]) { background: color-mix(in srgb, var(--surface), var(--foreground) 4%); } } +.trigger:active:not([data-disabled]), .trigger[data-state="open"] { background: color-mix(in srgb, var(--surface), var(--foreground) 8%); } +.trigger:focus-visible { box-shadow: 0 0 0 3px var(--focus-outline); } + .trigger[data-disabled] { opacity: .5; cursor: not-allowed; } .valueText { display: grid; flex: 1; min-width: 0; grid-template-columns: minmax(0, 1fr); } .valueText > span { grid-area: 1 / 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .valueText > [data-placeholder] { color: var(--text-muted); } .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } -.chevron { display: inline-flex; flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); } +.chevron { display: inline-flex; flex: 0 0 auto; color: var(--text-secondary); transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); } .trigger[data-state="open"] .chevron { transform: rotate(180deg); } .hint { color: var(--text-muted); font-size: var(--text-xs); } /* Radix holds the animation until the menu is positioned, so it always grows from the trigger edge. */ -.content { --menu-x: 0px; --menu-y: -6px; box-sizing: border-box; z-index: 1000; width: var(--radix-select-trigger-width); min-width: var(--radix-select-trigger-width); max-width: min(24rem, calc(100vw - 20px)); max-height: min(320px, var(--radix-select-content-available-height)); overflow: hidden; border: 1px solid var(--border); border-radius: calc(var(--radius-control) + 2px); padding: 6px; background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transform-origin: var(--radix-select-content-transform-origin); animation: select-in var(--duration-standard) var(--ease-enter) both; } +.content { --menu-x: 0px; --menu-y: -6px; box-sizing: border-box; z-index: 1000; width: var(--radix-select-trigger-width); min-width: var(--radix-select-trigger-width); max-width: min(24rem, calc(100vw - 20px)); max-height: min(320px, var(--radix-select-content-available-height)); overflow: hidden; border: 0; border-radius: 10px; padding: 5px; background: var(--material-menu); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); color: var(--foreground); box-shadow: var(--shadow-floating); transform-origin: var(--radix-select-content-transform-origin); animation: select-in var(--duration-standard) var(--ease-enter) both; } .content[data-side="top"] { --menu-y: 6px; } .content[data-side="left"] { --menu-x: 6px; --menu-y: 0px; } .content[data-side="right"] { --menu-x: -6px; --menu-y: 0px; } .content[data-state="closed"] { animation: select-out var(--duration-instant) var(--ease-standard) both; } .viewport { padding: 2px 0; } -.item { position: relative; display: flex; align-items: center; min-height: 36px; border-radius: calc(var(--radius-control) - 4px); padding: 0 34px 0 11px; color: var(--foreground); font-size: var(--text-sm); cursor: default; outline: none; user-select: none; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-instant) var(--ease-standard); } -.item[data-highlighted] { background: var(--surface-muted); } +.item { position: relative; display: flex; align-items: center; min-height: 24px; border-radius: 5px; padding: 0 28px 0 9px; color: var(--foreground); font-size: 13px; cursor: default; outline: none; user-select: none; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-instant) var(--ease-standard); } +.item[data-highlighted] { background: var(--accent); color: var(--accent-foreground); } +.item[data-highlighted] .indicator { color: var(--accent-foreground); } .item[data-disabled] { opacity: .45; } -.indicator { position: absolute; right: 10px; display: inline-flex; align-items: center; color: var(--foreground); animation: indicator-in var(--duration-standard) var(--ease-enter) 40ms both; } +.indicator { position: absolute; right: 8px; display: inline-flex; align-items: center; color: var(--foreground); animation: indicator-in var(--duration-standard) var(--ease-enter) 40ms both; } .scrollButton { display: grid; height: 28px; place-items: center; color: var(--text-muted); } @keyframes select-in { from { opacity: 0; transform: translate(var(--menu-x), var(--menu-y)) scale(.97); } } @keyframes select-out { to { opacity: 0; transform: translate(calc(var(--menu-x) / 2), calc(var(--menu-y) / 2)) scale(.98); } } diff --git a/src/renderer/src/components/arc/slider/slider.module.css b/src/renderer/src/components/arc/slider/slider.module.css index 39548600d..1c7ced375 100644 --- a/src/renderer/src/components/arc/slider/slider.module.css +++ b/src/renderer/src/components/arc/slider/slider.module.css @@ -1,4 +1,4 @@ -.root { --slider-thumb: 22px; --slider-track: 6px; display: grid; width: 100%; min-width: 0; color: var(--foreground); font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); user-select: none; -webkit-user-select: none; } +.root { --slider-thumb: 20px; --slider-track: 4px; display: grid; width: 100%; min-width: 0; color: var(--foreground); font-size: var(--text-sm); line-height: var(--leading-body); letter-spacing: var(--tracking-body); user-select: none; -webkit-user-select: none; } .header { display: flex; min-width: 0; align-items: baseline; justify-content: space-between; gap: var(--space-4); } .label { min-width: 0; font-weight: 500; } .readout { display: inline-flex; flex: none; align-items: center; gap: .3em; color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; } @@ -25,7 +25,7 @@ the track, written each frame, and cqw measures the track, so the clamp needs no JavaScript measuring. It floats higher on touch so a fingertip never hides it. */ .bubbleAnchor { --edge: calc(var(--slider-thumb) / 2); --x: calc(var(--at, 0) * 1cqw); position: absolute; right: 0; bottom: calc(50% + var(--slider-thumb) / 2 + 8px); display: flex; translate: clamp(calc(100% - var(--edge) - var(--x)), 50%, calc(100cqw + var(--edge) - var(--x))) 0; pointer-events: none; } @media (pointer: coarse) { .bubbleAnchor { bottom: calc(50% + var(--slider-thumb) / 2 + 18px); } } -.bubble { display: inline-flex; min-height: 1.75rem; align-items: center; padding: 0 var(--space-3); border: 1px solid color-mix(in oklab, var(--background) 14%, var(--foreground)); border-radius: var(--radius-pill); background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; transform-origin: 50% 100%; } +.bubble { display: inline-flex; min-height: 1.5rem; align-items: center; padding: 0 var(--space-2); border: 0; border-radius: 6px; background: var(--material-menu); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); color: var(--foreground); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; transform-origin: 50% 100%; } /* Mark labels sit on their ticks; one at a limit keeps its outer edge inside the component. */ .marks { position: relative; grid-area: 2 / 2; height: 1.25rem; margin: calc(var(--space-1) * -1) calc(var(--slider-thumb) / 2) 0; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; } .markLabel { position: absolute; top: 0; translate: -50% 0; white-space: nowrap; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); } diff --git a/src/renderer/src/components/arc/switch/switch.module.css b/src/renderer/src/components/arc/switch/switch.module.css index f98ca33a3..95484a144 100644 --- a/src/renderer/src/components/arc/switch/switch.module.css +++ b/src/renderer/src/components/arc/switch/switch.module.css @@ -1,12 +1,13 @@ .switch { display: inline-flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-3); padding: 0; border: 0; color: var(--foreground); background: transparent; font: inherit; font-size: var(--text-sm); cursor: pointer; -webkit-tap-highlight-color: transparent; } .switch:disabled { cursor: not-allowed; opacity: .5; } /* Fixed box: the thumb moves inside it with transforms, so nothing around the switch shifts. The track crossfades between the two fills. */ -.track { position: relative; box-sizing: border-box; display: flex; flex: 0 0 auto; width: 42px; height: 24px; align-items: center; padding: 3px; border-radius: var(--radius-pill); background: var(--control-track); isolation: isolate; transition: background-color var(--duration-fast) var(--ease-standard); } +.track { position: relative; box-sizing: border-box; display: flex; flex: 0 0 auto; width: 38px; height: 22px; align-items: center; padding: 2px; border-radius: var(--radius-pill); background: var(--control-track); box-shadow: inset 0 0 0 .5px rgb(0 0 0 / .06); isolation: isolate; transition: background-color var(--duration-fast) var(--ease-standard); } .track::before { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--control-on); opacity: 0; transition: opacity var(--duration-standard) var(--ease-standard); } .switch[data-state="checked"] .track::before { opacity: 1; } .thumb { flex: 0 0 auto; width: 18px; height: 18px; border-radius: var(--radius-pill); background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); will-change: transform; transition: background-color var(--duration-standard) var(--ease-standard); } .switch[data-state="checked"] .thumb { background: var(--control-thumb-on); } @media (hover: hover) and (pointer: fine) { .switch[data-state="unchecked"]:hover:not(:disabled) .track { background: var(--control-track-hover); } } .switch:focus-visible { outline: none; } +.switch:focus-visible .track { box-shadow: 0 0 0 3px var(--focus-outline); } .label { line-height: var(--leading-body); } @media (prefers-reduced-motion: reduce) { .track, .track::before, .thumb { transition: none; } } diff --git a/src/renderer/src/components/arc/textarea/textarea.module.css b/src/renderer/src/components/arc/textarea/textarea.module.css index 337fbe947..c009f37ae 100644 --- a/src/renderer/src/components/arc/textarea/textarea.module.css +++ b/src/renderer/src/components/arc/textarea/textarea.module.css @@ -1,10 +1,10 @@ .field { display: grid; min-width: 0; } .field label { margin-bottom: var(--space-2); font-size: var(--text-sm); font-weight: 500; } /* Same focus language as Input: the ring contracts onto the border while it fades in, and the box never changes size. */ -.control { width: 100%; min-height: 110px; resize: vertical; border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: var(--space-3); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); outline: none; box-shadow: 0 0 0 6px transparent; transition: border-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); } +.control { width: 100%; min-height: 110px; resize: vertical; border: 1px solid var(--border-strong); border-radius: 12px; padding: var(--space-2) var(--space-3); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); outline: none; box-shadow: 0 0 0 3px transparent; transition: border-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); } .control::placeholder { color: var(--text-muted); } -@media (hover: hover) and (pointer: fine) { .control:hover:not(:disabled, [aria-invalid="true"]) { border-color: var(--foreground); } } -.control:focus-visible { border-color: var(--foreground); box-shadow: 0 0 0 3px var(--focus-ring); outline: 2px solid transparent; outline-offset: 2px; } +@media (hover: hover) and (pointer: fine) { .control:hover:not(:disabled, [aria-invalid="true"]) { border-color: var(--border-strong); } } +.control:focus-visible { border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus-outline); outline: 2px solid transparent; outline-offset: 2px; } .control[aria-invalid="true"] { border-color: var(--danger); } .control:disabled { cursor: not-allowed; background: var(--surface-muted); opacity: .6; } /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */ diff --git a/src/renderer/src/components/arc/toast-stack/toast-stack.module.css b/src/renderer/src/components/arc/toast-stack/toast-stack.module.css index b97f23389..781a4f1f4 100644 --- a/src/renderer/src/components/arc/toast-stack/toast-stack.module.css +++ b/src/renderer/src/components/arc/toast-stack/toast-stack.module.css @@ -11,10 +11,10 @@ .list { position: relative; margin: 0; padding: 0; list-style: none; pointer-events: auto; } /* Tucked toasts scale from their top edge, so exactly the peek shows above the toast in front. */ .item { position: absolute; right: 0; bottom: 0; left: 0; transform-origin: 50% 0; } -.card { position: absolute; inset: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-raised); container-type: inline-size; touch-action: pan-y; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-standard) var(--ease-standard), border-color var(--duration-standard) var(--ease-standard); } +.card { position: absolute; inset: 0; overflow: hidden; border: 0; border-radius: 14px; background: var(--material-menu); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); color: var(--foreground); box-shadow: var(--shadow-raised); container-type: inline-size; touch-action: pan-y; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-standard) var(--ease-standard), border-color var(--duration-standard) var(--ease-standard); } .item[data-front="true"] .card, .item[data-expanded="true"] .card { box-shadow: var(--shadow-floating); } /* Tucked toasts sit a step back in tone as well as scale. */ -.item[data-front="false"][data-expanded="false"] .card { background: var(--surface-muted); } +.item[data-front="false"][data-expanded="false"] .card { background: color-mix(in srgb, var(--color-surface-raised) 92%, var(--foreground)); } .card[data-dragging] { cursor: grabbing; user-select: none; } .content { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4); } @@ -37,10 +37,10 @@ .actionSlot { display: flex; flex: none; justify-content: flex-end; margin-top: -4px; } .actionSlot[data-moving="true"] { overflow: hidden; } .action, .close { flex: none; border: 1px solid transparent; font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: scale var(--duration-spring) var(--ease-spring), background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); } -.action { height: 28px; padding: 0 var(--space-3); border-color: var(--border); border-radius: var(--radius-pill); background: var(--surface); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; } +.action { height: 24px; padding: 0 var(--space-3); border-color: transparent; border-radius: var(--radius-control); background: var(--surface); box-shadow: var(--shadow-resting); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; } .close { display: grid; width: 28px; height: 28px; margin: -4px -2px 0 0; place-items: center; border-radius: var(--radius-pill); background: transparent; color: var(--text-muted); } @media (hover: hover) and (pointer: fine) { - .action:hover { border-color: var(--border-strong); background: var(--surface-muted); } + .action:hover { background: color-mix(in srgb, var(--surface), var(--foreground) 5%); } .close:hover { background: var(--surface-muted); color: var(--foreground); } } /* Quick press, spring release. */ diff --git a/src/renderer/src/components/arc/tooltip/tooltip.module.css b/src/renderer/src/components/arc/tooltip/tooltip.module.css index 92e905dc5..dc5745027 100644 --- a/src/renderer/src/components/arc/tooltip/tooltip.module.css +++ b/src/renderer/src/components/arc/tooltip/tooltip.module.css @@ -1,4 +1,4 @@ -.tooltip { --tooltip-y: 3px; z-index: 90; max-width: 15rem; padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in oklab, var(--background) 14%, var(--foreground)); border-radius: var(--radius-control); background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 400; line-height: var(--leading-body); transform-origin: var(--radix-tooltip-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-fast) var(--ease-enter); } +.tooltip { --tooltip-y: 3px; z-index: 90; max-width: 15rem; padding: 4px 8px; border: 0; border-radius: 6px; background: var(--material-menu); -webkit-backdrop-filter: var(--material-blur); backdrop-filter: var(--material-blur); color: var(--foreground); box-shadow: var(--shadow-raised); font-size: 12px; font-weight: 400; line-height: var(--leading-body); transform-origin: var(--radix-tooltip-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-fast) var(--ease-enter); } .tooltip[data-side="bottom"] { --tooltip-y: -3px; } /* The first tooltip waits, then rises a few pixels from its trigger. Within the skip window the next one only fades. Transitions instead of keyframes, so returning to the trigger while it fades out reverses the fade instead of restarting it. */ @@ -10,8 +10,8 @@ /* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */ .tooltip[data-state="closed"] { opacity: 0; transform: scale(.98); transition: opacity var(--duration-instant) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); animation: tooltip-exit var(--duration-instant) linear both; } .text { position: relative; display: block; overflow: clip; overflow-clip-margin: var(--space-2); } -.measure { position: absolute; top: 0; left: 0; width: max-content; max-width: calc(15rem - 2 * var(--space-4) - 2px); visibility: hidden; pointer-events: none; } -.line { display: block; width: max-content; max-width: calc(15rem - 2 * var(--space-4) - 2px); } +.measure { position: absolute; top: 0; left: 0; width: max-content; max-width: calc(15rem - 16px); visibility: hidden; pointer-events: none; } +.line { display: block; width: max-content; max-width: calc(15rem - 16px); } @keyframes tooltip-exit { to { --tooltip-exit: 1; } } @media (prefers-reduced-motion: reduce) { .tooltip, .tooltip[data-state="closed"], .tooltip[data-instant][data-state$="-open"] { transform: none; transition: opacity var(--duration-instant) var(--ease-standard); } diff --git a/src/renderer/src/components/code/code.css b/src/renderer/src/components/code/code.css index 31f74a6b5..0579807aa 100644 --- a/src/renderer/src/components/code/code.css +++ b/src/renderer/src/components/code/code.css @@ -11,23 +11,23 @@ * 而且全程零报错。`.agent-markdown` 一并算作用域:markdown 正文里的围栏 * 不必再多包一层。 * - * 这些值原封不动来自 markdown.css(见那边留下的指路注释),不是重新量的。 + * 苹果风格分支:配色换成 Xcode 默认主题(Default Light / Default Dark)的语法色。 */ :is(.agent-markdown, .code-scope) { - --md-keyword: #c7a7e8; - --md-string: #a3c99e; - --md-number: #e1bb89; - --md-type: #8fc7d0; - --md-comment: #89938e; + --md-keyword: #ff7ab2; + --md-string: #ff8170; + --md-number: #d9c97c; + --md-type: #5dd8ff; + --md-comment: #7f8c98; } :root[data-theme='light'] :is(.agent-markdown, .code-scope) { - --md-keyword: #7953a1; - --md-string: #386a46; - --md-number: #985524; - --md-type: #2c687b; - --md-comment: #6a7770; + --md-keyword: #ad3da4; + --md-string: #c41a16; + --md-number: #1c00cf; + --md-type: #0b4f79; + --md-comment: #5d6c79; } :is(.agent-markdown, .code-scope) .tok-keyword, diff --git a/src/renderer/src/components/ui/ChoiceCards.tsx b/src/renderer/src/components/ui/ChoiceCards.tsx index 698affe15..8df2a5ac0 100644 --- a/src/renderer/src/components/ui/ChoiceCards.tsx +++ b/src/renderer/src/components/ui/ChoiceCards.tsx @@ -23,13 +23,13 @@ export interface ChoiceOption { */ /** 行的外框。**它不是按钮** —— 可点区是里面那层,理由见 `OptionRow`。 */ -const ROW_FRAME = 'app-no-drag rounded-[8px] border transition-[background-color,border-color] duration-150 motion-reduce:transition-none' +const ROW_FRAME = 'app-no-drag rounded-[10px] border transition-[background-color,border-color] duration-150 motion-reduce:transition-none' /** * 需求:审批选项要像一列紧凑的 agent 决策项,而不是一摞彼此争抢注意力的表单卡。 * 选中态仍保留整行槽色,确保键盘高亮与真实选择都能一眼看见。 */ -const ROW_ON = 'border-accent bg-tint' +const ROW_ON = 'border-accent/50 bg-accent/10' const ROW_OFF = 'border-transparent bg-transparent hover:border-hairline hover:bg-tint-hover/55' /** 可点区的类名。外框不可点,所以这层必须自己撑满、自己接焦点环。 */ diff --git a/src/renderer/src/components/ui/ContextMenu.tsx b/src/renderer/src/components/ui/ContextMenu.tsx index fcde10397..ad70af69d 100644 --- a/src/renderer/src/components/ui/ContextMenu.tsx +++ b/src/renderer/src/components/ui/ContextMenu.tsx @@ -142,7 +142,7 @@ export function ContextMenu({ visibility: placed === null ? 'hidden' : undefined }} className={cn( - 'app-no-drag fixed z-50 rounded-panel border-0 bg-surface-menu p-1.5 shadow-menu outline-none', + 'app-no-drag fixed z-50 rounded-[10px] border-0 bg-surface-menu p-[5px] shadow-menu outline-none', 'transition-[opacity,transform,translate,scale] duration-150 ease-panel motion-reduce:transition-none motion-reduce:transform-none motion-reduce:translate-y-0 motion-reduce:scale-100', shown && !closing ? 'translate-y-0 scale-100 opacity-100' diff --git a/src/renderer/src/components/ui/Menu.tsx b/src/renderer/src/components/ui/Menu.tsx index d6afe52b1..919867d5d 100644 --- a/src/renderer/src/components/ui/Menu.tsx +++ b/src/renderer/src/components/ui/Menu.tsx @@ -258,9 +258,9 @@ export function Menu({ visibility: pos === null ? 'hidden' : undefined }} className={cn( - 'app-no-drag scroll-thin fixed z-50 overflow-y-auto rounded-[12px]', + 'app-no-drag scroll-thin fixed z-50 overflow-y-auto rounded-[10px]', align === 'end' ? 'origin-top-right' : 'origin-top-left', - 'border-0 bg-surface-menu p-1.5 shadow-menu', + 'border-0 bg-surface-menu p-[5px] shadow-menu', 'transition-[opacity,transform,translate,scale] duration-180 ease-panel motion-reduce:transition-none motion-reduce:transform-none motion-reduce:translate-y-0 motion-reduce:scale-100', presence.shown ? 'translate-y-0 scale-100 opacity-100' @@ -327,44 +327,77 @@ export function MenuItem({ onFocus={activate} onClick={onSelect} className={cn( - 'relative flex w-full items-center gap-2.5 rounded-[8px] px-2.5 py-[7px] text-left text-[13px]', - 'transition-colors disabled:opacity-40', + 'group/item relative flex w-full items-center gap-2 rounded-[5px] px-2 py-[3px] text-left text-[13px]', + 'disabled:opacity-40', + // 苹果风格:选中行是强调色实底 + 白字(NSMenu)。 // 有 provider 时由那块滑动的高亮负责;没有时退回逐行 hover - !gliding && 'hover:bg-tint-strong disabled:hover:bg-transparent', - danger ? 'text-danger' : 'text-fg' + !gliding && 'hover:bg-accent hover:text-accent-fg disabled:hover:bg-transparent', + lit ? 'text-accent-fg' : danger ? 'text-danger' : 'text-fg' )} > {lit && ({t('devices.addDescription')}
-{t('devices.installTitle')}
+{t('devices.installHint')}
+{t(step.title)}
+{t(step.note)}
+{t('devices.installFinish')}
+{t('devices.installMore')}
+
+ {command}
+
+
+ {t(clearPassword ? 'ssh.passwordCleared' : 'ssh.passwordHint')}
diff --git a/src/renderer/src/settings/pages/model/EnabledModelList.tsx b/src/renderer/src/settings/pages/model/EnabledModelList.tsx index eb027059f..9e82a035f 100644 --- a/src/renderer/src/settings/pages/model/EnabledModelList.tsx +++ b/src/renderer/src/settings/pages/model/EnabledModelList.tsx @@ -47,7 +47,7 @@ export function EnabledModelList({ onClick={onAdd} className={cn( "app-no-drag mt-0.5 flex size-6 shrink-0 items-center justify-center", - "rounded-[7px] text-icon transition-colors hover:bg-tint hover:text-fg", + "rounded-[8px] text-icon transition-colors hover:bg-tint hover:text-fg", )} >{t("models.imageProviders")}
{t("models.firstIsDefault")}
{error}
}{error}
}
@@ -1657,7 +1657,7 @@ function CatalogModelEditor({
type="button"
onClick={() => void bind()}
disabled={busy !== null || bindingProviderId === ""}
- className="rounded-[7px] border border-border px-3 text-[11px] text-fg-muted hover:bg-tint-hover disabled:opacity-50"
+ className="rounded-[8px] border border-border px-3 text-[11px] text-fg-muted hover:bg-tint-hover disabled:opacity-50"
>
{busy === "bind" ? t("models.binding") : t("models.bindAndSave")}
@@ -1673,7 +1673,7 @@ function CatalogModelEditor({
type="button"
onClick={() => void save()}
disabled={busy !== null}
- className="rounded-[7px] bg-accent px-4 py-1.5 text-[11.5px] text-accent-fg disabled:opacity-50"
+ className="rounded-[8px] bg-accent px-4 py-1.5 text-[11.5px] text-accent-fg disabled:opacity-50"
>
{busy === "save" ? t("models.binding") : t("models.saveCatalog")}
diff --git a/src/renderer/src/settings/pages/model/ModelProtocol.tsx b/src/renderer/src/settings/pages/model/ModelProtocol.tsx
index e4d8292db..a64ce4b94 100644
--- a/src/renderer/src/settings/pages/model/ModelProtocol.tsx
+++ b/src/renderer/src/settings/pages/model/ModelProtocol.tsx
@@ -139,7 +139,7 @@ function ProtocolBindingForm({
type="button"
disabled={!changed || busy}
onClick={() => void save()}
- className="rounded-[7px] bg-accent px-3 py-1.5 text-[11px] text-accent-fg disabled:opacity-50"
+ className="rounded-[8px] bg-accent px-3 py-1.5 text-[11px] text-accent-fg disabled:opacity-50"
>
{busy ? t("models.protocolSaving") : t("models.protocolSave")}
diff --git a/src/renderer/src/settings/pages/model/ProviderAvatar.tsx b/src/renderer/src/settings/pages/model/ProviderAvatar.tsx
index b6a3ac0da..746d6dbc1 100644
--- a/src/renderer/src/settings/pages/model/ProviderAvatar.tsx
+++ b/src/renderer/src/settings/pages/model/ProviderAvatar.tsx
@@ -24,7 +24,7 @@ import { avatarInitial } from './enabled-models'
/** 两种尺寸对应两处既有几何:目录卡片 20px、列表与面板抬头 24px */
const BOX = {
sm: { box: 'size-5 rounded-[6px] text-[10px]', icon: 13 },
- md: { box: 'size-6 rounded-[7px] text-[11px]', icon: 15 }
+ md: { box: 'size-6 rounded-[8px] text-[11px]', icon: 15 }
} as const
export function ProviderAvatar({
diff --git a/src/renderer/src/settings/pages/model/ProviderCatalog.tsx b/src/renderer/src/settings/pages/model/ProviderCatalog.tsx
index 4a01d3362..e10c82046 100644
--- a/src/renderer/src/settings/pages/model/ProviderCatalog.tsx
+++ b/src/renderer/src/settings/pages/model/ProviderCatalog.tsx
@@ -133,7 +133,7 @@ export function ProviderCatalog({
onClick={() => setView("catalog")}
className={cn(
"app-no-drag flex size-6 shrink-0 items-center justify-center",
- "rounded-[7px] text-icon transition-colors hover:bg-tint hover:text-fg",
+ "rounded-[8px] text-icon transition-colors hover:bg-tint hover:text-fg",
)}
>