Modernize the UI and isolate fixture tests.

This commit is contained in:
2026-07-18 18:54:39 +02:00
parent 897e116db7
commit f96e6f1df4
23 changed files with 744 additions and 398 deletions

View File

@@ -54,7 +54,7 @@ fn active_button_style(_theme: &Theme, _status: button::Status) -> button::Style
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -72,7 +72,7 @@ fn inactive_button_style(_theme: &Theme, status: button::Status) -> button::Styl
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}

View File

@@ -238,6 +238,7 @@ pub fn view<'a>(
.on_press_maybe(
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::AnalyzeWithAi)),
)
.style(inputs::secondary_button)
.padding([6, 16])
.into()
} else {
@@ -247,12 +248,14 @@ pub fn view<'a>(
.on_press_maybe(
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::DetectLanguage)),
)
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "editor.translate")).size(13))
.on_press_maybe(
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::TranslateMetadata)),
)
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "common.save")).size(13))
@@ -287,7 +290,11 @@ pub fn view<'a>(
flag.language.clone(),
)))
.padding([4, 8])
.style(|_: &iced::Theme, _| button::Style::default())
.style(if flag.is_active {
inputs::primary_button
} else {
inputs::secondary_button
})
.into()
})
.collect();
@@ -331,6 +338,7 @@ pub fn view<'a>(
.color(Color::from_rgb(0.55, 0.58, 0.65)),
]
.padding(8);
let preview_card = inputs::card(column![preview, info].spacing(4)).padding(8);
// Metadata fields
let title_input = inputs::labeled_input(
@@ -385,6 +393,7 @@ pub fn view<'a>(
Space::with_width(Length::Fill),
button(text(t(locale, "editor.linkToPost")).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker))
.style(inputs::secondary_button)
.padding([4, 10]),
]
.align_y(iced::Alignment::Center)
@@ -478,19 +487,29 @@ pub fn view<'a>(
]
.padding(8);
let body = scrollable(
let metadata = inputs::card(
column![
header,
flags_bar,
preview,
info,
inputs::section_header(&t(locale, "editor.metadata")),
meta_row1,
meta_row2,
meta_row3,
linked_posts_header,
post_picker,
linked_posts_list,
]
.spacing(12)
.width(Length::Fill),
);
let linked_posts = inputs::card(
column![linked_posts_header, post_picker, linked_posts_list]
.spacing(10)
.width(Length::Fill),
);
let body = scrollable(
column![
header,
flags_bar,
preview_card,
metadata,
linked_posts,
footer,
]
.spacing(12)

View File

@@ -4,13 +4,14 @@ use iced::widget::text::Shaping;
use iced::widget::{
Space, button, checkbox, column, container, image, row, scrollable, text, text_input,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector};
use bds_core::i18n::UiLocale;
use bds_core::model::Media;
use bds_core::util::paths::thumbnail_path;
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
use crate::views::post_editor::PostEditorMsg;
@@ -138,61 +139,27 @@ fn modal_box_style(_theme: &Theme) -> container::Style {
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 8.0.into(),
radius: 10.0.into(),
},
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.4),
offset: Vector::new(0.0, 8.0),
blur_radius: 28.0,
},
..container::Style::default()
}
}
fn cancel_button_style(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.28, 0.28, 0.33),
_ => Color::from_rgb(0.22, 0.22, 0.27),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.80, 0.80, 0.85),
border: Border {
color: Color::from_rgb(0.35, 0.35, 0.40),
width: 1.0,
radius: 4.0.into(),
},
..button::Style::default()
}
fn cancel_button_style(theme: &Theme, status: button::Status) -> button::Style {
inputs::secondary_button(theme, status)
}
fn danger_button_style(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.85, 0.20, 0.20),
_ => Color::from_rgb(0.75, 0.15, 0.15),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 4.0.into(),
},
..button::Style::default()
}
fn danger_button_style(theme: &Theme, status: button::Status) -> button::Style {
inputs::danger_button(theme, status)
}
fn confirm_button_style(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.20, 0.55, 0.85),
_ => Color::from_rgb(0.15, 0.45, 0.75),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 4.0.into(),
},
..button::Style::default()
}
fn confirm_button_style(theme: &Theme, status: button::Status) -> button::Style {
inputs::primary_button(theme, status)
}
fn resolve_thumbnail_file(data_dir: Option<&Path>, media: &Media) -> Option<String> {
@@ -471,7 +438,8 @@ pub fn view(
.size(13)
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkSearch(s)))
.padding([6, 10])
.width(Length::Fill);
.width(Length::Fill)
.style(inputs::field_style);
let internal_content: Element<'static, Message> = if results.is_empty() {
column![
@@ -540,7 +508,7 @@ pub fn view(
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 4.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}),
@@ -557,7 +525,8 @@ pub fn view(
.size(13)
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkUrlChanged(s)))
.padding([6, 10])
.width(Length::Fill),
.width(Length::Fill)
.style(inputs::field_style),
text_input(
t(locale, "modal.postInsertLink.externalTextPlaceholder").as_str(),
&external_text,
@@ -565,7 +534,8 @@ pub fn view(
.size(13)
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkTextChanged(s)))
.padding([6, 10])
.width(Length::Fill),
.width(Length::Fill)
.style(inputs::field_style),
Space::with_height(12.0),
row![
Space::with_width(Length::Fill),
@@ -594,7 +564,7 @@ pub fn view(
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 4.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
})
@@ -659,7 +629,8 @@ pub fn view(
.size(13)
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertMediaSearch(s)))
.padding([6, 10])
.width(Length::Fill);
.width(Length::Fill)
.style(inputs::field_style);
let media_items: Vec<Element<'static, Message>> = media_list
.iter()

