diff --git a/desktop/frontend-next/src/actions.ts b/desktop/frontend-next/src/actions.ts index f604263407..42f061a4ac 100644 --- a/desktop/frontend-next/src/actions.ts +++ b/desktop/frontend-next/src/actions.ts @@ -393,7 +393,6 @@ export const ACTIONS: UIAction[] = [ // the row's own action, so this one never writes anything itself. { id: "tree.navigate", kind: "view", target: "none", proof: "interaction" }, { id: "settings.close", kind: "navigation", target: "none", proof: "interaction" }, - { id: "settings.expand", kind: "view", target: "none", proof: "interaction" }, { id: "pane.activate", kind: "navigation", target: "none", proof: "interaction" }, // How this session is being read. One id across a bar button and a menu row, // because reaching a view from the bar and from the detail menu is one intent diff --git a/desktop/frontend-next/src/design/settings.css b/desktop/frontend-next/src/design/settings.css index 2c322594b1..0fcaa1adb4 100644 --- a/desktop/frontend-next/src/design/settings.css +++ b/desktop/frontend-next/src/design/settings.css @@ -353,7 +353,7 @@ .ustack .utip .r { display: flex; justify-content: space-between; gap: 12px; color: var(--muted); font: var(--w-med) var(--fs-micro) var(--ui); } .ustack .utip .r b { color: var(--text); font-weight: var(--w-semi); } .prefs { padding: 24px; } -.prefs-sheet:not([data-wide]) { width: 100%; height: 100%; } +.prefs-sheet { width: 100%; height: 100%; } .srow-ctl .unit-field input { width: 72px; } .srow-val { color: var(--muted); font: var(--w-reg) var(--fs-meta) var(--ui); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .srow-val > * { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/desktop/frontend-next/src/i18n/en_settings.ts b/desktop/frontend-next/src/i18n/en_settings.ts index d4cd3ce025..00ca8f77f9 100644 --- a/desktop/frontend-next/src/i18n/en_settings.ts +++ b/desktop/frontend-next/src/i18n/en_settings.ts @@ -1372,11 +1372,9 @@ export const EN_SETTINGS: Record = { "已自动归档 {n} 个会话,可在“已归档”中恢复。": "{n} conversations were archived automatically. You can restore them from Archived.", "图标栏": "Icon rail", "导航入口": "Navigation entries", - "放大设置窗口": "Enlarge the settings window", "隐藏共同前缀": "Hide the shared prefix", "{name} 的模型名前缀": "Model name prefix of {name}", "路由类服务给每个模型加的同一段前缀(如 group/)在显示时省略,只改显示;模型 ID 不变,自己填的别名优先,保存后生效。": "The same prefix a routing service puts on every model (such as group/) is left out of how they are drawn. Display only: model ids do not change, an alias you typed wins, and it takes effect on save.", - "缩小设置窗口": "Shrink the settings window", "图标栏和侧栏里直达各页面的入口:可以隐藏,也可以调整先后。会话、反馈、账号和设置始终保留。": "The shortcuts to pages in the icon rail and the sidebar: hide any of them or change their order. Sessions, feedback, account and settings are always kept.", "还原排列": "Reset arrangement", "上移:{name}": "Move up: {name}", diff --git a/desktop/frontend-next/src/i18n/msg/settings.ts b/desktop/frontend-next/src/i18n/msg/settings.ts index 3a36251c64..c574f3c68c 100644 --- a/desktop/frontend-next/src/i18n/msg/settings.ts +++ b/desktop/frontend-next/src/i18n/msg/settings.ts @@ -44,7 +44,6 @@ export const SETTINGS: Messages = { "settings.empty_is_automatic_1_to": { zh: "留空表示自动;可填 1 到 {max}", en: "Empty is automatic; 1 to {max}" }, "settings.empty_means_automatic_a_browser": { zh: "默认留空:自动。浏览器对同一个地址只开约 6 个连接,会话列表占一个,所以只让少数标签保持实时连接,其余的标签在显示时从断点接着读,运行中的后台标签每 5 秒读一次状态。调高会让更多后台标签实时更新,但开太多会让停止、发送排不上队。", en: "Empty means automatic. A browser opens only about six connections to one address and the session list takes one, so only a few tabs stay live; the others resume from where they stopped when shown, and a running background tab has its status read every 5 seconds. A higher number updates more background tabs live, but too many makes Stop and Send queue behind them." }, "settings.english": { zh: "英文", en: "English" }, - "settings.enlarge_the_settings_window": { zh: "放大设置窗口", en: "Enlarge the settings window" }, "settings.environment_variables": { zh: "环境变量", en: "Environment variables" }, "settings.external_tools": { zh: "外部工具", en: "External tools" }, "settings.file_changes": { zh: "文件改动", en: "File changes" }, @@ -149,7 +148,6 @@ export const SETTINGS: Messages = { "settings.show_a_status_icon_in": { zh: "在托盘显示图标", en: "Show a status icon in the tray" }, "settings.show_file_changes_above_the": { zh: "在输入框上方显示文件改动", en: "Show file changes above the input box" }, "settings.show_name": { zh: "显示:{name}", en: "Show: {name}" }, - "settings.shrink_the_settings_window": { zh: "缩小设置窗口", en: "Shrink the settings window" }, "settings.sign_in_status_could_not": { zh: "无法读取登录状态", en: "Sign-in status could not be read" }, "settings.sign_out": { zh: "退出登录", en: "Sign out" }, "settings.signed_in": { zh: "已登录", en: "Signed in" }, diff --git a/desktop/frontend-next/src/state/settingsWide.test.ts b/desktop/frontend-next/src/state/settingsWide.test.ts deleted file mode 100644 index 71635c897d..0000000000 --- a/desktop/frontend-next/src/state/settingsWide.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -// @vitest-environment jsdom -import { beforeEach, expect, it } from "vitest"; -import { onSettingsWideChange, setSettingsWide, settingsWide } from "./settingsWide"; - -beforeEach(() => localStorage.clear()); - -it("is the default size until this machine chooses otherwise", () => { - expect(settingsWide()).toBe(false); -}); - -it("reads back a choice and tells subscribers", () => { - let heard = 0; - const stop = onSettingsWideChange(() => heard++); - setSettingsWide(true); - expect(settingsWide()).toBe(true); - setSettingsWide(false); - stop(); - expect(settingsWide()).toBe(false); - expect(heard).toBe(2); -}); - -it("reads anything unreadable as the default size", () => { - for (const raw of ["", "yes", "1", "{}"]) { - localStorage.setItem("rx-settings-wide", raw); - expect(settingsWide(), raw).toBe(false); - } -}); diff --git a/desktop/frontend-next/src/state/settingsWide.ts b/desktop/frontend-next/src/state/settingsWide.ts deleted file mode 100644 index 8f6be327f8..0000000000 --- a/desktop/frontend-next/src/state/settingsWide.ts +++ /dev/null @@ -1,27 +0,0 @@ -const KEY = "rx-settings-wide"; -const listeners = new Set<() => void>(); - -export function settingsWide(): boolean { - try { - return localStorage.getItem(KEY) === "on"; - } catch { - return false; - } -} - -export function onSettingsWideChange(fn: () => void): () => void { - listeners.add(fn); - return () => { - listeners.delete(fn); - }; -} - -export function setSettingsWide(on: boolean): void { - try { - if (on) localStorage.setItem(KEY, "on"); - else localStorage.removeItem(KEY); - } catch { - /* a private window keeps the default, which is the same answer it gives */ - } - listeners.forEach((fn) => fn()); -} diff --git a/desktop/frontend-next/src/styles/app.css b/desktop/frontend-next/src/styles/app.css index 20b600f543..1702827849 100644 --- a/desktop/frontend-next/src/styles/app.css +++ b/desktop/frontend-next/src/styles/app.css @@ -2139,8 +2139,6 @@ background: var(--float); box-shadow: var(--shadow-float); animation: m-panel var(--t-panel) var(--ease-spring) both; } - .prefs-sheet[data-wide] { width: 100%; height: 100% } - .prefs-sheet[data-wide] .prefs-main .prefs-col { max-width: min(1200px, 100%); margin-inline: auto } @supports (view-transition-name: none) { diff --git a/desktop/frontend-next/src/styles/dialogcenter.css b/desktop/frontend-next/src/styles/dialogcenter.css index 514b26bb5a..8c9a3556fd 100644 --- a/desktop/frontend-next/src/styles/dialogcenter.css +++ b/desktop/frontend-next/src/styles/dialogcenter.css @@ -1,3 +1,3 @@ :root:not([data-fold~="scene"]) :is(.prefs, .fbk-veil, .image-preview, .cpv, .agent-tx-scrim) { border-bottom: var(--chrome-h, 39px) solid transparent; -} :root:not([data-fold~="scene"]) .prefs:has(> .prefs-sheet[data-wide]) { border-bottom-width: 0; } :root .prefs:has(> .prefs-sheet[data-wide]) { inset: 0; } .prefs-sheet[data-wide] { border-width: 0; border-radius: 0; } [data-titlebar="app"] .prefs-sheet[data-wide] .prefs-hd { -webkit-app-region: drag; } [data-titlebar="app"] .prefs-sheet[data-wide] .prefs-hd :is(button, a[href], input, .winctl) { -webkit-app-region: no-drag; } [data-titlebar="app"][data-platform="darwin"] .prefs-sheet[data-wide] .prefs-hd { padding-left: calc(var(--lights-w, 96px) + 8px); } .prefs-sheet[data-wide] .prefs-hd .winctl { margin-right: -16px; margin-left: 0; } +} diff --git a/desktop/frontend-next/src/styles/dialogcenter.test.ts b/desktop/frontend-next/src/styles/dialogcenter.test.ts index 9cc9c59d7f..fbb8a27636 100644 --- a/desktop/frontend-next/src/styles/dialogcenter.test.ts +++ b/desktop/frontend-next/src/styles/dialogcenter.test.ts @@ -6,15 +6,3 @@ const css = CSS["./dialogcenter.css"]!; it("keeps the transparent bottom border that centres a dialog under the title bar", () => { expect(css).toMatch(/:is\(\.prefs, \.fbk-veil, \.image-preview, \.cpv, \.agent-tx-scrim\)\s*\{\s*border-bottom:\s*var\(--chrome-h, 39px\) solid transparent;/); }); - -it("takes it off a settings window that has been enlarged, so the sheet reaches the bottom of the window", () => { - const rule = css.match(/\.prefs:has\(> \.prefs-sheet\[data-wide\]\)\s*\{([^}]*)\}/)?.[1] ?? ""; - expect(rule).toMatch(/border-bottom-width:\s*0/); -}); - -it("lets an enlarged settings window cover the title bar too, with its header standing in for the bar", () => { - expect(css).toMatch(/\.prefs:has\(> \.prefs-sheet\[data-wide\]\)\s*\{\s*inset:\s*0;/); - expect(css).toMatch(/\.prefs-sheet\[data-wide\]\s*\{\s*border-width:\s*0;\s*border-radius:\s*0;/); - expect(css).toMatch(/\[data-titlebar="app"\] \.prefs-sheet\[data-wide\] \.prefs-hd\s*\{\s*-webkit-app-region:\s*drag;/); - expect(css).toMatch(/\[data-platform="darwin"\] \.prefs-sheet\[data-wide\] \.prefs-hd\s*\{\s*padding-left:\s*calc\(var\(--lights-w, 96px\) \+ 8px\);/); -}); diff --git a/desktop/frontend-next/src/styles/settings-header-drag.test.ts b/desktop/frontend-next/src/styles/settings-header-drag.test.ts deleted file mode 100644 index 6199b14201..0000000000 --- a/desktop/frontend-next/src/styles/settings-header-drag.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, it } from "vitest"; - -const CSS = import.meta.glob("./dialogcenter.css", { query: "?raw", import: "default", eager: true }) as Record; -const css = CSS["./dialogcenter.css"]!; - -// Electron treats everything inside a drag region as part of the title bar, so -// the settings header that takes over the title bar swallows clicks on its own -// buttons and search field unless each is carved back out. -const rulesOf = (re: RegExp) => [...css.matchAll(re)].map((m) => m[1]!).join("\n"); - -it("makes the full-window settings header a drag region and carves its controls back out", () => { - const drag = css.match(/\[data-titlebar="app"\] \.prefs-sheet\[data-wide\] \.prefs-hd\s*\{[^}]*-webkit-app-region:\s*drag/); - expect(drag).not.toBeNull(); - const carved = rulesOf(/\[data-titlebar="app"\] \.prefs-sheet\[data-wide\] \.prefs-hd :is\(([^)]*)\)\s*\{[^}]*-webkit-app-region:\s*no-drag[^}]*\}/g); - for (const control of ["button", "input", ".winctl"]) expect(carved).toContain(control); -}); diff --git a/desktop/frontend-next/src/ui/Settings.tsx b/desktop/frontend-next/src/ui/Settings.tsx index c820295aec..baa07b57e7 100644 --- a/desktop/frontend-next/src/ui/Settings.tsx +++ b/desktop/frontend-next/src/ui/Settings.tsx @@ -1,6 +1,4 @@ -import { WindowControls } from "./WindowControls"; -import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react"; -import { onSettingsWideChange, setSettingsWide, settingsWide } from "../state/settingsWide"; +import { useCallback, useEffect, useRef, useState } from "react"; import { StudioIcon } from "./StudioIcon"; import { useModalFocus } from "./useModalFocus"; import { SettingsLeaveContext, useSettingsLeave } from "./SettingsLeave"; @@ -77,7 +75,6 @@ const SOLO_TITLE: Record = { ext: "ext.extension", usage: "app.u export function Settings({ hub, onError, port, networkPort, localWorkspacePort, networkHost, status, theme, onTheme, contrast, onContrast, weight, onWeight, look, onLook, onClose, onChanged, onSessionsRecovered, reloadThemes, at: opened, account: acct, accountUnread, reloadAccount, workspaceRoot }: Props) { const navigation = useSettingsLeave(); const close = useCallback(() => navigation.leave(onClose), [navigation.leave, onClose]); - const wide = useSyncExternalStore(onSettingsWideChange, settingsWide, settingsWide); const homePort = networkPort ?? port; const localPort = localWorkspacePort ?? homePort; const [openedName, namedAnchor = ""] = (opened ?? "").split(":", 2); @@ -348,7 +345,7 @@ export function Settings({ hub, onError, port, networkPort, localWorkspacePort, onMouseUp={(e) => { if (veiled.current && e.target === e.currentTarget && !updatingPkg.applying) close(); }} data-action-mouseup="settings.close" > -
+

{t(solo ? SOLO_TITLE[soloSection] : "common.settings")}

{/* 原来这里挂着一句「改动立刻生效;需要重建运行时的……」。它对三档里的 @@ -374,13 +371,9 @@ export function Settings({ hub, onError, port, networkPort, localWorkspacePort, }} />
- - {wide && }
{found !== null && (
diff --git a/desktop/frontend-next/src/ui/settings-actions.interaction.test.tsx b/desktop/frontend-next/src/ui/settings-actions.interaction.test.tsx new file mode 100644 index 0000000000..0d30a1b661 --- /dev/null +++ b/desktop/frontend-next/src/ui/settings-actions.interaction.test.tsx @@ -0,0 +1,33 @@ +// @vitest-environment jsdom +import { afterEach, expect, it, vi } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "./testkit"; +import { Settings } from "./Settings"; +import { MockPort } from "../port/mock"; +import { MockHub } from "../port/mock_hub"; +import type { AgentPort, SessionStatus } from "../port/port"; + +afterEach(cleanup); + +function draw(onClose = vi.fn()) { + render( {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}} + look={{} as never} onLook={() => {}} reloadThemes={() => {}} + onClose={onClose} onChanged={vi.fn()} onError={() => {}} at="appearance" + account={null} accountUnread="" reloadAccount={() => {}} + />); + return onClose; +} + +it("has one control in its corner, the one that closes it", async () => { + const onClose = draw(); + const buttons = document.querySelectorAll(".prefs-actions button"); + expect(buttons).toHaveLength(1); + expect(screen.queryByRole("button", { name: "放大设置窗口" })).toBeNull(); + expect(document.querySelector(".prefs-sheet")?.hasAttribute("data-wide")).toBe(false); + await userEvent.click(buttons[0]!); + await vi.waitFor(() => expect(onClose).toHaveBeenCalled()); +}); diff --git a/desktop/frontend-next/src/ui/settings-expand.interaction.test.tsx b/desktop/frontend-next/src/ui/settings-expand.interaction.test.tsx deleted file mode 100644 index d214964639..0000000000 --- a/desktop/frontend-next/src/ui/settings-expand.interaction.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// @vitest-environment jsdom -import { afterEach, beforeEach, expect, it, vi } from "vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import "./testkit"; -import { Settings } from "./Settings"; -import { MockPort } from "../port/mock"; -import { MockHub } from "../port/mock_hub"; -import type { AgentPort, SessionStatus } from "../port/port"; - -beforeEach(() => localStorage.clear()); -afterEach(cleanup); - -function draw(onClose = vi.fn()) { - render( {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}} - look={{} as never} onLook={() => {}} reloadThemes={() => {}} - onClose={onClose} onChanged={vi.fn()} onError={() => {}} at="appearance" - account={null} accountUnread="" reloadAccount={() => {}} - />); - return document.querySelector(".prefs-sheet")!; -} - -it("offers a control that fills the window, and says whether it is on", async () => { - const sheet = draw(); - const expand = screen.getByRole("button", { name: "放大设置窗口" }); - expect(expand.getAttribute("aria-pressed")).toBe("false"); - expect(sheet.hasAttribute("data-wide")).toBe(false); - await userEvent.click(expand); - expect(expand.getAttribute("aria-pressed")).toBe("true"); - expect(sheet.hasAttribute("data-wide")).toBe(true); - await userEvent.click(expand); - expect(sheet.hasAttribute("data-wide")).toBe(false); -}); - -it("opens at the size it was last left at", async () => { - draw(); - await userEvent.click(screen.getByRole("button", { name: "放大设置窗口" })); - cleanup(); - const sheet = draw(); - expect(sheet.hasAttribute("data-wide")).toBe(true); - expect(screen.getByRole("button", { name: "放大设置窗口" }).getAttribute("aria-pressed")).toBe("true"); -}); - -it("draws the arrows outward while the window is small and inward once it is enlarged", async () => { - draw(); - const expand = screen.getByRole("button", { name: "放大设置窗口" }); - const path = () => expand.querySelector("path")?.getAttribute("d"); - const outward = path(); - expect(expand.getAttribute("title")).toBe("放大设置窗口"); - await userEvent.click(expand); - expect(path()).not.toBe(outward); - expect(expand.getAttribute("title")).toBe("缩小设置窗口"); - await userEvent.click(expand); - expect(path()).toBe(outward); -}); - -it("keeps search Escape and dismissal available after expanding and restoring", async () => { - const onClose = vi.fn(); - const sheet = draw(onClose); - const expand = screen.getByRole("button", { name: "放大设置窗口" }); - await userEvent.click(expand); - const search = screen.getByRole("textbox", { name: "搜索设置" }); - await userEvent.type(search, "模型"); - await userEvent.keyboard("{Escape}"); - expect(search.value).toBe(""); - expect(onClose).not.toHaveBeenCalled(); - expect(sheet.hasAttribute("data-wide")).toBe(true); - await userEvent.click(expand); - expect(sheet.hasAttribute("data-wide")).toBe(false); - await userEvent.click(screen.getByRole("button", { name: /关闭.*Esc/ })); - expect(onClose).toHaveBeenCalledTimes(1); - await userEvent.keyboard("{Escape}"); - expect(onClose).toHaveBeenCalledTimes(2); -}); diff --git a/tools/repolint/baseline.json b/tools/repolint/baseline.json index fee54642ac..1bcadbf69d 100644 --- a/tools/repolint/baseline.json +++ b/tools/repolint/baseline.json @@ -7,7 +7,7 @@ "commented-code": 0, "complexity": 666, "css-important": 127, - "css-size": 8878, + "css-size": 8876, "doc-language": 0, "doc-owner": 27, "doc-prose": 1434, @@ -78,7 +78,7 @@ }, "desktop/frontend-next/src/styles/app.css": { "css-important": 24, - "css-size": 5260 + "css-size": 5258 }, "desktop/frontend-next/src/styles/studio.css": { "css-important": 103,