feat: first take at M2
This commit is contained in:
89
crates/bds-ui/src/views/activity_bar.rs
Normal file
89
crates/bds-ui/src/views/activity_bar.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use iced::widget::{button, column, container, text, Column, Space};
|
||||
use iced::{Alignment, Element, Length};
|
||||
|
||||
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 {
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
/// Top group of activity items.
|
||||
const TOP_ACTIVITIES: &[SidebarView] = &[
|
||||
SidebarView::Posts,
|
||||
SidebarView::Pages,
|
||||
SidebarView::Media,
|
||||
SidebarView::Scripts,
|
||||
SidebarView::Templates,
|
||||
SidebarView::Tags,
|
||||
SidebarView::Chat,
|
||||
SidebarView::Import,
|
||||
];
|
||||
|
||||
/// Bottom group of activity items.
|
||||
const BOTTOM_ACTIVITIES: &[SidebarView] = &[
|
||||
SidebarView::Git,
|
||||
SidebarView::Settings,
|
||||
];
|
||||
|
||||
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(),
|
||||
)
|
||||
.width(Length::Fixed(48.0))
|
||||
.height(Length::Fixed(48.0))
|
||||
.on_press(Message::SetActiveView(view));
|
||||
|
||||
if view == active_view {
|
||||
btn = btn.style(button::primary);
|
||||
} else {
|
||||
btn = btn.style(button::secondary);
|
||||
}
|
||||
|
||||
container(btn)
|
||||
.center_x(Length::Fixed(48.0))
|
||||
.into()
|
||||
};
|
||||
|
||||
let top_items: Vec<Element<'static, Message>> = TOP_ACTIVITIES
|
||||
.iter()
|
||||
.map(|v| make_btn(*v))
|
||||
.collect();
|
||||
|
||||
let bottom_items: Vec<Element<'static, Message>> = BOTTOM_ACTIVITIES
|
||||
.iter()
|
||||
.map(|v| make_btn(*v))
|
||||
.collect();
|
||||
|
||||
let top = Column::with_children(top_items).spacing(2);
|
||||
let bottom = Column::with_children(bottom_items).spacing(2);
|
||||
|
||||
column![
|
||||
top,
|
||||
Space::with_height(Length::Fill),
|
||||
bottom,
|
||||
]
|
||||
.width(Length::Fixed(48.0))
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
7
crates/bds-ui/src/views/mod.rs
Normal file
7
crates/bds-ui/src/views/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod workspace;
|
||||
pub mod activity_bar;
|
||||
pub mod sidebar;
|
||||
pub mod tab_bar;
|
||||
pub mod status_bar;
|
||||
pub mod panel;
|
||||
pub mod welcome;
|
||||
111
crates/bds-ui/src/views/panel.rs
Normal file
111
crates/bds-ui/src/views/panel.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||
use iced::{Alignment, Element, Length};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::{OutputEntry, PanelTab, TaskSnapshot};
|
||||
|
||||
pub fn view(
|
||||
panel_tab: PanelTab,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
output_entries: &[OutputEntry],
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
// 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 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 close_btn = button(text("×").size(12))
|
||||
.on_press(Message::TogglePanel)
|
||||
.padding([4, 6])
|
||||
.style(button::text);
|
||||
|
||||
let tab_header = row![
|
||||
tasks_btn,
|
||||
output_btn,
|
||||
Space::with_width(Length::Fill),
|
||||
close_btn,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
// 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))
|
||||
.padding(8)
|
||||
.into()
|
||||
} else {
|
||||
let items: Vec<Element<'static, Message>> = task_snapshots
|
||||
.iter()
|
||||
.map(|snap| {
|
||||
let status_text = format!(
|
||||
"{} — {}{}",
|
||||
snap.label,
|
||||
snap.status,
|
||||
snap.progress
|
||||
.map(|p| format!(" ({:.0}%)", p * 100.0))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
text(status_text).size(11).into()
|
||||
})
|
||||
.collect();
|
||||
scrollable(
|
||||
iced::widget::Column::with_children(items)
|
||||
.spacing(4)
|
||||
.padding(8),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
PanelTab::Output => {
|
||||
if output_entries.is_empty() {
|
||||
container(text(t(locale, "panel.noOutput")).size(12))
|
||||
.padding(8)
|
||||
.into()
|
||||
} else {
|
||||
let items: Vec<Element<'static, Message>> = output_entries
|
||||
.iter()
|
||||
.map(|entry| text(entry.text.clone()).size(11).into())
|
||||
.collect();
|
||||
scrollable(
|
||||
iced::widget::Column::with_children(items)
|
||||
.spacing(2)
|
||||
.padding(8),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
container(
|
||||
column![tab_header, content].spacing(4),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(200.0))
|
||||
.into()
|
||||
}
|
||||
37
crates/bds-ui/src/views/sidebar.rs
Normal file
37
crates/bds-ui/src/views/sidebar.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use iced::widget::{column, container, scrollable, text};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::SidebarView;
|
||||
|
||||
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 content = column![
|
||||
text(header_text).size(14),
|
||||
text(placeholder).size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding(12);
|
||||
|
||||
container(scrollable(content))
|
||||
.width(Length::Fixed(280.0))
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
97
crates/bds-ui/src/views/status_bar.rs
Normal file
97
crates/bds-ui/src/views/status_bar.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use iced::widget::{button, container, row, text, Space};
|
||||
use iced::{Alignment, Element, Length};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::{t, tw};
|
||||
use crate::state::navigation::TaskSnapshot;
|
||||
|
||||
pub fn view(
|
||||
active_project_name: Option<&str>,
|
||||
post_count: usize,
|
||||
media_count: usize,
|
||||
locale: UiLocale,
|
||||
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 running: Vec<&TaskSnapshot> = task_snapshots
|
||||
.iter()
|
||||
.filter(|t| t.status == "running")
|
||||
.collect();
|
||||
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()
|
||||
} else {
|
||||
text(first).size(11).into()
|
||||
}
|
||||
} else {
|
||||
Space::with_width(0).into()
|
||||
};
|
||||
|
||||
let left = row![project_text, task_indicator].spacing(12);
|
||||
|
||||
// Right: post count, media count, airplane mode, locale selector, brand
|
||||
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))
|
||||
.on_press(Message::SetOfflineMode(!offline_mode))
|
||||
.padding([2, 6])
|
||||
.style(if offline_mode { button::primary } else { button::text });
|
||||
|
||||
let ui_label = t(locale, "statusBar.ui");
|
||||
|
||||
let locale_buttons: Vec<Element<'static, Message>> = 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()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let locale_row = iced::widget::Row::with_children(locale_buttons).spacing(2);
|
||||
|
||||
let right = row![
|
||||
text(posts_label).size(11),
|
||||
text(media_label).size(11),
|
||||
airplane_btn,
|
||||
text(ui_label).size(11),
|
||||
locale_row,
|
||||
text("bDS").size(11),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
container(
|
||||
row![
|
||||
left,
|
||||
Space::with_width(Length::Fill),
|
||||
right,
|
||||
]
|
||||
.align_y(Alignment::Center)
|
||||
.padding([0, 8]),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(22.0))
|
||||
.into()
|
||||
}
|
||||
58
crates/bds-ui/src/views/tab_bar.rs
Normal file
58
crates/bds-ui/src/views/tab_bar.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use iced::widget::{button, container, row, text, Space};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::state::tabs::Tab;
|
||||
|
||||
pub fn view(
|
||||
tabs: &[Tab],
|
||||
active_tab: Option<&str>,
|
||||
) -> Element<'static, Message> {
|
||||
if tabs.is_empty() {
|
||||
return Space::with_height(0).into();
|
||||
}
|
||||
|
||||
let tab_buttons: Vec<Element<'static, Message>> = tabs
|
||||
.iter()
|
||||
.map(|tab| {
|
||||
let is_active = active_tab == Some(tab.id.as_str());
|
||||
let title_text = if tab.is_transient {
|
||||
format!("{} (preview)", tab.title)
|
||||
} else {
|
||||
tab.title.clone()
|
||||
};
|
||||
let tab_id = tab.id.clone();
|
||||
let close_id = tab.id.clone();
|
||||
|
||||
let label = row![
|
||||
text(title_text).size(12),
|
||||
button(text("×").size(12))
|
||||
.on_press(Message::CloseTab(close_id))
|
||||
.padding(2)
|
||||
.style(button::text),
|
||||
]
|
||||
.spacing(4);
|
||||
|
||||
let mut btn = 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()
|
||||
})
|
||||
.collect();
|
||||
|
||||
container(
|
||||
iced::widget::Row::with_children(tab_buttons)
|
||||
.spacing(1)
|
||||
.height(Length::Fixed(35.0)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(35.0))
|
||||
.into()
|
||||
}
|
||||
26
crates/bds-ui/src/views/welcome.rs
Normal file
26
crates/bds-ui/src/views/welcome.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use iced::widget::{column, container, text};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
|
||||
pub fn view(locale: UiLocale) -> Element<'static, Message> {
|
||||
let title = t(locale, "welcome.title");
|
||||
let subtitle = t(locale, "welcome.subtitle");
|
||||
|
||||
container(
|
||||
column![
|
||||
text(title).size(28),
|
||||
text(subtitle).size(14),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_x(iced::Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
84
crates/bds-ui/src/views/workspace.rs
Normal file
84
crates/bds-ui/src/views/workspace.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use iced::widget::{column, row};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
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};
|
||||
|
||||
/// Compose the full workspace layout.
|
||||
pub fn view(
|
||||
// Navigation
|
||||
sidebar_view: SidebarView,
|
||||
sidebar_visible: bool,
|
||||
// Tabs
|
||||
tabs: &[Tab],
|
||||
active_tab: Option<&str>,
|
||||
// Panel
|
||||
panel_visible: bool,
|
||||
panel_tab: PanelTab,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
output_entries: &[OutputEntry],
|
||||
// Status bar
|
||||
active_project_name: Option<&str>,
|
||||
post_count: usize,
|
||||
media_count: usize,
|
||||
offline_mode: bool,
|
||||
// i18n
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
// 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);
|
||||
}
|
||||
let right = right_col.width(Length::Fill).height(Length::Fill);
|
||||
|
||||
// Main row: activity bar + sidebar? + right column
|
||||
let mut main_row = row![activity];
|
||||
if let Some(sb) = sidebar_el {
|
||||
main_row = main_row.push(sb);
|
||||
}
|
||||
main_row = main_row.push(right);
|
||||
let main_row = main_row.height(Length::Fill);
|
||||
|
||||
// Status bar at bottom
|
||||
let status = status_bar::view(
|
||||
active_project_name,
|
||||
post_count,
|
||||
media_count,
|
||||
locale,
|
||||
offline_mode,
|
||||
task_snapshots,
|
||||
);
|
||||
|
||||
column![main_row, status]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
Reference in New Issue
Block a user