From fba3c90864aaed4d69c76d6ef287f0bce6aa35f2 Mon Sep 17 00:00:00 2001 From: token <61819790+239573049@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:07:22 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(ui):=20=E6=8D=A2=E6=88=90=E8=8B=B9?= =?UTF-8?q?=E6=9E=9C=E9=A3=8E=E6=A0=BC=E9=85=8D=E8=89=B2,=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E8=B7=9F=E4=B8=8A=E6=AF=9B=E7=8E=BB=E7=92=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基准色板从参考实现那组奶油/墨绿换成 macOS 系统语义色(浅 #f5f5f7/#1d1d1f,深 systemGray6→4 的阶梯),强调色两个外观都用 apple.com 的 #0071e3 —— 白字压在系统蓝 #007aff / #0a84ff 上过不了可读性护栏要求的 4.5:1。旧的墨绿整套留在 COLOR_THEMES 里当一档,默认档换成新的 apple。 控件跟着这套色走:菜单选中行改成强调色实底白字(NSMenu)、各面板统一到 8px 圆角, 输入框抽成 .apple-field、按钮抽成 .apple-btn、设置与搜索面板抽成 .apple-sheet。 macOS 上窗口开 vibrancy,渲染层按 data-vibrancy 把窗口底和主面板改成半透明玻璃。 --- src/main/index.ts | 13 +- src/renderer/index.html | 2 +- .../src/components/ProviderModelMenu.tsx | 2 +- .../action-button/action-button.module.css | 4 +- .../components/arc/button/button.module.css | 32 +- .../arc/context-menu/context-menu.module.css | 12 +- .../components/arc/dialog/dialog.module.css | 20 +- .../dropdown-menu/dropdown-menu.module.css | 22 +- .../arc/empty-state/empty-state.module.css | 4 +- .../src/components/arc/input/input.module.css | 8 +- .../arc/number-field/number-field.module.css | 6 +- .../arc/progress/progress.module.css | 2 +- .../arc/radio-cards/radio-cards.module.css | 6 +- .../arc/search-field/search-field.module.css | 7 +- .../segmented-control.module.css | 9 +- .../components/arc/select/select.module.css | 20 +- .../components/arc/slider/slider.module.css | 4 +- .../components/arc/switch/switch.module.css | 3 +- .../arc/textarea/textarea.module.css | 6 +- .../arc/toast-stack/toast-stack.module.css | 8 +- .../components/arc/tooltip/tooltip.module.css | 6 +- src/renderer/src/components/code/code.css | 22 +- .../src/components/ui/ChoiceCards.tsx | 4 +- .../src/components/ui/ContextMenu.tsx | 2 +- src/renderer/src/components/ui/Menu.tsx | 59 ++- src/renderer/src/components/ui/Select.tsx | 22 +- src/renderer/src/components/ui/Submenu.tsx | 2 +- src/renderer/src/components/ui/TextArea.tsx | 2 +- src/renderer/src/components/ui/TextInput.tsx | 2 +- src/renderer/src/main.tsx | 4 + src/renderer/src/settings/SettingsOverlay.tsx | 12 +- .../src/settings/pages/GeneralPage.tsx | 2 +- .../pages/connection/McpServerDialog.tsx | 2 +- .../connection/RemoteConnectionsPane.tsx | 4 +- .../settings/pages/model/EnabledModelList.tsx | 6 +- .../settings/pages/model/ImageModelPage.tsx | 14 +- .../src/settings/pages/model/ModelPage.tsx | 16 +- .../settings/pages/model/ModelProtocol.tsx | 2 +- .../settings/pages/model/ProviderAvatar.tsx | 2 +- .../settings/pages/model/ProviderCatalog.tsx | 12 +- .../settings/pages/model/ProviderPanel.tsx | 10 +- .../src/settings/pages/model/UsageTab.tsx | 6 +- .../settings/pages/model/VideoModelPage.tsx | 12 +- .../src/settings/pages/usage/UsagePage.tsx | 13 +- .../settings/theme-studio/DesktopPreview.tsx | 2 +- .../settings/theme-studio/ThemeStudioPane.tsx | 2 +- src/renderer/src/shell/AccountMenu.tsx | 4 +- src/renderer/src/shell/ConnectionDialogs.tsx | 2 +- .../src/shell/CreateSshWorkspaceDialog.tsx | 4 +- src/renderer/src/shell/SearchPalette.tsx | 10 +- src/renderer/src/shell/Sidebar.tsx | 8 +- src/renderer/src/shell/UpdateIndicator.tsx | 2 +- src/renderer/src/styles/arc.css | 100 ++++- src/renderer/src/styles/theme.css | 392 ++++++++++++++---- src/renderer/src/theme/widget-tokens.ts | 18 +- .../src/views/ClientTeamSelectionView.tsx | 4 +- src/renderer/src/views/WelcomeView.tsx | 2 +- .../src/views/browser/BrowserFeature.tsx | 8 +- .../src/views/chat/AttachmentTray.tsx | 2 +- src/renderer/src/views/chat/Composer.tsx | 5 +- src/renderer/src/views/chat/GoalPanel.tsx | 2 +- src/renderer/src/views/chat/ImageLightbox.tsx | 2 +- .../src/views/chat/InteractionPanel.tsx | 9 +- .../src/views/chat/MessageFileRef.tsx | 2 +- src/renderer/src/views/chat/PendingQueue.tsx | 2 +- src/renderer/src/views/chat/Thread.tsx | 4 +- .../src/views/extensions/modes/ModesPanel.tsx | 2 +- src/renderer/src/views/files/FilesView.tsx | 6 +- .../src/views/files/TreeHoverGlide.tsx | 2 +- src/renderer/src/views/git/GitFeature.tsx | 2 +- .../src/views/scheduled/ScheduledFeature.tsx | 4 +- .../src/views/terminal/TerminalView.tsx | 69 +-- src/shared/domain/theme.ts | 146 ++++--- 73 files changed, 831 insertions(+), 414 deletions(-) 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/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 && ( )} {icon !== undefined && ( - {icon} + + {icon} + )} {children} {description !== undefined && ( - {description} + + {description} + )} {accelerator !== undefined && ( - {accelerator} + + {accelerator} + )} {checked !== undefined && ( - + )} ) } export function MenuSeparator(): ReactNode { - return
+ return
} export function MenuLabel({ children }: { children: ReactNode }): ReactNode { - return
{children}
+ return
{children}
} diff --git a/src/renderer/src/components/ui/Select.tsx b/src/renderer/src/components/ui/Select.tsx index 8164192c2..dd3dc1ba0 100644 --- a/src/renderer/src/components/ui/Select.tsx +++ b/src/renderer/src/components/ui/Select.tsx @@ -82,12 +82,12 @@ export function Select({ aria-label={ariaLabel} disabled={disabled} className={cn( - "app-no-drag group flex h-7 w-full items-center gap-1.5 rounded-[9px] border border-border", - "bg-surface-field px-2.5 text-left text-[11.5px] text-fg outline-none", + "app-no-drag group flex h-7 w-full items-center gap-1.5 rounded-full border border-[var(--border-strong)]", + "bg-[var(--surface)] px-3 text-left text-[12px] text-fg outline-none", "transition-[background-color,border-color,box-shadow,border-radius] duration-150", - "hover:bg-tint focus-visible:border-fg-faint focus-visible:ring-2 focus-visible:ring-fg-faint/15", - // 展开时朝向面板的那条边先收平,面板再分离出去 —— 两者像是一整块被捏开 - "data-[state=open]:border-fg-faint data-[state=open]:bg-tint data-[state=open]:rounded-b-[4px]", + "hover:bg-tint focus-visible:ring-3 focus-visible:ring-accent/50", + // 苹果风格:弹出按钮展开时只压暗一档,不再改圆角 + "data-[state=open]:bg-tint", className, )} > @@ -113,8 +113,8 @@ export function Select({ className={cn( "app-no-drag max-h-[min(240px,var(--radix-select-content-available-height))]", inModal ? "z-[150]" : "z-[60]", - "w-[var(--radix-select-trigger-width)] overflow-hidden rounded-[12px] border-0", - "bg-surface-menu p-1.5 shadow-menu outline-none", + "w-[var(--radix-select-trigger-width)] overflow-hidden rounded-[10px] border-0", + "bg-surface-menu p-[5px] shadow-menu outline-none", // 入场:从触发器那一侧轻轻展开。transform-origin 用 Radix 算好的锚点,翻到上方时自动跟着翻 "origin-[var(--radix-select-content-transform-origin)]", "data-[state=open]:animate-[select-unfold_0.22s_cubic-bezier(0.16,1,0.3,1)] motion-reduce:animate-none", @@ -135,14 +135,14 @@ export function Select({ {option.label} - + diff --git a/src/renderer/src/components/ui/Submenu.tsx b/src/renderer/src/components/ui/Submenu.tsx index 39e328424..dd2c697c4 100644 --- a/src/renderer/src/components/ui/Submenu.tsx +++ b/src/renderer/src/components/ui/Submenu.tsx @@ -90,7 +90,7 @@ export function Submenu({ // 量到位置之前先渲染出来(否则量不到高度),但别让人看见左上角那一帧 visibility: placed === null ? 'hidden' : undefined }} - className="app-no-drag scroll-thin fixed z-[60] overflow-y-auto rounded-panel border-0 bg-surface-menu p-1.5 shadow-menu" + className="app-no-drag scroll-thin fixed z-[60] overflow-y-auto rounded-[10px] border-0 bg-surface-menu p-[5px] shadow-menu" > {children}
, diff --git a/src/renderer/src/components/ui/TextArea.tsx b/src/renderer/src/components/ui/TextArea.tsx index 66e693c00..0c2fe97ca 100644 --- a/src/renderer/src/components/ui/TextArea.tsx +++ b/src/renderer/src/components/ui/TextArea.tsx @@ -50,7 +50,7 @@ export function TextArea({ onChange={(e) => setDraft(e.target.value)} onBlur={() => commit(onCommit)} className={cn( - 'app-no-drag selectable w-full resize-none rounded-[8px] border border-border', + 'apple-field app-no-drag selectable w-full resize-none rounded-[8px] border border-border', 'bg-surface-field px-2.5 py-2 text-[13px] leading-[1.6] text-fg outline-none', 'transition-colors placeholder:text-fg-faint focus:border-fg-faint disabled:opacity-40', className diff --git a/src/renderer/src/components/ui/TextInput.tsx b/src/renderer/src/components/ui/TextInput.tsx index 78c8e1e73..4d8f5badc 100644 --- a/src/renderer/src/components/ui/TextInput.tsx +++ b/src/renderer/src/components/ui/TextInput.tsx @@ -84,7 +84,7 @@ export function TextInput({ ref={fieldRef} data-state={hasError ? 'error' : success ? 'success' : 'idle'} className={cn( - 'app-no-drag flex items-center gap-2 rounded-[9px] border transition-[border-color,box-shadow] duration-200', + 'apple-field app-no-drag flex items-center gap-2', 'bg-surface-field', size === 'sm' ? 'h-7 px-2' : 'h-8 px-2.5', hasError diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index c378fb57c..083d156a1 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -5,6 +5,10 @@ import './styles/theme.css' import './styles/arc-integration.css' import { I18nProvider } from './i18n' import { MigrationGateHost } from './views/MigrationGate' +import { IS_MAC } from './lib/platform' + +// 苹果风格:macOS 窗口开了 vibrancy(见 main/index.ts),界面据此把窗口底和主面板改成半透明玻璃 +if (IS_MAC) document.documentElement.dataset.vibrancy = 'true' const container = document.getElementById('root') if (!container) throw new Error('#root not found') diff --git a/src/renderer/src/settings/SettingsOverlay.tsx b/src/renderer/src/settings/SettingsOverlay.tsx index 2bd4f0d1c..90fe51afe 100644 --- a/src/renderer/src/settings/SettingsOverlay.tsx +++ b/src/renderer/src/settings/SettingsOverlay.tsx @@ -160,15 +160,15 @@ export function SettingsOverlay({ aria-modal="true" aria-label={t('common.settings')} > - {/* 纯黑 35% —— 这个数是从参考图反解出来的,见 theme.css 的 --color-scrim */} -
+ {/* 苹果风格:轻遮罩、不模糊 —— 面板自己是毛玻璃,背景糊掉反而看不出玻璃感 */} +
- + {pageLabel(t, p.id)} diff --git a/src/renderer/src/settings/pages/GeneralPage.tsx b/src/renderer/src/settings/pages/GeneralPage.tsx index bc18d93f9..02debefdd 100644 --- a/src/renderer/src/settings/pages/GeneralPage.tsx +++ b/src/renderer/src/settings/pages/GeneralPage.tsx @@ -485,7 +485,7 @@ function RoleModelPickerComponent({ } // 视觉上照抄 `Select` 的触发器类名,这一屏其余控件才不会显得它是外来的。 triggerClassName={cn( - 'group flex h-7 w-full items-center gap-1.5 rounded-[7px] border border-border', + 'apple-field group flex h-7 w-full items-center gap-1.5 rounded-[8px] border border-border', 'bg-surface-field px-2 text-left text-[11.5px] text-fg outline-none', 'transition-[background-color,border-color,box-shadow] duration-150', 'hover:bg-tint focus-visible:border-fg-faint focus-visible:ring-2 focus-visible:ring-fg-faint/15' diff --git a/src/renderer/src/settings/pages/connection/McpServerDialog.tsx b/src/renderer/src/settings/pages/connection/McpServerDialog.tsx index 57bb3f521..327b3f6c3 100644 --- a/src/renderer/src/settings/pages/connection/McpServerDialog.tsx +++ b/src/renderer/src/settings/pages/connection/McpServerDialog.tsx @@ -338,7 +338,7 @@ export function McpServerDialog({ } onChange={(e) => set("secretsText", e.target.value)} className={cn( - "app-no-drag selectable w-full resize-none rounded-[8px] border border-hairline", + "apple-field app-no-drag selectable w-full resize-none rounded-[8px] border border-hairline", "bg-surface-field px-2.5 py-2 font-mono text-[12px] leading-[1.7] text-fg outline-none", "placeholder:text-fg-faint focus:border-fg-faint", )} diff --git a/src/renderer/src/settings/pages/connection/RemoteConnectionsPane.tsx b/src/renderer/src/settings/pages/connection/RemoteConnectionsPane.tsx index 1405818f3..30c6a579c 100644 --- a/src/renderer/src/settings/pages/connection/RemoteConnectionsPane.tsx +++ b/src/renderer/src/settings/pages/connection/RemoteConnectionsPane.tsx @@ -147,7 +147,7 @@ export function ServerEditor({ editing, hasPassword, onClose }: { editing: Conne {field(t('ssh.mode'),
{ setMode(value as 'manual' | 'config'); setConfirmed(false) }} />
)} {field(t(mode === 'config' ? 'ssh.alias' : 'ssh.host'),
- {mode === 'manual' && setPort(event.target.value)} className="selectable h-8 w-[72px] shrink-0 rounded-[8px] border border-border bg-surface-field px-2.5 text-[13px] text-fg outline-none focus:border-fg-faint aria-invalid:border-danger" />} + {mode === 'manual' && setPort(event.target.value)} className="apple-field selectable h-8 w-[72px] shrink-0 rounded-[8px] border border-border bg-surface-field px-2.5 text-[13px] text-fg outline-none focus:border-fg-faint aria-invalid:border-danger" />}
, t(mode === 'config' ? 'ssh.configHint' : 'ssh.addressHint'))} {mode === 'config' && field(t('ssh.configFile'), filePicker(configFile, setConfigFile, t('ssh.configFile')))} @@ -166,7 +166,7 @@ export function ServerEditor({ editing, hasPassword, onClose }: { editing: Conne setPassword(event.target.value)} - className="selectable h-8 min-w-0 flex-1 rounded-[8px] border border-border bg-surface-field px-2.5 text-[13px] text-fg outline-none focus:border-fg-faint disabled:opacity-40" /> + className="apple-field selectable h-8 min-w-0 flex-1 rounded-[8px] border border-border bg-surface-field px-2.5 text-[13px] text-fg outline-none focus:border-fg-faint disabled:opacity-40" /> {hasPassword && !clearPassword && }

{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", )} > @@ -62,7 +62,7 @@ export function EnabledModelList({ onClick={() => onSelect(e.provider.id)} aria-current={e.provider.id === selectedId} className={cn( - "app-no-drag flex w-full items-center gap-2 rounded-[9px] px-1.5 py-2 text-left", + "app-no-drag flex w-full items-center gap-2 rounded-[8px] px-1.5 py-2 text-left", "transition-colors", e.provider.id === selectedId ? "bg-tint" : "hover:bg-tint/60", )} @@ -105,7 +105,7 @@ export function EnabledModelList({ type="button" onClick={onAdd} className={cn( - "app-no-drag mt-1 flex w-full items-center gap-1.5 rounded-[9px] px-1.5 py-2", + "app-no-drag mt-1 flex w-full items-center gap-1.5 rounded-[8px] px-1.5 py-2", "text-[12.5px] text-fg-muted transition-colors hover:bg-tint/60 hover:text-fg", )} > diff --git a/src/renderer/src/settings/pages/model/ImageModelPage.tsx b/src/renderer/src/settings/pages/model/ImageModelPage.tsx index d11355c45..ce257d6e9 100644 --- a/src/renderer/src/settings/pages/model/ImageModelPage.tsx +++ b/src/renderer/src/settings/pages/model/ImageModelPage.tsx @@ -96,7 +96,7 @@ export function ImageModelPage({ settings, patch }: Pick ) : (
-
+
{t("models.imageEmptyHint")}
@@ -223,7 +223,7 @@ function ImageGenModelRow({ } // 视觉上照抄 `Select` 的触发器类名(与 GeneralPage 的 RoleModelPicker 同款) triggerClassName={cn( - "group flex h-7 w-full items-center gap-1.5 rounded-[7px] border border-border", + "apple-field group flex h-7 w-full items-center gap-1.5 rounded-[8px] border border-border", "bg-surface-field px-2 text-left text-[11.5px] text-fg outline-none", "transition-[background-color,border-color,box-shadow] duration-150", "hover:bg-tint focus-visible:border-fg-faint focus-visible:ring-2 focus-visible:ring-fg-faint/15" @@ -250,12 +250,12 @@ function ImageProviderList({ entries, onAdd, selectedId, onSelect }: { entries:

{t("models.imageProviders")}

{t("models.firstIsDefault")}

- +
    - {entries.map((entry) =>
  • )} + {entries.map((entry) =>
  • )}
- +
); } @@ -287,8 +287,8 @@ function ImageProviderCatalog({ providers, onClose, onAdded }: { providers: read }; return (
-
{t("models.imageAddProvider")}
- {custom ?
setCustom(false)} onAdded={onAdded} />
:
{t("models.imageCatalogHint")}
{error !== null &&

{error}

}
} />
{list.map((item) => { const added = providers.some((p) => p.id === item.id); return ; })}
} +
{t("models.imageAddProvider")}
+ {custom ?
setCustom(false)} onAdded={onAdded} />
:
{t("models.imageCatalogHint")}
{error !== null &&

{error}

}
} />
{list.map((item) => { const added = providers.some((p) => p.id === item.id); return ; })}
}
); } diff --git a/src/renderer/src/settings/pages/model/ModelPage.tsx b/src/renderer/src/settings/pages/model/ModelPage.tsx index 8105f11c3..5446c1f0b 100644 --- a/src/renderer/src/settings/pages/model/ModelPage.tsx +++ b/src/renderer/src/settings/pages/model/ModelPage.tsx @@ -421,7 +421,7 @@ function ModelConsole(): ReactNode {
{catalogError !== null && ( -
+
{t("chat.status.error")}: {catalogError}
)} @@ -480,7 +480,7 @@ function ModelConsole(): ReactNode { onClick={() => setEditingModel({ model: blankCatalogModel(), isNew: true }) } - className="inline-flex h-8 items-center gap-1 rounded-[7px] bg-accent px-2.5 text-[11.5px] text-accent-fg" + className="inline-flex h-8 items-center gap-1 rounded-[8px] bg-accent px-2.5 text-[11.5px] text-accent-fg" > {t("models.addModel")} @@ -798,7 +798,7 @@ function EditorSelect({ } const editorInputClass = - "h-8 w-full rounded-[9px] border border-border bg-surface-field px-2.5 text-[11.5px] text-fg outline-none transition-[border-color,box-shadow] duration-200 focus:border-fg-faint focus:ring-2 focus:ring-fg-faint/15"; + "apple-field h-8 w-full text-[12px]"; function cloneCatalogDraft(model: ModelCatalogDefinition): CatalogDraft { return { @@ -1112,7 +1112,7 @@ function CatalogModelEditor({
{error !== null && ( -
+
{t("chat.status.error")}: {error}
)} @@ -1417,7 +1417,7 @@ function CatalogModelEditor({ thinking.mode === "unsupported" || thinking.mode === "always" } className={cn( - "flex h-8 items-center justify-end rounded-[7px] border border-border bg-surface-field px-2", + "flex h-8 items-center justify-end rounded-[8px] border border-border bg-surface-field px-2", (thinking.mode === "unsupported" || thinking.mode === "always") && "opacity-60", @@ -1587,7 +1587,7 @@ function CatalogModelEditor({ value={patchText} onChange={(event) => setPatchText(event.target.value)} spellCheck={false} - className="selectable mt-2 min-h-[112px] w-full resize-y rounded-[7px] border border-border bg-surface-field p-2 font-mono text-[10.5px] text-fg outline-none focus:border-fg-faint" + className="apple-field selectable mt-2 min-h-[112px] w-full resize-y rounded-[8px] border border-border bg-surface-field p-2 font-mono text-[10.5px] text-fg outline-none focus:border-fg-faint" />

@@ -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", )} > @@ -152,7 +152,7 @@ export function ProviderCatalog({ onClick={onClose} 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", )} > @@ -248,7 +248,7 @@ function CustomProviderCard({ onOpen }: { onOpen: () => void }): ReactNode { type="button" onClick={onOpen} className={cn( - "app-no-drag min-w-0 rounded-[9px] border border-border bg-canvas px-2.5 py-2 text-left", + "app-no-drag min-w-0 rounded-[8px] border border-border bg-canvas px-2.5 py-2 text-left", "transition-colors hover:bg-tint/60", )} > @@ -291,7 +291,7 @@ function ImportFromAppsCard({ onOpen }: { onOpen: () => void }): ReactNode { type="button" onClick={onOpen} className={cn( - "app-no-drag min-w-0 rounded-[9px] border border-border bg-canvas px-2.5 py-2 text-left", + "app-no-drag min-w-0 rounded-[8px] border border-border bg-canvas px-2.5 py-2 text-left", "transition-colors hover:bg-tint/60", )} > @@ -464,7 +464,7 @@ function ImportProvidersForm({ disabled={!selectable} onClick={() => toggle(p.sourceKey)} className={cn( - "app-no-drag flex w-full min-w-0 items-start gap-2.5 rounded-[9px] border px-2.5 py-2 text-left transition-colors", + "app-no-drag flex w-full min-w-0 items-start gap-2.5 rounded-[8px] border px-2.5 py-2 text-left transition-colors", selectable ? "border-border hover:bg-tint/60" : "border-hairline opacity-60", active && selectable && "border-accent bg-accent/5", )} @@ -745,7 +745,7 @@ function PresetCard({ return (

diff --git a/src/renderer/src/settings/pages/model/ProviderPanel.tsx b/src/renderer/src/settings/pages/model/ProviderPanel.tsx index 30547d171..d0802f788 100644 --- a/src/renderer/src/settings/pages/model/ProviderPanel.tsx +++ b/src/renderer/src/settings/pages/model/ProviderPanel.tsx @@ -993,7 +993,7 @@ export function ProviderPanel({ aria-label={t("provider.modelIdLabel")} placeholder={t("provider.modelIdPlaceholder")} disabled={busy} - className="selectable min-w-0 flex-1 rounded-[7px] border border-border bg-canvas px-2 text-[12px] text-fg outline-none placeholder:text-fg-faint focus:border-fg-faint" + className="apple-field selectable min-w-0 flex-1 rounded-[8px] border border-border bg-canvas px-2 text-[12px] text-fg outline-none placeholder:text-fg-faint focus:border-fg-faint" /> @@ -544,7 +544,7 @@ function RequestLogs({ type="button" onClick={onPrevious} disabled={page === null || page.offset === 0 || loading} - className="rounded-[7px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" + className="rounded-[8px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" > {t('usage.previousPage')} @@ -552,7 +552,7 @@ function RequestLogs({ type="button" onClick={onNext} disabled={page === null || page.offset + page.items.length >= page.total || loading} - className="rounded-[7px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" + className="rounded-[8px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" > {t('usage.nextPage')} diff --git a/src/renderer/src/settings/pages/model/VideoModelPage.tsx b/src/renderer/src/settings/pages/model/VideoModelPage.tsx index e23bcec77..95cefdcbe 100644 --- a/src/renderer/src/settings/pages/model/VideoModelPage.tsx +++ b/src/renderer/src/settings/pages/model/VideoModelPage.tsx @@ -174,7 +174,7 @@ export function VideoModelPage({ settings, patch }: Pick ) : selected === null ? (
-
+
{t('videoGen.catalogHint')}
@@ -208,7 +208,7 @@ function ProviderList({ rows, models, disabled, selectedId, onAdd, onSelect }: {

{t('videoGen.providers')}

{t('videoGen.catalogHint')}

-
@@ -219,7 +219,7 @@ function ProviderList({ rows, models, disabled, selectedId, onAdd, onSelect }: { type="button" disabled={disabled} onClick={() => onSelect(row.id)} - className={cn('flex w-full items-center gap-2 rounded-[9px] px-1.5 py-2 text-left disabled:opacity-40', row.id === selectedId ? 'bg-tint' : 'hover:bg-tint/60')} + className={cn('flex w-full items-center gap-2 rounded-[8px] px-1.5 py-2 text-left disabled:opacity-40', row.id === selectedId ? 'bg-tint' : 'hover:bg-tint/60')} > @@ -415,7 +415,7 @@ function ProviderCatalog({ rows, busy, error, onClose, onAdd, onAdded, onBusyCha
) : (
-
+
{t('videoGen.catalogHint')}
@@ -656,7 +656,7 @@ function VideoProviderForm({ provider, preset, models, onCancel, onSaved, onBusy
updateDraft(entry.key, { model })} ariaLabel={`${t('provider.modelIdLabel')} ${String(index + 1)}`} placeholder={compatiblePreset?.models.find((model) => model.profileId === entry.profileId)?.model ?? 'model-id'} disabled={saving} />
- +

{t('videoGen.profile')}

@@ -751,7 +751,7 @@ function VideoGenModelRow({ settings, patch, models, providers, loaded }: { sett
{triggerLabel}} - triggerClassName={cn('group flex h-7 w-full items-center gap-1.5 rounded-[7px] border border-border', 'bg-surface-field px-2 text-left text-[11.5px] text-fg outline-none', 'transition-[background-color,border-color,box-shadow] duration-150', 'hover:bg-tint focus-visible:border-fg-faint focus-visible:ring-2 focus-visible:ring-fg-faint/15')} + triggerClassName={cn('apple-field group flex h-7 w-full items-center gap-1.5 rounded-[8px] border border-border', 'bg-surface-field px-2 text-left text-[11.5px] text-fg outline-none', 'transition-[background-color,border-color,box-shadow] duration-150', 'hover:bg-tint focus-visible:border-fg-faint focus-visible:ring-2 focus-visible:ring-fg-faint/15')} className="w-full" align="end" width={280} diff --git a/src/renderer/src/settings/pages/usage/UsagePage.tsx b/src/renderer/src/settings/pages/usage/UsagePage.tsx index 4558989b0..4065d47cb 100644 --- a/src/renderer/src/settings/pages/usage/UsagePage.tsx +++ b/src/renderer/src/settings/pages/usage/UsagePage.tsx @@ -170,7 +170,7 @@ export function UsagePage(): ReactNode { @@ -475,7 +475,7 @@ function RequestLogs({ type="button" onClick={onPrevious} disabled={page === null || page.offset === 0 || loading} - className="rounded-[7px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" + className="rounded-[8px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" > {t('usage.previousPage')} @@ -483,7 +483,7 @@ function RequestLogs({ type="button" onClick={onNext} disabled={page === null || page.offset + page.items.length >= page.total || loading} - className="rounded-[7px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" + className="rounded-[8px] border border-border px-2 py-1 text-fg-muted disabled:opacity-35" > {t('usage.nextPage')} @@ -540,8 +540,11 @@ function RequestRow({ {record.httpStatus ?? diff --git a/src/renderer/src/settings/theme-studio/DesktopPreview.tsx b/src/renderer/src/settings/theme-studio/DesktopPreview.tsx index f8418ad8a..5c5b8e6b2 100644 --- a/src/renderer/src/settings/theme-studio/DesktopPreview.tsx +++ b/src/renderer/src/settings/theme-studio/DesktopPreview.tsx @@ -68,7 +68,7 @@ export function DesktopPreview({ profile, images }: { profile: ThemeProfile; ima

theme.ts

TypeScript · 4 KB

{t('themeStudio.ready')}
:
- {content === 'editor' ?
{'export const theme = {\n  canvas: "#1e2020",\n  accent: "#71b98c"\n}'}
: <>
https://nextcowork.local

NextCoWork

{t('themeStudio.previewReply')}

} + {content === 'editor' ?
{'export const theme = {\n  canvas: "#1c1c1e",\n  accent: "#0071e3"\n}'}
: <>
https://nextcowork.local

NextCoWork

{t('themeStudio.previewReply')}

}
}
{bottom &&
diff --git a/src/renderer/src/settings/theme-studio/ThemeStudioPane.tsx b/src/renderer/src/settings/theme-studio/ThemeStudioPane.tsx index ae9886786..ce700c77d 100644 --- a/src/renderer/src/settings/theme-studio/ThemeStudioPane.tsx +++ b/src/renderer/src/settings/theme-studio/ThemeStudioPane.tsx @@ -154,7 +154,7 @@ export function ThemeStudioPane({ settings, patch }: Omit{GROUPS.map((g) => )}
{group === 'wallpaper' && <> - changeWallpaper(resolveImageTheme(e.target.value, images))}>{[...IMAGE_THEMES, ...images].map((i) => { const key = i.source.kind === 'builtin' ? builtinThemeNameKey('image', i.id) : null return })} diff --git a/src/renderer/src/shell/AccountMenu.tsx b/src/renderer/src/shell/AccountMenu.tsx index a1ce61774..bc9b2832a 100644 --- a/src/renderer/src/shell/AccountMenu.tsx +++ b/src/renderer/src/shell/AccountMenu.tsx @@ -262,12 +262,12 @@ function BalanceRow({ }): ReactNode { const { t } = useI18n() return ( -
+
diff --git a/src/renderer/src/shell/SearchPalette.tsx b/src/renderer/src/shell/SearchPalette.tsx index 69922f34b..21eb3e5da 100644 --- a/src/renderer/src/shell/SearchPalette.tsx +++ b/src/renderer/src/shell/SearchPalette.tsx @@ -174,14 +174,14 @@ export function SearchPalette({ aria-modal="true" aria-label={t('search.title')} > -
+
@@ -249,7 +249,7 @@ export function SearchPalette({ key={hit.messageId} value={hit.messageId} onSelect={() => select(hit)} - className="flex cursor-pointer items-start gap-2.5 rounded-[9px] px-2.5 py-2 aria-selected:bg-tint-hover" + className="flex cursor-pointer items-start gap-2.5 rounded-[8px] px-2.5 py-2 aria-selected:bg-accent/15" > diff --git a/src/renderer/src/shell/Sidebar.tsx b/src/renderer/src/shell/Sidebar.tsx index 86c0937af..78faaed7f 100644 --- a/src/renderer/src/shell/Sidebar.tsx +++ b/src/renderer/src/shell/Sidebar.tsx @@ -548,7 +548,7 @@ function SessionGroupBlock({ setMenu({ session, position: { x: event.clientX, y: event.clientY } }) }} className={cn( - 'flex w-full items-center gap-2 rounded-[7px] px-2.5 py-1.5 text-left text-[12.5px] transition-colors', + 'flex w-full items-center gap-2 rounded-[8px] px-2.5 py-1.5 text-left text-[12.5px] transition-colors', active ? 'bg-canvas text-fg' : 'text-fg-muted hover:bg-tint-hover hover:text-fg' )} > @@ -667,14 +667,14 @@ function SessionGroupBlock({ } > - setRenameDraft(event.target.value)} className="selectable mt-2 h-9 w-full rounded-[8px] border border-border bg-surface-field px-2.5 text-[13px] text-fg outline-none focus:border-fg-faint" /> + setRenameDraft(event.target.value)} className="apple-field selectable mt-2 h-9 w-full rounded-[8px] border border-border bg-surface-field px-2.5 text-[13px] text-fg outline-none focus:border-fg-faint" /> ) } function MenuAction({ icon, label, danger = false, onSelect }: { icon: ReactNode; label: string; danger?: boolean; onSelect: () => void }): ReactNode { - return + return } /** @@ -722,7 +722,7 @@ function ArchivedSessionItem({ setMenu({ x: event.clientX, y: event.clientY }) }} aria-current={active ? 'page' : undefined} - className={cn('flex w-full items-center gap-2 rounded-[7px] px-2.5 py-1.5 text-left text-[12.5px] transition-colors', + className={cn('flex w-full items-center gap-2 rounded-[8px] px-2.5 py-1.5 text-left text-[12.5px] transition-colors', active ? 'bg-canvas text-fg' : 'text-fg-muted hover:bg-tint-hover hover:text-fg')} > {session.title} diff --git a/src/renderer/src/shell/UpdateIndicator.tsx b/src/renderer/src/shell/UpdateIndicator.tsx index 3f1e1bf88..cc9367050 100644 --- a/src/renderer/src/shell/UpdateIndicator.tsx +++ b/src/renderer/src/shell/UpdateIndicator.tsx @@ -231,7 +231,7 @@ export function UpdateIndicator(): ReactNode { style={{ left: pos?.left ?? -9999, top: pos?.top ?? -9999, width: CARD_WIDTH }} // z-150:portal 到 body 的浮层那一档,见 theme.css 末尾 z 轴那段。 // 和 `Tooltip` 同档 —— 它俩不会同时出现在同一个位置上。 - className="app-no-drag fixed z-[150] rounded-card border border-stroke bg-surface-raised p-3 text-fg shadow-lg" + className="app-no-drag fixed z-[150] rounded-[14px] bg-surface-menu p-3 text-fg shadow-menu" >
{t('about.updates.banner', { version: info.version })} diff --git a/src/renderer/src/styles/arc.css b/src/renderer/src/styles/arc.css index 3c98f551f..458faf725 100644 --- a/src/renderer/src/styles/arc.css +++ b/src/renderer/src/styles/arc.css @@ -11,16 +11,100 @@ 工具类 —— 往 Arc 组件上传 `h-7`、`px-3` 这类类名是不会生效的,要调尺寸就改下面的 token。 */ @import '../components/arc/foundation.css'; -@import '@fontsource-variable/inter/index.css'; -@import '@fontsource-variable/geist/index.css'; /* - 桌面端密度。Arc 默认是网页尺寸(36 / 44 / 50px 高、18px 圆角),而本应用的 - 标题栏、工具栏、设置行都是按 24–34px 排的。只改 token,不改组件内部样式。 + 桌面端密度,按 macOS 控件尺寸取:小号 22、常规 28、大号 32;圆角 6px(NSButton 的圆角)。 */ :root { - --control-height-sm: 1.75rem; - --control-height-md: 2rem; - --control-height-lg: 2.25rem; - --radius-control: 0.5rem; + --control-height-sm: 1.5rem; + --control-height-md: 1.75rem; + --control-height-lg: 2rem; + /* Liquid Glass:控件一律胶囊形 */ + --radius-control: 999px; + --radius-surface: 0.875rem; +} + +/* + ── 苹果风格:把 Arc 的中性灰 token 接到应用的主题 token 上 ── + + Arc 自带一套和应用无关的黑白灰(`--foreground` / `--surface` / `--accent` …),于是 + 按钮、开关、下拉一直是「另一套皮肤」,换颜色主题也不跟着走。这里让它们**全部从 + `--color-*` 取值** —— 那组变量由 `theme/apply.ts` 写成行内样式,深浅两套都会落到这里。 + + ★ 选择器特异性是 (0,3,0),而且排在 foundation.css 之后,所以压得住它的 + `:root[data-theme="dark"]` 和 `:root[data-theme="dark"][data-accent="…"]` —— + 不然深色下 Arc 会退回它自己的那套灰 / 预设强调色(index.html 曾经挂着 + `data-accent="green"`,深色下开关和主按钮因此一直是绿的)。 +*/ +:root:is([data-theme], :not([data-theme])):is([data-accent], :not([data-accent])) { + --background: var(--color-canvas); + /* 输入框、下拉、次要按钮的底:半透明玻璃填充 + 一圈浅色描边(见 --border-strong) */ + --surface: color-mix(in srgb, var(--color-surface-input) 62%, transparent); + --surface-raised: var(--color-surface-raised); + --surface-muted: var(--color-tint); + --foreground: var(--color-fg); + --text-secondary: var(--color-fg-muted); + --text-muted: var(--color-fg-faint); + --border: color-mix(in srgb, var(--color-fg) 12%, transparent); + --border-subtle: color-mix(in srgb, var(--color-fg) 7%, transparent); + --border-strong: color-mix(in srgb, var(--color-fg) 14%, transparent); + --accent: var(--color-accent); + --accent-strong: color-mix(in srgb, var(--color-accent), black 18%); + --accent-subtle: color-mix(in srgb, var(--color-accent) 14%, transparent); + --accent-foreground: var(--color-accent-fg); + --danger: var(--color-danger); + --warning: var(--color-warning); + --success: #34c759; + + /* 开关、单选、滑块:macOS 的「开」就是强调色,滑块恒为白色圆钮 */ + --control-on: var(--color-accent); + --control-glyph: #ffffff; + --control-track: color-mix(in srgb, var(--color-fg) 14%, transparent); + --control-track-hover: color-mix(in srgb, var(--color-fg) 20%, transparent); + --control-thumb: #ffffff; + --control-thumb-on: #ffffff; + --control-fill: var(--color-accent); + --control-thumb-shadow: 0 0 0 0.5px rgb(0 0 0 / 0.12), 0 1px 1px rgb(0 0 0 / 0.1), 0 2px 4px rgb(0 0 0 / 0.14); + + /* 键盘焦点:macOS 的焦点环 —— 强调色半透明外扩 3px */ + --focus-outline: color-mix(in srgb, var(--color-accent) 55%, transparent); + --focus-outline-width: 3px; + --focus-outline-offset: 0px; + + /* 阴影:贴近 NSWindow / NSPopover 的投影 —— 一圈 0.5px 发丝收边 + 大而淡的柔影 */ + --shadow-resting: 0 0 0 0.5px rgb(0 0 0 / 0.06), 0 1px 1.5px rgb(0 0 0 / 0.1); + --shadow-raised: 0 0 0 0.5px rgb(0 0 0 / 0.08), 0 4px 12px rgb(0 0 0 / 0.1); + --shadow-floating: 0 0 0 0.5px rgb(0 0 0 / 0.12), 0 12px 36px rgb(0 0 0 / 0.18), 0 3px 10px rgb(0 0 0 / 0.08); + + /* 字体:系统字体(SF Pro),不再加载 Inter / Geist */ + --font-display: var(--font-sans); + --font-body: var(--font-sans); + --tracking-display: -0.022em; + --tracking-body: -0.006em; + --leading-body: 1.38; + + /* 苹果的弹簧:快起、轻微回弹 */ + --ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1); + --ease-enter: cubic-bezier(0.2, 0.9, 0.3, 1); + + /* 毛玻璃材质(菜单 / 弹窗 / 提示共用) */ + --material-menu: color-mix(in srgb, var(--color-surface-raised) 78%, transparent); + --material-sheet: color-mix(in srgb, var(--color-surface-raised) 88%, transparent); + --material-blur: saturate(180%) blur(24px); +} + +:root[data-theme='dark']:is([data-accent], :not([data-accent])) { + --success: #30d158; + --border: color-mix(in srgb, var(--color-fg) 14%, transparent); + --border-strong: color-mix(in srgb, var(--color-fg) 16%, transparent); + --surface: color-mix(in srgb, var(--color-fg) 6%, transparent); + --control-track: color-mix(in srgb, var(--color-fg) 18%, transparent); + --control-track-hover: color-mix(in srgb, var(--color-fg) 24%, transparent); + --control-thumb-shadow: 0 0 0 0.5px rgb(0 0 0 / 0.3), 0 1px 2px rgb(0 0 0 / 0.35); + --shadow-resting: 0 0 0 0.5px rgb(0 0 0 / 0.4), 0 1px 1.5px rgb(0 0 0 / 0.3); + --shadow-raised: 0 0 0 0.5px rgb(0 0 0 / 0.5), 0 6px 16px rgb(0 0 0 / 0.35); + /* 深色浮层多一道内侧高光,像 macOS 深色菜单的顶边 */ + --shadow-floating: inset 0 0.5px 0 rgb(255 255 255 / 0.08), 0 0 0 0.5px rgb(0 0 0 / 0.6), + 0 16px 40px rgb(0 0 0 / 0.45), 0 3px 10px rgb(0 0 0 / 0.3); + --material-menu: color-mix(in srgb, var(--color-surface-raised) 72%, transparent); } diff --git a/src/renderer/src/styles/theme.css b/src/renderer/src/styles/theme.css index c9ec49c8e..e5645f83c 100644 --- a/src/renderer/src/styles/theme.css +++ b/src/renderer/src/styles/theme.css @@ -3,6 +3,10 @@ @import 'tailwindcss'; /* + ★ 苹果风格分支(feat/apple-style-ui):下面 @theme / 浅色 / 首屏兜底三处的色值已换成 + macOS 系统色,权威来源是 `shared/domain/theme.ts` 的 `BASE`(那边有选值理由)。 + 本段以下关于「量自参考实现」的说明保留作历史,数值以 BASE 为准。 + 方案 §8 的 token 层。数值不是调出来的,是**从参考实现的截图上量出来的** —— 量法是区域直方图,不是取单像素: @@ -81,31 +85,32 @@ */ @theme { /* ── 结构:量自 docs/image-latest/dark.png,全部 100% 单色 ── */ - --color-app: #2a2d2b; /* 窗口底 —— 面板之间露出来的那条缝(实测 3px 宽) */ - --color-canvas: #1e2020; /* 内容面板 / 激活的外层 Tab / 内层 Tab 条 */ - --color-surface: #232726; /* 侧边栏 */ - --color-chrome: #343a37; /* 外层 Tab 条 —— 深色下**比侧边栏亮**,见上 §1 */ - --color-surface-raised: #2a2d2b; /* 侧边栏里的分区卡 —— 和窗口底同色,深浅都成立 */ - --color-surface-input: #2a2d2b; /* 画布上的输入框 —— 深色下并入 raised,浅色下是纯白 */ + --color-app: #252527; /* 窗口底 —— 面板之间露出来的那条缝(实测 3px 宽) */ + --color-canvas: #1c1c1e; /* 内容面板 / 激活的外层 Tab / 内层 Tab 条 */ + --color-surface: #202022; /* 侧边栏 */ + --color-chrome: #2c2c2e; /* 外层 Tab 条 —— 深色下**比侧边栏亮**,见上 §1 */ + --color-surface-raised: #2c2c2e; /* 侧边栏里的分区卡 —— 和窗口底同色,深浅都成立 */ + --color-surface-input: #2c2c2e; /* 画布上的输入框 —— 深色下并入 raised,浅色下是纯白 */ /* **浮层里的**输入框(设置浮层的搜索框、端口框、代理地址框)。这批截图里没有浮层, 按 v1.1.15 的步长推:raised 往上一级(+10)。深色下和 chrome 撞值,浅色下分开 —— 和 surface/chrome 那一对正好是镜像,同一个道理:两个 token 在一个主题里恰好相等, 不等于它们是同一个东西。 */ - --color-surface-field: #343a37; - --color-surface-sunken: #252b28; /* 按下 / 激活的图标按钮:从 chrome 往下挖(推的) */ + --color-surface-field: #3a3a3c; + --color-surface-sunken: #232325; /* 按下 / 激活的图标按钮:从 chrome 往下挖(推的) */ /* 菜单与子菜单以底色和阴影浮起,避免被 border 的对比度护栏提亮出一圈描边。 */ - --color-surface-menu: color-mix(in srgb, var(--color-surface-raised), var(--color-surface-field) 50%); - --shadow-menu: 0 10px 15px -3px rgb(0 0 0 / 0.2), 0 4px 6px -4px rgb(0 0 0 / 0.2); + /* 苹果风格:菜单是半透明材质,配合下面 `.bg-surface-menu` 的背景模糊读作毛玻璃(NSVisualEffectView) */ + --color-surface-menu: color-mix(in srgb, var(--color-surface-raised) 80%, transparent); + --shadow-menu: 0 0 0 0.5px rgb(0 0 0 / 0.12), 0 10px 30px rgb(0 0 0 / 0.18), 0 2px 8px rgb(0 0 0 / 0.08); /* ── 交互态 ── */ - --color-tint: #2b2e2d; /* 激活的内层 Tab(实测)/ 控件槽 / 用户气泡 */ - --color-tint-hover: #363b38; /* 悬停(推的:+11,带绿) */ - --color-tint-strong: #3c423e; /* 菜单行悬停 / 分类 chip(推的:+17) */ + --color-tint: #2c2c2e; /* 激活的内层 Tab(实测)/ 控件槽 / 用户气泡 */ + --color-tint-hover: #323234; /* 悬停(推的:+11,带绿) */ + --color-tint-strong: #3a3a3c; /* 菜单行悬停 / 分类 chip(推的:+17) */ - --color-border: #2b2e2d; /* 与 tint 同值 —— 沿用 v1.1.15 里两者相等的关系 */ + --color-border: #48484a; /* 与 tint 同值 —— 沿用 v1.1.15 里两者相等的关系 */ /* ★ **`stroke` 和 `border` 值相同,但不是同一个东西 —— 别合并。** @@ -124,18 +129,18 @@ stroke → 卡片 / 面板 / 区块的收边,**纯装饰**,永远是设计值 分隔线仍然走 `hairline`(卡片**内部**的横线),这三个别混。 */ - --color-stroke: #2b2e2d; - --color-hairline: #252727; /* 内容区里的行分隔线,比 canvas 只高一点点 */ - - --color-fg: #ececec; - --color-fg-muted: #959897; - --color-fg-faint: #7b7d7c; - - --color-icon: #cfcfcf; /* ★ 中性灰,**不是**强调色 —— 见上 §3。缩图下界 */ - --color-accent: #36d285; /* ★ 亮绿(激活芯片实心底 / 「完全访问」盾牌与文字) */ - --color-accent-fg: #0a2615; /* 压在 accent 上的文字 —— 深绿近黑 */ - --color-accent-soft: #5a8a72; /* 正文里的装饰性小图标(推的:accent 去饱和) */ - --color-danger: #d9614e; /* 未量到,沿用 v1.1.15 */ + --color-stroke: #2e2e30; + --color-hairline: #28282a; /* 内容区里的行分隔线,比 canvas 只高一点点 */ + + --color-fg: #f5f5f7; + --color-fg-muted: #aeaeb2; + --color-fg-faint: #8e8e93; + + --color-icon: #c7c7cc; /* ★ 中性灰,**不是**强调色 —— 见上 §3。缩图下界 */ + --color-accent: #0071e3; /* ★ 亮绿(激活芯片实心底 / 「完全访问」盾牌与文字) */ + --color-accent-fg: #ffffff; /* 压在 accent 上的文字 —— 深绿近黑 */ + --color-accent-soft: #4a6c99; /* 正文里的装饰性小图标(推的:accent 去饱和) */ + --color-danger: #ff453a; /* 未量到,沿用 v1.1.15 */ /* ★ **`warning` 之前根本不存在。** @@ -151,7 +156,8 @@ 色值取 accent 的补色方向(琥珀),在 canvas #1e2020 上对比度 ≈ 8.9:1。 */ - --color-warning: #e0a33a; + --color-warning: #ff9f0a; /* systemOrange(深色) */ + --color-success: #30d158; /* systemGreen(深色)—— 状态药丸「成功」用 */ /* ── 图表分类色(使用统计页:按模型着色)── @@ -174,15 +180,15 @@ `chart-other` 是「其他(合并的尾部)」专用的中性灰 —— 它不是一个模型, 不该抢一个分类色。 */ - --color-chart-1: #5b9cf5; - --color-chart-2: #f2b14c; - --color-chart-3: #a98bfa; - --color-chart-4: #2fc4a4; - --color-chart-5: #f07a6a; - --color-chart-6: #4cc3e0; - --color-chart-7: #ec78b4; - --color-chart-8: #a9c95a; - --color-chart-other: #6b706e; + --color-chart-1: #0a84ff; + --color-chart-2: #ff9f0a; + --color-chart-3: #bf5af2; + --color-chart-4: #30d158; + --color-chart-5: #ff6961; + --color-chart-6: #64d2ff; + --color-chart-7: #ff375f; + --color-chart-8: #ffd60a; + --color-chart-other: #636366; /* ── 文档页面上的墨色(办公画布的文字光标)── @@ -215,8 +221,8 @@ */ --ease-panel: cubic-bezier(0.32, 0.72, 0, 1); - --radius-panel: 12px; /* 浮动面板 —— 采样:侧边栏左上角圆角 ≈ 12px */ - --radius-card: 10px; + --radius-panel: 16px; /* 浮动面板 —— Liquid Glass 的面板圆角 */ + --radius-card: 14px; /* 卡片(工具卡、思考块、计划面板…)*/ --radius-pill: 999px; /* @@ -232,9 +238,9 @@ --spacing-window-controls: 128px; --font-sans: - -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', - 'Microsoft YaHei', sans-serif; - --font-mono: 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace; + -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro', 'Helvetica Neue', 'PingFang SC', + 'Hiragino Sans GB', 'Segoe UI', 'Microsoft YaHei', sans-serif; + --font-mono: ui-monospace, 'SF Mono', Menlo, 'JetBrains Mono', Consolas, monospace; /* 品牌字标专用 —— 圆润几何感,和正文的 --font-sans 拉开区分度,只用在「NextCoWork」标题上 */ --font-brand: 'SF Pro Rounded', 'Avenir Next', 'PingFang SC', var(--font-sans); } @@ -248,49 +254,50 @@ 所以这一版改的只有深色,浅色原样保留。 */ :root[data-theme='light'] { - --color-app: #f2eee6; /* 窗口底 —— 面板之间的缝 */ - --color-canvas: #faf9f5; /* 内容面板 / 激活的外层 Tab / 选中的会话行 */ - --color-surface: #f6f4ef; /* 侧边栏 / 画布上的工具卡 */ - --color-chrome: #e8e4dd; /* 外层 Tab 条 —— 浅色下**比侧边栏暗**,见上 §1 */ - --color-surface-raised: #f2eee6; /* 侧边栏里的分区卡 / 菜单 —— 同窗口底 */ + --color-app: #ececf0; /* 窗口底 —— 面板之间的缝 */ + --color-canvas: #ffffff; /* 内容面板 / 激活的外层 Tab / 选中的会话行 */ + --color-surface: #f5f5f7; /* 侧边栏 / 画布上的工具卡 */ + --color-chrome: #e3e3e8; /* 外层 Tab 条 —— 浅色下**比侧边栏暗**,见上 §1 */ + --color-surface-raised: #f2f2f7; /* 侧边栏里的分区卡 / 菜单 —— 同窗口底 */ --color-surface-input: #ffffff; /* 输入框:整个界面唯一的纯白 */ --color-surface-field: #ffffff; /* 浮层里的输入框 —— 浅色下靠纯白+描边分出来 */ - --color-surface-sunken: #dbd8d1; /* 按下 / 激活的图标按钮(量自收起态的侧边栏按钮) */ + --color-surface-sunken: #dcdce1; /* 按下 / 激活的图标按钮(量自收起态的侧边栏按钮) */ - --color-tint: #efefec; /* 激活的内层 Tab / 时间戳药丸 / 发丝线,都是这一个值 */ - --color-tint-hover: #edeae6; /* Explore 芯片 / 头像圆 */ - --color-tint-strong: #e2ded7; + --color-tint: #ededf2; /* 激活的内层 Tab / 时间戳药丸 / 发丝线,都是这一个值 */ + --color-tint-hover: #e8e8ed; /* Explore 芯片 / 头像圆 */ + --color-tint-strong: #dedee3; - --color-border: #e6e6e2; /* 输入框描边 */ - --color-stroke: #e6e6e2; /* 卡片收边 —— 见深色那段:和 border 同值但不受护栏改写 */ - --color-hairline: #efefec; /* 内层 Tab 条下那一道 */ + --color-border: #c7c7cc; /* 输入框描边 */ + --color-stroke: #e5e5ea; /* 卡片收边 —— 见深色那段:和 border 同值但不受护栏改写 */ + --color-hairline: #ececf0; /* 内层 Tab 条下那一道 */ - --color-fg: #181c19; - --color-fg-muted: #72736f; - --color-fg-faint: #96958f; + --color-fg: #1d1d1f; + --color-fg-muted: #5f5f64; + --color-fg-faint: #8e8e93; - --color-icon: #7e7f7e; /* ★ 中性灰,不是强调色 —— 见上 §3 */ - --color-accent: #2d4739; /* ★ 墨绿 —— 深色是亮绿 #36d285,明暗相反 */ + --color-icon: #6e6e73; /* ★ 中性灰,不是强调色 —— 见上 §3 */ + --color-accent: #0071e3; /* ★ 墨绿 —— 深色是亮绿 #36d285,明暗相反 */ --color-accent-fg: #ffffff; - --color-accent-soft: #8e8f8d; - --color-danger: #c4412c; + --color-accent-soft: #86a8d6; + --color-danger: #e0352b; /* 浅色下压暗一档:#e0a33a 在近白底上只有 2.1:1,读不出来 */ - --color-warning: #9a6b12; + --color-warning: #b25000; /* systemOrange 压暗到浅底可读 */ + --color-success: #248a3d; /* systemGreen 压暗到浅底可读 */ /* 图表分类色:色相顺序同深色,明度压到中低档 —— 深色那套在近白底上 琥珀 / 橄榄远低于 3:1,色块边界会糊。这套在 surface #f6f4ef 上 3.3–4.9:1, 过非文本 3:1 线(推的 + 算的,理由见 @theme 里那段) */ - --color-chart-1: #2f6bd0; - --color-chart-2: #b87a10; - --color-chart-3: #7353cf; - --color-chart-4: #13866f; - --color-chart-5: #c94c3a; - --color-chart-6: #16859c; - --color-chart-7: #bd4486; - --color-chart-8: #6c8a1c; - --color-chart-other: #b3b1aa; + --color-chart-1: #0071e3; + --color-chart-2: #c46a00; + --color-chart-3: #8944ab; + --color-chart-4: #248a3d; + --color-chart-5: #d70015; + --color-chart-6: #0071a4; + --color-chart-7: #d30f45; + --color-chart-8: #a05a00; + --color-chart-other: #aeaeb2; - --color-scrim: #2b2a27; /* 暖近黑 —— 浅色下压暗要压暖,压蓝会发灰(无参考,按规律推) */ + --color-scrim: #000000; /* 苹果风格:两个外观都是纯黑遮罩 */ } /* @@ -313,14 +320,14 @@ */ @media (prefers-color-scheme: light) { :root:not([data-theme='dark']) { - --color-app: #f2eee6; - --color-canvas: #faf9f5; - --color-surface: #f6f4ef; - --color-chrome: #e8e4dd; - --color-tint: #efefec; - --color-border: #e6e6e2; - --color-stroke: #e6e6e2; - --color-fg: #181c19; + --color-app: #ececf0; + --color-canvas: #ffffff; + --color-surface: #f5f5f7; + --color-chrome: #e3e3e8; + --color-tint: #ededf2; + --color-border: #c7c7cc; + --color-stroke: #e5e5ea; + --color-fg: #1d1d1f; } } @@ -456,7 +463,7 @@ body { .theme-studio-layout { display: grid; grid-template-columns: 190px minmax(300px, 1fr) minmax(280px, 340px); gap: 16px; min-height: 0; flex: 1; } .theme-studio-library, .theme-studio-inspector { min-height: 0; overflow: auto; } .theme-studio-center { min-width: 0; display: flex; flex-direction: column; } -.studio-select { width: 100%; border: 1px solid var(--color-border); border-radius: 7px; background: var(--color-surface-field); color: var(--color-fg); padding: 7px 9px; font-size: 12px; } +.studio-select { width: 100%; padding-block: 6px; font-size: 12px; } .theme-preview-frame { position: relative; display: flex; min-height: 390px; overflow: hidden; border: 1px solid var(--color-border); border-radius: 12px; background: var(--color-canvas); background-size: cover; isolation: isolate; } .theme-preview-sidebar { width: 22%; min-width: 82px; display: flex; flex-direction: column; gap: 13px; padding: 14px 10px; background: color-mix(in srgb, var(--color-surface) 90%, transparent); backdrop-filter: blur(16px); } .theme-preview-logo { width: 56px; height: 8px; border-radius: 999px; background: var(--color-accent); } @@ -533,7 +540,7 @@ body { */ * { scrollbar-width: thin; - scrollbar-color: var(--color-tint) transparent; + scrollbar-color: color-mix(in srgb, var(--color-fg) 22%, transparent) transparent; } *::-webkit-scrollbar { width: 8px; @@ -543,13 +550,14 @@ body { /* thumb 靠 border + background-clip 挤出内缩,而不是直接把 scrollbar 调窄: 8px 的命中区保留给鼠标,视觉上只有中间 4px 有颜色。改成 width:4px 会很难点中。 */ *::-webkit-scrollbar-thumb { - background: var(--color-tint); + /* 苹果风格:macOS 叠加式滚动条 —— 前景色半透明,深浅两个外观都自然 */ + background: color-mix(in srgb, var(--color-fg) 22%, transparent); border: 2px solid transparent; background-clip: content-box; border-radius: 999px; } *::-webkit-scrollbar-thumb:hover { - background: var(--color-tint-strong); + background: color-mix(in srgb, var(--color-fg) 38%, transparent); background-clip: content-box; } *::-webkit-scrollbar-corner, @@ -982,3 +990,221 @@ body { 照样能点,这一档就是为了让另外两个平台不比它差 —— 否则设置面板一开 就没法最小化/关窗。别往这一档里放第二样东西。 */ + +/* + ── 苹果风格的全局收尾(feat/apple-style-ui)────────────────────────────── + + · 菜单材质:所有 `bg-surface-menu` 的浮层(Menu / ContextMenu / Submenu / Select) + 都是半透明底,这里补上背景模糊和饱和度提升,读作 macOS 的毛玻璃。 + 系统里开了「降低透明度」就退回实底。 + · 文本选区用强调色(macOS 的 selectedTextBackgroundColor 是强调色的浅版)。 + · 字体渲染:关掉 Chromium 的合成字重,开启光学尺寸(SF Text / Display 自动切换)。 +*/ +.bg-surface-menu { + -webkit-backdrop-filter: saturate(180%) blur(24px); + backdrop-filter: saturate(180%) blur(24px); +} +@media (prefers-reduced-transparency: reduce) { + .bg-surface-menu { + background-color: var(--color-surface-raised); + -webkit-backdrop-filter: none; + backdrop-filter: none; + } +} + +::selection { + background: color-mix(in srgb, var(--color-accent) 30%, transparent); +} + +body { + font-synthesis-weight: none; + font-optical-sizing: auto; + text-rendering: optimizeLegibility; +} + +/* + ── Liquid Glass:半透明窗口 + 浮动玻璃面板(仅 macOS,`data-vibrancy`)────────── + + 窗口本身是系统毛玻璃(main/index.ts 的 `vibrancy: 'under-window'`),这里把各层 + 底色换成**同色的半透明版本**,让桌面壁纸的颜色透上来。 + + ★ 做法是在面板节点上**重定义 `--color-*` 变量**,而不是逐个改组件的 bg 类: + 主题 token 是写在 上的行内样式,面板节点自己声明的同名变量会遮住它, + 并一路继承给整棵子树 —— 于是面板里所有 `bg-canvas` / `bg-chrome` 一次性变成玻璃。 + `--glass-*` 在 :root 上先算好(var() 在声明它的元素上就地求值),面板里再引用, + 避免「--color-canvas 引用自己」的循环。 + ★ 菜单、弹窗 portal 到 body,不在面板子树里,保持原来的不透明度,读起来才清楚。 +*/ +:root[data-vibrancy] { + --glass-app: color-mix(in srgb, var(--color-app) 18%, transparent); + --glass-surface: color-mix(in srgb, var(--color-surface) 52%, transparent); + --glass-canvas: color-mix(in srgb, var(--color-canvas) 66%, transparent); + --glass-chrome: color-mix(in srgb, var(--color-canvas) 30%, transparent); + --glass-edge: rgb(255 255 255 / 0.55); + --glass-shadow: 0 10px 30px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.06); +} +:root[data-vibrancy][data-theme='dark'] { + --glass-surface: color-mix(in srgb, var(--color-surface) 48%, transparent); + --glass-canvas: color-mix(in srgb, var(--color-canvas) 62%, transparent); + --glass-edge: rgb(255 255 255 / 0.1); + --glass-shadow: 0 12px 36px rgb(0 0 0 / 0.35), 0 1px 3px rgb(0 0 0 / 0.3); +} +:root[data-vibrancy], +:root[data-vibrancy] body { + background: transparent; +} +:root[data-vibrancy] .app-ground { + --color-app: var(--glass-app); +} +:root[data-vibrancy] [data-theme-region='sidebar'], +:root[data-vibrancy] main.app-canvas { + border-radius: 16px; + box-shadow: inset 0 0 0 0.5px var(--glass-edge), var(--glass-shadow); +} +:root[data-vibrancy] [data-theme-region='sidebar'] { + --color-surface: var(--glass-surface); + --color-surface-raised: color-mix(in srgb, var(--color-fg) 6%, transparent); +} +:root[data-vibrancy] [data-theme-region='sidebar'] > aside { + border-radius: 16px; +} +:root[data-vibrancy] main.app-canvas { + --color-canvas: var(--glass-canvas); + --color-chrome: var(--glass-chrome); +} +/* 内容区里本来就要求不透明的视图(终端、代码编辑器)仍然挡住玻璃:它们用的是 surface,不受上面影响 */ + +/* + ── 苹果风格的共享控件类 ────────────────────────────────────────────────── + 给**不走 Arc 组件、自己手写 /