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

@@ -1,9 +1,11 @@
//! Verify that the Rust Diesel models read the compatibility fixture produced by bDS.
mod support;
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
use bds_core::db::Database;
use bds_core::db::queries::{
media, post, post_link, post_media, post_translation, project, script, setting, tag, template,
};
@@ -17,11 +19,21 @@ const GHOSTTY_ID: &str = "6745981d-da41-4cfd-80ec-95ad339acf6f";
const CMUX_ID: &str = "2665bfaa-8251-468d-a710-a4cf34dd81e2";
const SPIDER_ID: &str = "eb0cf9d7-6fbd-4b74-9be3-759d6e16f240";
fn fixture_db() -> Database {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/compatibility-projects/rfc1437-sample/bds.db");
assert!(path.exists(), "fixture DB not found at {}", path.display());
Database::open(&path).unwrap()
fn fixture_db() -> support::FixtureDatabase {
support::fixture_database()
}
#[test]
fn fixture_database_files_are_not_modified_by_tests() {
let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/compatibility-projects/rfc1437-sample");
let paths = ["bds.db", "bds.db-shm", "bds.db-wal"].map(|name| directory.join(name));
let before = paths.each_ref().map(|path| fs::read(path).unwrap());
drop(fixture_db());
let after = paths.each_ref().map(|path| fs::read(path).unwrap());
assert_eq!(after, before);
}
#[test]

View File

@@ -1,7 +1,8 @@
//! Executable checks for storage-related Allium claims.
mod support;
use std::collections::HashSet;
use std::path::PathBuf;
use bds_core::db::Database;
use bds_core::db::queries::{
@@ -13,12 +14,8 @@ use bds_core::model::{
};
use diesel::prelude::*;
fn fixture_db() -> Database {
Database::open(
&PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/compatibility-projects/rfc1437-sample/bds.db"),
)
.unwrap()
fn fixture_db() -> support::FixtureDatabase {
support::fixture_database()
}
fn memory_db() -> Database {

View File

@@ -0,0 +1,34 @@
use std::fs;
use std::ops::Deref;
use std::path::PathBuf;
use bds_core::db::Database;
use tempfile::TempDir;
pub struct FixtureDatabase {
database: Database,
_directory: TempDir,
}
impl Deref for FixtureDatabase {
type Target = Database;
fn deref(&self) -> &Self::Target {
&self.database
}
}
pub fn fixture_database() -> FixtureDatabase {
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/compatibility-projects/rfc1437-sample");
let directory = TempDir::new().unwrap();
for name in ["bds.db", "bds.db-shm", "bds.db-wal"] {
fs::copy(source.join(name), directory.path().join(name)).unwrap();
}
FixtureDatabase {
database: Database::open(&directory.path().join("bds.db")).unwrap(),
_directory: directory,
}
}

View File

@@ -10,7 +10,7 @@ use iced::advanced::{Clipboard, Shell};
use iced::event::Status;
use iced::keyboard;
use iced::mouse;
use iced::{Color, Element, Event, Length, Pixels, Point, Rectangle, Size, Theme};
use iced::{Color, Element, Event, Length, Pixels, Point, Rectangle, Shadow, Size, Theme, Vector};
use crate::buffer::EditorBuffer;
use crate::highlight::Highlighter;
@@ -76,21 +76,33 @@ pub fn mono_metrics() -> &'static MonoMetrics {
})
}
const GUTTER_WIDTH: f32 = 50.0;
const GUTTER_WIDTH: f32 = 44.0;
const TEXT_PADDING: f32 = 12.0;
const FONT_SIZE: f32 = 14.0;
const BG_COLOR: Color = Color::from_rgb(0.18, 0.20, 0.25);
const GUTTER_BG: Color = Color::from_rgb(0.15, 0.17, 0.21);
const TEXT_COLOR: Color = Color::from_rgb(0.85, 0.85, 0.85);
const GUTTER_TEXT: Color = Color::from_rgb(0.45, 0.48, 0.55);
const CURSOR_COLOR: Color = Color::from_rgb(0.9, 0.9, 0.2);
const ACTIVE_LINE_NUM: Color = Color::from_rgb(0.75, 0.78, 0.85);
const SELECTION_BG: Color = Color::from_rgba(0.26, 0.54, 0.79, 0.40);
const TEXT_COLOR: Color = rgb8(0xD4, 0xD4, 0xD4);
const GUTTER_TEXT: Color = rgb8(0x85, 0x85, 0x85);
const CURSOR_COLOR: Color = rgb8(0xAE, 0xAF, 0xAD);
const ACTIVE_LINE_NUM: Color = rgb8(0xC6, 0xC6, 0xC6);
const SELECTION_BG: Color = rgba8(0x26, 0x4F, 0x78, 0.85);
const SCROLLBAR_WIDTH: f32 = 10.0;
const SCROLLBAR_TRACK: Color = Color::from_rgba(0.25, 0.27, 0.32, 0.5);
const SCROLLBAR_THUMB: Color = Color::from_rgba(0.50, 0.53, 0.60, 0.7);
const SCROLLBAR_THUMB_HOVER: Color = Color::from_rgba(0.60, 0.63, 0.70, 0.9);
const SCROLLBAR_TRACK: Color = Color::TRANSPARENT;
const SCROLLBAR_THUMB: Color = rgba8(0x79, 0x79, 0x79, 0.45);
const SCROLLBAR_THUMB_HOVER: Color = rgba8(0x79, 0x79, 0x79, 0.75);
const MIN_THUMB_HEIGHT: f32 = 20.0;
const fn rgb8(red: u8, green: u8, blue: u8) -> Color {
rgba8(red, green, blue, 1.0)
}
const fn rgba8(red: u8, green: u8, blue: u8, alpha: f32) -> Color {
Color {
r: red as f32 / 255.0,
g: green as f32 / 255.0,
b: blue as f32 / 255.0,
a: alpha,
}
}
fn committed_text_input(text: Option<&str>, is_command_shortcut: bool) -> Option<&str> {
if is_command_shortcut {
return None;
@@ -303,7 +315,7 @@ where
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Theme,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
@@ -317,24 +329,33 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
border: iced::Border::default(),
shadow: iced::Shadow::default(),
border: iced::Border {
color: Color::from_rgb8(0x35, 0x35, 0x35),
width: 1.0,
radius: 8.0.into(),
},
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.32),
offset: Vector::new(0.0, 3.0),
blur_radius: 14.0,
},
},
BG_COLOR,
theme.palette().background,
);
// Gutter background
let gutter_bounds = Rectangle {
width: GUTTER_WIDTH,
..bounds
};
// Subtle gutter divider; the shared background keeps the editor visually light.
renderer.fill_quad(
renderer::Quad {
bounds: gutter_bounds,
bounds: Rectangle {
x: bounds.x + GUTTER_WIDTH,
y: bounds.y,
width: 1.0,
height: bounds.height,
},
border: iced::Border::default(),
shadow: iced::Shadow::default(),
},
GUTTER_BG,
Color::from_rgb8(0x2B, 0x2B, 0x2B),
);
let metrics = mono_metrics();
@@ -349,7 +370,7 @@ where
let highlighted = self.highlighter.highlight_lines(&full_text, syntax);
let font = iced::Font::MONOSPACE;
let text_area_width = bounds.width - GUTTER_WIDTH - 8.0;
let text_area_width = bounds.width - GUTTER_WIDTH - TEXT_PADDING;
let max_chars = if self.word_wrap && text_area_width > metrics.char_width {
(text_area_width / metrics.char_width).floor() as usize
} else {
@@ -382,7 +403,7 @@ where
line_idx += 1;
}
let text_x = bounds.x + GUTTER_WIDTH + 8.0;
let text_x = bounds.x + GUTTER_WIDTH + TEXT_PADDING;
// Render visual rows
for (vis_idx, &(line_idx, char_start, char_end, is_first)) in visual_rows.iter().enumerate()
@@ -666,7 +687,7 @@ where
let state = tree.state.downcast_mut::<EditorState>();
let bounds = layout.bounds();
let metrics = mono_metrics();
let text_area_width = bounds.width - GUTTER_WIDTH - 8.0;
let text_area_width = bounds.width - GUTTER_WIDTH - TEXT_PADDING;
let cpl = if self.word_wrap && text_area_width > metrics.char_width {
(text_area_width / metrics.char_width).floor() as usize
} else {
@@ -752,8 +773,8 @@ where
if let Some(pos) = cursor.position_in(bounds) {
let mut buf = self.buffer.borrow_mut();
let vis_row = (pos.y / metrics.line_height) as usize;
let raw_col =
((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width) as usize;
let raw_col = ((pos.x - GUTTER_WIDTH - TEXT_PADDING).max(0.0)
/ metrics.char_width) as usize;
let (line, char_off) =
visual_to_logical(&buf, buf.scroll_offset(), vis_row, cpl)
.unwrap_or((buf.line_count().saturating_sub(1), 0));
@@ -792,8 +813,8 @@ where
if let Some(pos) = cursor.position_in(bounds) {
let mut buf = self.buffer.borrow_mut();
let vis_row = (pos.y / metrics.line_height) as usize;
let raw_col =
((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width) as usize;
let raw_col = ((pos.x - GUTTER_WIDTH - TEXT_PADDING).max(0.0)
/ metrics.char_width) as usize;
let (line, char_off) =
visual_to_logical(&buf, buf.scroll_offset(), vis_row, cpl)
.unwrap_or((buf.line_count().saturating_sub(1), 0));

View File

@@ -1,15 +1,104 @@
use iced::widget::text::Shaping;
use iced::widget::{Space, button, checkbox, column, container, pick_list, row, text, text_input};
use iced::{Alignment, Background, Color, Element, Length, Theme};
use iced::widget::{
Container, button, checkbox, column, container, pick_list, row, text, text_editor, text_input,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector};
/// Standard form field label color.
pub const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75);
const _FIELD_BG: Color = Color::from_rgb(0.15, 0.16, 0.20);
pub const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58);
const DANGER_BG: Color = Color::from_rgb(0.60, 0.15, 0.15);
const DANGER_HOVER: Color = Color::from_rgb(0.70, 0.20, 0.20);
const PRIMARY_BG: Color = Color::from_rgb(0.20, 0.40, 0.70);
const PRIMARY_HOVER: Color = Color::from_rgb(0.25, 0.48, 0.80);
pub const LABEL_COLOR: Color = rgb8(0xB5, 0xBA, 0xC4);
pub const SECTION_COLOR: Color = rgb8(0x9D, 0xA5, 0xB4);
const FIELD_BG: Color = rgb8(0x24, 0x24, 0x26);
const SURFACE_BG: Color = rgb8(0x25, 0x25, 0x26);
const BORDER_COLOR: Color = rgb8(0x3C, 0x3C, 0x3C);
const FOCUS_COLOR: Color = rgb8(0x00, 0x7F, 0xD4);
const DANGER_BG: Color = rgb8(0xB6, 0x23, 0x24);
const DANGER_HOVER: Color = rgb8(0xCF, 0x2F, 0x30);
const PRIMARY_BG: Color = rgb8(0x0E, 0x63, 0x9C);
const PRIMARY_HOVER: Color = rgb8(0x11, 0x77, 0xBB);
const SECONDARY_BG: Color = rgb8(0x2D, 0x2D, 0x30);
const SECONDARY_HOVER: Color = rgb8(0x3A, 0x3D, 0x41);
const fn rgb8(red: u8, green: u8, blue: u8) -> Color {
Color {
r: red as f32 / 255.0,
g: green as f32 / 255.0,
b: blue as f32 / 255.0,
a: 1.0,
}
}
/// Application-wide VS Code-like palette for native Iced controls.
pub fn app_theme() -> Theme {
Theme::custom(
"bDS".to_string(),
iced::theme::Palette {
background: Color::from_rgb8(0x1E, 0x1E, 0x1E),
text: Color::from_rgb8(0xCC, 0xCC, 0xCC),
primary: PRIMARY_BG,
success: Color::from_rgb8(0x2E, 0x7D, 0x32),
danger: DANGER_BG,
},
)
}
pub fn field_style(_theme: &Theme, status: text_input::Status) -> text_input::Style {
let border_color = match status {
text_input::Status::Focused => FOCUS_COLOR,
text_input::Status::Hovered => Color::from_rgb8(0x5A, 0x5A, 0x5A),
_ => BORDER_COLOR,
};
text_input::Style {
background: Background::Color(FIELD_BG),
border: Border {
color: border_color,
width: 1.0,
radius: 6.0.into(),
},
icon: SECTION_COLOR,
placeholder: Color::from_rgb8(0x7D, 0x7D, 0x7D),
value: Color::from_rgb8(0xE4, 0xE4, 0xE4),
selection: Color::from_rgba8(0x26, 0x4F, 0x78, 0.8),
}
}
/// Multi-line editor style matching the standard form fields.
pub fn text_editor_style(_theme: &Theme, status: text_editor::Status) -> text_editor::Style {
let border_color = match status {
text_editor::Status::Focused => FOCUS_COLOR,
text_editor::Status::Hovered => Color::from_rgb8(0x5A, 0x5A, 0x5A),
_ => BORDER_COLOR,
};
text_editor::Style {
background: Background::Color(FIELD_BG),
border: Border {
color: border_color,
width: 1.0,
radius: 6.0.into(),
},
icon: SECTION_COLOR,
placeholder: Color::from_rgb8(0x7D, 0x7D, 0x7D),
value: Color::from_rgb8(0xE4, 0xE4, 0xE4),
selection: Color::from_rgba8(0x26, 0x4F, 0x78, 0.8),
}
}
fn select_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
let border_color = match status {
pick_list::Status::Hovered | pick_list::Status::Opened => FOCUS_COLOR,
pick_list::Status::Active => BORDER_COLOR,
};
pick_list::Style {
text_color: Color::from_rgb8(0xE4, 0xE4, 0xE4),
placeholder_color: Color::from_rgb8(0x7D, 0x7D, 0x7D),
handle_color: SECTION_COLOR,
background: Background::Color(FIELD_BG),
border: Border {
color: border_color,
width: 1.0,
radius: 6.0.into(),
},
}
}
/// A labeled text input field.
pub fn labeled_input<'a, Message: Clone + 'a>(
@@ -23,9 +112,15 @@ pub fn labeled_input<'a, Message: Clone + 'a>(
.size(12)
.color(LABEL_COLOR)
.shaping(Shaping::Advanced),
text_input(placeholder, value).on_input(on_change).size(14),
text_input(placeholder, value)
.on_input(on_change)
.size(14)
.padding([8, 10])
.width(Length::Fill)
.style(field_style),
]
.spacing(4)
.spacing(6)
.width(Length::Fill)
.into()
}
@@ -46,9 +141,13 @@ where
.size(12)
.color(LABEL_COLOR)
.shaping(Shaping::Advanced),
pick_list(list, selected.cloned(), on_select),
pick_list(list, selected.cloned(), on_select)
.padding([8, 10])
.width(Length::Fill)
.style(select_style),
]
.spacing(4)
.spacing(6)
.width(Length::Fill)
.into()
}
@@ -67,21 +166,11 @@ pub fn labeled_checkbox<'a, Message: Clone + 'a>(
/// A section header with optional separator line.
pub fn section_header<'a, Message: 'a>(label: &str) -> Element<'a, Message> {
column![
text(label.to_string())
.size(11)
.color(SECTION_COLOR)
.shaping(Shaping::Advanced),
container(Space::new(0, 0))
.width(Length::Fill)
.height(Length::Fixed(1.0))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.25, 0.25, 0.30))),
..container::Style::default()
}),
]
.spacing(4)
.into()
text(label.to_string())
.size(12)
.color(SECTION_COLOR)
.shaping(Shaping::Advanced)
.into()
}
/// Primary action button style.
@@ -92,7 +181,7 @@ pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
background: Some(Background::Color(PRIMARY_HOVER)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
radius: 6.0.into(),
..iced::Border::default()
},
..button::Style::default()
@@ -101,7 +190,7 @@ pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
background: Some(Background::Color(PRIMARY_BG)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
radius: 6.0.into(),
..iced::Border::default()
},
..button::Style::default()
@@ -117,7 +206,7 @@ pub fn danger_button(theme: &Theme, status: button::Status) -> button::Style {
background: Some(Background::Color(DANGER_HOVER)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
radius: 6.0.into(),
..iced::Border::default()
},
..button::Style::default()
@@ -126,7 +215,7 @@ pub fn danger_button(theme: &Theme, status: button::Status) -> button::Style {
background: Some(Background::Color(DANGER_BG)),
text_color: Color::WHITE,
border: iced::Border {
radius: 4.0.into(),
radius: 6.0.into(),
..iced::Border::default()
},
..button::Style::default()
@@ -134,6 +223,66 @@ pub fn danger_button(theme: &Theme, status: button::Status) -> button::Style {
}
}
/// Secondary editor action button style.
pub fn secondary_button(_theme: &Theme, status: button::Status) -> button::Style {
let background = match status {
button::Status::Hovered => SECONDARY_HOVER,
button::Status::Disabled => SECONDARY_BG.scale_alpha(0.45),
_ => SECONDARY_BG,
};
button::Style {
background: Some(Background::Color(background)),
text_color: if matches!(status, button::Status::Disabled) {
LABEL_COLOR.scale_alpha(0.55)
} else {
Color::from_rgb8(0xE4, 0xE4, 0xE4)
},
border: Border {
color: BORDER_COLOR,
width: 1.0,
radius: 6.0.into(),
},
..button::Style::default()
}
}
/// Hoverable disclosure row style for collapsible editor sections.
pub fn disclosure_button(_theme: &Theme, status: button::Status) -> button::Style {
button::Style {
background: match status {
button::Status::Hovered => Some(Background::Color(SECONDARY_BG)),
_ => None,
},
text_color: SECTION_COLOR,
border: Border {
radius: 6.0.into(),
..Border::default()
},
..button::Style::default()
}
}
/// Raised surface used to group editor metadata and controls.
pub fn card<'a, Message: 'a>(content: impl Into<Element<'a, Message>>) -> Container<'a, Message> {
container(content)
.padding(16)
.width(Length::Fill)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(SURFACE_BG)),
border: Border {
color: Color::from_rgb8(0x31, 0x31, 0x33),
width: 1.0,
radius: 10.0.into(),
},
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.28),
offset: Vector::new(0.0, 4.0),
blur_radius: 16.0,
},
..container::Style::default()
})
}
/// Toolbar-style row with right-aligned actions.
pub fn toolbar<'a, Message: 'a>(
left: Vec<Element<'a, Message>>,
@@ -151,12 +300,29 @@ pub fn toolbar<'a, Message: 'a>(
.align_y(Alignment::Center)
.wrap();
row![left_row, right_row]
.padding(8)
.spacing(8)
.align_y(Alignment::Center)
.width(Length::Fill)
.into()
container(
row![left_row, right_row]
.spacing(10)
.align_y(Alignment::Center)
.width(Length::Fill),
)
.padding(10)
.width(Length::Fill)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(SURFACE_BG)),
border: Border {
color: Color::from_rgb8(0x31, 0x31, 0x33),
width: 1.0,
radius: 10.0.into(),
},
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.2),
offset: Vector::new(0.0, 2.0),
blur_radius: 10.0,
},
..container::Style::default()
})
.into()
}
/// Date display (read-only, locale-formatted).
@@ -184,3 +350,31 @@ fn format_timestamp(ms: i64) -> String {
let min = ((secs % 3600) / 60) as u32;
format!("{y}-{m:02}-{d:02} {h:02}:{min:02}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn editor_controls_have_distinct_hover_and_focus_states() {
let theme = app_theme();
assert_ne!(
primary_button(&theme, button::Status::Active).background,
primary_button(&theme, button::Status::Hovered).background
);
assert_ne!(
field_style(&theme, text_input::Status::Active).border.color,
field_style(&theme, text_input::Status::Focused)
.border
.color
);
assert_ne!(
text_editor_style(&theme, text_editor::Status::Active)
.border
.color,
text_editor_style(&theme, text_editor::Status::Focused)
.border
.color
);
}
}

View File

@@ -1,6 +1,7 @@
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
use bds_ui::BdsApp;
use bds_ui::components::inputs;
fn main() -> iced::Result {
let icon =
@@ -9,7 +10,7 @@ fn main() -> iced::Result {
iced::application("bDS", BdsApp::update, BdsApp::view)
.subscription(BdsApp::subscription)
.theme(|_| iced::Theme::Dark)
.theme(|_| inputs::app_theme())
.window(iced::window::Settings {
size: iced::Size::new(1200.0, 800.0),
icon: Some(icon),

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