View File

@@ -26,7 +26,7 @@ fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
border: Border {
color: Color::from_rgb(0.30, 0.55, 0.90),
width: 0.0,
radius: 3.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -44,7 +44,7 @@ fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}

View File

@@ -396,7 +396,7 @@ pub fn view<'a>(
)
.on_press_maybe(ai_enabled.then_some(Message::PostEditor(PostEditorMsg::ToggleQuickActions)))
.padding([6, 16])
.style(status_bar::dropdown_trigger)
.style(inputs::secondary_button)
.into();
let mut header_action_items: Vec<Element<'a, Message>> = vec![
@@ -421,6 +421,7 @@ pub fn view<'a>(
)
.on_press(Message::PostEditor(PostEditorMsg::Duplicate))
.padding([6, 16])
.style(inputs::secondary_button)
.into(),
);
}
@@ -446,6 +447,7 @@ pub fn view<'a>(
)
.on_press(Message::PostEditor(PostEditorMsg::Discard))
.padding([6, 16])
.style(inputs::secondary_button)
.into(),
);
}
@@ -520,7 +522,7 @@ pub fn view<'a>(
Space::new(0, 0).into()
};
let header = column![header_row, header_menu].spacing(6);
let header = inputs::card(column![header_row, header_menu].spacing(6)).padding(10);
// ── Collapsible Metadata Section ──
let meta_toggle_label = if state.metadata_expanded {
@@ -535,15 +537,13 @@ pub fn view<'a>(
.shaping(Shaping::Advanced),
)
.on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata))
.padding([4, 0])
.style(|_, _| button::Style {
background: None,
..button::Style::default()
});
.padding([8, 10])
.width(Length::Fill)
.style(inputs::disclosure_button);
// ── Translation Flags Bar (inline with metadata toggle) ──
let flags = state.translation_flags();
let meta_toggle_row: Element<'a, Message> = if flags.is_empty() {
let meta_toggle_content: Element<'a, Message> = if flags.is_empty() {
meta_toggle.into()
} else {
let mut flag_row = row![].spacing(2);
@@ -560,11 +560,13 @@ pub fn view<'a>(
});
flag_row = flag_row.push(btn);
}
row![meta_toggle, Space::with_width(Length::Fill), flag_row]
row![meta_toggle, flag_row]
.spacing(8)
.align_y(iced::Alignment::Center)
.into()
};
let meta_toggle_row: Element<'a, Message> =
inputs::card(meta_toggle_content).padding([2, 4]).into();
let metadata_section: Element<'a, Message> = if state.metadata_expanded && !on_translation {
let title_input = inputs::labeled_input(
@@ -598,13 +600,6 @@ pub fn view<'a>(
Some(&state.language),
|lang| Message::PostEditor(PostEditorMsg::LanguageChanged(lang)),
);
let detect_language = button(
text(t(locale, "editor.detectLanguage"))
.size(12)
.shaping(Shaping::Advanced),
)
.on_press_maybe(ai_enabled.then_some(Message::PostEditor(PostEditorMsg::DetectLanguage)))
.padding([6, 12]);
let template_input = inputs::labeled_input(
&t(locale, "editor.templateSlug"),
"",
@@ -616,9 +611,9 @@ pub fn view<'a>(
state.do_not_translate,
|b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)),
);
let language_block = column![language_input, detect_language].spacing(6);
let meta_row2 = row![author_input, language_block, template_input, dnt]
let meta_row2 = row![author_input, language_input, template_input, dnt]
.spacing(16)
.align_y(iced::Alignment::End)
.width(Length::Fill);
// Tags chip input
@@ -695,7 +690,7 @@ pub fn view<'a>(
)
.on_press(Message::PostEditor(PostEditorMsg::LinkExistingMedia))
.padding([4, 10])
.style(|_: &Theme, _| button::Style::default())
.style(inputs::secondary_button)
.into();
let linked_media_header: Element<'a, Message> = row![
@@ -783,17 +778,19 @@ pub fn view<'a>(
Column::with_children(items).spacing(6).into()
};
column![
meta_row1,
meta_row2,
tags_section,
categories_section,
outlinks_section,
backlinks_section,
linked_media_section,
]
.spacing(8)
.width(Length::Fill)
inputs::card(
column![
meta_row1,
meta_row2,
tags_section,
categories_section,
outlinks_section,
backlinks_section,
linked_media_section,
]
.spacing(12)
.width(Length::Fill),
)
.into()
} else {
Space::new(0, 0).into()
@@ -812,19 +809,19 @@ pub fn view<'a>(
.shaping(Shaping::Advanced),
)
.on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt))
.padding([4, 0])
.style(|_, _| button::Style {
background: None,
..button::Style::default()
});
.padding([8, 10])
.width(Length::Fill)
.style(inputs::disclosure_button);
let excerpt_toggle: Element<'a, Message> = inputs::card(excerpt_toggle).padding([2, 4]).into();
let excerpt_section: Element<'a, Message> = if state.excerpt_expanded {
inputs::labeled_input(
inputs::card(inputs::labeled_input(
&t(locale, "editor.excerpt"),
&t(locale, "editor.excerptPlaceholder"),
&state.excerpt,
|s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)),
)
))
.into()
} else {
Space::new(0, 0).into()
};
@@ -866,6 +863,7 @@ pub fn view<'a>(
)
.on_press(Message::PostEditor(PostEditorMsg::InsertLink))
.padding([6, 16])
.style(inputs::secondary_button)
.into()
} else {
Space::new(0, 0).into()
@@ -878,6 +876,7 @@ pub fn view<'a>(
)
.on_press(Message::PostEditor(PostEditorMsg::InsertMedia))
.padding([6, 16])
.style(inputs::secondary_button)
.into()
} else {
Space::new(0, 0).into()
@@ -889,6 +888,7 @@ pub fn view<'a>(
)
.on_press(Message::PostEditor(PostEditorMsg::Gallery))
.padding([6, 16])
.style(inputs::secondary_button)
.into(),
],
);
@@ -946,14 +946,14 @@ pub fn view<'a>(
excerpt_toggle,
excerpt_section,
]
.spacing(4)
.spacing(8)
.width(Length::Fill),
)
.height(Length::Shrink);
// ── Full layout: top pane (shrink), editor (fill), footer (shrink) ──
column![top_pane, body_toolbar, editor_widget, footer,]
.spacing(4)
.spacing(8)
.padding(16)
.width(Length::Fill)
.height(Length::Fill)
@@ -998,7 +998,8 @@ fn chip_input_field<'a>(
.on_input(on_input)
.on_submit(on_submit)
.size(13)
.padding([4, 6]),
.padding([8, 10])
.style(inputs::field_style),
]
.spacing(4)
.width(Length::Fill)

