fix: make code aligned with spec
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"article",
|
||||
"aside",
|
||||
"page",
|
||||
"picture"
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "P",
|
||||
"maxPostsPerPage": 50,
|
||||
"semanticSimilarityEnabled": false,
|
||||
"blogLanguages": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"article",
|
||||
"aside",
|
||||
"page",
|
||||
"picture"
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "P",
|
||||
"maxPostsPerPage": 50,
|
||||
"semanticSimilarityEnabled": false,
|
||||
"blogLanguages": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"article",
|
||||
"aside",
|
||||
"page",
|
||||
"picture"
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "P",
|
||||
"maxPostsPerPage": 50,
|
||||
"semanticSimilarityEnabled": false,
|
||||
"blogLanguages": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"article",
|
||||
"aside",
|
||||
"page",
|
||||
"picture"
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "P",
|
||||
"maxPostsPerPage": 50,
|
||||
"semanticSimilarityEnabled": false,
|
||||
"blogLanguages": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"article",
|
||||
"aside",
|
||||
"page",
|
||||
"picture"
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "P",
|
||||
"maxPostsPerPage": 50,
|
||||
"semanticSimilarityEnabled": false,
|
||||
"blogLanguages": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -136,6 +136,7 @@ pub struct BdsApp {
|
||||
offline_mode: bool,
|
||||
locale_dropdown_open: bool,
|
||||
project_dropdown_open: bool,
|
||||
theme_badge: String,
|
||||
|
||||
// Toasts
|
||||
toasts: Vec<Toast>,
|
||||
@@ -253,6 +254,7 @@ impl BdsApp {
|
||||
offline_mode: false,
|
||||
locale_dropdown_open: false,
|
||||
project_dropdown_open: false,
|
||||
theme_badge: String::from("pico"),
|
||||
toasts: Vec::new(),
|
||||
#[cfg(target_os = "macos")]
|
||||
_lifecycle_rx,
|
||||
@@ -296,6 +298,7 @@ impl BdsApp {
|
||||
if let Some(t) = self.tabs.get(idx) {
|
||||
self.active_tab = Some(t.id.clone());
|
||||
}
|
||||
self.enforce_panel_tab_fallback();
|
||||
self.sync_menu_state();
|
||||
Task::none()
|
||||
}
|
||||
@@ -305,6 +308,7 @@ impl BdsApp {
|
||||
} else {
|
||||
self.active_tab = None;
|
||||
}
|
||||
self.enforce_panel_tab_fallback();
|
||||
self.sync_menu_state();
|
||||
Task::none()
|
||||
}
|
||||
@@ -312,6 +316,7 @@ impl BdsApp {
|
||||
if self.tabs.iter().any(|t| t.id == id) {
|
||||
self.active_tab = Some(id);
|
||||
}
|
||||
self.enforce_panel_tab_fallback();
|
||||
Task::none()
|
||||
}
|
||||
Message::PinTab(id) => {
|
||||
@@ -643,6 +648,7 @@ impl BdsApp {
|
||||
self.offline_mode,
|
||||
self.locale_dropdown_open,
|
||||
self.project_dropdown_open,
|
||||
&self.theme_badge,
|
||||
self.ui_locale,
|
||||
&self.toasts,
|
||||
)
|
||||
@@ -692,6 +698,7 @@ impl BdsApp {
|
||||
tab_type: TabType::Post,
|
||||
title: post.title.clone(),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(t) = self.tabs.get(idx) {
|
||||
@@ -804,10 +811,11 @@ impl BdsApp {
|
||||
|
||||
fn open_singleton_tab(&mut self, tab_type: TabType, title: &str) {
|
||||
let tab = Tab {
|
||||
id: format!("singleton-{title}"),
|
||||
id: tab_type.singleton_id().to_string(),
|
||||
tab_type,
|
||||
title: title.to_string(),
|
||||
is_transient: false,
|
||||
is_dirty: false,
|
||||
};
|
||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(t) = self.tabs.get(idx) {
|
||||
@@ -927,6 +935,31 @@ impl BdsApp {
|
||||
0,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
// Read pico theme from project metadata for status bar badge
|
||||
if let Some(ref data_dir) = self.data_dir {
|
||||
if let Ok(meta) = engine::meta::read_project_json(data_dir) {
|
||||
if let Some(theme) = meta.pico_theme {
|
||||
self.theme_badge = theme;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per layout.allium PanelTabFallback invariant: if the active panel tab
|
||||
/// becomes unavailable (post_links when no post tab active, git_log when
|
||||
/// neither post nor media tab active), fall back to Tasks.
|
||||
fn enforce_panel_tab_fallback(&mut self) {
|
||||
let active_tab_type = self.active_tab.as_ref().and_then(|id| {
|
||||
self.tabs.iter().find(|t| t.id == *id).map(|t| &t.tab_type)
|
||||
});
|
||||
let is_post = active_tab_type == Some(&TabType::Post);
|
||||
let is_post_or_media = is_post || active_tab_type == Some(&TabType::Media);
|
||||
|
||||
match self.panel_tab {
|
||||
PanelTab::PostLinks if !is_post => self.panel_tab = PanelTab::Tasks,
|
||||
PanelTab::GitLog if !is_post_or_media => self.panel_tab = PanelTab::Tasks,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,19 +32,23 @@ impl SidebarView {
|
||||
Self::Scripts => "activity.scripts",
|
||||
Self::Templates => "activity.templates",
|
||||
Self::Tags => "activity.tags",
|
||||
Self::Chat => "activity.chat",
|
||||
Self::Chat => "activity.aiAssistant",
|
||||
Self::Import => "activity.import",
|
||||
Self::Git => "activity.git",
|
||||
Self::Settings => "activity.settings",
|
||||
Self::Git => "activity.sourceControl",
|
||||
Self::Settings => "common.settings",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which tab is selected in the bottom panel.
|
||||
///
|
||||
/// Per layout.allium: tasks, output, post_links, git_log.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PanelTab {
|
||||
Tasks,
|
||||
Output,
|
||||
PostLinks,
|
||||
GitLog,
|
||||
}
|
||||
|
||||
/// A snapshot of a running or completed task shown in the panel.
|
||||
@@ -121,6 +125,6 @@ mod tests {
|
||||
#[test]
|
||||
fn display_returns_i18n_key() {
|
||||
assert_eq!(SidebarView::Posts.to_string(), "activity.posts");
|
||||
assert_eq!(SidebarView::Settings.to_string(), "activity.settings");
|
||||
assert_eq!(SidebarView::Settings.to_string(), "common.settings");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/// The kind of content a tab holds.
|
||||
///
|
||||
/// 17 tab types per tabs.allium spec. Each maps 1:1 to an editor route.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TabType {
|
||||
Post,
|
||||
@@ -10,18 +12,57 @@ pub enum TabType {
|
||||
Import,
|
||||
MenuEditor,
|
||||
MetadataDiff,
|
||||
GitDiff,
|
||||
Scripts,
|
||||
Templates,
|
||||
Documentation,
|
||||
ApiDocumentation,
|
||||
SiteValidation,
|
||||
TranslationValidation,
|
||||
FindDuplicates,
|
||||
}
|
||||
|
||||
impl TabType {
|
||||
/// Singleton tabs may only appear once in the tab bar.
|
||||
/// Every type except `Post` and `Media` is a singleton.
|
||||
/// Singleton tool tabs per tabs.allium: always one instance, id = type name.
|
||||
///
|
||||
/// Singleton types (10): settings, tags, style, menu_editor,
|
||||
/// documentation, api_documentation, metadata_diff, site_validation,
|
||||
/// translation_validation, find_duplicates.
|
||||
///
|
||||
/// Entity types (keyed by external ID): post, media, scripts (keyed),
|
||||
/// templates (keyed), chat, import, git_diff.
|
||||
pub fn is_singleton(&self) -> bool {
|
||||
!matches!(self, Self::Post | Self::Media)
|
||||
matches!(
|
||||
self,
|
||||
Self::Settings
|
||||
| Self::Style
|
||||
| Self::Tags
|
||||
| Self::MenuEditor
|
||||
| Self::MetadataDiff
|
||||
| Self::Documentation
|
||||
| Self::ApiDocumentation
|
||||
| Self::SiteValidation
|
||||
| Self::TranslationValidation
|
||||
| Self::FindDuplicates
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the canonical string ID used for singleton tabs.
|
||||
pub fn singleton_id(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Settings => "settings",
|
||||
Self::Style => "style",
|
||||
Self::Tags => "tags",
|
||||
Self::MenuEditor => "menu_editor",
|
||||
Self::MetadataDiff => "metadata_diff",
|
||||
Self::Documentation => "documentation",
|
||||
Self::ApiDocumentation => "api_documentation",
|
||||
Self::SiteValidation => "site_validation",
|
||||
Self::TranslationValidation => "translation_validation",
|
||||
Self::FindDuplicates => "find_duplicates",
|
||||
// Entity types don't have a singleton ID — callers should use external IDs
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,24 +73,44 @@ pub struct Tab {
|
||||
pub tab_type: TabType,
|
||||
pub title: String,
|
||||
pub is_transient: bool,
|
||||
/// Only post tabs show dirty state (content differs from saved).
|
||||
pub is_dirty: bool,
|
||||
}
|
||||
|
||||
/// Open (or focus) a tab in the tab list.
|
||||
///
|
||||
/// * **Singleton** — if a tab with the same `TabType` already exists, return
|
||||
/// its index instead of inserting a duplicate.
|
||||
/// * **Transient** — replace an existing transient tab of the same type, or
|
||||
/// append if none exists.
|
||||
/// * Otherwise — append unconditionally.
|
||||
/// Per tabs.allium `OpenTab` rule:
|
||||
///
|
||||
/// * **Dedup** — if a tab with same `(type, id)` already exists, activate it.
|
||||
/// If the intent was `pin`, also set `is_transient = false`.
|
||||
/// * **Singleton** — if a tab of the same singleton type exists, return its
|
||||
/// index (singletons have id == type name, so dedup covers this).
|
||||
/// * **Transient replacement** — if opening as transient and a transient tab
|
||||
/// of same type exists, replace it with the new tab.
|
||||
/// * Otherwise — append a new tab.
|
||||
///
|
||||
/// Returns the index of the resulting tab.
|
||||
pub fn open_tab(tabs: &mut Vec<Tab>, new_tab: Tab) -> usize {
|
||||
// Dedup: exact (type, id) match → reuse
|
||||
if let Some(idx) = tabs
|
||||
.iter()
|
||||
.position(|t| t.tab_type == new_tab.tab_type && t.id == new_tab.id)
|
||||
{
|
||||
// If intent is pin, upgrade to permanent
|
||||
if !new_tab.is_transient {
|
||||
tabs[idx].is_transient = false;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Singleton: only one instance allowed (catches id mismatch edge cases)
|
||||
if new_tab.tab_type.is_singleton() {
|
||||
if let Some(idx) = tabs.iter().position(|t| t.tab_type == new_tab.tab_type) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Transient replacement: replace existing transient of same type
|
||||
if new_tab.is_transient {
|
||||
if let Some(idx) = tabs
|
||||
.iter()
|
||||
@@ -96,17 +157,18 @@ mod tests {
|
||||
tab_type,
|
||||
title: id.to_string(),
|
||||
is_transient: transient,
|
||||
is_dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singleton_dedup() {
|
||||
let mut tabs = Vec::new();
|
||||
let idx1 = open_tab(&mut tabs, make_tab("s1", TabType::Settings, false));
|
||||
let idx2 = open_tab(&mut tabs, make_tab("s2", TabType::Settings, false));
|
||||
let idx1 = open_tab(&mut tabs, make_tab("settings", TabType::Settings, false));
|
||||
let idx2 = open_tab(&mut tabs, make_tab("settings", TabType::Settings, false));
|
||||
assert_eq!(idx1, idx2);
|
||||
assert_eq!(tabs.len(), 1);
|
||||
assert_eq!(tabs[0].id, "s1");
|
||||
assert_eq!(tabs[0].id, "settings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -158,4 +220,47 @@ mod tests {
|
||||
assert_eq!(i1, 1);
|
||||
assert_eq!(tabs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_dedup_by_type_and_id() {
|
||||
// Re-opening the same (type, id) should not create a new tab.
|
||||
let mut tabs = Vec::new();
|
||||
open_tab(&mut tabs, make_tab("s1", TabType::Scripts, true));
|
||||
open_tab(&mut tabs, make_tab("s2", TabType::Scripts, true));
|
||||
// s2 replaced s1 (transient replacement)
|
||||
assert_eq!(tabs.len(), 1);
|
||||
assert_eq!(tabs[0].id, "s2");
|
||||
|
||||
// Re-open s2 — dedup should reuse
|
||||
let idx = open_tab(&mut tabs, make_tab("s2", TabType::Scripts, false));
|
||||
assert_eq!(tabs.len(), 1);
|
||||
assert_eq!(idx, 0);
|
||||
// Pin intent should upgrade transient to permanent
|
||||
assert!(!tabs[0].is_transient);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scripts_not_singleton() {
|
||||
// Script tabs are entity tabs (keyed by scriptId), not singletons
|
||||
let mut tabs = Vec::new();
|
||||
open_tab(&mut tabs, make_tab("s1", TabType::Scripts, false));
|
||||
open_tab(&mut tabs, make_tab("s2", TabType::Scripts, false));
|
||||
assert_eq!(tabs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn templates_not_singleton() {
|
||||
let mut tabs = Vec::new();
|
||||
open_tab(&mut tabs, make_tab("t1", TabType::Templates, false));
|
||||
open_tab(&mut tabs, make_tab("t2", TabType::Templates, false));
|
||||
assert_eq!(tabs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_not_singleton() {
|
||||
let mut tabs = Vec::new();
|
||||
open_tab(&mut tabs, make_tab("c1", TabType::Chat, false));
|
||||
open_tab(&mut tabs, make_tab("c2", TabType::Chat, false));
|
||||
assert_eq!(tabs.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use iced::widget::{button, column, container, svg, Column, Space};
|
||||
use iced::widget::{button, column, container, svg, text, tooltip, Column, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::SidebarView;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -93,11 +95,14 @@ fn bar_background_style(_theme: &Theme) -> container::Style {
|
||||
|
||||
pub fn view(
|
||||
active_view: SidebarView,
|
||||
_locale: UiLocale,
|
||||
sidebar_visible: bool,
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let make_btn = |view: SidebarView| -> Element<'static, Message> {
|
||||
let handle = svg::Handle::from_memory(icon_svg(view));
|
||||
let is_active = view == active_view;
|
||||
// Per layout.allium ActivityActiveHighlight invariant:
|
||||
// button shows active iff its view == active_view AND sidebar is visible
|
||||
let is_active = view == active_view && sidebar_visible;
|
||||
|
||||
// Render SVG: active = full opacity, inactive = 0.4 opacity (like bDS 60% opacity)
|
||||
let icon = svg(handle)
|
||||
@@ -117,7 +122,7 @@ pub fn view(
|
||||
.style(if is_active { active_button_style } else { inactive_button_style });
|
||||
|
||||
// Active indicator: 2px left border (like bDS/VS Code)
|
||||
if is_active {
|
||||
let btn_row: Element<'static, Message> = if is_active {
|
||||
let indicator = container(Space::new(0, 0))
|
||||
.width(Length::Fixed(2.0))
|
||||
.height(Length::Fixed(48.0))
|
||||
@@ -129,7 +134,13 @@ pub fn view(
|
||||
} else {
|
||||
let spacer = Space::with_width(2.0);
|
||||
iced::widget::row![spacer, btn].into()
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap in tooltip per layout.allium ActivityButton.label_key
|
||||
let tip_text = t(locale, view.i18n_key());
|
||||
tooltip(btn_row, text(tip_text).size(12).shaping(Shaping::Advanced), tooltip::Position::Right)
|
||||
.gap(4)
|
||||
.into()
|
||||
};
|
||||
|
||||
let top_items: Vec<Element<'static, Message>> = TOP_ACTIVITIES
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||
use iced::widget::{button, column, container, scrollable, text, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
@@ -67,10 +67,14 @@ pub fn view(
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
output_entries: &[OutputEntry],
|
||||
locale: UiLocale,
|
||||
active_tab_is_post: bool,
|
||||
active_tab_is_post_or_media: bool,
|
||||
) -> Element<'static, Message> {
|
||||
let muted = Color::from_rgb(0.50, 0.50, 0.55);
|
||||
|
||||
// Tab header
|
||||
// Tab header — per layout.allium: tasks, output, post_links (only when
|
||||
// active editor tab is a post), git_log (only when active tab is post or
|
||||
// media).
|
||||
let tasks_btn = button(text(t(locale, "common.tasks")).size(12).shaping(Shaping::Advanced))
|
||||
.on_press(Message::SetPanelTab(PanelTab::Tasks))
|
||||
.padding([4, 8])
|
||||
@@ -86,15 +90,34 @@ pub fn view(
|
||||
.padding([4, 6])
|
||||
.style(close_btn_style);
|
||||
|
||||
let tab_header = row![
|
||||
tasks_btn,
|
||||
output_btn,
|
||||
Space::with_width(Length::Fill),
|
||||
close_btn,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(Alignment::Center)
|
||||
.padding([4, 8]);
|
||||
let mut tab_row: Vec<Element<'static, Message>> = vec![
|
||||
tasks_btn.into(),
|
||||
output_btn.into(),
|
||||
];
|
||||
|
||||
if active_tab_is_post {
|
||||
let post_links_btn = button(text(t(locale, "panel.postLinks")).size(12).shaping(Shaping::Advanced))
|
||||
.on_press(Message::SetPanelTab(PanelTab::PostLinks))
|
||||
.padding([4, 8])
|
||||
.style(if panel_tab == PanelTab::PostLinks { tab_active } else { tab_inactive });
|
||||
tab_row.push(post_links_btn.into());
|
||||
}
|
||||
|
||||
if active_tab_is_post_or_media {
|
||||
let git_log_btn = button(text(t(locale, "panel.gitLog")).size(12).shaping(Shaping::Advanced))
|
||||
.on_press(Message::SetPanelTab(PanelTab::GitLog))
|
||||
.padding([4, 8])
|
||||
.style(if panel_tab == PanelTab::GitLog { tab_active } else { tab_inactive });
|
||||
tab_row.push(git_log_btn.into());
|
||||
}
|
||||
|
||||
tab_row.push(Space::with_width(Length::Fill).into());
|
||||
tab_row.push(close_btn.into());
|
||||
|
||||
let tab_header = iced::widget::Row::with_children(tab_row)
|
||||
.spacing(4)
|
||||
.align_y(Alignment::Center)
|
||||
.padding([4, 8]);
|
||||
|
||||
// Tab content
|
||||
let content: Element<'static, Message> = match panel_tab {
|
||||
@@ -156,6 +179,18 @@ pub fn view(
|
||||
.into()
|
||||
}
|
||||
}
|
||||
PanelTab::PostLinks => {
|
||||
// Post Links content populated in M3 (editor integration)
|
||||
container(text(t(locale, "panel.postLinksPlaceholder")).size(12).shaping(Shaping::Advanced).color(muted))
|
||||
.padding(8)
|
||||
.into()
|
||||
}
|
||||
PanelTab::GitLog => {
|
||||
// Git Log content populated in extension bucket A (git integration)
|
||||
container(text(t(locale, "panel.gitLogPlaceholder")).size(12).shaping(Shaping::Advanced).color(muted))
|
||||
.padding(8)
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
container(
|
||||
|
||||
@@ -91,6 +91,7 @@ pub fn view(
|
||||
tab_type: TabType::Post,
|
||||
title: p.title.clone(),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
}))
|
||||
.padding([3, 6])
|
||||
.width(Length::Fill)
|
||||
@@ -123,6 +124,7 @@ pub fn view(
|
||||
tab_type: TabType::Media,
|
||||
title: display_name.to_string(),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
}))
|
||||
.padding([3, 6])
|
||||
.width(Length::Fill)
|
||||
@@ -153,7 +155,7 @@ pub fn view(
|
||||
.padding(12);
|
||||
|
||||
container(scrollable(content))
|
||||
.width(Length::Fixed(240.0))
|
||||
.width(Length::Fixed(280.0))
|
||||
.height(Length::Fill)
|
||||
.style(sidebar_style)
|
||||
.into()
|
||||
|
||||
@@ -117,6 +117,8 @@ pub fn view(
|
||||
locale: UiLocale,
|
||||
offline_mode: bool,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
theme_badge: &str,
|
||||
active_post_status: Option<&str>,
|
||||
) -> Element<'static, Message> {
|
||||
let label_color = Color::from_rgb(0.60, 0.60, 0.65);
|
||||
|
||||
@@ -157,6 +159,19 @@ pub fn view(
|
||||
|
||||
// ── Right side ──
|
||||
|
||||
// Post status indicator dot (per layout.allium StatusBarRight.post_status)
|
||||
let post_status_el: Element<'static, Message> = if let Some(status) = active_post_status {
|
||||
let (dot, color) = match status {
|
||||
"draft" => ("\u{25CB}", Color::from_rgb(0.60, 0.60, 0.65)), // hollow circle
|
||||
"published" => ("\u{25CF}", Color::from_rgb(0.30, 0.75, 0.40)), // green filled
|
||||
"archived" => ("\u{25A0}", Color::from_rgb(0.60, 0.60, 0.65)), // square
|
||||
_ => ("\u{25CF}", Color::from_rgb(0.60, 0.60, 0.65)),
|
||||
};
|
||||
text(dot).size(11).shaping(Shaping::Advanced).color(color).into()
|
||||
} else {
|
||||
Space::with_width(0).into()
|
||||
};
|
||||
|
||||
// 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())]);
|
||||
@@ -180,8 +195,10 @@ pub fn view(
|
||||
.style(dropdown_trigger);
|
||||
|
||||
let right = row![
|
||||
post_status_el,
|
||||
text(posts_label).size(11).shaping(Shaping::Advanced).color(label_color),
|
||||
text(media_label).size(11).shaping(Shaping::Advanced).color(label_color),
|
||||
text(theme_badge.to_string()).size(11).shaping(Shaping::Advanced).color(label_color),
|
||||
airplane_btn,
|
||||
locale_trigger,
|
||||
text("bDS").size(11).shaping(Shaping::Advanced).color(Color::from_rgb(0.45, 0.45, 0.50)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use iced::widget::{button, container, row, text, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
use iced::{Background, Border, Color, Element, Font, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::state::tabs::Tab;
|
||||
@@ -68,28 +68,41 @@ pub fn view(
|
||||
tabs: &[Tab],
|
||||
active_tab: Option<&str>,
|
||||
) -> Element<'static, Message> {
|
||||
// Per tabs.allium: "Hidden when no tabs exist."
|
||||
if tabs.is_empty() {
|
||||
return container(Space::new(0, 0))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(35.0))
|
||||
.style(bar_style)
|
||||
.into();
|
||||
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!("{} \u{25CB}", tab.title) // hollow circle for transient
|
||||
} else {
|
||||
tab.title.clone()
|
||||
};
|
||||
let tab_id = tab.id.clone();
|
||||
let close_id = tab.id.clone();
|
||||
|
||||
// Per tabs.allium: transient tabs show italic title
|
||||
let title_label = if tab.is_transient {
|
||||
text(tab.title.clone())
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.font(Font { style: iced::font::Style::Italic, ..Font::DEFAULT })
|
||||
} else {
|
||||
text(tab.title.clone())
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
};
|
||||
|
||||
// Per tabs.allium DirtyIndicator: dot for dirty post tabs
|
||||
let dirty_indicator = if tab.is_dirty {
|
||||
text(" \u{25CF}").size(10).shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.90, 0.70, 0.30))
|
||||
} else {
|
||||
text("").size(10)
|
||||
};
|
||||
|
||||
let label = row![
|
||||
text(title_text).size(12).shaping(Shaping::Advanced),
|
||||
title_label,
|
||||
dirty_indicator,
|
||||
button(text("\u{2715}").size(10).shaping(Shaping::Advanced))
|
||||
.on_press(Message::CloseTab(close_id))
|
||||
.padding(2)
|
||||
|
||||
@@ -7,7 +7,7 @@ use bds_core::model::{Media, Post, Project};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot};
|
||||
use crate::state::tabs::Tab;
|
||||
use crate::state::tabs::{Tab, TabType};
|
||||
use crate::state::toast::Toast;
|
||||
use crate::views::{activity_bar, panel, project_selector, sidebar, status_bar, tab_bar, toast, welcome};
|
||||
|
||||
@@ -66,13 +66,14 @@ pub fn view(
|
||||
offline_mode: bool,
|
||||
locale_dropdown_open: bool,
|
||||
project_dropdown_open: bool,
|
||||
theme_badge: &str,
|
||||
// i18n
|
||||
locale: UiLocale,
|
||||
// Toasts
|
||||
toasts: &[Toast],
|
||||
) -> Element<'static, Message> {
|
||||
// Activity bar (leftmost column)
|
||||
let activity = activity_bar::view(sidebar_view, locale);
|
||||
let activity = activity_bar::view(sidebar_view, sidebar_visible, locale);
|
||||
|
||||
// Tab bar
|
||||
let tabs_el = tab_bar::view(tabs, active_tab);
|
||||
@@ -83,8 +84,20 @@ pub fn view(
|
||||
// Right column: tab bar + content + panel
|
||||
let mut right_col = column![tabs_el, content_area];
|
||||
if panel_visible {
|
||||
// Determine active tab type for panel tab availability (per layout.allium PanelTabAvailability)
|
||||
let active_tab_type = active_tab.and_then(|id| tabs.iter().find(|t| t.id == id)).map(|t| &t.tab_type);
|
||||
let active_tab_is_post = active_tab_type == Some(&TabType::Post);
|
||||
let active_tab_is_post_or_media = active_tab_is_post || active_tab_type == Some(&TabType::Media);
|
||||
|
||||
right_col = right_col.push(separator_h());
|
||||
right_col = right_col.push(panel::view(panel_tab, task_snapshots, output_entries, locale));
|
||||
right_col = right_col.push(panel::view(
|
||||
panel_tab,
|
||||
task_snapshots,
|
||||
output_entries,
|
||||
locale,
|
||||
active_tab_is_post,
|
||||
active_tab_is_post_or_media,
|
||||
));
|
||||
}
|
||||
let right = container(right_col.width(Length::Fill).height(Length::Fill))
|
||||
.width(Length::Fill)
|
||||
@@ -102,7 +115,19 @@ pub fn view(
|
||||
main_row = main_row.push(right);
|
||||
let main_row = main_row.height(Length::Fill);
|
||||
|
||||
// Status bar at bottom
|
||||
// Status bar at bottom — determine active post status for status dot
|
||||
let active_post_status: Option<String> = active_tab.and_then(|id| {
|
||||
tabs.iter().find(|t| t.id == id && t.tab_type == TabType::Post)
|
||||
}).and_then(|tab| {
|
||||
sidebar_posts.iter().find(|p| p.id == tab.id).map(|p| {
|
||||
match p.status {
|
||||
bds_core::model::PostStatus::Draft => "draft".to_string(),
|
||||
bds_core::model::PostStatus::Published => "published".to_string(),
|
||||
bds_core::model::PostStatus::Archived => "archived".to_string(),
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let status = status_bar::view(
|
||||
active_project_name,
|
||||
post_count,
|
||||
@@ -110,6 +135,8 @@ pub fn view(
|
||||
locale,
|
||||
offline_mode,
|
||||
task_snapshots,
|
||||
theme_badge,
|
||||
active_post_status.as_deref(),
|
||||
);
|
||||
|
||||
let base_layout: Element<'static, Message> = column![main_row, separator_h(), status]
|
||||
|
||||
@@ -44,6 +44,7 @@ fn new_message_variants_constructable() {
|
||||
tab_type: TabType::Post,
|
||||
title: "Test".to_string(),
|
||||
is_transient: false,
|
||||
is_dirty: false,
|
||||
};
|
||||
let _open = Message::OpenTab(tab);
|
||||
let _close = Message::CloseTab("test".into());
|
||||
|
||||
@@ -38,10 +38,9 @@
|
||||
"activity.scripts": "Skripte",
|
||||
"activity.tags": "Schlagwörter",
|
||||
"activity.templates": "Vorlagen",
|
||||
"activity.chat": "KI-Assistent",
|
||||
"activity.aiAssistant": "KI-Assistent",
|
||||
"activity.import": "Importieren",
|
||||
"activity.git": "Versionskontrolle",
|
||||
"activity.settings": "Einstellungen",
|
||||
"activity.sourceControl": "Versionskontrolle",
|
||||
"common.save": "Speichern",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.settings": "Einstellungen",
|
||||
@@ -58,6 +57,10 @@
|
||||
"app.databaseRebuildFailed": "Datenbank-Neuaufbau fehlgeschlagen",
|
||||
"app.textReindexFailed": "Text-Neuindizierung fehlgeschlagen",
|
||||
"panel.output": "Ausgabe",
|
||||
"panel.postLinks": "Beitragsverknüpfungen",
|
||||
"panel.gitLog": "Git-Verlauf",
|
||||
"panel.postLinksPlaceholder": "Keine Beitragsverknüpfungen vorhanden",
|
||||
"panel.gitLogPlaceholder": "Kein Git-Verlauf vorhanden",
|
||||
"panel.closeTitle": "Panel schließen",
|
||||
"panel.noRecentTasks": "Keine aktuellen Aufgaben",
|
||||
"panel.noOutput": "Keine Ausgabe",
|
||||
@@ -83,6 +86,8 @@
|
||||
"projectSelector.toast.deleteFailed": "Projekt konnte nicht gelöscht werden",
|
||||
"tabBar.closeHint": "Schließen",
|
||||
"tabBar.loading": "Laden...",
|
||||
"tabBar.unknown": "Unbekannt",
|
||||
"tabBar.style": "Stil",
|
||||
"sidebar.noPostsYet": "Noch keine Beiträge",
|
||||
"sidebar.noPagesYet": "Noch keine Seiten",
|
||||
"sidebar.noMediaYet": "Noch keine Medien",
|
||||
|
||||
@@ -38,10 +38,9 @@
|
||||
"activity.scripts": "Scripts",
|
||||
"activity.tags": "Tags",
|
||||
"activity.templates": "Templates",
|
||||
"activity.chat": "AI Assistant",
|
||||
"activity.aiAssistant": "AI Assistant",
|
||||
"activity.import": "Import",
|
||||
"activity.git": "Source Control",
|
||||
"activity.settings": "Settings",
|
||||
"activity.sourceControl": "Source Control",
|
||||
"common.save": "Save",
|
||||
"common.cancel": "Cancel",
|
||||
"common.settings": "Settings",
|
||||
@@ -58,6 +57,10 @@
|
||||
"app.databaseRebuildFailed": "Database rebuild failed",
|
||||
"app.textReindexFailed": "Text reindex failed",
|
||||
"panel.output": "Output",
|
||||
"panel.postLinks": "Post Links",
|
||||
"panel.gitLog": "Git Log",
|
||||
"panel.postLinksPlaceholder": "No post links to display",
|
||||
"panel.gitLogPlaceholder": "No git history to display",
|
||||
"panel.closeTitle": "Close panel",
|
||||
"panel.noRecentTasks": "No recent tasks",
|
||||
"panel.noOutput": "No output",
|
||||
@@ -83,6 +86,8 @@
|
||||
"projectSelector.toast.deleteFailed": "Failed to delete project",
|
||||
"tabBar.closeHint": "Close",
|
||||
"tabBar.loading": "Loading...",
|
||||
"tabBar.unknown": "Unknown",
|
||||
"tabBar.style": "Style",
|
||||
"sidebar.noPostsYet": "No posts yet",
|
||||
"sidebar.noPagesYet": "No pages yet",
|
||||
"sidebar.noMediaYet": "No media yet",
|
||||
|
||||
@@ -38,10 +38,9 @@
|
||||
"activity.scripts": "Scripts",
|
||||
"activity.tags": "Etiquetas",
|
||||
"activity.templates": "Plantillas",
|
||||
"activity.chat": "Asistente IA",
|
||||
"activity.aiAssistant": "Asistente IA",
|
||||
"activity.import": "Importar",
|
||||
"activity.git": "Control de código fuente",
|
||||
"activity.settings": "Configuración",
|
||||
"activity.sourceControl": "Control de código fuente",
|
||||
"common.save": "Guardar",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.settings": "Configuración",
|
||||
@@ -58,6 +57,10 @@
|
||||
"app.databaseRebuildFailed": "La reconstrucción de la base de datos falló",
|
||||
"app.textReindexFailed": "La reindexación de texto falló",
|
||||
"panel.output": "Salida",
|
||||
"panel.postLinks": "Enlaces de entradas",
|
||||
"panel.gitLog": "Historial de Git",
|
||||
"panel.postLinksPlaceholder": "No hay enlaces de entradas para mostrar",
|
||||
"panel.gitLogPlaceholder": "No hay historial de Git para mostrar",
|
||||
"panel.closeTitle": "Cerrar panel",
|
||||
"panel.noRecentTasks": "No hay tareas recientes",
|
||||
"panel.noOutput": "Sin salida",
|
||||
@@ -83,6 +86,8 @@
|
||||
"projectSelector.toast.deleteFailed": "No se pudo eliminar el proyecto",
|
||||
"tabBar.closeHint": "Cerrar",
|
||||
"tabBar.loading": "Cargando...",
|
||||
"tabBar.unknown": "Desconocido",
|
||||
"tabBar.style": "Estilo",
|
||||
"sidebar.noPostsYet": "Aún no hay entradas",
|
||||
"sidebar.noPagesYet": "Aún no hay páginas",
|
||||
"sidebar.noMediaYet": "Aún no hay medios",
|
||||
|
||||
@@ -38,10 +38,9 @@
|
||||
"activity.scripts": "Scripts",
|
||||
"activity.tags": "Étiquettes",
|
||||
"activity.templates": "Modèles",
|
||||
"activity.chat": "Assistant IA",
|
||||
"activity.aiAssistant": "Assistant IA",
|
||||
"activity.import": "Importation",
|
||||
"activity.git": "Contrôle de source",
|
||||
"activity.settings": "Paramètres",
|
||||
"activity.sourceControl": "Contrôle de source",
|
||||
"common.save": "Enregistrer",
|
||||
"common.cancel": "Annuler",
|
||||
"common.settings": "Paramètres",
|
||||
@@ -58,6 +57,10 @@
|
||||
"app.databaseRebuildFailed": "Échec de la reconstruction de la base de données",
|
||||
"app.textReindexFailed": "Échec de la réindexation du texte",
|
||||
"panel.output": "Sortie",
|
||||
"panel.postLinks": "Liens d'articles",
|
||||
"panel.gitLog": "Historique Git",
|
||||
"panel.postLinksPlaceholder": "Aucun lien d'article à afficher",
|
||||
"panel.gitLogPlaceholder": "Aucun historique Git à afficher",
|
||||
"panel.closeTitle": "Fermer le panneau",
|
||||
"panel.noRecentTasks": "Aucune tâche récente",
|
||||
"panel.noOutput": "Aucune sortie",
|
||||
@@ -83,6 +86,8 @@
|
||||
"projectSelector.toast.deleteFailed": "Impossible de supprimer le projet",
|
||||
"tabBar.closeHint": "Fermer",
|
||||
"tabBar.loading": "Chargement...",
|
||||
"tabBar.unknown": "Inconnu",
|
||||
"tabBar.style": "Style",
|
||||
"sidebar.noPostsYet": "Aucun article pour le moment",
|
||||
"sidebar.noPagesYet": "Aucune page pour le moment",
|
||||
"sidebar.noMediaYet": "Aucun média pour le moment",
|
||||
|
||||
@@ -38,10 +38,9 @@
|
||||
"activity.scripts": "Script",
|
||||
"activity.tags": "Tag",
|
||||
"activity.templates": "Modelli",
|
||||
"activity.chat": "Assistente IA",
|
||||
"activity.aiAssistant": "Assistente IA",
|
||||
"activity.import": "Importa",
|
||||
"activity.git": "Controllo sorgente",
|
||||
"activity.settings": "Impostazioni",
|
||||
"activity.sourceControl": "Controllo sorgente",
|
||||
"common.save": "Salva",
|
||||
"common.cancel": "Annulla",
|
||||
"common.settings": "Impostazioni",
|
||||
@@ -58,6 +57,10 @@
|
||||
"app.databaseRebuildFailed": "Ricostruzione database non riuscita",
|
||||
"app.textReindexFailed": "Reindicizzazione testo non riuscita",
|
||||
"panel.output": "Uscita",
|
||||
"panel.postLinks": "Link ai post",
|
||||
"panel.gitLog": "Cronologia Git",
|
||||
"panel.postLinksPlaceholder": "Nessun link ai post da visualizzare",
|
||||
"panel.gitLogPlaceholder": "Nessuna cronologia Git da visualizzare",
|
||||
"panel.closeTitle": "Chiudi pannello",
|
||||
"panel.noRecentTasks": "Nessuna attività recente",
|
||||
"panel.noOutput": "Nessun output",
|
||||
@@ -83,6 +86,8 @@
|
||||
"projectSelector.toast.deleteFailed": "Eliminazione progetto non riuscita",
|
||||
"tabBar.closeHint": "Chiudi",
|
||||
"tabBar.loading": "Caricamento...",
|
||||
"tabBar.unknown": "Sconosciuto",
|
||||
"tabBar.style": "Stile",
|
||||
"sidebar.noPostsYet": "Nessun post",
|
||||
"sidebar.noPagesYet": "Nessuna pagina",
|
||||
"sidebar.noMediaYet": "Nessun media",
|
||||
|
||||
Reference in New Issue
Block a user