diff --git a/crates/bds-core/src/i18n/mod.rs b/crates/bds-core/src/i18n/mod.rs
index 5644e6f..20bf920 100644
--- a/crates/bds-core/src/i18n/mod.rs
+++ b/crates/bds-core/src/i18n/mod.rs
@@ -27,6 +27,17 @@ impl UiLocale {
pub fn all() -> &'static [UiLocale] {
&[Self::En, Self::De, Self::Fr, Self::It, Self::Es]
}
+
+ /// Unicode regional-indicator flag emoji for this locale.
+ pub fn flag_emoji(self) -> &'static str {
+ match self {
+ Self::En => "\u{1F1EC}\u{1F1E7}", // 🇬🇧
+ Self::De => "\u{1F1E9}\u{1F1EA}", // 🇩🇪
+ Self::Fr => "\u{1F1EB}\u{1F1F7}", // 🇫🇷
+ Self::It => "\u{1F1EE}\u{1F1F9}", // 🇮🇹
+ Self::Es => "\u{1F1EA}\u{1F1F8}", // 🇪🇸
+ }
+ }
}
impl std::fmt::Display for UiLocale {
diff --git a/crates/bds-ui/assets/flags/de.svg b/crates/bds-ui/assets/flags/de.svg
new file mode 100644
index 0000000..6a9c1cb
--- /dev/null
+++ b/crates/bds-ui/assets/flags/de.svg
@@ -0,0 +1,5 @@
+
diff --git a/crates/bds-ui/assets/flags/es.svg b/crates/bds-ui/assets/flags/es.svg
new file mode 100644
index 0000000..6f7843c
--- /dev/null
+++ b/crates/bds-ui/assets/flags/es.svg
@@ -0,0 +1,5 @@
+
diff --git a/crates/bds-ui/assets/flags/fr.svg b/crates/bds-ui/assets/flags/fr.svg
new file mode 100644
index 0000000..491bcf1
--- /dev/null
+++ b/crates/bds-ui/assets/flags/fr.svg
@@ -0,0 +1,5 @@
+
diff --git a/crates/bds-ui/assets/flags/gb.svg b/crates/bds-ui/assets/flags/gb.svg
new file mode 100644
index 0000000..82a0a1e
--- /dev/null
+++ b/crates/bds-ui/assets/flags/gb.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/bds-ui/assets/flags/it.svg b/crates/bds-ui/assets/flags/it.svg
new file mode 100644
index 0000000..118ee39
--- /dev/null
+++ b/crates/bds-ui/assets/flags/it.svg
@@ -0,0 +1,5 @@
+
diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs
index e806abf..f95896e 100644
--- a/crates/bds-ui/src/app.rs
+++ b/crates/bds-ui/src/app.rs
@@ -64,6 +64,7 @@ pub enum Message {
// Settings
SetOfflineMode(bool),
SetUiLocale(UiLocale),
+ ToggleLocaleDropdown,
// Blog actions (dispatched to engine)
RebuildDatabase,
@@ -113,6 +114,7 @@ pub struct BdsApp {
// Flags
offline_mode: bool,
+ locale_dropdown_open: bool,
// macOS lifecycle receiver
#[cfg(target_os = "macos")]
@@ -213,6 +215,7 @@ impl BdsApp {
menu_registry: registry,
ui_locale: locale,
offline_mode: false,
+ locale_dropdown_open: false,
#[cfg(target_os = "macos")]
_lifecycle_rx,
},
@@ -422,9 +425,14 @@ impl BdsApp {
}
Message::SetUiLocale(locale) => {
self.ui_locale = locale;
+ self.locale_dropdown_open = false;
menu::update_menu_labels(&self.menu_registry, locale);
Task::none()
}
+ Message::ToggleLocaleDropdown => {
+ self.locale_dropdown_open = !self.locale_dropdown_open;
+ Task::none()
+ }
// ── Blog engine actions ──
Message::RebuildDatabase => {
@@ -474,6 +482,7 @@ impl BdsApp {
0, // post_count — populated in later milestones
0, // media_count — populated in later milestones
self.offline_mode,
+ self.locale_dropdown_open,
self.ui_locale,
)
}
diff --git a/crates/bds-ui/src/views/status_bar.rs b/crates/bds-ui/src/views/status_bar.rs
index 6d22ebe..61a12c9 100644
--- a/crates/bds-ui/src/views/status_bar.rs
+++ b/crates/bds-ui/src/views/status_bar.rs
@@ -1,45 +1,13 @@
-use iced::widget::{button, container, pick_list, row, text, Space};
+use iced::widget::{button, column, container, row, text, Space};
+use iced::widget::text::Shaping;
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use crate::app::Message;
-use crate::i18n::{t, tw};
+use crate::i18n::tw;
use crate::state::navigation::TaskSnapshot;
-// ---------------------------------------------------------------------------
-// Locale option wrapper for pick_list (needs ToString + PartialEq + Clone)
-// ---------------------------------------------------------------------------
-
-/// Wraps a UiLocale with a display label for the pick_list dropdown.
-#[derive(Debug, Clone, PartialEq)]
-struct LocaleOption {
- locale: UiLocale,
- label: String,
-}
-
-impl ToString for LocaleOption {
- fn to_string(&self) -> String {
- self.label.clone()
- }
-}
-
-impl LocaleOption {
- fn new(locale: UiLocale, display_locale: UiLocale) -> Self {
- let key = match locale {
- UiLocale::En => "language.en",
- UiLocale::De => "language.de",
- UiLocale::Fr => "language.fr",
- UiLocale::It => "language.it",
- UiLocale::Es => "language.es",
- };
- Self {
- locale,
- label: t(display_locale, key),
- }
- }
-}
-
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
@@ -88,22 +56,52 @@ fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
}
}
-/// Pick list style matching status bar.
-fn locale_pick_list_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
+/// Dropdown trigger button style.
+fn dropdown_trigger(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
- pick_list::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
+ button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
_ => Color::TRANSPARENT,
};
- pick_list::Style {
- text_color: Color::from_rgb(0.70, 0.70, 0.75),
- placeholder_color: Color::from_rgb(0.50, 0.50, 0.55),
- handle_color: Color::from_rgb(0.50, 0.50, 0.55),
- background: Background::Color(bg),
+ button::Style {
+ background: Some(Background::Color(bg)),
+ text_color: Color::WHITE,
border: Border {
- color: Color::from_rgb(0.25, 0.25, 0.30),
- width: 1.0,
+ color: Color::TRANSPARENT,
+ width: 0.0,
radius: 3.0.into(),
},
+ ..button::Style::default()
+ }
+}
+
+/// Dropdown item style.
+fn dropdown_item(_theme: &Theme, status: button::Status) -> button::Style {
+ let bg = match status {
+ button::Status::Hovered => Color::from_rgb(0.25, 0.25, 0.30),
+ _ => Color::from_rgb(0.18, 0.18, 0.22),
+ };
+ button::Style {
+ background: Some(Background::Color(bg)),
+ text_color: Color::WHITE,
+ border: Border {
+ color: Color::TRANSPARENT,
+ width: 0.0,
+ radius: 2.0.into(),
+ },
+ ..button::Style::default()
+ }
+}
+
+/// Dropdown container background.
+fn dropdown_bg(_theme: &Theme) -> container::Style {
+ container::Style {
+ background: Some(Background::Color(Color::from_rgb(0.18, 0.18, 0.22))),
+ border: Border {
+ color: Color::from_rgb(0.30, 0.30, 0.35),
+ width: 1.0,
+ radius: 4.0.into(),
+ },
+ ..container::Style::default()
}
}
@@ -117,6 +115,7 @@ pub fn view(
media_count: usize,
locale: UiLocale,
offline_mode: bool,
+ locale_dropdown_open: bool,
task_snapshots: &[TaskSnapshot],
) -> Element<'static, Message> {
let label_color = Color::from_rgb(0.60, 0.60, 0.65);
@@ -148,41 +147,35 @@ pub fn view(
let posts_label = tw(locale, "statusBar.posts", &[("count", &post_count.to_string())]);
let media_label = tw(locale, "statusBar.media", &[("count", &media_count.to_string())]);
- // Airplane mode toggle — just the ✈ icon, like bDS
- let airplane_btn = button(text("\u{2708}").size(13))
+ // Airplane mode toggle — ✈ icon
+ let airplane_btn = button(
+ text("\u{2708}").size(13).shaping(Shaping::Advanced),
+ )
.on_press(Message::SetOfflineMode(!offline_mode))
.padding([2, 4])
.style(if offline_mode { airplane_active } else { airplane_inactive });
- // Language selector — "UI" label + pick_list dropdown, like bDS
- let options: Vec = UiLocale::all()
- .iter()
- .map(|l| LocaleOption::new(*l, locale))
- .collect();
- let selected = Some(LocaleOption::new(locale, locale));
+ // Language selector — current flag as trigger
+ let trigger_flag = text(locale.flag_emoji())
+ .size(14)
+ .shaping(Shaping::Advanced);
- let locale_picker = pick_list(
- options,
- selected,
- |opt: LocaleOption| Message::SetUiLocale(opt.locale),
- )
- .text_size(11)
- .padding([2, 4])
- .width(Length::Shrink)
- .style(locale_pick_list_style);
+ let locale_trigger = button(trigger_flag)
+ .on_press(Message::ToggleLocaleDropdown)
+ .padding([1, 4])
+ .style(dropdown_trigger);
let right = row![
text(posts_label).size(11).color(label_color),
text(media_label).size(11).color(label_color),
airplane_btn,
- text(t(locale, "statusBar.ui")).size(11).color(label_color),
- locale_picker,
+ locale_trigger,
text("bDS").size(11).color(Color::from_rgb(0.45, 0.45, 0.50)),
]
.spacing(8)
.align_y(Alignment::Center);
- container(
+ let status_row = container(
row![
left,
Space::with_width(Length::Fill),
@@ -193,6 +186,43 @@ pub fn view(
)
.width(Length::Fill)
.height(Length::Fixed(24.0))
- .style(bar_style)
- .into()
+ .style(bar_style);
+
+ // If dropdown is open, stack it above the status bar
+ if locale_dropdown_open {
+ let items: Vec> = UiLocale::all()
+ .iter()
+ .map(|&l| {
+ let flag_text = text(l.flag_emoji())
+ .size(16)
+ .shaping(Shaping::Advanced);
+
+ button(flag_text)
+ .on_press(Message::SetUiLocale(l))
+ .padding([4, 8])
+ .width(Length::Fill)
+ .style(dropdown_item)
+ .into()
+ })
+ .collect();
+
+ let dropdown_menu = container(
+ iced::widget::Column::with_children(items).spacing(2).padding(4),
+ )
+ .style(dropdown_bg);
+
+ // Align dropdown to the right, above the status bar
+ let dropdown_row = row![
+ Space::with_width(Length::Fill),
+ dropdown_menu,
+ // offset from right edge to roughly align with the flag trigger
+ Space::with_width(Length::Fixed(40.0)),
+ ];
+
+ column![dropdown_row, status_row]
+ .width(Length::Fill)
+ .into()
+ } else {
+ status_row.into()
+ }
}
diff --git a/crates/bds-ui/src/views/workspace.rs b/crates/bds-ui/src/views/workspace.rs
index 5af1035..0456d52 100644
--- a/crates/bds-ui/src/views/workspace.rs
+++ b/crates/bds-ui/src/views/workspace.rs
@@ -56,6 +56,7 @@ pub fn view(
post_count: usize,
media_count: usize,
offline_mode: bool,
+ locale_dropdown_open: bool,
// i18n
locale: UiLocale,
) -> Element<'static, Message> {
@@ -97,6 +98,7 @@ pub fn view(
media_count,
locale,
offline_mode,
+ locale_dropdown_open,
task_snapshots,
);