View File

@@ -18,7 +18,7 @@ fn dropdown_bg(_theme: &Theme) -> container::Style {
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 4.0.into(),
radius: 10.0.into(),
},
..container::Style::default()
}
@@ -35,7 +35,7 @@ fn project_item(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -52,7 +52,7 @@ fn project_item_active(_theme: &Theme, status: button::Status) -> button::Style
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -69,7 +69,7 @@ fn new_project_btn(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -214,7 +214,7 @@ pub fn trigger_style(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}

View File

@@ -132,10 +132,12 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
.into(),
button(text(t(locale, "editor.run")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Run))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "editor.checkSyntax")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
@@ -200,6 +202,7 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
});
let meta_row2 = row![kind_select, entrypoint_input, enabled_check]
.spacing(16)
.align_y(iced::Alignment::End)
.width(Length::Fill);
// Content editor (CodeEditor with syntax highlighting based on file extension)
@@ -208,21 +211,28 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
} else {
"lua"
};
let content_section: Element<'a, Message> = column![
inputs::section_header(&t(locale, "editor.content")),
Element::from(
CodeEditor::new(&state.editor_buffer, highlighter(), syntax_ext,).on_change(|msg| {
match msg {
EditorMessage::ContentChanged(s) => {
Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s))
let content_section: Element<'a, Message> = inputs::card(
column![
inputs::section_header(&t(locale, "editor.content")),
Element::from(
CodeEditor::new(&state.editor_buffer, highlighter(), syntax_ext,).on_change(
|msg| {
match msg {
EditorMessage::ContentChanged(s) => {
Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s))
}
EditorMessage::SaveRequested => {
Message::ScriptEditor(ScriptEditorMsg::Save)
}
}
}
EditorMessage::SaveRequested => Message::ScriptEditor(ScriptEditorMsg::Save),
}
})
),
]
.spacing(8)
.width(Length::Fill)
)
),
]
.spacing(10)
.width(Length::Fill)
.height(Length::Fill),
)
.height(Length::Fill)
.into();
@@ -248,18 +258,18 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
.padding(8);
// Top pane: header + metadata (scrollable for overflow)
let top_pane = scrollable(
column![header, meta_row1, meta_row2,]
let metadata = inputs::card(
column![meta_row1, meta_row2]
.spacing(12)
.padding(16)
.width(Length::Fill),
)
.height(Length::Shrink);
);
let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill))
.height(Length::Shrink);
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
column![top_pane, content_section, validation, footer,]
.spacing(4)
.padding([0, 16])
.spacing(8)
.padding(16)
.width(Length::Fill)
.height(Length::Fill)
.into()

