From 03bcc8a8f076c4e70ba276abf66c9a8e3ea90055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 17 Apr 2026 21:07:52 -0400 Subject: [PATCH] feat(settings): add top-row tab navigation; fold Providers + mode + secrets into one IA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings page was a single vertical scroll containing seven unrelated concerns. Users had to scroll past theme preferences to reach Providers — which is the most-touched surface — and mode + secrets + shortcuts lived below that. There was no signal about where anything was. Introduced `SettingsShell` with a horizontal tab row along the top: General | Execution mode | Providers | Shortcuts | Data | Secrets Each tab swaps the content pane. Active tab is visually anchored with a 2px primary underline + foreground text; inactive tabs stay muted with a hover promotion. All tabs are real `role="tab"` + `aria-selected` so screen readers announce the current pane; the nav has `aria-label="Settings categories"`. Tab state is local — settings are ephemeral navigation, no reason to persist across company switches or app restarts. Default is "general" because that's where first-time users land. Refactor consequences: - `OperationsSections.tsx` now renders `` + the OAuth prompt modal portal; the old double-Panel wrapper with "Profile and release" + "Providers" titles is gone. The "Profile and release" label always fought with what was actually inside the pane — Jobs audit item S-S2 addressed. - Providers moved from a separate sibling Panel into the main IA. Previously Providers came AFTER a 7-section scroll; now it is a peer of General / Mode / etc., reachable in one click. - Deleted the now-unused `SettingsPane.tsx` orchestrator. Its single-column rendering survived only as a composition step that SettingsShell replaces. Added i18n keys (zh + en): settings.tabs.general / mode / providers / shortcuts / data / secrets; settings.tabsAria. Covers J-S2 / J-S7 / S-S5 / S-S7 from the settings audit. --- .../components/execution/SettingsPane.tsx | 62 --------- .../components/execution/SettingsShell.tsx | 121 ++++++++++++++++++ src/renderer/i18n/en.ts | 7 + src/renderer/i18n/zh.ts | 7 + .../views/sections/OperationsSections.tsx | 20 ++- 5 files changed, 144 insertions(+), 73 deletions(-) delete mode 100644 src/renderer/components/execution/SettingsPane.tsx create mode 100644 src/renderer/components/execution/SettingsShell.tsx diff --git a/src/renderer/components/execution/SettingsPane.tsx b/src/renderer/components/execution/SettingsPane.tsx deleted file mode 100644 index 5ec5e0fe..00000000 --- a/src/renderer/components/execution/SettingsPane.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useState } from "react"; -import type { ProfileSnapshot, SecretRecord } from "@shared/types"; -import type { AppLocale } from "@shared/locale"; -import { useLocale } from "../../i18n"; -import { GeneralSection } from "./settings-sections/GeneralSection"; -import { ExecutionModeSection } from "./settings-sections/ExecutionModeSection"; -import { ShortcutsSection } from "./settings-sections/ShortcutsSection"; -import { DataSection } from "./settings-sections/DataSection"; -import { SecretsSection } from "./settings-sections/SecretsSection"; - -interface SettingsPaneProps { - snapshot: ProfileSnapshot; - secrets: SecretRecord[]; - companyId: string; - onSaved: () => Promise; -} - -/** - * Settings page orchestrator. Each concern lives in its own ≤200 LOC - * section under `settings-sections/` so the contents of this file stay - * scannable instead of hiding 2000-char JSX statements inside one - * 73-"line" god file. - * - * Save model: every preference in GeneralSection writes to - * `updateSettings` on change, so there is no global "Save" button. - * DataSection's backup / restore / update-check remain explicit - * actions because they have side effects beyond a single row of app - * state. - */ -export function SettingsPane({ snapshot, secrets, companyId, onSaved }: SettingsPaneProps) { - const currentLocale = useLocale(); - const [theme, setTheme] = useState(snapshot.theme); - const [selectedLocale, setSelectedLocale] = useState(snapshot.locale ?? currentLocale); - const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled); - - useEffect(() => { - setTheme(snapshot.theme); - setUpdatesEnabled(snapshot.updatesEnabled); - if (snapshot.locale) setSelectedLocale(snapshot.locale); - }, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale]); - - const company = snapshot.companies.find((entry) => entry.id === companyId) ?? null; - - return ( -
- - {company ? : null} - - - -
- ); -} diff --git a/src/renderer/components/execution/SettingsShell.tsx b/src/renderer/components/execution/SettingsShell.tsx new file mode 100644 index 00000000..7e47a63c --- /dev/null +++ b/src/renderer/components/execution/SettingsShell.tsx @@ -0,0 +1,121 @@ +import { useState, useMemo } from "react"; +import type { ProfileSnapshot, SecretRecord } from "@shared/types"; +import type { AppLocale } from "@shared/locale"; +import { useEffect } from "react"; +import { useLocale, useT } from "../../i18n"; +import { GeneralSection } from "./settings-sections/GeneralSection"; +import { ExecutionModeSection } from "./settings-sections/ExecutionModeSection"; +import { ShortcutsSection } from "./settings-sections/ShortcutsSection"; +import { DataSection } from "./settings-sections/DataSection"; +import { SecretsSection } from "./settings-sections/SecretsSection"; +import { ProvidersPanel } from "../settings/providers/ProvidersPanel"; + +type SettingsTab = "general" | "mode" | "providers" | "shortcuts" | "data" | "secrets"; + +interface SettingsShellProps { + snapshot: ProfileSnapshot; + secrets: SecretRecord[]; + companyId: string; + onSaved: () => Promise; +} + +/** + * Settings page with top-row tab navigation. Replaces the previous + * single-scroll layout that stacked seven unrelated concerns + * (preferences, execution mode, providers, keyboard reference, data + * ops, secrets) in one column forcing users to scroll past everything + * they didn't need. + * + * Tabs live as local state — settings are transient; there is no + * benefit to persisting the active tab across company switches or + * app restarts. Default is "general" because first-time users most + * often land here for theme / language. + */ +export function SettingsShell({ snapshot, secrets, companyId, onSaved }: SettingsShellProps) { + const t = useT(); + const currentLocale = useLocale(); + const [activeTab, setActiveTab] = useState("general"); + const [theme, setTheme] = useState(snapshot.theme); + const [selectedLocale, setSelectedLocale] = useState(snapshot.locale ?? currentLocale); + const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled); + + useEffect(() => { + setTheme(snapshot.theme); + setUpdatesEnabled(snapshot.updatesEnabled); + if (snapshot.locale) setSelectedLocale(snapshot.locale); + }, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale]); + + const company = useMemo( + () => snapshot.companies.find((entry) => entry.id === companyId) ?? null, + [snapshot.companies, companyId], + ); + + const tabs: Array<{ id: SettingsTab; label: string }> = [ + { id: "general", label: t("settings.tabs.general") }, + { id: "mode", label: t("settings.tabs.mode") }, + { id: "providers", label: t("settings.tabs.providers") }, + { id: "shortcuts", label: t("settings.tabs.shortcuts") }, + { id: "data", label: t("settings.tabs.data") }, + { id: "secrets", label: t("settings.tabs.secrets") }, + ]; + + return ( +
+ + +
+ {activeTab === "general" ? ( + + ) : null} + {activeTab === "mode" && company ? ( + + ) : null} + {activeTab === "providers" ? : null} + {activeTab === "shortcuts" ? : null} + {activeTab === "data" ? : null} + {activeTab === "secrets" ? ( + + ) : null} +
+
+ ); +} diff --git a/src/renderer/i18n/en.ts b/src/renderer/i18n/en.ts index a1e370e7..269868b5 100644 --- a/src/renderer/i18n/en.ts +++ b/src/renderer/i18n/en.ts @@ -1276,6 +1276,13 @@ export const en: Record = { "org.statusAria": "Status: {status}", // ── Settings pane ── + "settings.tabs.general": "General", + "settings.tabs.mode": "Execution mode", + "settings.tabs.providers": "Providers", + "settings.tabs.shortcuts": "Shortcuts", + "settings.tabs.data": "Data", + "settings.tabs.secrets": "Secrets", + "settings.tabsAria": "Settings categories", "settings.updaterHint": "Check for application updates at launch", "settings.modeScope": "Applies to current company: {company}", "settings.backupRestore": "Backup & restore", diff --git a/src/renderer/i18n/zh.ts b/src/renderer/i18n/zh.ts index 077a2a7d..d9102248 100644 --- a/src/renderer/i18n/zh.ts +++ b/src/renderer/i18n/zh.ts @@ -1276,6 +1276,13 @@ export const zh: Record = { "org.statusAria": "状态:{status}", // ── 设置面板 ── + "settings.tabs.general": "通用", + "settings.tabs.mode": "运营模式", + "settings.tabs.providers": "连接器", + "settings.tabs.shortcuts": "快捷键", + "settings.tabs.data": "数据", + "settings.tabs.secrets": "密钥", + "settings.tabsAria": "设置分类", "settings.updaterHint": "启动时自动检查应用更新", "settings.modeScope": "应用于当前公司:{company}", "settings.backupRestore": "备份与恢复", diff --git a/src/renderer/views/sections/OperationsSections.tsx b/src/renderer/views/sections/OperationsSections.tsx index 6d772359..e4b9a098 100644 --- a/src/renderer/views/sections/OperationsSections.tsx +++ b/src/renderer/views/sections/OperationsSections.tsx @@ -4,11 +4,10 @@ import { StandupView } from "../../components/inbox/StandupView"; import { PerformanceView } from "../../components/inbox/PerformanceView"; import { Dashboard } from "../../components/dashboard/Dashboard"; import { TwoPaneLayout } from "../../components/TwoPaneLayout"; -import { SettingsPane } from "../../components/execution/SettingsPane"; +import { SettingsShell } from "../../components/execution/SettingsShell"; import { WorkspaceCreator } from "../../components/execution/WorkspaceCreator"; import { WorkspaceInspector } from "../../components/execution/WorkspaceInspector"; import { WorkspaceList } from "../../components/execution/WorkspaceList"; -import { ProvidersPanel } from "../../components/settings/providers"; import { OAuthPromptDialog } from "../../components/settings/dialogs/OAuthPromptDialog"; import { SocialAccountsPanel } from "../../components/social/SocialPanel"; import { AutomationPanel } from "../../components/automation/AutomationPanel"; @@ -49,7 +48,6 @@ export function OperationsSections({ section }: OperationsSectionsProps) { } = useSectionContext(); const { refresh, openPathWithFeedback, setError } = useAppActions(); - const locale = useAppStore((s) => s.locale); const inboxItems = useAppStore((s) => s.inboxItems); const companyMetrics = useAppStore((s) => s.companyMetrics); const standupReport = useAppStore((s) => s.standupReport); @@ -272,15 +270,15 @@ export function OperationsSections({ section }: OperationsSectionsProps) { ) : null} {section === "settings" && currentCompany ? ( -
- - - - - - + <> + -
+ ) : null} );