feat: more work on UI

This commit is contained in:
2026-04-04 19:23:12 +02:00
parent eedd0a9118
commit 03dcaea88e
25 changed files with 713 additions and 178 deletions

135
Cargo.lock generated
View File

@@ -950,7 +950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2"
dependencies = [
"bitflags 2.11.0",
"fontdb",
"fontdb 0.16.2",
"log",
"rangemap",
"rayon",
@@ -1073,6 +1073,12 @@ dependencies = [
"zbus 4.4.0",
]
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "dconf_rs"
version = "0.3.0"
@@ -1456,6 +1462,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1500,6 +1512,20 @@ dependencies = [
"ttf-parser 0.20.0",
]
[[package]]
name = "fontdb"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
dependencies = [
"fontconfig-parser",
"log",
"memmap2",
"slotmap",
"tinyvec",
"ttf-parser 0.21.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -2256,8 +2282,9 @@ dependencies = [
"bytemuck",
"cosmic-text",
"iced_graphics",
"kurbo",
"kurbo 0.10.4",
"log",
"resvg",
"rustc-hash 2.1.2",
"softbuffer",
"tiny-skia",
@@ -2278,6 +2305,7 @@ dependencies = [
"iced_graphics",
"log",
"once_cell",
"resvg",
"rustc-hash 2.1.2",
"thiserror 1.0.69",
"wgpu",
@@ -2479,6 +2507,12 @@ dependencies = [
"quick-error",
]
[[package]]
name = "imagesize"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
[[package]]
name = "imgref"
version = "1.12.0"
@@ -2687,6 +2721,17 @@ dependencies = [
"smallvec",
]
[[package]]
name = "kurbo"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
dependencies = [
"arrayvec",
"euclid",
"smallvec",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -3751,6 +3796,12 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project"
version = "1.1.11"
@@ -4327,6 +4378,22 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "resvg"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051"
dependencies = [
"gif 0.13.3",
"jpeg-decoder",
"log",
"pico-args",
"rgb",
"svgtypes",
"tiny-skia",
"usvg",
]
[[package]]
name = "rfd"
version = "0.15.4"
@@ -4356,6 +4423,9 @@ name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]]
name = "ropey"
@@ -4674,6 +4744,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simplecss"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
dependencies = [
"log",
]
[[package]]
name = "siphasher"
version = "1.0.2"
@@ -4857,6 +4936,9 @@ name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
"float-cmp",
]
[[package]]
name = "svg_fmt"
@@ -4864,6 +4946,16 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
[[package]]
name = "svgtypes"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
dependencies = [
"kurbo 0.11.3",
"siphasher",
]
[[package]]
name = "swash"
version = "0.1.19"
@@ -5357,6 +5449,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-vo"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
version = "0.1.14"
@@ -5394,6 +5492,33 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "usvg"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
dependencies = [
"base64",
"data-url",
"flate2",
"fontdb 0.18.0",
"imagesize",
"kurbo 0.11.3",
"log",
"pico-args",
"roxmltree",
"rustybuzz",
"simplecss",
"siphasher",
"strict-num",
"svgtypes",
"tiny-skia-path",
"unicode-bidi",
"unicode-script",
"unicode-vo",
"xmlwriter",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -6398,6 +6523,12 @@ version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "xmlwriter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "y4m"
version = "0.8.0"

View File

@@ -34,7 +34,7 @@ dirs = "5"
open = "5"
# UI framework
iced = { version = "0.13", features = ["wgpu", "advanced", "image", "tokio"] }
iced = { version = "0.13", features = ["wgpu", "advanced", "image", "svg", "tokio"] }
# Editor widget
cosmic-text = "0.12"

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -38,9 +38,10 @@
"activity.scripts": "Skripte",
"activity.tags": "Schlagwörter",
"activity.templates": "Vorlagen",
"activity.aiAssistant": "KI-Assistent",
"activity.chat": "KI-Assistent",
"activity.import": "Importieren",
"activity.sourceControl": "Versionskontrolle",
"activity.git": "Versionskontrolle",
"activity.settings": "Einstellungen",
"common.save": "Speichern",
"common.cancel": "Abbrechen",
"common.settings": "Einstellungen",
@@ -85,6 +86,11 @@
"sidebar.noPostsYet": "Noch keine Beiträge",
"sidebar.noPagesYet": "Noch keine Seiten",
"sidebar.noMediaYet": "Noch keine Medien",
"sidebar.noScriptsYet": "Noch keine Skripte",
"sidebar.noTemplatesYet": "Noch keine Vorlagen",
"sidebar.chatPlaceholder": "KI-Assistent erscheint hier",
"sidebar.importPlaceholder": "Inhalte aus externen Quellen importieren",
"sidebar.gitPlaceholder": "Versionskontrolle-Integration",
"sidebar.loading": "Lädt...",
"sidebar.settingsHeader": "Einstellungen",
"sidebar.tagsHeader": "Schlagwörter",

View File

@@ -38,9 +38,10 @@
"activity.scripts": "Scripts",
"activity.tags": "Tags",
"activity.templates": "Templates",
"activity.aiAssistant": "AI Assistant",
"activity.chat": "AI Assistant",
"activity.import": "Import",
"activity.sourceControl": "Source Control",
"activity.git": "Source Control",
"activity.settings": "Settings",
"common.save": "Save",
"common.cancel": "Cancel",
"common.settings": "Settings",
@@ -85,6 +86,11 @@
"sidebar.noPostsYet": "No posts yet",
"sidebar.noPagesYet": "No pages yet",
"sidebar.noMediaYet": "No media yet",
"sidebar.noScriptsYet": "No scripts yet",
"sidebar.noTemplatesYet": "No templates yet",
"sidebar.chatPlaceholder": "AI assistant will appear here",
"sidebar.importPlaceholder": "Import content from external sources",
"sidebar.gitPlaceholder": "Source control integration",
"sidebar.loading": "Loading...",
"sidebar.settingsHeader": "Settings",
"sidebar.tagsHeader": "Tags",

View File

@@ -38,9 +38,10 @@
"activity.scripts": "Scripts",
"activity.tags": "Etiquetas",
"activity.templates": "Plantillas",
"activity.aiAssistant": "Asistente IA",
"activity.chat": "Asistente IA",
"activity.import": "Importar",
"activity.sourceControl": "Control de código fuente",
"activity.git": "Control de código fuente",
"activity.settings": "Configuración",
"common.save": "Guardar",
"common.cancel": "Cancelar",
"common.settings": "Configuración",
@@ -85,6 +86,11 @@
"sidebar.noPostsYet": "Aún no hay entradas",
"sidebar.noPagesYet": "Aún no hay páginas",
"sidebar.noMediaYet": "Aún no hay medios",
"sidebar.noScriptsYet": "Aún no hay scripts",
"sidebar.noTemplatesYet": "Aún no hay plantillas",
"sidebar.chatPlaceholder": "El asistente IA aparecerá aquí",
"sidebar.importPlaceholder": "Importar contenido desde fuentes externas",
"sidebar.gitPlaceholder": "Integración del control de código fuente",
"sidebar.loading": "Cargando...",
"sidebar.settingsHeader": "Configuración",
"sidebar.tagsHeader": "Etiquetas",

View File

@@ -38,9 +38,10 @@
"activity.scripts": "Scripts",
"activity.tags": "Étiquettes",
"activity.templates": "Modèles",
"activity.aiAssistant": "Assistant IA",
"activity.chat": "Assistant IA",
"activity.import": "Importation",
"activity.sourceControl": "Contrôle de source",
"activity.git": "Contrôle de source",
"activity.settings": "Paramètres",
"common.save": "Enregistrer",
"common.cancel": "Annuler",
"common.settings": "Paramètres",
@@ -85,6 +86,11 @@
"sidebar.noPostsYet": "Aucun article pour le moment",
"sidebar.noPagesYet": "Aucune page pour le moment",
"sidebar.noMediaYet": "Aucun média pour le moment",
"sidebar.noScriptsYet": "Aucun script",
"sidebar.noTemplatesYet": "Aucun modèle",
"sidebar.chatPlaceholder": "L'assistant IA apparaîtra ici",
"sidebar.importPlaceholder": "Importer du contenu depuis des sources externes",
"sidebar.gitPlaceholder": "Intégration du contrôle de source",
"sidebar.loading": "Chargement...",
"sidebar.settingsHeader": "Paramètres",
"sidebar.tagsHeader": "Étiquettes",

View File

@@ -38,9 +38,10 @@
"activity.scripts": "Script",
"activity.tags": "Tag",
"activity.templates": "Modelli",
"activity.aiAssistant": "Assistente IA",
"activity.chat": "Assistente IA",
"activity.import": "Importa",
"activity.sourceControl": "Controllo sorgente",
"activity.git": "Controllo sorgente",
"activity.settings": "Impostazioni",
"common.save": "Salva",
"common.cancel": "Annulla",
"common.settings": "Impostazioni",
@@ -85,6 +86,11 @@
"sidebar.noPostsYet": "Nessun post",
"sidebar.noPagesYet": "Nessuna pagina",
"sidebar.noMediaYet": "Nessun media",
"sidebar.noScriptsYet": "Nessuno script",
"sidebar.noTemplatesYet": "Nessun modello",
"sidebar.chatPlaceholder": "L'assistente IA apparirà qui",
"sidebar.importPlaceholder": "Importa contenuti da fonti esterne",
"sidebar.gitPlaceholder": "Integrazione del controllo sorgente",
"sidebar.loading": "Caricamento...",
"sidebar.settingsHeader": "Impostazioni",
"sidebar.tagsHeader": "Tag",