View File

@@ -1,6 +1,6 @@
use iced::widget::text::Shaping;
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
use iced::{Alignment, Color, Element, Length, Theme};
use iced::{Alignment, Color, Element, Length};
use bds_core::i18n::UiLocale;
@@ -347,7 +347,9 @@ pub enum SettingsMsg {
pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query)
.on_input(|s| Message::Settings(SettingsMsg::SearchChanged(s)))
.size(14);
.size(14)
.padding([8, 10])
.style(inputs::field_style);
let query_lower = state.search_query.to_lowercase();
@@ -369,6 +371,7 @@ pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, M
.color(Color::from_rgb(0.7, 0.72, 0.78)),
button(text(t(locale, "common.clear")).size(13))
.on_press(Message::Settings(SettingsMsg::SearchChanged(String::new())))
.style(inputs::secondary_button)
.padding([6, 12]),
]
.spacing(8)
@@ -413,10 +416,10 @@ fn render_section<'a>(
)))
.padding([6, 8])
.width(Length::Fill)
.style(|_: &Theme, _| button::Style::default());
.style(inputs::disclosure_button);
if collapsed {
return column![header].into();
return inputs::card(header).padding([2, 4]).into();
}
let content: Element<'a, Message> = match section {
@@ -430,7 +433,7 @@ fn render_section<'a>(
SettingsSection::MCP => section_mcp(locale),
};
column![header, content].spacing(4).into()
inputs::card(column![header, content].spacing(8)).into()
}
fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
@@ -448,7 +451,8 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
text_editor(&state.project_description)
.on_action(|a| Message::Settings(SettingsMsg::ProjectDescriptionAction(a)))
.height(Length::Fixed(80.0))
.size(14),
.size(14)
.style(inputs::text_editor_style),
]
.spacing(4);
let data_path = row![
@@ -457,9 +461,11 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
},),
button(text(t(locale, "settings.browse")).size(12))
.on_press(Message::Settings(SettingsMsg::BrowseDataPath))
.style(inputs::secondary_button)
.padding([6, 12]),
button(text(t(locale, "settings.reset")).size(12))
.on_press(Message::Settings(SettingsMsg::ResetDataPath))
.style(inputs::secondary_button)
.padding([6, 12]),
]
.spacing(8)
@@ -694,7 +700,7 @@ fn section_content<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
.spacing(8);
column.push(
container(
inputs::card(
column![
text(category.name.clone()).size(15).color(Color::WHITE),
title,
@@ -721,6 +727,7 @@ fn section_content<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
.padding([6, 12]),
button(text(t(locale, "settings.resetCategories")).size(13))
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
.style(inputs::secondary_button)
.padding([6, 12]),
]
.spacing(8)
@@ -788,6 +795,7 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
);
let online_refresh = button(text(t(locale, "settings.refreshModels")).size(13))
.on_press(Message::Settings(SettingsMsg::RefreshOnlineModels))
.style(inputs::secondary_button)
.padding([6, 16]);
let airplane_url = inputs::labeled_input(
@@ -804,6 +812,7 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
);
let airplane_refresh = button(text(t(locale, "settings.refreshModels")).size(13))
.on_press(Message::Settings(SettingsMsg::RefreshAirplaneModels))
.style(inputs::secondary_button)
.padding([6, 16]);
let default_model = inputs::labeled_select(
@@ -838,7 +847,8 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
text_editor(&state.system_prompt)
.on_action(|a| Message::Settings(SettingsMsg::SystemPromptAction(a)))
.height(Length::Fixed(200.0))
.size(14),
.size(14)
.style(inputs::text_editor_style),
]
.spacing(4);
let btns = row![
@@ -848,6 +858,7 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
.padding([6, 16]),
button(text(t(locale, "settings.resetToDefault")).size(13))
.on_press(Message::Settings(SettingsMsg::ResetSystemPrompt))
.style(inputs::secondary_button)
.padding([6, 16]),
]
.spacing(8);
@@ -930,30 +941,37 @@ fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
let rebuild_btns = column![
button(text(t(locale, "settings.rebuildPosts")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildPosts))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildMedia")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildMedia))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildScripts")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildScripts))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildTemplates")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildTemplates))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildLinks")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildLinks))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildSearchIndex")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildSearchIndex))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.regenerateThumbnails")).size(13))
.on_press(Message::Settings(SettingsMsg::RegenerateThumbnails))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
]
@@ -961,6 +979,7 @@ fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
let open = button(text(t(locale, "settings.openDataFolder")).size(13))
.on_press(Message::Settings(SettingsMsg::OpenDataFolder))
.style(inputs::secondary_button)
.padding([6, 16]);
column![rebuild_btns, open]

