diff --git a/frontend/src/app/pages/Settings/Settings.tsx b/frontend/src/app/pages/Settings/Settings.tsx index d675c15c..cd6f3951 100644 --- a/frontend/src/app/pages/Settings/Settings.tsx +++ b/frontend/src/app/pages/Settings/Settings.tsx @@ -11,12 +11,19 @@ import { onboardingBus } from '@/app/components/Onboarding/eventBus'; import { fetchModels } from '@/shared/state/modelsSlice'; import { fetchModes } from '@/shared/state/modesSlice'; import { useThemeMode, useThemeAccent, useClaudeTokens } from '@/shared/styles/ThemeContext'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import { X } from 'lucide-react'; import DirectoryBrowser from '@/app/components/editor/DirectoryBrowser'; import { CommandsContent } from '@/app/pages/Commands/Commands'; -import GeneralTab from './sections/general/GeneralTab'; +import AccountCard from './sections/subscription/AccountCard'; +import GeneralAgentDefaults from './sections/general/GeneralAgentDefaults'; +import GeneralInterface from './sections/general/GeneralInterface'; +import GeneralAdvanced from './sections/general/GeneralAdvanced'; +import DataPrivacySection from './sections/general/DataPrivacySection'; import ModelsTab from './sections/models/ModelsTab'; import UsageStats from './sections/usage/UsageStats'; -import SettingsHeader from './sections/SettingsHeader'; +import SettingsRail, { railLabelFor } from './sections/SettingsRail'; import { makeSettingsStyles } from './sections/settingsStyles'; // Skills/Tools moved here from the old sidebar Customization section; lazy since both pull heavy deps and Settings opens nearly every session. @@ -92,7 +99,7 @@ const Settings: React.FC = () => { }, [modelsByProvider, modelsLoaded, settings.connection_mode, settings.default_model]); const initialTab = useAppSelector((s) => s.settings.initialTab); - const TAB_VALUES = ['general', 'models', 'skills', 'tools', 'commands', 'usage'] as const; + const TAB_VALUES = ['account', 'general', 'appearance', 'privacy', 'advanced', 'models', 'skills', 'tools', 'commands', 'usage'] as const; type SettingsTab = typeof TAB_VALUES[number]; const isValidTab = (t: string | null | undefined): t is SettingsTab => !!t && (TAB_VALUES as readonly string[]).includes(t); @@ -242,23 +249,30 @@ const Settings: React.FC = () => { maxWidth={false} PaperProps={{ sx: { - width: 780, + width: 880, height: '85vh', display: 'flex', - flexDirection: 'column', + flexDirection: 'row', bgcolor: c.bg.page, borderRadius: 2, border: `1px solid ${c.border.subtle}`, boxShadow: c.shadow.md, transition: 'none', + overflow: 'hidden', }, }} > - setActiveTab(v)} - onClose={handleRequestClose} - /> + setActiveTab(v as SettingsTab)} /> + + + + + {railLabelFor(activeTab)} + + + + + { scrollbarWidth: 'thin', scrollbarColor: `${c.border.medium} transparent`, }}> - {activeTab === 'general' ? ( - + {activeTab === 'account' ? ( + + + + ) : activeTab === 'general' ? ( + + + + ) : activeTab === 'appearance' ? ( + + + + ) : activeTab === 'privacy' ? ( + + + + ) : activeTab === 'advanced' ? ( + + + ) : activeTab === 'models' ? ( { )} + void; - onClose: () => void; -}> = ({ activeTab, onTabChange, onClose }) => { - const c = useClaudeTokens(); - return ( - - - - Settings - - - - - - onTabChange(v)} - sx={{ - minHeight: 34, - pb: 1, - // Pill segmented control: filled active pill, no underline indicator. - '& .MuiTabs-indicator': { display: 'none' }, - '& .MuiTab-root': { - minHeight: 30, - textTransform: 'none', - fontSize: '0.875rem', - fontWeight: 500, - color: c.text.muted, - px: 1.75, - mr: 0.5, - borderRadius: '999px', - transition: 'background-color 0.12s, color 0.12s', - '&:hover': { color: c.text.secondary, bgcolor: `${c.text.tertiary}0A` }, - '&.Mui-selected': { color: c.text.primary, fontWeight: 600, bgcolor: `${c.accent.primary}26` }, - }, - }} - > - - - - - - - - - ); -}; - -export default SettingsHeader; diff --git a/frontend/src/app/pages/Settings/sections/SettingsRail.tsx b/frontend/src/app/pages/Settings/sections/SettingsRail.tsx new file mode 100644 index 00000000..e494e33d --- /dev/null +++ b/frontend/src/app/pages/Settings/sections/SettingsRail.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, Sparkles, Hammer, SquareSlash, BarChart3 } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import { useClaudeTokens } from '@/shared/styles/ThemeContext'; + +// ChatGPT/Apple-style settings nav: a left rail of short, focused sections instead of one giant +// General scroll. Grouped so the eye lands (account, then app-wide, then capabilities). +export interface RailSection { + value: string; + label: string; + Icon: LucideIcon; +} + +interface RailGroup { + header: string | null; + sections: RailSection[]; +} + +export const RAIL_GROUPS: RailGroup[] = [ + { header: null, sections: [ + { value: 'account', label: 'Account', Icon: User }, + ] }, + { header: 'App', sections: [ + { value: 'general', label: 'General', Icon: Settings2 }, + { value: 'appearance', label: 'Appearance', Icon: Palette }, + { value: 'privacy', label: 'Privacy', Icon: ShieldCheck }, + { value: 'advanced', label: 'Advanced', Icon: Wrench }, + ] }, + { header: 'Capabilities', sections: [ + { value: 'models', label: 'Models', Icon: Boxes }, + { value: 'skills', label: 'Skills', Icon: Sparkles }, + { value: 'tools', label: 'Tools', Icon: Hammer }, + { value: 'commands', label: 'Commands', Icon: SquareSlash }, + { value: 'usage', label: 'Usage', Icon: BarChart3 }, + ] }, +]; + +export function railLabelFor(value: string): string { + for (const g of RAIL_GROUPS) { + const hit = g.sections.find((s) => s.value === value); + if (hit) return hit.label; + } + return 'Settings'; +} + +const SettingsRail: React.FC<{ + activeTab: string; + onTabChange: (v: string) => void; +}> = ({ activeTab, onTabChange }) => { + const c = useClaudeTokens(); + return ( + + + Settings + + {RAIL_GROUPS.map((group) => ( + + {group.header && ( + + {group.header} + + )} + {group.sections.map((s) => { + const selected = activeTab === s.value; + return ( + onTabChange(s.value)} + data-onboarding={s.value === 'models' ? 'settings-models-tab' : undefined} + sx={{ + display: 'flex', alignItems: 'center', gap: 1.25, width: '100%', + px: 1, py: 0.75, mb: '2px', border: 'none', borderRadius: '8px', + bgcolor: selected ? `${c.accent.primary}1F` : 'transparent', + color: selected ? c.text.primary : c.text.secondary, + fontFamily: 'inherit', fontSize: c.font.size.base, fontWeight: selected ? 600 : 500, + cursor: 'pointer', textAlign: 'left', + transition: 'background-color 0.12s, color 0.12s', + '&:hover': selected ? {} : { bgcolor: `${c.text.tertiary}0F`, color: c.text.primary }, + }} + > + + {s.label} + + ); + })} + + ))} + + ); +}; + +export default SettingsRail; diff --git a/frontend/src/app/pages/Settings/sections/general/DataPrivacySection.tsx b/frontend/src/app/pages/Settings/sections/general/DataPrivacySection.tsx index 70a742ef..ea416943 100644 --- a/frontend/src/app/pages/Settings/sections/general/DataPrivacySection.tsx +++ b/frontend/src/app/pages/Settings/sections/general/DataPrivacySection.tsx @@ -112,7 +112,6 @@ const DataPrivacySection: React.FC<{ styles: SettingsStyles }> = ({ styles }) => return ( - Data & Privacy diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx index b731ce06..104b3fa1 100644 --- a/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx +++ b/frontend/src/app/pages/Settings/sections/general/GeneralAdvanced.tsx @@ -35,7 +35,6 @@ const GeneralAdvanced: React.FC<{ return ( <> - Advanced diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralAgentDefaults.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralAgentDefaults.tsx index 9e8b0ef7..8ad13b64 100644 --- a/frontend/src/app/pages/Settings/sections/general/GeneralAgentDefaults.tsx +++ b/frontend/src/app/pages/Settings/sections/general/GeneralAgentDefaults.tsx @@ -33,7 +33,6 @@ const GeneralAgentDefaults: React.FC<{ return ( <> - Agent Defaults diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx index 52132607..564984a0 100644 --- a/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx +++ b/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx @@ -28,7 +28,6 @@ const GeneralInterface: React.FC<{ return ( <> - Interface diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralTab.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralTab.tsx deleted file mode 100644 index 4640c64e..00000000 --- a/frontend/src/app/pages/Settings/sections/general/GeneralTab.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { AppSettings } from '@/shared/state/settingsSlice'; -import AccountCard from '../subscription/AccountCard'; -import GeneralAgentDefaults from './GeneralAgentDefaults'; -import GeneralInterface from './GeneralInterface'; -import GeneralAdvanced from './GeneralAdvanced'; -import DataPrivacySection from './DataPrivacySection'; -import type { SettingsStyles } from '../settingsStyles'; - -type ModelOption = { value: string; label: string }; - -const GeneralTab: React.FC<{ - form: AppSettings; - setForm: React.Dispatch>; - styles: SettingsStyles; - setBrowseOpen: (v: boolean) => void; - modelOptions: { grouped: Record; flat: Array }; - modesList: Array<{ id: string; name: string }>; - providerColors: Record; - openswarmGradient: string; -}> = ({ form, setForm, styles, setBrowseOpen, modelOptions, modesList, providerColors, openswarmGradient }) => { - const { sectionSx } = styles; - return ( - - - Account - - - - - - - - - - - - ); -}; - -export default GeneralTab;