Modernize the UI and isolate fixture tests.
This commit is contained in:
@@ -7,6 +7,11 @@ This project has an allium spec in the folder specs/ - use it to verify behaviou
|
||||
|
||||
Invariants and behaviours in the allium spec should be covered by unit tests of the application code, to make sure the spec is followed.
|
||||
|
||||
## UI styling
|
||||
|
||||
- Before implementing or changing UI, read and follow `docs/UI_STYLE_GUIDE.md`.
|
||||
- Reuse the shared styling primitives described there so new sidebars and editor areas remain consistent with the post editor.
|
||||
|
||||
## Plan Mode
|
||||
|
||||
- Make the plan extremely concise. Sacrifice grammar for the sake of concision.
|
||||
@@ -45,4 +50,3 @@ Invariants and behaviours in the allium spec should be covered by unit tests of
|
||||
- when creating rust source code, always follow what the allium spec is saying for that part
|
||||
- when tending the allium spec, make sure you validate the spec with the installed command line utility
|
||||
- don't be lazy. don't defer or skip implementations just because you have to write code for that, that is ridiculous. if the spec says something has to be there, it has to be there.
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
34
crates/bds-core/tests/support/mod.rs
Normal file
34
crates/bds-core/tests/support/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
49
docs/UI_STYLE_GUIDE.md
Normal file
49
docs/UI_STYLE_GUIDE.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# UI Style Guide
|
||||
|
||||
Use the post editor as the visual reference for every screen. New UI must feel like part of the same application in both the sidebar and editor area.
|
||||
|
||||
## Source of truth
|
||||
|
||||
- Reuse the primitives in `crates/bds-ui/src/components/inputs.rs`.
|
||||
- Reuse the code-editor theme in `crates/bds-editor/src/widget.rs`.
|
||||
- Follow `crates/bds-ui/src/views/post_editor.rs` for editor hierarchy and spacing.
|
||||
- Extend a shared primitive when several screens need the same treatment; do not copy color or border closures into each view.
|
||||
|
||||
## Layout
|
||||
|
||||
- Use 16 px outer editor padding and 8–12 px spacing between groups.
|
||||
- Put the editor header, metadata, summaries, previews, and content editors on `inputs::card` surfaces.
|
||||
- Use `inputs::toolbar` for content modes and related actions.
|
||||
- Use `inputs::disclosure_button` inside a compact card for collapsible sections. Expanded content belongs in a separate card directly below it.
|
||||
- Keep sidebars visually quieter than editor content. Sidebar rows use 6 px corner radii, 5×8 px padding, and a distinct selected state.
|
||||
- Avoid separators and boxes when spacing or a shared surface already communicates the grouping.
|
||||
|
||||
## Controls
|
||||
|
||||
- Text fields use `inputs::labeled_input` or `inputs::field_style`.
|
||||
- Selects use `inputs::labeled_select`; multiline fields use `inputs::text_editor_style`.
|
||||
- Primary actions use `inputs::primary_button`, secondary actions use `inputs::secondary_button`, and destructive actions use `inputs::danger_button`.
|
||||
- Controls have 6 px corner radii; cards use 10 px; chips and badges may use pill radii.
|
||||
- Keep one visually dominant primary action per action group.
|
||||
- Put infrequent operations in Quick Actions instead of adding buttons to metadata forms.
|
||||
- Do not use Iced's default field or button styling in application screens.
|
||||
|
||||
## Editor consistency
|
||||
|
||||
Every entity editor should have the same order:
|
||||
|
||||
1. Header card with title/status and actions.
|
||||
2. Metadata card or collapsible metadata section.
|
||||
3. Content toolbar when the content has modes or insertion actions.
|
||||
4. Content editor or preview card that receives the remaining height.
|
||||
5. Validation and timestamps as quiet supporting information.
|
||||
|
||||
Labels, placeholders, actions, empty states, and status text must use the UI localization system.
|
||||
|
||||
## Before merging UI work
|
||||
|
||||
- Compare the changed screen with the post editor at the same window size.
|
||||
- Check its sidebar as well as its editor area.
|
||||
- Check collapsed and expanded disclosures, hover/focus states, empty states, and disabled actions.
|
||||
- Check media, script, template, Settings, and Tags screens when changing a shared primitive.
|
||||
- Run `cargo test --workspace`, `cargo build --workspace`, and visually inspect the macOS app bundle.
|
||||
Reference in New Issue
Block a user