View File

@@ -8,6 +8,7 @@ use bds_core::i18n::UiLocale;
use bds_core::model::{Media, Post, Script, Template};
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
use crate::state::navigation::SidebarView;
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
@@ -30,7 +31,10 @@ fn item_style(_theme: &Theme, status: button::Status) -> button::Style {
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.80, 0.80, 0.85),
border: Border::default(),
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
}
@@ -44,7 +48,10 @@ fn item_active_style(_theme: &Theme, status: button::Status) -> button::Style {
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::WHITE,
border: Border::default(),
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
}
@@ -54,7 +61,7 @@ fn thumbnail_container_style(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.17))),
border: Border {
radius: 4.0.into(),
radius: 6.0.into(),
..Border::default()
},
..container::Style::default()
@@ -145,19 +152,8 @@ fn format_post_date(unix_ms: i64) -> String {
}
/// Search input style.
fn search_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(Color::from_rgb(0.12, 0.12, 0.15)),
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 3.0.into(),
},
icon: Color::from_rgb(0.50, 0.50, 0.55),
placeholder: Color::from_rgb(0.45, 0.45, 0.50),
value: Color::from_rgb(0.85, 0.85, 0.90),
selection: Color::from_rgba(0.35, 0.55, 0.85, 0.4),
}
fn search_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
inputs::field_style(theme, status)
}
/// Style for filter toggle / clear buttons in the header.
@@ -169,7 +165,10 @@ fn filter_button_style(_theme: &Theme, status: button::Status) -> button::Style
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.60, 0.60, 0.65),
border: Border::default(),
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
}
@@ -183,7 +182,10 @@ fn filter_button_active_style(_theme: &Theme, status: button::Status) -> button:
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.55, 0.70, 0.95),
border: Border::default(),
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
}
@@ -773,7 +775,7 @@ pub fn view(
is_transient: true,
is_dirty: false,
}))
.padding([3, 6])
.padding([5, 8])
.width(Length::Fill)
.style(style_fn)
.into()
@@ -1025,7 +1027,7 @@ pub fn view(
is_transient: true,
is_dirty: false,
}))
.padding([3, 6])
.padding([5, 8])
.width(Length::Fill)
.style(style_fn)
.into()
@@ -1065,7 +1067,7 @@ pub fn view(
is_transient: true,
is_dirty: false,
}))
.padding([3, 6])
.padding([5, 8])
.width(Length::Fill)
.style(style_fn)
.into()
@@ -1106,7 +1108,7 @@ pub fn view(
};
button(container(label_text).width(Length::Fill))
.on_press(msg)
.padding([3, 6])
.padding([5, 8])
.width(Length::Fill)
.style(item_style)
.into()
@@ -1130,7 +1132,7 @@ pub fn view(
is_transient: false,
is_dirty: false,
}))
.padding([3, 6])
.padding([5, 8])
.width(Length::Fill)
.style(item_style)
.into()

