feat: more work on UI
6
crates/bds-ui/assets/icons/chat.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
|
||||
<circle cx="8" cy="10" r="1.5"/>
|
||||
<circle cx="12" cy="10" r="1.5"/>
|
||||
<circle cx="16" cy="10" r="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
3
crates/bds-ui/assets/icons/git.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M22 11.73L12.27 2a1 1 0 0 0-1.41 0L8.84 4.02l2.56 2.56a1.2 1.2 0 0 1 1.52 1.53l2.47 2.47a1.2 1.2 0 1 1-.72.67l-2.3-2.3v6.06a1.2 1.2 0 1 1-.85 0V8.9a1.2 1.2 0 0 1-.66-1.59L8.35 4.8 2 11.16a1 1 0 0 0 0 1.41L11.73 22a1 1 0 0 0 1.41 0L22 13.14a1 1 0 0 0 0-1.41z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
3
crates/bds-ui/assets/icons/import.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 162 B |
3
crates/bds-ui/assets/icons/media.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
3
crates/bds-ui/assets/icons/pages.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M4 4h10v4h6v12H4V4zm10 1.5V9h4.5L14 5.5zM7 12h10v1.5H7V12zm0 3h10v1.5H7V15z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
4
crates/bds-ui/assets/icons/posts.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/>
|
||||
<path d="M8 12h8v2H8zm0 4h8v2H8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
3
crates/bds-ui/assets/icons/scripts.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
3
crates/bds-ui/assets/icons/settings.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
3
crates/bds-ui/assets/icons/tags.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
3
crates/bds-ui/assets/icons/templates.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff">
|
||||
<path d="M4 4h7v7H4V4zm9 0h7v7h-7V4zM4 13h7v7H4v-7zm9 0h7v7h-7v-7zM5.5 5.5v4h4v-4h-4zm9 0v4h4v-4h-4zm-9 9v4h4v-4h-4zm9 0v4h4v-4h-4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -3,6 +3,7 @@ use bds_ui::BdsApp;
|
||||
fn main() -> iced::Result {
|
||||
iced::application("bDS", BdsApp::update, BdsApp::view)
|
||||
.subscription(BdsApp::subscription)
|
||||
.theme(|_| iced::Theme::Dark)
|
||||
.window_size((1200.0, 800.0))
|
||||
.run_with(BdsApp::new)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
use iced::widget::{button, column, container, text, Column, Space};
|
||||
use iced::{Alignment, Element, Length};
|
||||
use iced::widget::{button, column, container, svg, Column, Space};
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::state::navigation::SidebarView;
|
||||
|
||||
/// Activity-bar short labels used in M2 (no SVG icons yet).
|
||||
fn short_label(view: SidebarView) -> &'static str {
|
||||
// ---------------------------------------------------------------------------
|
||||
// SVG icon data — ported from bDS ActivityBar.tsx inline SVGs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn icon_svg(view: SidebarView) -> &'static [u8] {
|
||||
match view {
|
||||
SidebarView::Posts => "Po",
|
||||
SidebarView::Pages => "Pa",
|
||||
SidebarView::Media => "Me",
|
||||
SidebarView::Scripts => "Sc",
|
||||
SidebarView::Templates => "Tp",
|
||||
SidebarView::Tags => "Ta",
|
||||
SidebarView::Chat => "AI",
|
||||
SidebarView::Import => "Im",
|
||||
SidebarView::Git => "Gi",
|
||||
SidebarView::Settings => "Se",
|
||||
SidebarView::Posts => include_bytes!("../../assets/icons/posts.svg"),
|
||||
SidebarView::Pages => include_bytes!("../../assets/icons/pages.svg"),
|
||||
SidebarView::Media => include_bytes!("../../assets/icons/media.svg"),
|
||||
SidebarView::Scripts => include_bytes!("../../assets/icons/scripts.svg"),
|
||||
SidebarView::Templates => include_bytes!("../../assets/icons/templates.svg"),
|
||||
SidebarView::Tags => include_bytes!("../../assets/icons/tags.svg"),
|
||||
SidebarView::Chat => include_bytes!("../../assets/icons/chat.svg"),
|
||||
SidebarView::Import => include_bytes!("../../assets/icons/import.svg"),
|
||||
SidebarView::Git => include_bytes!("../../assets/icons/git.svg"),
|
||||
SidebarView::Settings => include_bytes!("../../assets/icons/settings.svg"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,28 +43,93 @@ const BOTTOM_ACTIVITIES: &[SidebarView] = &[
|
||||
SidebarView::Settings,
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Styles — matching bDS ActivityBar.css
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Active button: transparent bg.
|
||||
fn active_button_style(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inactive button: hover brightens background.
|
||||
fn inactive_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
|
||||
_ => Color::TRANSPARENT,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.55, 0.55, 0.60),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Activity bar container: dark background.
|
||||
fn bar_background_style(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.18))),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// View
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn view(
|
||||
active_view: SidebarView,
|
||||
_locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let make_btn = |view: SidebarView| -> Element<'static, Message> {
|
||||
let label = short_label(view);
|
||||
let mut btn = button(
|
||||
text(label).size(12).center(),
|
||||
let handle = svg::Handle::from_memory(icon_svg(view));
|
||||
let is_active = view == active_view;
|
||||
|
||||
// Render SVG: active = full opacity, inactive = 0.4 opacity (like bDS 60% opacity)
|
||||
let icon = svg(handle)
|
||||
.width(Length::Fixed(24.0))
|
||||
.height(Length::Fixed(24.0))
|
||||
.opacity(if is_active { 1.0 } else { 0.4 });
|
||||
|
||||
let btn = button(
|
||||
container(icon)
|
||||
.center_x(Length::Fixed(48.0))
|
||||
.center_y(Length::Fixed(48.0)),
|
||||
)
|
||||
.width(Length::Fixed(48.0))
|
||||
.height(Length::Fixed(48.0))
|
||||
.on_press(Message::SetActiveView(view));
|
||||
.padding(0)
|
||||
.on_press(Message::SetActiveView(view))
|
||||
.style(if is_active { active_button_style } else { inactive_button_style });
|
||||
|
||||
if view == active_view {
|
||||
btn = btn.style(button::primary);
|
||||
// Active indicator: 2px left border (like bDS/VS Code)
|
||||
if is_active {
|
||||
let indicator = container(Space::new(0, 0))
|
||||
.width(Length::Fixed(2.0))
|
||||
.height(Length::Fixed(48.0))
|
||||
.style(|_theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::WHITE)),
|
||||
..container::Style::default()
|
||||
});
|
||||
iced::widget::row![indicator, btn].into()
|
||||
} else {
|
||||
btn = btn.style(button::secondary);
|
||||
let spacer = Space::with_width(2.0);
|
||||
iced::widget::row![spacer, btn].into()
|
||||
}
|
||||
|
||||
container(btn)
|
||||
.center_x(Length::Fixed(48.0))
|
||||
.into()
|
||||
};
|
||||
|
||||
let top_items: Vec<Element<'static, Message>> = TOP_ACTIVITIES
|
||||
@@ -74,16 +142,21 @@ pub fn view(
|
||||
.map(|v| make_btn(*v))
|
||||
.collect();
|
||||
|
||||
let top = Column::with_children(top_items).spacing(2);
|
||||
let bottom = Column::with_children(bottom_items).spacing(2);
|
||||
let top = Column::with_children(top_items).spacing(0);
|
||||
let bottom = Column::with_children(bottom_items).spacing(0);
|
||||
|
||||
column![
|
||||
top,
|
||||
Space::with_height(Length::Fill),
|
||||
bottom,
|
||||
]
|
||||
.width(Length::Fixed(48.0))
|
||||
container(
|
||||
column![
|
||||
top,
|
||||
Space::with_height(Length::Fill),
|
||||
bottom,
|
||||
]
|
||||
.width(Length::Fixed(50.0))
|
||||
.height(Length::Fill)
|
||||
.padding([4, 0]),
|
||||
)
|
||||
.width(Length::Fixed(50.0))
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.style(bar_background_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||
use iced::{Alignment, Element, Length};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
@@ -7,41 +7,83 @@ use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::{OutputEntry, PanelTab, TaskSnapshot};
|
||||
|
||||
/// Panel background style.
|
||||
fn panel_style(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.13, 0.13, 0.16))),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Panel tab button — active.
|
||||
fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.20, 0.20, 0.25))),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.30, 0.55, 0.90),
|
||||
width: 0.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Panel tab button — inactive.
|
||||
fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.18, 0.18, 0.22),
|
||||
_ => Color::TRANSPARENT,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.60, 0.60, 0.65),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Close button style.
|
||||
fn close_btn_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let color = match status {
|
||||
button::Status::Hovered => Color::WHITE,
|
||||
_ => Color::from_rgb(0.50, 0.50, 0.55),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: color,
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
panel_tab: PanelTab,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
output_entries: &[OutputEntry],
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let muted = Color::from_rgb(0.50, 0.50, 0.55);
|
||||
|
||||
// Tab header
|
||||
let tasks_btn = {
|
||||
let mut btn = button(text(t(locale, "common.tasks")).size(12))
|
||||
.on_press(Message::SetPanelTab(PanelTab::Tasks))
|
||||
.padding([4, 8]);
|
||||
if panel_tab == PanelTab::Tasks {
|
||||
btn = btn.style(button::primary);
|
||||
} else {
|
||||
btn = btn.style(button::secondary);
|
||||
}
|
||||
btn
|
||||
};
|
||||
let tasks_btn = button(text(t(locale, "common.tasks")).size(12))
|
||||
.on_press(Message::SetPanelTab(PanelTab::Tasks))
|
||||
.padding([4, 8])
|
||||
.style(if panel_tab == PanelTab::Tasks { tab_active } else { tab_inactive });
|
||||
|
||||
let output_btn = {
|
||||
let mut btn = button(text(t(locale, "panel.output")).size(12))
|
||||
.on_press(Message::SetPanelTab(PanelTab::Output))
|
||||
.padding([4, 8]);
|
||||
if panel_tab == PanelTab::Output {
|
||||
btn = btn.style(button::primary);
|
||||
} else {
|
||||
btn = btn.style(button::secondary);
|
||||
}
|
||||
btn
|
||||
};
|
||||
let output_btn = button(text(t(locale, "panel.output")).size(12))
|
||||
.on_press(Message::SetPanelTab(PanelTab::Output))
|
||||
.padding([4, 8])
|
||||
.style(if panel_tab == PanelTab::Output { tab_active } else { tab_inactive });
|
||||
|
||||
let close_btn = button(text("×").size(12))
|
||||
let close_btn = button(text("\u{2715}").size(12))
|
||||
.on_press(Message::TogglePanel)
|
||||
.padding([4, 6])
|
||||
.style(button::text);
|
||||
.style(close_btn_style);
|
||||
|
||||
let tab_header = row![
|
||||
tasks_btn,
|
||||
@@ -50,13 +92,14 @@ pub fn view(
|
||||
close_btn,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(Alignment::Center);
|
||||
.align_y(Alignment::Center)
|
||||
.padding([4, 8]);
|
||||
|
||||
// Tab content
|
||||
let content: Element<'static, Message> = match panel_tab {
|
||||
PanelTab::Tasks => {
|
||||
if task_snapshots.is_empty() {
|
||||
container(text(t(locale, "tasks.noActive")).size(12))
|
||||
container(text(t(locale, "tasks.noActive")).size(12).color(muted))
|
||||
.padding(8)
|
||||
.into()
|
||||
} else {
|
||||
@@ -64,14 +107,14 @@ pub fn view(
|
||||
.iter()
|
||||
.map(|snap| {
|
||||
let status_text = format!(
|
||||
"{} — {}{}",
|
||||
"{} \u{2014} {}{}",
|
||||
snap.label,
|
||||
snap.status,
|
||||
snap.progress
|
||||
.map(|p| format!(" ({:.0}%)", p * 100.0))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
text(status_text).size(11).into()
|
||||
text(status_text).size(11).color(Color::from_rgb(0.70, 0.70, 0.75)).into()
|
||||
})
|
||||
.collect();
|
||||
scrollable(
|
||||
@@ -84,13 +127,18 @@ pub fn view(
|
||||
}
|
||||
PanelTab::Output => {
|
||||
if output_entries.is_empty() {
|
||||
container(text(t(locale, "panel.noOutput")).size(12))
|
||||
container(text(t(locale, "panel.noOutput")).size(12).color(muted))
|
||||
.padding(8)
|
||||
.into()
|
||||
} else {
|
||||
let items: Vec<Element<'static, Message>> = output_entries
|
||||
.iter()
|
||||
.map(|entry| text(entry.text.clone()).size(11).into())
|
||||
.map(|entry| {
|
||||
text(entry.text.clone())
|
||||
.size(11)
|
||||
.color(Color::from_rgb(0.70, 0.70, 0.75))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
scrollable(
|
||||
iced::widget::Column::with_children(items)
|
||||
@@ -103,9 +151,10 @@ pub fn view(
|
||||
};
|
||||
|
||||
container(
|
||||
column![tab_header, content].spacing(4),
|
||||
column![tab_header, content].spacing(0),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(200.0))
|
||||
.style(panel_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use iced::widget::{column, container, scrollable, text};
|
||||
use iced::{Element, Length};
|
||||
use iced::widget::{column, container, scrollable, text, Space};
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
@@ -7,31 +7,57 @@ use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::SidebarView;
|
||||
|
||||
/// Sidebar container style — dark background with right border separator.
|
||||
fn sidebar_style(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.16, 0.16, 0.20))),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.25, 0.25, 0.30),
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the appropriate empty-state message key for each sidebar view.
|
||||
fn placeholder_key(view: SidebarView) -> &'static str {
|
||||
match view {
|
||||
SidebarView::Posts => "sidebar.noPostsYet",
|
||||
SidebarView::Pages => "sidebar.noPagesYet",
|
||||
SidebarView::Media => "sidebar.noMediaYet",
|
||||
SidebarView::Scripts => "sidebar.noScriptsYet",
|
||||
SidebarView::Templates => "sidebar.noTemplatesYet",
|
||||
SidebarView::Tags => "sidebar.tagsHeader",
|
||||
SidebarView::Chat => "sidebar.chatPlaceholder",
|
||||
SidebarView::Import => "sidebar.importPlaceholder",
|
||||
SidebarView::Git => "sidebar.gitPlaceholder",
|
||||
SidebarView::Settings => "sidebar.settingsHeader",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
sidebar_view: SidebarView,
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let header_key = sidebar_view.i18n_key();
|
||||
let header_text = t(locale, header_key);
|
||||
|
||||
let placeholder = match sidebar_view {
|
||||
SidebarView::Posts => t(locale, "sidebar.noPostsYet"),
|
||||
SidebarView::Pages => t(locale, "sidebar.noPagesYet"),
|
||||
SidebarView::Media => t(locale, "sidebar.noMediaYet"),
|
||||
SidebarView::Settings => t(locale, "sidebar.settingsHeader"),
|
||||
SidebarView::Tags => t(locale, "sidebar.tagsHeader"),
|
||||
_ => t(locale, "sidebar.loading"),
|
||||
};
|
||||
let header_text = t(locale, sidebar_view.i18n_key());
|
||||
let placeholder = t(locale, placeholder_key(sidebar_view));
|
||||
|
||||
let content = column![
|
||||
text(header_text).size(14),
|
||||
text(placeholder).size(12),
|
||||
text(header_text)
|
||||
.size(13)
|
||||
.color(Color::from_rgb(0.85, 0.85, 0.90)),
|
||||
Space::with_height(8.0),
|
||||
text(placeholder)
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.50, 0.50, 0.55)),
|
||||
]
|
||||
.spacing(8)
|
||||
.spacing(4)
|
||||
.padding(12);
|
||||
|
||||
container(scrollable(content))
|
||||
.width(Length::Fixed(280.0))
|
||||
.width(Length::Fixed(240.0))
|
||||
.height(Length::Fill)
|
||||
.style(sidebar_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use iced::widget::{button, container, row, text, Space};
|
||||
use iced::{Alignment, Element, Length};
|
||||
use iced::widget::{button, container, pick_list, row, text, Space};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
@@ -7,6 +7,110 @@ use crate::app::Message;
|
||||
use crate::i18n::{t, 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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Status bar background.
|
||||
fn bar_style(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.12, 0.12, 0.16))),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Airplane button — inactive (dim).
|
||||
fn airplane_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
|
||||
_ => Color::TRANSPARENT,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgba(0.70, 0.70, 0.75, 0.4),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Airplane button — active (warning color).
|
||||
fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.85, 0.65, 0.10),
|
||||
_ => Color::from_rgb(0.75, 0.55, 0.05),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick list style matching status bar.
|
||||
fn locale_pick_list_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
|
||||
let bg = match status {
|
||||
pick_list::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),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.25, 0.25, 0.30),
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// View
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn view(
|
||||
active_project_name: Option<&str>,
|
||||
post_count: usize,
|
||||
@@ -15,9 +119,11 @@ pub fn view(
|
||||
offline_mode: bool,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
) -> Element<'static, Message> {
|
||||
// Left: project name + task indicator
|
||||
let project_label = active_project_name.unwrap_or("—").to_string();
|
||||
let project_text = text(project_label).size(12);
|
||||
let label_color = Color::from_rgb(0.60, 0.60, 0.65);
|
||||
|
||||
// ── Left: project name + task indicator ──
|
||||
let project_label = active_project_name.unwrap_or("\u{2014}").to_string();
|
||||
let project_text = text(project_label).size(12).color(Color::from_rgb(0.80, 0.80, 0.85));
|
||||
|
||||
let running: Vec<&TaskSnapshot> = task_snapshots
|
||||
.iter()
|
||||
@@ -26,9 +132,9 @@ pub fn view(
|
||||
let task_indicator: Element<'static, Message> = if !running.is_empty() {
|
||||
let first = running[0].label.clone();
|
||||
if running.len() > 1 {
|
||||
text(format!("{first} (+{})", running.len() - 1)).size(11).into()
|
||||
text(format!("{first} (+{})", running.len() - 1)).size(11).color(label_color).into()
|
||||
} else {
|
||||
text(first).size(11).into()
|
||||
text(first).size(11).color(label_color).into()
|
||||
}
|
||||
} else {
|
||||
Space::with_width(0).into()
|
||||
@@ -36,50 +142,44 @@ pub fn view(
|
||||
|
||||
let left = row![project_text, task_indicator].spacing(12);
|
||||
|
||||
// Right: post count, media count, airplane mode, locale selector, brand
|
||||
// ── Right side ──
|
||||
|
||||
// Post + media counts
|
||||
let posts_label = tw(locale, "statusBar.posts", &[("count", &post_count.to_string())]);
|
||||
let media_label = tw(locale, "statusBar.media", &[("count", &media_count.to_string())]);
|
||||
|
||||
let airplane_label = if offline_mode {
|
||||
t(locale, "statusBar.offlineModeActive")
|
||||
} else {
|
||||
t(locale, "statusBar.offlineMode")
|
||||
};
|
||||
|
||||
let airplane_btn = button(text(airplane_label).size(11))
|
||||
// Airplane mode toggle — just the ✈ icon, like bDS
|
||||
let airplane_btn = button(text("\u{2708}").size(13))
|
||||
.on_press(Message::SetOfflineMode(!offline_mode))
|
||||
.padding([2, 6])
|
||||
.style(if offline_mode { button::primary } else { button::text });
|
||||
.padding([2, 4])
|
||||
.style(if offline_mode { airplane_active } else { airplane_inactive });
|
||||
|
||||
let ui_label = t(locale, "statusBar.ui");
|
||||
|
||||
let locale_buttons: Vec<Element<'static, Message>> = UiLocale::all()
|
||||
// Language selector — "UI" label + pick_list dropdown, like bDS
|
||||
let options: Vec<LocaleOption> = UiLocale::all()
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let code = l.code().to_string();
|
||||
let mut btn = button(text(code).size(11))
|
||||
.on_press(Message::SetUiLocale(*l))
|
||||
.padding([2, 4]);
|
||||
if *l == locale {
|
||||
btn = btn.style(button::primary);
|
||||
} else {
|
||||
btn = btn.style(button::text);
|
||||
}
|
||||
btn.into()
|
||||
})
|
||||
.map(|l| LocaleOption::new(*l, locale))
|
||||
.collect();
|
||||
let selected = Some(LocaleOption::new(locale, locale));
|
||||
|
||||
let locale_row = iced::widget::Row::with_children(locale_buttons).spacing(2);
|
||||
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 right = row![
|
||||
text(posts_label).size(11),
|
||||
text(media_label).size(11),
|
||||
text(posts_label).size(11).color(label_color),
|
||||
text(media_label).size(11).color(label_color),
|
||||
airplane_btn,
|
||||
text(ui_label).size(11),
|
||||
locale_row,
|
||||
text("bDS").size(11),
|
||||
text(t(locale, "statusBar.ui")).size(11).color(label_color),
|
||||
locale_picker,
|
||||
text("bDS").size(11).color(Color::from_rgb(0.45, 0.45, 0.50)),
|
||||
]
|
||||
.spacing(12)
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
container(
|
||||
@@ -92,6 +192,7 @@ pub fn view(
|
||||
.padding([0, 8]),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(22.0))
|
||||
.height(Length::Fixed(24.0))
|
||||
.style(bar_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,78 @@
|
||||
use iced::widget::{button, container, row, text, Space};
|
||||
use iced::{Element, Length};
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::state::tabs::Tab;
|
||||
|
||||
/// Tab bar background.
|
||||
fn bar_style(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.18))),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.25, 0.25, 0.30),
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Active tab style.
|
||||
fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.11, 0.11, 0.14))),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.30, 0.55, 0.90),
|
||||
width: 0.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inactive tab style.
|
||||
fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.18, 0.18, 0.22),
|
||||
_ => Color::from_rgb(0.14, 0.14, 0.18),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.60, 0.60, 0.65),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Close button on tab.
|
||||
fn close_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let color = match status {
|
||||
button::Status::Hovered => Color::WHITE,
|
||||
_ => Color::from_rgb(0.45, 0.45, 0.50),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: color,
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
tabs: &[Tab],
|
||||
active_tab: Option<&str>,
|
||||
) -> Element<'static, Message> {
|
||||
if tabs.is_empty() {
|
||||
return Space::with_height(0).into();
|
||||
return container(Space::new(0, 0))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(35.0))
|
||||
.style(bar_style)
|
||||
.into();
|
||||
}
|
||||
|
||||
let tab_buttons: Vec<Element<'static, Message>> = tabs
|
||||
@@ -17,7 +80,7 @@ pub fn view(
|
||||
.map(|tab| {
|
||||
let is_active = active_tab == Some(tab.id.as_str());
|
||||
let title_text = if tab.is_transient {
|
||||
format!("{} (preview)", tab.title)
|
||||
format!("{} \u{25CB}", tab.title) // hollow circle for transient
|
||||
} else {
|
||||
tab.title.clone()
|
||||
};
|
||||
@@ -26,24 +89,19 @@ pub fn view(
|
||||
|
||||
let label = row![
|
||||
text(title_text).size(12),
|
||||
button(text("×").size(12))
|
||||
button(text("\u{2715}").size(10))
|
||||
.on_press(Message::CloseTab(close_id))
|
||||
.padding(2)
|
||||
.style(button::text),
|
||||
.style(close_style),
|
||||
]
|
||||
.spacing(4);
|
||||
.spacing(6)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
let mut btn = button(label)
|
||||
button(label)
|
||||
.on_press(Message::SelectTab(tab_id))
|
||||
.padding([4, 8]);
|
||||
|
||||
if is_active {
|
||||
btn = btn.style(button::primary);
|
||||
} else {
|
||||
btn = btn.style(button::secondary);
|
||||
}
|
||||
|
||||
btn.into()
|
||||
.padding([6, 12])
|
||||
.style(if is_active { tab_active } else { tab_inactive })
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -54,5 +112,6 @@ pub fn view(
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(35.0))
|
||||
.style(bar_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use iced::widget::{column, container, text};
|
||||
use iced::{Element, Length};
|
||||
use iced::{Background, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
@@ -12,8 +12,12 @@ pub fn view(locale: UiLocale) -> Element<'static, Message> {
|
||||
|
||||
container(
|
||||
column![
|
||||
text(title).size(28),
|
||||
text(subtitle).size(14),
|
||||
text(title)
|
||||
.size(28)
|
||||
.color(Color::from_rgb(0.85, 0.85, 0.90)),
|
||||
text(subtitle)
|
||||
.size(14)
|
||||
.color(Color::from_rgb(0.55, 0.55, 0.60)),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_x(iced::Alignment::Center),
|
||||
@@ -22,5 +26,9 @@ pub fn view(locale: UiLocale) -> Element<'static, Message> {
|
||||
.height(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.style(|_theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.11, 0.11, 0.14))),
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use iced::widget::{column, row};
|
||||
use iced::{Element, Length};
|
||||
use iced::widget::{column, container, row, Space};
|
||||
use iced::{Background, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
@@ -8,6 +8,36 @@ use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot}
|
||||
use crate::state::tabs::Tab;
|
||||
use crate::views::{activity_bar, panel, sidebar, status_bar, tab_bar, welcome};
|
||||
|
||||
/// Main content area background.
|
||||
fn content_bg(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.11, 0.11, 0.14))),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal separator line between regions.
|
||||
fn separator_v() -> iced::widget::Container<'static, Message> {
|
||||
container(Space::new(0, 0))
|
||||
.width(Length::Fixed(1.0))
|
||||
.height(Length::Fill)
|
||||
.style(|_theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.25, 0.25, 0.30))),
|
||||
..container::Style::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Horizontal line separator (full width).
|
||||
fn separator_h() -> iced::widget::Container<'static, Message> {
|
||||
container(Space::new(0, 0))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(1.0))
|
||||
.style(|_theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.25, 0.25, 0.30))),
|
||||
..container::Style::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Compose the full workspace layout.
|
||||
pub fn view(
|
||||
// Navigation
|
||||
@@ -32,38 +62,31 @@ pub fn view(
|
||||
// Activity bar (leftmost column)
|
||||
let activity = activity_bar::view(sidebar_view, locale);
|
||||
|
||||
// Sidebar (conditionally visible)
|
||||
let sidebar_el: Option<Element<'static, Message>> = if sidebar_visible {
|
||||
Some(sidebar::view(sidebar_view, locale))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Tab bar
|
||||
let tabs_el = tab_bar::view(tabs, active_tab);
|
||||
|
||||
// Content area
|
||||
let content_area = welcome::view(locale);
|
||||
|
||||
// Panel (conditionally visible)
|
||||
let panel_el: Option<Element<'static, Message>> = if panel_visible {
|
||||
Some(panel::view(panel_tab, task_snapshots, output_entries, locale))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Right column: tab bar + content + panel
|
||||
let mut right_col = column![tabs_el, content_area];
|
||||
if let Some(p) = panel_el {
|
||||
right_col = right_col.push(p);
|
||||
if panel_visible {
|
||||
right_col = right_col.push(separator_h());
|
||||
right_col = right_col.push(panel::view(panel_tab, task_snapshots, output_entries, locale));
|
||||
}
|
||||
let right = right_col.width(Length::Fill).height(Length::Fill);
|
||||
let right = container(right_col.width(Length::Fill).height(Length::Fill))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(content_bg);
|
||||
|
||||
// Main row: activity bar + sidebar? + right column
|
||||
// Main row: activity bar | separator | sidebar? | separator | right column
|
||||
let mut main_row = row![activity];
|
||||
if let Some(sb) = sidebar_el {
|
||||
main_row = main_row.push(sb);
|
||||
|
||||
if sidebar_visible {
|
||||
main_row = main_row.push(sidebar::view(sidebar_view, locale));
|
||||
main_row = main_row.push(separator_v());
|
||||
}
|
||||
|
||||
main_row = main_row.push(right);
|
||||
let main_row = main_row.height(Length::Fill);
|
||||
|
||||
@@ -77,7 +100,7 @@ pub fn view(
|
||||
task_snapshots,
|
||||
);
|
||||
|
||||
column![main_row, status]
|
||||
column![main_row, separator_h(), status]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
|
||||