diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index 727a87a..5c55a25 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -44,6 +44,7 @@ pub enum Message { SetActiveView(SidebarView), ToggleSidebar, TogglePanel, + OpenSettingsSection(crate::views::settings_view::SettingsSection), // Sidebar resize SidebarResizeStart, @@ -748,6 +749,19 @@ impl BdsApp { self.panel_visible = !self.panel_visible; Task::none() } + Message::OpenSettingsSection(section) => { + self.sidebar_view = SidebarView::Settings; + self.sidebar_visible = true; + self.open_singleton_tab(TabType::Settings, "common.settings"); + if self.settings_state.is_none() { + self.settings_state = Some(self.hydrate_settings_state()); + } + if let Some(state) = self.settings_state.as_mut() { + state.focus_section(section); + } + self.sync_menu_state(); + Task::none() + } Message::SidebarResizeStart => { self.sidebar_dragging = true; Task::none() @@ -3463,16 +3477,7 @@ impl BdsApp { } } SettingsMsg::FocusSection(section) => { - // Expand the target section, collapse all others - use crate::views::settings_view::SettingsSection; - let all_others: Vec = SettingsSection::all() - .iter() - .filter(|s| **s != section) - .cloned() - .collect(); - state.collapsed = all_others; - // Clear search filter to ensure section is visible - state.search_query.clear(); + state.focus_section(section); } } Task::none() diff --git a/crates/bds-ui/src/views/settings_view.rs b/crates/bds-ui/src/views/settings_view.rs index 909f0cb..42e089d 100644 --- a/crates/bds-ui/src/views/settings_view.rs +++ b/crates/bds-ui/src/views/settings_view.rs @@ -53,6 +53,7 @@ impl SettingsSection { pub struct SettingsViewState { pub search_query: String, pub collapsed: Vec, + pub active_section: Option, // Project pub project_name: String, pub project_description: text_editor::Content, @@ -88,6 +89,7 @@ impl Clone for SettingsViewState { Self { search_query: self.search_query.clone(), collapsed: self.collapsed.clone(), + active_section: self.active_section.clone(), project_name: self.project_name.clone(), project_description: text_editor::Content::with_text(&self.project_description.text()), data_path: self.data_path.clone(), @@ -113,6 +115,7 @@ impl Default for SettingsViewState { Self { search_query: String::new(), collapsed: Vec::new(), + active_section: None, project_name: String::new(), project_description: text_editor::Content::new(), data_path: String::new(), @@ -133,6 +136,29 @@ impl Default for SettingsViewState { } } +impl SettingsViewState { + pub fn focus_section(&mut self, section: SettingsSection) { + self.collapsed = SettingsSection::all() + .iter() + .filter(|candidate| **candidate != section) + .cloned() + .collect(); + self.search_query.clear(); + self.active_section = Some(section); + } + + fn ordered_sections(&self) -> Vec { + let mut sections = SettingsSection::all().to_vec(); + if let Some(active) = &self.active_section { + if let Some(index) = sections.iter().position(|section| section == active) { + let focused = sections.remove(index); + sections.insert(0, focused); + } + } + sections + } +} + /// Settings view messages. #[derive(Debug, Clone)] pub enum SettingsMsg { @@ -190,13 +216,13 @@ pub fn view<'a>( let query_lower = state.search_query.to_lowercase(); let mut sections = column![].spacing(12).width(Length::Fill); - for section in SettingsSection::all() { + for section in state.ordered_sections() { let label = t(locale, section.i18n_key()); if !query_lower.is_empty() && !label.to_lowercase().contains(&query_lower) { continue; } - let collapsed = state.collapsed.contains(section); - let section_el = render_section(state, section, &label, collapsed, locale); + let collapsed = state.collapsed.contains(§ion); + let section_el = render_section(state, §ion, &label, collapsed, locale); sections = sections.push(section_el); } @@ -215,6 +241,38 @@ pub fn view<'a>( .into() } +#[cfg(test)] +mod tests { + use super::{SettingsSection, SettingsViewState}; + + #[test] + fn focus_section_collapses_other_sections_and_clears_search() { + let mut state = SettingsViewState { + search_query: "publish".to_string(), + ..SettingsViewState::default() + }; + + state.focus_section(SettingsSection::Publishing); + + assert_eq!(state.active_section, Some(SettingsSection::Publishing)); + assert!(state.search_query.is_empty()); + assert!(!state.collapsed.contains(&SettingsSection::Publishing)); + assert!(state.collapsed.contains(&SettingsSection::Project)); + assert!(state.collapsed.contains(&SettingsSection::MCP)); + } + + #[test] + fn focused_section_is_rendered_first() { + let mut state = SettingsViewState::default(); + state.focus_section(SettingsSection::Technology); + + let ordered = state.ordered_sections(); + + assert_eq!(ordered.first(), Some(&SettingsSection::Technology)); + assert_eq!(ordered.len(), SettingsSection::all().len()); + } +} + fn render_section<'a>( state: &'a SettingsViewState, section: &SettingsSection, diff --git a/crates/bds-ui/src/views/sidebar.rs b/crates/bds-ui/src/views/sidebar.rs index 877fe74..25d65f6 100644 --- a/crates/bds-ui/src/views/sidebar.rs +++ b/crates/bds-ui/src/views/sidebar.rs @@ -930,7 +930,7 @@ pub fn view( } SidebarView::Settings => { // Per sidebar_views.allium SettingsNav: 9 fixed-order sections - use crate::views::settings_view::{SettingsSection, SettingsMsg}; + use crate::views::settings_view::SettingsSection; let sections: &[(&str, Option)] = &[ ("settings.nav.project", Some(SettingsSection::Project)), ("settings.nav.editor", Some(SettingsSection::Editor)), @@ -950,12 +950,12 @@ pub fn view( .size(12) .shaping(Shaping::Advanced); let msg = if let Some(section) = section_opt { - Message::Settings(SettingsMsg::FocusSection(section.clone())) + Message::OpenSettingsSection(section.clone()) } else { Message::OpenTab(Tab { - id: "settings".to_string(), - tab_type: TabType::Settings, - title: t(locale, "common.settings"), + id: "style".to_string(), + tab_type: TabType::Style, + title: t(locale, "tabBar.style"), is_transient: false, is_dirty: false, })