View File

@@ -5,6 +5,7 @@ use iced::{Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
#[derive(Debug, Clone, Default)]
@@ -25,6 +26,8 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.size(13)
.shaping(Shaping::Advanced),
)
.style(inputs::primary_button)
.padding([6, 16])
} else {
button(
text(t(locale, "siteValidation.run"))
@@ -32,6 +35,8 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.shaping(Shaping::Advanced),
)
.on_press(Message::RunSiteValidation)
.style(inputs::primary_button)
.padding([6, 16])
};
let has_issues = !state.missing_files.is_empty()
|| !state.extra_files.is_empty()
@@ -42,6 +47,8 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.size(13)
.shaping(Shaping::Advanced),
)
.style(inputs::secondary_button)
.padding([6, 16])
} else if !state.is_running && state.error_message.is_none() && has_issues {
button(
text(t(locale, "siteValidation.apply"))
@@ -49,12 +56,16 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.shaping(Shaping::Advanced),
)
.on_press(Message::ApplySiteValidation)
.style(inputs::primary_button)
.padding([6, 16])
} else {
button(
text(t(locale, "siteValidation.apply"))
.size(13)
.shaping(Shaping::Advanced),
)
.style(inputs::secondary_button)
.padding([6, 16])
};
let mut content = column![
@@ -132,17 +143,12 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
}
fn help_text<'a>(value: String) -> Element<'a, Message> {
container(
inputs::card(
text(value)
.size(14)
.shaping(Shaping::Advanced)
.color(Color::from_rgb(0.66, 0.66, 0.72)),
)
.padding(16)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.18))),
..container::Style::default()
})
.into()
}
@@ -156,7 +162,7 @@ fn section<'a>(title: String, entries: &'a [String], accent: Color) -> Element<'
)
});
container(
inputs::card(
column![
text(title.to_string())
.size(16)
@@ -166,10 +172,5 @@ fn section<'a>(title: String, entries: &'a [String], accent: Color) -> Element<'
]
.spacing(10),
)
.padding(16)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.18))),
..container::Style::default()
})
.into()
}

View File

@@ -33,7 +33,7 @@ fn airplane_inactive(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -51,7 +51,7 @@ fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -69,7 +69,7 @@ pub fn dropdown_trigger(_theme: &Theme, status: button::Status) -> button::Style
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -87,7 +87,7 @@ pub fn dropdown_item(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -100,7 +100,7 @@ pub fn dropdown_bg(_theme: &Theme) -> container::Style {
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 4.0.into(),
radius: 6.0.into(),
},
..container::Style::default()
}

View File

@@ -41,7 +41,7 @@ fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
border: Border {
color: Color::from_rgb(0.30, 0.55, 0.90),
width: 0.0,
radius: 4.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -59,7 +59,7 @@ fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 4.0.into(),
radius: 6.0.into(),
},
..button::Style::default()
}
@@ -86,7 +86,7 @@ fn tooltip_style(_theme: &Theme) -> container::Style {
border: Border {
color: Color::from_rgb(0.35, 0.35, 0.40),
width: 1.0,
radius: 4.0.into(),
radius: 6.0.into(),
},
..container::Style::default()
}

View File

@@ -117,30 +117,32 @@ pub enum TagsMsg {
/// Render the tags management view.
pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
let section_nav = row![
section_tab(
&t(locale, "tags.nav.cloud"),
state.section == TagsSection::Cloud,
TagsSection::Cloud
),
section_tab(
&t(locale, "tags.nav.manage"),
state.section == TagsSection::Manage,
TagsSection::Manage
),
section_tab(
&t(locale, "tags.nav.merge"),
state.section == TagsSection::Merge,
TagsSection::Merge
),
section_tab(
&t(locale, "tags.nav.discover"),
state.section == TagsSection::Discover,
TagsSection::Discover
),
]
.spacing(4)
.padding(8);
let section_nav = inputs::card(
row![
section_tab(
&t(locale, "tags.nav.cloud"),
state.section == TagsSection::Cloud,
TagsSection::Cloud
),
section_tab(
&t(locale, "tags.nav.manage"),
state.section == TagsSection::Manage,
TagsSection::Manage
),
section_tab(
&t(locale, "tags.nav.merge"),
state.section == TagsSection::Merge,
TagsSection::Merge
),
section_tab(
&t(locale, "tags.nav.discover"),
state.section == TagsSection::Discover,
TagsSection::Discover
),
]
.spacing(6),
)
.padding(6);
let content: Element<'a, Message> = match state.section {
TagsSection::Cloud => view_cloud(state, locale),
@@ -150,33 +152,20 @@ pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Messa
};
column![section_nav, content]
.spacing(4)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
let color = if active {
Color::WHITE
} else {
Color::from_rgb(0.55, 0.58, 0.65)
};
button(text(label.to_string()).size(13).color(color))
button(text(label.to_string()).size(13))
.on_press(Message::Tags(TagsMsg::SetSection(section)))
.padding([6, 12])
.style(move |_theme: &Theme, _status| {
if active {
button::Style {
background: Some(Background::Color(Color::from_rgb(0.25, 0.27, 0.33))),
border: iced::Border {
radius: 4.0.into(),
..iced::Border::default()
},
..button::Style::default()
}
} else {
button::Style::default()
}
.style(if active {
inputs::primary_button
} else {
inputs::secondary_button
})
.into()
}
@@ -212,8 +201,8 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
} else {
1.0
};
const MIN_FONT: f32 = 11.0;
const MAX_FONT: f32 = 24.0;
const MIN_FONT: f32 = 12.0;
const MAX_FONT: f32 = 18.0;
let chips: Vec<Element<'a, Message>> = state
.tags
@@ -243,16 +232,17 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
)
.on_press(Message::Tags(TagsMsg::ToggleTagSelection(tag.id.clone())))
.padding([vert_pad, 10])
.style(move |_: &Theme, _| button::Style {
background: Some(Background::Color(color)),
.style(move |_: &Theme, status| button::Style {
background: Some(Background::Color(match (selected, status) {
(true, button::Status::Hovered) => Color::from_rgb(0.20, 0.24, 0.30),
(true, _) => Color::from_rgb(0.17, 0.20, 0.25),
(false, button::Status::Hovered) => Color::from_rgb(0.23, 0.24, 0.26),
(false, _) => Color::from_rgb(0.18, 0.18, 0.19),
})),
border: iced::Border {
radius: 12.0.into(),
width: if selected { 2.0 } else { 0.0 },
color: if selected {
Color::WHITE
} else {
Color::TRANSPARENT
},
width: if selected { 2.0 } else { 1.0 },
color,
},
text_color: Color::WHITE,
..button::Style::default()
@@ -277,6 +267,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
.color(Color::from_rgb(0.75, 0.77, 0.82)),
button(text(t(locale, "tags.clearSelection")).size(12))
.on_press(Message::Tags(TagsMsg::ClearSelection))
.style(inputs::secondary_button)
.padding([4, 8]),
]
.spacing(8)
@@ -285,13 +276,15 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
};
scrollable(
column![
inputs::section_header(&t(locale, "tags.cloudSection")),
selection_summary,
row(chips).spacing(6).wrap(),
]
.spacing(12)
.width(Length::Fill)
column![inputs::card(
column![
inputs::section_header(&t(locale, "tags.cloudSection")),
selection_summary,
row(chips).spacing(6).wrap(),
]
.spacing(12)
.width(Length::Fill),
)]
.padding(16),
)
.width(Length::Fill)
@@ -326,7 +319,9 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query)
.on_input(|value| Message::Tags(TagsMsg::SearchChanged(value)))
.size(14);
.size(14)
.padding([8, 10])
.style(inputs::field_style);
let filtered: Vec<&Tag> = state
.tags
@@ -446,11 +441,16 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
scrollable(
column![
create_panel,
inputs::section_header(&t(locale, "tags.manageSection")),
search,
tag_list,
edit_panel,
inputs::card(create_panel),
inputs::card(
column![
inputs::section_header(&t(locale, "tags.manageSection")),
search,
tag_list,
edit_panel,
]
.spacing(12)
),
]
.spacing(12)
.padding(16)
@@ -491,56 +491,62 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
})
.unwrap_or_default();
column![
inputs::section_header(&t(locale, "tags.mergeSection")),
text(tw(
locale,
"tags.selectedCount",
&[("count", &state.selected_tags.len().to_string())]
))
.size(12)
.color(Color::from_rgb(0.75, 0.77, 0.82)),
inputs::labeled_select(
&t(locale, "tags.mergeTarget"),
&tag_options,
selected_target,
|choice| Message::Tags(TagsMsg::SetMergeTarget(choice)),
),
if merge_preview.is_empty() {
Element::from(Space::new(0, 0))
} else {
container(text(tw(locale, "tags.mergePreview", &[("tags", &merge_preview)])).size(12))
container(inputs::card(
column![
inputs::section_header(&t(locale, "tags.mergeSection")),
text(tw(
locale,
"tags.selectedCount",
&[("count", &state.selected_tags.len().to_string())]
))
.size(12)
.color(Color::from_rgb(0.75, 0.77, 0.82)),
inputs::labeled_select(
&t(locale, "tags.mergeTarget"),
&tag_options,
selected_target,
|choice| Message::Tags(TagsMsg::SetMergeTarget(choice)),
),
if merge_preview.is_empty() {
Element::from(Space::new(0, 0))
} else {
container(
text(tw(locale, "tags.mergePreview", &[("tags", &merge_preview)])).size(12),
)
.padding([4, 0])
.into()
},
button(text(t(locale, "tags.merge")).size(13))
.on_press_maybe(
state
.merge_target
.is_some()
.then_some(Message::Tags(TagsMsg::MergeTags))
)
.style(inputs::primary_button)
.padding([6, 16]),
]
.spacing(12)
},
button(text(t(locale, "tags.merge")).size(13))
.on_press_maybe(
state
.merge_target
.is_some()
.then_some(Message::Tags(TagsMsg::MergeTags))
)
.style(inputs::primary_button)
.padding([6, 16]),
]
.spacing(12),
))
.padding(16)
.width(Length::Fill)
.into()
}
fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
column![
inputs::section_header(&t(locale, "tags.discoverSection")),
text(t(locale, "tags.discoverDescription"))
.size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
button(text(t(locale, "tags.discoverButton")).size(13))
.on_press(Message::Tags(TagsMsg::SyncTags))
.style(inputs::primary_button)
.padding([6, 16]),
]
.spacing(12)
container(inputs::card(
column![
inputs::section_header(&t(locale, "tags.discoverSection")),
text(t(locale, "tags.discoverDescription"))
.size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
button(text(t(locale, "tags.discoverButton")).size(13))
.on_press(Message::Tags(TagsMsg::SyncTags))
.style(inputs::primary_button)
.padding([6, 16]),
]
.spacing(12),
))
.padding(16)
.width(Length::Fill)
.into()

View File

@@ -121,6 +121,7 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
.into(),
button(text(t(locale, "editor.validate")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Validate))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
@@ -168,26 +169,30 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
});
let meta_row2 = row![kind_select, enabled_check]
.spacing(16)
.align_y(iced::Alignment::End)
.width(Length::Fill);
// Content editor (CodeEditor with Liquid/HTML syntax highlighting)
let content_section: Element<'a, Message> = column![
inputs::section_header(&t(locale, "editor.content")),
Element::from(
CodeEditor::new(&state.editor_buffer, highlighter(), "liquid",).on_change(|msg| {
match msg {
EditorMessage::ContentChanged(s) => {
Message::TemplateEditor(TemplateEditorMsg::ContentChanged(s))
let content_section: Element<'a, Message> = inputs::card(
column![
inputs::section_header(&t(locale, "editor.content")),
Element::from(
CodeEditor::new(&state.editor_buffer, highlighter(), "liquid",).on_change(|msg| {
match msg {
EditorMessage::ContentChanged(s) => {
Message::TemplateEditor(TemplateEditorMsg::ContentChanged(s))
}
EditorMessage::SaveRequested => {
Message::TemplateEditor(TemplateEditorMsg::Save)
}
}
EditorMessage::SaveRequested => {
Message::TemplateEditor(TemplateEditorMsg::Save)
}
}
})
),
]
.spacing(8)
.width(Length::Fill)
})
),
]
.spacing(10)
.width(Length::Fill)
.height(Length::Fill),
)
.height(Length::Fill)
.into();
@@ -213,18 +218,18 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
.padding(8);
// Top pane: header + metadata (scrollable for overflow)
let top_pane = scrollable(
column![header, meta_row1, meta_row2,]
let metadata = inputs::card(
column![meta_row1, meta_row2]
.spacing(12)
.padding(16)
.width(Length::Fill),
)
.height(Length::Shrink);
);
let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill))
.height(Length::Shrink);
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
column![top_pane, content_section, validation, footer,]
.spacing(4)
.padding([0, 16])
.spacing(8)
.padding(16)
.width(Length::Fill)
.height(Length::Fill)
.into()

View File

@@ -31,7 +31,7 @@ fn toast_style(level: ToastLevel) -> impl Fn(&Theme) -> container::Style {
border: Border {
color: toast_border(level),
width: 1.0,
radius: 4.0.into(),
radius: 8.0.into(),
},
..container::Style::default()
}