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.
|
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
|
## Plan Mode
|
||||||
|
|
||||||
- Make the plan extremely concise. Sacrifice grammar for the sake of concision.
|
- 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 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
|
- 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.
|
- 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.
|
//! Verify that the Rust Diesel models read the compatibility fixture produced by bDS.
|
||||||
|
|
||||||
|
mod support;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use bds_core::db::Database;
|
|
||||||
use bds_core::db::queries::{
|
use bds_core::db::queries::{
|
||||||
media, post, post_link, post_media, post_translation, project, script, setting, tag, template,
|
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 CMUX_ID: &str = "2665bfaa-8251-468d-a710-a4cf34dd81e2";
|
||||||
const SPIDER_ID: &str = "eb0cf9d7-6fbd-4b74-9be3-759d6e16f240";
|
const SPIDER_ID: &str = "eb0cf9d7-6fbd-4b74-9be3-759d6e16f240";
|
||||||
|
|
||||||
fn fixture_db() -> Database {
|
fn fixture_db() -> support::FixtureDatabase {
|
||||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
support::fixture_database()
|
||||||
.join("../../fixtures/compatibility-projects/rfc1437-sample/bds.db");
|
}
|
||||||
assert!(path.exists(), "fixture DB not found at {}", path.display());
|
|
||||||
Database::open(&path).unwrap()
|
#[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]
|
#[test]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Executable checks for storage-related Allium claims.
|
//! Executable checks for storage-related Allium claims.
|
||||||
|
|
||||||
|
mod support;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use bds_core::db::Database;
|
use bds_core::db::Database;
|
||||||
use bds_core::db::queries::{
|
use bds_core::db::queries::{
|
||||||
@@ -13,12 +14,8 @@ use bds_core::model::{
|
|||||||
};
|
};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
fn fixture_db() -> Database {
|
fn fixture_db() -> support::FixtureDatabase {
|
||||||
Database::open(
|
support::fixture_database()
|
||||||
&PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../../fixtures/compatibility-projects/rfc1437-sample/bds.db"),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn memory_db() -> 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::event::Status;
|
||||||
use iced::keyboard;
|
use iced::keyboard;
|
||||||
use iced::mouse;
|
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::buffer::EditorBuffer;
|
||||||
use crate::highlight::Highlighter;
|
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 FONT_SIZE: f32 = 14.0;
|
||||||
const BG_COLOR: Color = Color::from_rgb(0.18, 0.20, 0.25);
|
const TEXT_COLOR: Color = rgb8(0xD4, 0xD4, 0xD4);
|
||||||
const GUTTER_BG: Color = Color::from_rgb(0.15, 0.17, 0.21);
|
const GUTTER_TEXT: Color = rgb8(0x85, 0x85, 0x85);
|
||||||
const TEXT_COLOR: Color = Color::from_rgb(0.85, 0.85, 0.85);
|
const CURSOR_COLOR: Color = rgb8(0xAE, 0xAF, 0xAD);
|
||||||
const GUTTER_TEXT: Color = Color::from_rgb(0.45, 0.48, 0.55);
|
const ACTIVE_LINE_NUM: Color = rgb8(0xC6, 0xC6, 0xC6);
|
||||||
const CURSOR_COLOR: Color = Color::from_rgb(0.9, 0.9, 0.2);
|
const SELECTION_BG: Color = rgba8(0x26, 0x4F, 0x78, 0.85);
|
||||||
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 SCROLLBAR_WIDTH: f32 = 10.0;
|
const SCROLLBAR_WIDTH: f32 = 10.0;
|
||||||
const SCROLLBAR_TRACK: Color = Color::from_rgba(0.25, 0.27, 0.32, 0.5);
|
const SCROLLBAR_TRACK: Color = Color::TRANSPARENT;
|
||||||
const SCROLLBAR_THUMB: Color = Color::from_rgba(0.50, 0.53, 0.60, 0.7);
|
const SCROLLBAR_THUMB: Color = rgba8(0x79, 0x79, 0x79, 0.45);
|
||||||
const SCROLLBAR_THUMB_HOVER: Color = Color::from_rgba(0.60, 0.63, 0.70, 0.9);
|
const SCROLLBAR_THUMB_HOVER: Color = rgba8(0x79, 0x79, 0x79, 0.75);
|
||||||
const MIN_THUMB_HEIGHT: f32 = 20.0;
|
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> {
|
fn committed_text_input(text: Option<&str>, is_command_shortcut: bool) -> Option<&str> {
|
||||||
if is_command_shortcut {
|
if is_command_shortcut {
|
||||||
return None;
|
return None;
|
||||||
@@ -303,7 +315,7 @@ where
|
|||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
tree: &widget::Tree,
|
||||||
renderer: &mut Renderer,
|
renderer: &mut Renderer,
|
||||||
_theme: &Theme,
|
theme: &Theme,
|
||||||
_style: &renderer::Style,
|
_style: &renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
@@ -317,24 +329,33 @@ where
|
|||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
renderer::Quad {
|
renderer::Quad {
|
||||||
bounds,
|
bounds,
|
||||||
border: iced::Border::default(),
|
border: iced::Border {
|
||||||
shadow: iced::Shadow::default(),
|
color: Color::from_rgb8(0x35, 0x35, 0x35),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 8.0.into(),
|
||||||
},
|
},
|
||||||
BG_COLOR,
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme.palette().background,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gutter background
|
// Subtle gutter divider; the shared background keeps the editor visually light.
|
||||||
let gutter_bounds = Rectangle {
|
|
||||||
width: GUTTER_WIDTH,
|
|
||||||
..bounds
|
|
||||||
};
|
|
||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
renderer::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(),
|
border: iced::Border::default(),
|
||||||
shadow: iced::Shadow::default(),
|
shadow: iced::Shadow::default(),
|
||||||
},
|
},
|
||||||
GUTTER_BG,
|
Color::from_rgb8(0x2B, 0x2B, 0x2B),
|
||||||
);
|
);
|
||||||
|
|
||||||
let metrics = mono_metrics();
|
let metrics = mono_metrics();
|
||||||
@@ -349,7 +370,7 @@ where
|
|||||||
let highlighted = self.highlighter.highlight_lines(&full_text, syntax);
|
let highlighted = self.highlighter.highlight_lines(&full_text, syntax);
|
||||||
|
|
||||||
let font = iced::Font::MONOSPACE;
|
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 {
|
let max_chars = if self.word_wrap && text_area_width > metrics.char_width {
|
||||||
(text_area_width / metrics.char_width).floor() as usize
|
(text_area_width / metrics.char_width).floor() as usize
|
||||||
} else {
|
} else {
|
||||||
@@ -382,7 +403,7 @@ where
|
|||||||
line_idx += 1;
|
line_idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_x = bounds.x + GUTTER_WIDTH + 8.0;
|
let text_x = bounds.x + GUTTER_WIDTH + TEXT_PADDING;
|
||||||
|
|
||||||
// Render visual rows
|
// Render visual rows
|
||||||
for (vis_idx, &(line_idx, char_start, char_end, is_first)) in visual_rows.iter().enumerate()
|
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 state = tree.state.downcast_mut::<EditorState>();
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
let metrics = mono_metrics();
|
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 {
|
let cpl = if self.word_wrap && text_area_width > metrics.char_width {
|
||||||
(text_area_width / metrics.char_width).floor() as usize
|
(text_area_width / metrics.char_width).floor() as usize
|
||||||
} else {
|
} else {
|
||||||
@@ -752,8 +773,8 @@ where
|
|||||||
if let Some(pos) = cursor.position_in(bounds) {
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
let mut buf = self.buffer.borrow_mut();
|
let mut buf = self.buffer.borrow_mut();
|
||||||
let vis_row = (pos.y / metrics.line_height) as usize;
|
let vis_row = (pos.y / metrics.line_height) as usize;
|
||||||
let raw_col =
|
let raw_col = ((pos.x - GUTTER_WIDTH - TEXT_PADDING).max(0.0)
|
||||||
((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width) as usize;
|
/ metrics.char_width) as usize;
|
||||||
let (line, char_off) =
|
let (line, char_off) =
|
||||||
visual_to_logical(&buf, buf.scroll_offset(), vis_row, cpl)
|
visual_to_logical(&buf, buf.scroll_offset(), vis_row, cpl)
|
||||||
.unwrap_or((buf.line_count().saturating_sub(1), 0));
|
.unwrap_or((buf.line_count().saturating_sub(1), 0));
|
||||||
@@ -792,8 +813,8 @@ where
|
|||||||
if let Some(pos) = cursor.position_in(bounds) {
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
let mut buf = self.buffer.borrow_mut();
|
let mut buf = self.buffer.borrow_mut();
|
||||||
let vis_row = (pos.y / metrics.line_height) as usize;
|
let vis_row = (pos.y / metrics.line_height) as usize;
|
||||||
let raw_col =
|
let raw_col = ((pos.x - GUTTER_WIDTH - TEXT_PADDING).max(0.0)
|
||||||
((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width) as usize;
|
/ metrics.char_width) as usize;
|
||||||
let (line, char_off) =
|
let (line, char_off) =
|
||||||
visual_to_logical(&buf, buf.scroll_offset(), vis_row, cpl)
|
visual_to_logical(&buf, buf.scroll_offset(), vis_row, cpl)
|
||||||
.unwrap_or((buf.line_count().saturating_sub(1), 0));
|
.unwrap_or((buf.line_count().saturating_sub(1), 0));
|
||||||
|
|||||||
@@ -1,15 +1,104 @@
|
|||||||
use iced::widget::text::Shaping;
|
use iced::widget::text::Shaping;
|
||||||
use iced::widget::{Space, button, checkbox, column, container, pick_list, row, text, text_input};
|
use iced::widget::{
|
||||||
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
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.
|
/// Standard form field label color.
|
||||||
pub const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75);
|
pub const LABEL_COLOR: Color = rgb8(0xB5, 0xBA, 0xC4);
|
||||||
const _FIELD_BG: Color = Color::from_rgb(0.15, 0.16, 0.20);
|
pub const SECTION_COLOR: Color = rgb8(0x9D, 0xA5, 0xB4);
|
||||||
pub const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58);
|
const FIELD_BG: Color = rgb8(0x24, 0x24, 0x26);
|
||||||
const DANGER_BG: Color = Color::from_rgb(0.60, 0.15, 0.15);
|
const SURFACE_BG: Color = rgb8(0x25, 0x25, 0x26);
|
||||||
const DANGER_HOVER: Color = Color::from_rgb(0.70, 0.20, 0.20);
|
const BORDER_COLOR: Color = rgb8(0x3C, 0x3C, 0x3C);
|
||||||
const PRIMARY_BG: Color = Color::from_rgb(0.20, 0.40, 0.70);
|
const FOCUS_COLOR: Color = rgb8(0x00, 0x7F, 0xD4);
|
||||||
const PRIMARY_HOVER: Color = Color::from_rgb(0.25, 0.48, 0.80);
|
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.
|
/// A labeled text input field.
|
||||||
pub fn labeled_input<'a, Message: Clone + 'a>(
|
pub fn labeled_input<'a, Message: Clone + 'a>(
|
||||||
@@ -23,9 +112,15 @@ pub fn labeled_input<'a, Message: Clone + 'a>(
|
|||||||
.size(12)
|
.size(12)
|
||||||
.color(LABEL_COLOR)
|
.color(LABEL_COLOR)
|
||||||
.shaping(Shaping::Advanced),
|
.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()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,9 +141,13 @@ where
|
|||||||
.size(12)
|
.size(12)
|
||||||
.color(LABEL_COLOR)
|
.color(LABEL_COLOR)
|
||||||
.shaping(Shaping::Advanced),
|
.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()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,20 +166,10 @@ pub fn labeled_checkbox<'a, Message: Clone + 'a>(
|
|||||||
|
|
||||||
/// A section header with optional separator line.
|
/// A section header with optional separator line.
|
||||||
pub fn section_header<'a, Message: 'a>(label: &str) -> Element<'a, Message> {
|
pub fn section_header<'a, Message: 'a>(label: &str) -> Element<'a, Message> {
|
||||||
column![
|
|
||||||
text(label.to_string())
|
text(label.to_string())
|
||||||
.size(11)
|
.size(12)
|
||||||
.color(SECTION_COLOR)
|
.color(SECTION_COLOR)
|
||||||
.shaping(Shaping::Advanced),
|
.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()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +181,7 @@ pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
background: Some(Background::Color(PRIMARY_HOVER)),
|
background: Some(Background::Color(PRIMARY_HOVER)),
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
border: iced::Border {
|
border: iced::Border {
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
..iced::Border::default()
|
..iced::Border::default()
|
||||||
},
|
},
|
||||||
..button::Style::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)),
|
background: Some(Background::Color(PRIMARY_BG)),
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
border: iced::Border {
|
border: iced::Border {
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
..iced::Border::default()
|
..iced::Border::default()
|
||||||
},
|
},
|
||||||
..button::Style::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)),
|
background: Some(Background::Color(DANGER_HOVER)),
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
border: iced::Border {
|
border: iced::Border {
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
..iced::Border::default()
|
..iced::Border::default()
|
||||||
},
|
},
|
||||||
..button::Style::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)),
|
background: Some(Background::Color(DANGER_BG)),
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
border: iced::Border {
|
border: iced::Border {
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
..iced::Border::default()
|
..iced::Border::default()
|
||||||
},
|
},
|
||||||
..button::Style::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.
|
/// Toolbar-style row with right-aligned actions.
|
||||||
pub fn toolbar<'a, Message: 'a>(
|
pub fn toolbar<'a, Message: 'a>(
|
||||||
left: Vec<Element<'a, Message>>,
|
left: Vec<Element<'a, Message>>,
|
||||||
@@ -151,11 +300,28 @@ pub fn toolbar<'a, Message: 'a>(
|
|||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.wrap();
|
.wrap();
|
||||||
|
|
||||||
|
container(
|
||||||
row![left_row, right_row]
|
row![left_row, right_row]
|
||||||
.padding(8)
|
.spacing(10)
|
||||||
.spacing(8)
|
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
|
.padding(10)
|
||||||
.width(Length::Fill)
|
.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()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,3 +350,31 @@ fn format_timestamp(ms: i64) -> String {
|
|||||||
let min = ((secs % 3600) / 60) as u32;
|
let min = ((secs % 3600) / 60) as u32;
|
||||||
format!("{y}-{m:02}-{d:02} {h:02}:{min:02}")
|
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")]
|
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
|
||||||
|
|
||||||
use bds_ui::BdsApp;
|
use bds_ui::BdsApp;
|
||||||
|
use bds_ui::components::inputs;
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
let icon =
|
let icon =
|
||||||
@@ -9,7 +10,7 @@ fn main() -> iced::Result {
|
|||||||
|
|
||||||
iced::application("bDS", BdsApp::update, BdsApp::view)
|
iced::application("bDS", BdsApp::update, BdsApp::view)
|
||||||
.subscription(BdsApp::subscription)
|
.subscription(BdsApp::subscription)
|
||||||
.theme(|_| iced::Theme::Dark)
|
.theme(|_| inputs::app_theme())
|
||||||
.window(iced::window::Settings {
|
.window(iced::window::Settings {
|
||||||
size: iced::Size::new(1200.0, 800.0),
|
size: iced::Size::new(1200.0, 800.0),
|
||||||
icon: Some(icon),
|
icon: Some(icon),
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ fn active_button_style(_theme: &Theme, _status: button::Status) -> button::Style
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 0.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ fn inactive_button_style(_theme: &Theme, status: button::Status) -> button::Styl
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 0.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ pub fn view<'a>(
|
|||||||
.on_press_maybe(
|
.on_press_maybe(
|
||||||
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::AnalyzeWithAi)),
|
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::AnalyzeWithAi)),
|
||||||
)
|
)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
@@ -247,12 +248,14 @@ pub fn view<'a>(
|
|||||||
.on_press_maybe(
|
.on_press_maybe(
|
||||||
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::DetectLanguage)),
|
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::DetectLanguage)),
|
||||||
)
|
)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "editor.translate")).size(13))
|
button(text(t(locale, "editor.translate")).size(13))
|
||||||
.on_press_maybe(
|
.on_press_maybe(
|
||||||
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::TranslateMetadata)),
|
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::TranslateMetadata)),
|
||||||
)
|
)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "common.save")).size(13))
|
button(text(t(locale, "common.save")).size(13))
|
||||||
@@ -287,7 +290,11 @@ pub fn view<'a>(
|
|||||||
flag.language.clone(),
|
flag.language.clone(),
|
||||||
)))
|
)))
|
||||||
.padding([4, 8])
|
.padding([4, 8])
|
||||||
.style(|_: &iced::Theme, _| button::Style::default())
|
.style(if flag.is_active {
|
||||||
|
inputs::primary_button
|
||||||
|
} else {
|
||||||
|
inputs::secondary_button
|
||||||
|
})
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -331,6 +338,7 @@ pub fn view<'a>(
|
|||||||
.color(Color::from_rgb(0.55, 0.58, 0.65)),
|
.color(Color::from_rgb(0.55, 0.58, 0.65)),
|
||||||
]
|
]
|
||||||
.padding(8);
|
.padding(8);
|
||||||
|
let preview_card = inputs::card(column![preview, info].spacing(4)).padding(8);
|
||||||
|
|
||||||
// Metadata fields
|
// Metadata fields
|
||||||
let title_input = inputs::labeled_input(
|
let title_input = inputs::labeled_input(
|
||||||
@@ -385,6 +393,7 @@ pub fn view<'a>(
|
|||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
button(text(t(locale, "editor.linkToPost")).size(12))
|
button(text(t(locale, "editor.linkToPost")).size(12))
|
||||||
.on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker))
|
.on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([4, 10]),
|
.padding([4, 10]),
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
@@ -478,19 +487,29 @@ pub fn view<'a>(
|
|||||||
]
|
]
|
||||||
.padding(8);
|
.padding(8);
|
||||||
|
|
||||||
let body = scrollable(
|
let metadata = inputs::card(
|
||||||
column![
|
column![
|
||||||
header,
|
|
||||||
flags_bar,
|
|
||||||
preview,
|
|
||||||
info,
|
|
||||||
inputs::section_header(&t(locale, "editor.metadata")),
|
inputs::section_header(&t(locale, "editor.metadata")),
|
||||||
meta_row1,
|
meta_row1,
|
||||||
meta_row2,
|
meta_row2,
|
||||||
meta_row3,
|
meta_row3,
|
||||||
linked_posts_header,
|
]
|
||||||
post_picker,
|
.spacing(12)
|
||||||
linked_posts_list,
|
.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,
|
footer,
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ use iced::widget::text::Shaping;
|
|||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
Space, button, checkbox, column, container, image, row, scrollable, text, text_input,
|
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::i18n::UiLocale;
|
||||||
use bds_core::model::Media;
|
use bds_core::model::Media;
|
||||||
use bds_core::util::paths::thumbnail_path;
|
use bds_core::util::paths::thumbnail_path;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::components::inputs;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use crate::views::post_editor::PostEditorMsg;
|
use crate::views::post_editor::PostEditorMsg;
|
||||||
|
|
||||||
@@ -138,61 +139,27 @@ fn modal_box_style(_theme: &Theme) -> container::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||||
width: 1.0,
|
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()
|
..container::Style::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
fn cancel_button_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let bg = match status {
|
inputs::secondary_button(theme, 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 danger_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
fn danger_button_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let bg = match status {
|
inputs::danger_button(theme, 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 confirm_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
fn confirm_button_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let bg = match status {
|
inputs::primary_button(theme, 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 resolve_thumbnail_file(data_dir: Option<&Path>, media: &Media) -> Option<String> {
|
fn resolve_thumbnail_file(data_dir: Option<&Path>, media: &Media) -> Option<String> {
|
||||||
@@ -471,7 +438,8 @@ pub fn view(
|
|||||||
.size(13)
|
.size(13)
|
||||||
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkSearch(s)))
|
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkSearch(s)))
|
||||||
.padding([6, 10])
|
.padding([6, 10])
|
||||||
.width(Length::Fill);
|
.width(Length::Fill)
|
||||||
|
.style(inputs::field_style);
|
||||||
|
|
||||||
let internal_content: Element<'static, Message> = if results.is_empty() {
|
let internal_content: Element<'static, Message> = if results.is_empty() {
|
||||||
column![
|
column![
|
||||||
@@ -540,7 +508,7 @@ pub fn view(
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}),
|
}),
|
||||||
@@ -557,7 +525,8 @@ pub fn view(
|
|||||||
.size(13)
|
.size(13)
|
||||||
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkUrlChanged(s)))
|
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkUrlChanged(s)))
|
||||||
.padding([6, 10])
|
.padding([6, 10])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill)
|
||||||
|
.style(inputs::field_style),
|
||||||
text_input(
|
text_input(
|
||||||
t(locale, "modal.postInsertLink.externalTextPlaceholder").as_str(),
|
t(locale, "modal.postInsertLink.externalTextPlaceholder").as_str(),
|
||||||
&external_text,
|
&external_text,
|
||||||
@@ -565,7 +534,8 @@ pub fn view(
|
|||||||
.size(13)
|
.size(13)
|
||||||
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkTextChanged(s)))
|
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertLinkTextChanged(s)))
|
||||||
.padding([6, 10])
|
.padding([6, 10])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill)
|
||||||
|
.style(inputs::field_style),
|
||||||
Space::with_height(12.0),
|
Space::with_height(12.0),
|
||||||
row![
|
row![
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
@@ -594,7 +564,7 @@ pub fn view(
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
})
|
})
|
||||||
@@ -659,7 +629,8 @@ pub fn view(
|
|||||||
.size(13)
|
.size(13)
|
||||||
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertMediaSearch(s)))
|
.on_input(|s| Message::PostEditor(PostEditorMsg::PostInsertMediaSearch(s)))
|
||||||
.padding([6, 10])
|
.padding([6, 10])
|
||||||
.width(Length::Fill);
|
.width(Length::Fill)
|
||||||
|
.style(inputs::field_style);
|
||||||
|
|
||||||
let media_items: Vec<Element<'static, Message>> = media_list
|
let media_items: Vec<Element<'static, Message>> = media_list
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.55, 0.90),
|
color: Color::from_rgb(0.30, 0.55, 0.90),
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press_maybe(ai_enabled.then_some(Message::PostEditor(PostEditorMsg::ToggleQuickActions)))
|
.on_press_maybe(ai_enabled.then_some(Message::PostEditor(PostEditorMsg::ToggleQuickActions)))
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.style(status_bar::dropdown_trigger)
|
.style(inputs::secondary_button)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let mut header_action_items: Vec<Element<'a, Message>> = vec![
|
let mut header_action_items: Vec<Element<'a, Message>> = vec![
|
||||||
@@ -421,6 +421,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::Duplicate))
|
.on_press(Message::PostEditor(PostEditorMsg::Duplicate))
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -446,6 +447,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::Discard))
|
.on_press(Message::PostEditor(PostEditorMsg::Discard))
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -520,7 +522,7 @@ pub fn view<'a>(
|
|||||||
Space::new(0, 0).into()
|
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 ──
|
// ── Collapsible Metadata Section ──
|
||||||
let meta_toggle_label = if state.metadata_expanded {
|
let meta_toggle_label = if state.metadata_expanded {
|
||||||
@@ -535,15 +537,13 @@ pub fn view<'a>(
|
|||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata))
|
.on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata))
|
||||||
.padding([4, 0])
|
.padding([8, 10])
|
||||||
.style(|_, _| button::Style {
|
.width(Length::Fill)
|
||||||
background: None,
|
.style(inputs::disclosure_button);
|
||||||
..button::Style::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Translation Flags Bar (inline with metadata toggle) ──
|
// ── Translation Flags Bar (inline with metadata toggle) ──
|
||||||
let flags = state.translation_flags();
|
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()
|
meta_toggle.into()
|
||||||
} else {
|
} else {
|
||||||
let mut flag_row = row![].spacing(2);
|
let mut flag_row = row![].spacing(2);
|
||||||
@@ -560,11 +560,13 @@ pub fn view<'a>(
|
|||||||
});
|
});
|
||||||
flag_row = flag_row.push(btn);
|
flag_row = flag_row.push(btn);
|
||||||
}
|
}
|
||||||
row![meta_toggle, Space::with_width(Length::Fill), flag_row]
|
row![meta_toggle, flag_row]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.into()
|
.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 metadata_section: Element<'a, Message> = if state.metadata_expanded && !on_translation {
|
||||||
let title_input = inputs::labeled_input(
|
let title_input = inputs::labeled_input(
|
||||||
@@ -598,13 +600,6 @@ pub fn view<'a>(
|
|||||||
Some(&state.language),
|
Some(&state.language),
|
||||||
|lang| Message::PostEditor(PostEditorMsg::LanguageChanged(lang)),
|
|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(
|
let template_input = inputs::labeled_input(
|
||||||
&t(locale, "editor.templateSlug"),
|
&t(locale, "editor.templateSlug"),
|
||||||
"",
|
"",
|
||||||
@@ -616,9 +611,9 @@ pub fn view<'a>(
|
|||||||
state.do_not_translate,
|
state.do_not_translate,
|
||||||
|b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)),
|
|b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)),
|
||||||
);
|
);
|
||||||
let language_block = column![language_input, detect_language].spacing(6);
|
let meta_row2 = row![author_input, language_input, template_input, dnt]
|
||||||
let meta_row2 = row![author_input, language_block, template_input, dnt]
|
|
||||||
.spacing(16)
|
.spacing(16)
|
||||||
|
.align_y(iced::Alignment::End)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// Tags chip input
|
// Tags chip input
|
||||||
@@ -695,7 +690,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::LinkExistingMedia))
|
.on_press(Message::PostEditor(PostEditorMsg::LinkExistingMedia))
|
||||||
.padding([4, 10])
|
.padding([4, 10])
|
||||||
.style(|_: &Theme, _| button::Style::default())
|
.style(inputs::secondary_button)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let linked_media_header: Element<'a, Message> = row![
|
let linked_media_header: Element<'a, Message> = row![
|
||||||
@@ -783,6 +778,7 @@ pub fn view<'a>(
|
|||||||
Column::with_children(items).spacing(6).into()
|
Column::with_children(items).spacing(6).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inputs::card(
|
||||||
column![
|
column![
|
||||||
meta_row1,
|
meta_row1,
|
||||||
meta_row2,
|
meta_row2,
|
||||||
@@ -792,8 +788,9 @@ pub fn view<'a>(
|
|||||||
backlinks_section,
|
backlinks_section,
|
||||||
linked_media_section,
|
linked_media_section,
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(12)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
Space::new(0, 0).into()
|
Space::new(0, 0).into()
|
||||||
@@ -812,19 +809,19 @@ pub fn view<'a>(
|
|||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt))
|
.on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt))
|
||||||
.padding([4, 0])
|
.padding([8, 10])
|
||||||
.style(|_, _| button::Style {
|
.width(Length::Fill)
|
||||||
background: None,
|
.style(inputs::disclosure_button);
|
||||||
..button::Style::default()
|
let excerpt_toggle: Element<'a, Message> = inputs::card(excerpt_toggle).padding([2, 4]).into();
|
||||||
});
|
|
||||||
|
|
||||||
let excerpt_section: Element<'a, Message> = if state.excerpt_expanded {
|
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.excerpt"),
|
||||||
&t(locale, "editor.excerptPlaceholder"),
|
&t(locale, "editor.excerptPlaceholder"),
|
||||||
&state.excerpt,
|
&state.excerpt,
|
||||||
|s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)),
|
|s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)),
|
||||||
)
|
))
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
Space::new(0, 0).into()
|
Space::new(0, 0).into()
|
||||||
};
|
};
|
||||||
@@ -866,6 +863,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::InsertLink))
|
.on_press(Message::PostEditor(PostEditorMsg::InsertLink))
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
Space::new(0, 0).into()
|
Space::new(0, 0).into()
|
||||||
@@ -878,6 +876,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::InsertMedia))
|
.on_press(Message::PostEditor(PostEditorMsg::InsertMedia))
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
Space::new(0, 0).into()
|
Space::new(0, 0).into()
|
||||||
@@ -889,6 +888,7 @@ pub fn view<'a>(
|
|||||||
)
|
)
|
||||||
.on_press(Message::PostEditor(PostEditorMsg::Gallery))
|
.on_press(Message::PostEditor(PostEditorMsg::Gallery))
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.into(),
|
.into(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -946,14 +946,14 @@ pub fn view<'a>(
|
|||||||
excerpt_toggle,
|
excerpt_toggle,
|
||||||
excerpt_section,
|
excerpt_section,
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(8)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
.height(Length::Shrink);
|
.height(Length::Shrink);
|
||||||
|
|
||||||
// ── Full layout: top pane (shrink), editor (fill), footer (shrink) ──
|
// ── Full layout: top pane (shrink), editor (fill), footer (shrink) ──
|
||||||
column![top_pane, body_toolbar, editor_widget, footer,]
|
column![top_pane, body_toolbar, editor_widget, footer,]
|
||||||
.spacing(4)
|
.spacing(8)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -998,7 +998,8 @@ fn chip_input_field<'a>(
|
|||||||
.on_input(on_input)
|
.on_input(on_input)
|
||||||
.on_submit(on_submit)
|
.on_submit(on_submit)
|
||||||
.size(13)
|
.size(13)
|
||||||
.padding([4, 6]),
|
.padding([8, 10])
|
||||||
|
.style(inputs::field_style),
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fn dropdown_bg(_theme: &Theme) -> container::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 4.0.into(),
|
radius: 10.0.into(),
|
||||||
},
|
},
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ fn project_item(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 2.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ fn project_item_active(_theme: &Theme, status: button::Status) -> button::Style
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 2.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ fn new_project_btn(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 2.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ pub fn trigger_style(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,10 +132,12 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
|
|||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "editor.run")).size(13))
|
button(text(t(locale, "editor.run")).size(13))
|
||||||
.on_press(Message::ScriptEditor(ScriptEditorMsg::Run))
|
.on_press(Message::ScriptEditor(ScriptEditorMsg::Run))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "editor.checkSyntax")).size(13))
|
button(text(t(locale, "editor.checkSyntax")).size(13))
|
||||||
.on_press(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax))
|
.on_press(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
|
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]
|
let meta_row2 = row![kind_select, entrypoint_input, enabled_check]
|
||||||
.spacing(16)
|
.spacing(16)
|
||||||
|
.align_y(iced::Alignment::End)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// Content editor (CodeEditor with syntax highlighting based on file extension)
|
// 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 {
|
} else {
|
||||||
"lua"
|
"lua"
|
||||||
};
|
};
|
||||||
let content_section: Element<'a, Message> = column![
|
let content_section: Element<'a, Message> = inputs::card(
|
||||||
|
column![
|
||||||
inputs::section_header(&t(locale, "editor.content")),
|
inputs::section_header(&t(locale, "editor.content")),
|
||||||
Element::from(
|
Element::from(
|
||||||
CodeEditor::new(&state.editor_buffer, highlighter(), syntax_ext,).on_change(|msg| {
|
CodeEditor::new(&state.editor_buffer, highlighter(), syntax_ext,).on_change(
|
||||||
|
|msg| {
|
||||||
match msg {
|
match msg {
|
||||||
EditorMessage::ContentChanged(s) => {
|
EditorMessage::ContentChanged(s) => {
|
||||||
Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s))
|
Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s))
|
||||||
}
|
}
|
||||||
EditorMessage::SaveRequested => Message::ScriptEditor(ScriptEditorMsg::Save),
|
EditorMessage::SaveRequested => {
|
||||||
|
Message::ScriptEditor(ScriptEditorMsg::Save)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(10)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill),
|
||||||
|
)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
@@ -248,18 +258,18 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
|
|||||||
.padding(8);
|
.padding(8);
|
||||||
|
|
||||||
// Top pane: header + metadata (scrollable for overflow)
|
// Top pane: header + metadata (scrollable for overflow)
|
||||||
let top_pane = scrollable(
|
let metadata = inputs::card(
|
||||||
column![header, meta_row1, meta_row2,]
|
column![meta_row1, meta_row2]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.padding(16)
|
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
);
|
||||||
|
let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill))
|
||||||
.height(Length::Shrink);
|
.height(Length::Shrink);
|
||||||
|
|
||||||
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
|
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
|
||||||
column![top_pane, content_section, validation, footer,]
|
column![top_pane, content_section, validation, footer,]
|
||||||
.spacing(4)
|
.spacing(8)
|
||||||
.padding([0, 16])
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use iced::widget::text::Shaping;
|
use iced::widget::text::Shaping;
|
||||||
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
|
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;
|
use bds_core::i18n::UiLocale;
|
||||||
|
|
||||||
@@ -347,7 +347,9 @@ pub enum SettingsMsg {
|
|||||||
pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||||
let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query)
|
let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query)
|
||||||
.on_input(|s| Message::Settings(SettingsMsg::SearchChanged(s)))
|
.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();
|
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)),
|
.color(Color::from_rgb(0.7, 0.72, 0.78)),
|
||||||
button(text(t(locale, "common.clear")).size(13))
|
button(text(t(locale, "common.clear")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::SearchChanged(String::new())))
|
.on_press(Message::Settings(SettingsMsg::SearchChanged(String::new())))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 12]),
|
.padding([6, 12]),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
@@ -413,10 +416,10 @@ fn render_section<'a>(
|
|||||||
)))
|
)))
|
||||||
.padding([6, 8])
|
.padding([6, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(|_: &Theme, _| button::Style::default());
|
.style(inputs::disclosure_button);
|
||||||
|
|
||||||
if collapsed {
|
if collapsed {
|
||||||
return column![header].into();
|
return inputs::card(header).padding([2, 4]).into();
|
||||||
}
|
}
|
||||||
|
|
||||||
let content: Element<'a, Message> = match section {
|
let content: Element<'a, Message> = match section {
|
||||||
@@ -430,7 +433,7 @@ fn render_section<'a>(
|
|||||||
SettingsSection::MCP => section_mcp(locale),
|
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> {
|
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)
|
text_editor(&state.project_description)
|
||||||
.on_action(|a| Message::Settings(SettingsMsg::ProjectDescriptionAction(a)))
|
.on_action(|a| Message::Settings(SettingsMsg::ProjectDescriptionAction(a)))
|
||||||
.height(Length::Fixed(80.0))
|
.height(Length::Fixed(80.0))
|
||||||
.size(14),
|
.size(14)
|
||||||
|
.style(inputs::text_editor_style),
|
||||||
]
|
]
|
||||||
.spacing(4);
|
.spacing(4);
|
||||||
let data_path = row![
|
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))
|
button(text(t(locale, "settings.browse")).size(12))
|
||||||
.on_press(Message::Settings(SettingsMsg::BrowseDataPath))
|
.on_press(Message::Settings(SettingsMsg::BrowseDataPath))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 12]),
|
.padding([6, 12]),
|
||||||
button(text(t(locale, "settings.reset")).size(12))
|
button(text(t(locale, "settings.reset")).size(12))
|
||||||
.on_press(Message::Settings(SettingsMsg::ResetDataPath))
|
.on_press(Message::Settings(SettingsMsg::ResetDataPath))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 12]),
|
.padding([6, 12]),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
@@ -694,7 +700,7 @@ fn section_content<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
|||||||
.spacing(8);
|
.spacing(8);
|
||||||
|
|
||||||
column.push(
|
column.push(
|
||||||
container(
|
inputs::card(
|
||||||
column![
|
column![
|
||||||
text(category.name.clone()).size(15).color(Color::WHITE),
|
text(category.name.clone()).size(15).color(Color::WHITE),
|
||||||
title,
|
title,
|
||||||
@@ -721,6 +727,7 @@ fn section_content<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
|||||||
.padding([6, 12]),
|
.padding([6, 12]),
|
||||||
button(text(t(locale, "settings.resetCategories")).size(13))
|
button(text(t(locale, "settings.resetCategories")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
|
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 12]),
|
.padding([6, 12]),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.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))
|
let online_refresh = button(text(t(locale, "settings.refreshModels")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RefreshOnlineModels))
|
.on_press(Message::Settings(SettingsMsg::RefreshOnlineModels))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16]);
|
.padding([6, 16]);
|
||||||
|
|
||||||
let airplane_url = inputs::labeled_input(
|
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))
|
let airplane_refresh = button(text(t(locale, "settings.refreshModels")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RefreshAirplaneModels))
|
.on_press(Message::Settings(SettingsMsg::RefreshAirplaneModels))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16]);
|
.padding([6, 16]);
|
||||||
|
|
||||||
let default_model = inputs::labeled_select(
|
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)
|
text_editor(&state.system_prompt)
|
||||||
.on_action(|a| Message::Settings(SettingsMsg::SystemPromptAction(a)))
|
.on_action(|a| Message::Settings(SettingsMsg::SystemPromptAction(a)))
|
||||||
.height(Length::Fixed(200.0))
|
.height(Length::Fixed(200.0))
|
||||||
.size(14),
|
.size(14)
|
||||||
|
.style(inputs::text_editor_style),
|
||||||
]
|
]
|
||||||
.spacing(4);
|
.spacing(4);
|
||||||
let btns = row![
|
let btns = row![
|
||||||
@@ -848,6 +858,7 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
|
|||||||
.padding([6, 16]),
|
.padding([6, 16]),
|
||||||
button(text(t(locale, "settings.resetToDefault")).size(13))
|
button(text(t(locale, "settings.resetToDefault")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::ResetSystemPrompt))
|
.on_press(Message::Settings(SettingsMsg::ResetSystemPrompt))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16]),
|
.padding([6, 16]),
|
||||||
]
|
]
|
||||||
.spacing(8);
|
.spacing(8);
|
||||||
@@ -930,30 +941,37 @@ fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
|
|||||||
let rebuild_btns = column![
|
let rebuild_btns = column![
|
||||||
button(text(t(locale, "settings.rebuildPosts")).size(13))
|
button(text(t(locale, "settings.rebuildPosts")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RebuildPosts))
|
.on_press(Message::Settings(SettingsMsg::RebuildPosts))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
button(text(t(locale, "settings.rebuildMedia")).size(13))
|
button(text(t(locale, "settings.rebuildMedia")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RebuildMedia))
|
.on_press(Message::Settings(SettingsMsg::RebuildMedia))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
button(text(t(locale, "settings.rebuildScripts")).size(13))
|
button(text(t(locale, "settings.rebuildScripts")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RebuildScripts))
|
.on_press(Message::Settings(SettingsMsg::RebuildScripts))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
button(text(t(locale, "settings.rebuildTemplates")).size(13))
|
button(text(t(locale, "settings.rebuildTemplates")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RebuildTemplates))
|
.on_press(Message::Settings(SettingsMsg::RebuildTemplates))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
button(text(t(locale, "settings.rebuildLinks")).size(13))
|
button(text(t(locale, "settings.rebuildLinks")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RebuildLinks))
|
.on_press(Message::Settings(SettingsMsg::RebuildLinks))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
button(text(t(locale, "settings.rebuildSearchIndex")).size(13))
|
button(text(t(locale, "settings.rebuildSearchIndex")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RebuildSearchIndex))
|
.on_press(Message::Settings(SettingsMsg::RebuildSearchIndex))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
button(text(t(locale, "settings.regenerateThumbnails")).size(13))
|
button(text(t(locale, "settings.regenerateThumbnails")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::RegenerateThumbnails))
|
.on_press(Message::Settings(SettingsMsg::RegenerateThumbnails))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.width(Length::Fill),
|
.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))
|
let open = button(text(t(locale, "settings.openDataFolder")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::OpenDataFolder))
|
.on_press(Message::Settings(SettingsMsg::OpenDataFolder))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16]);
|
.padding([6, 16]);
|
||||||
|
|
||||||
column![rebuild_btns, open]
|
column![rebuild_btns, open]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use bds_core::i18n::UiLocale;
|
|||||||
use bds_core::model::{Media, Post, Script, Template};
|
use bds_core::model::{Media, Post, Script, Template};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::components::inputs;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use crate::state::navigation::SidebarView;
|
use crate::state::navigation::SidebarView;
|
||||||
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
|
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
|
||||||
@@ -30,7 +31,10 @@ fn item_style(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
button::Style {
|
button::Style {
|
||||||
background: Some(Background::Color(bg)),
|
background: Some(Background::Color(bg)),
|
||||||
text_color: Color::from_rgb(0.80, 0.80, 0.85),
|
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()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +48,10 @@ fn item_active_style(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
button::Style {
|
button::Style {
|
||||||
background: Some(Background::Color(bg)),
|
background: Some(Background::Color(bg)),
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
border: Border::default(),
|
border: Border {
|
||||||
|
radius: 6.0.into(),
|
||||||
|
..Border::default()
|
||||||
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +61,7 @@ fn thumbnail_container_style(_theme: &Theme) -> container::Style {
|
|||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.17))),
|
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.17))),
|
||||||
border: Border {
|
border: Border {
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
..Border::default()
|
..Border::default()
|
||||||
},
|
},
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
@@ -145,19 +152,8 @@ fn format_post_date(unix_ms: i64) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Search input style.
|
/// Search input style.
|
||||||
fn search_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
|
fn search_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||||
text_input::Style {
|
inputs::field_style(theme, status)
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Style for filter toggle / clear buttons in the header.
|
/// 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 {
|
button::Style {
|
||||||
background: Some(Background::Color(bg)),
|
background: Some(Background::Color(bg)),
|
||||||
text_color: Color::from_rgb(0.60, 0.60, 0.65),
|
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()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +182,10 @@ fn filter_button_active_style(_theme: &Theme, status: button::Status) -> button:
|
|||||||
button::Style {
|
button::Style {
|
||||||
background: Some(Background::Color(bg)),
|
background: Some(Background::Color(bg)),
|
||||||
text_color: Color::from_rgb(0.55, 0.70, 0.95),
|
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()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -773,7 +775,7 @@ pub fn view(
|
|||||||
is_transient: true,
|
is_transient: true,
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
}))
|
}))
|
||||||
.padding([3, 6])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(style_fn)
|
.style(style_fn)
|
||||||
.into()
|
.into()
|
||||||
@@ -1025,7 +1027,7 @@ pub fn view(
|
|||||||
is_transient: true,
|
is_transient: true,
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
}))
|
}))
|
||||||
.padding([3, 6])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(style_fn)
|
.style(style_fn)
|
||||||
.into()
|
.into()
|
||||||
@@ -1065,7 +1067,7 @@ pub fn view(
|
|||||||
is_transient: true,
|
is_transient: true,
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
}))
|
}))
|
||||||
.padding([3, 6])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(style_fn)
|
.style(style_fn)
|
||||||
.into()
|
.into()
|
||||||
@@ -1106,7 +1108,7 @@ pub fn view(
|
|||||||
};
|
};
|
||||||
button(container(label_text).width(Length::Fill))
|
button(container(label_text).width(Length::Fill))
|
||||||
.on_press(msg)
|
.on_press(msg)
|
||||||
.padding([3, 6])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(item_style)
|
.style(item_style)
|
||||||
.into()
|
.into()
|
||||||
@@ -1130,7 +1132,7 @@ pub fn view(
|
|||||||
is_transient: false,
|
is_transient: false,
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
}))
|
}))
|
||||||
.padding([3, 6])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(item_style)
|
.style(item_style)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use iced::{Background, Color, Element, Length, Theme};
|
|||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
use crate::components::inputs;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -25,6 +26,8 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
|
|||||||
.size(13)
|
.size(13)
|
||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
|
.style(inputs::primary_button)
|
||||||
|
.padding([6, 16])
|
||||||
} else {
|
} else {
|
||||||
button(
|
button(
|
||||||
text(t(locale, "siteValidation.run"))
|
text(t(locale, "siteValidation.run"))
|
||||||
@@ -32,6 +35,8 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
|
|||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
.on_press(Message::RunSiteValidation)
|
.on_press(Message::RunSiteValidation)
|
||||||
|
.style(inputs::primary_button)
|
||||||
|
.padding([6, 16])
|
||||||
};
|
};
|
||||||
let has_issues = !state.missing_files.is_empty()
|
let has_issues = !state.missing_files.is_empty()
|
||||||
|| !state.extra_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)
|
.size(13)
|
||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.padding([6, 16])
|
||||||
} else if !state.is_running && state.error_message.is_none() && has_issues {
|
} else if !state.is_running && state.error_message.is_none() && has_issues {
|
||||||
button(
|
button(
|
||||||
text(t(locale, "siteValidation.apply"))
|
text(t(locale, "siteValidation.apply"))
|
||||||
@@ -49,12 +56,16 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
|
|||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
.on_press(Message::ApplySiteValidation)
|
.on_press(Message::ApplySiteValidation)
|
||||||
|
.style(inputs::primary_button)
|
||||||
|
.padding([6, 16])
|
||||||
} else {
|
} else {
|
||||||
button(
|
button(
|
||||||
text(t(locale, "siteValidation.apply"))
|
text(t(locale, "siteValidation.apply"))
|
||||||
.size(13)
|
.size(13)
|
||||||
.shaping(Shaping::Advanced),
|
.shaping(Shaping::Advanced),
|
||||||
)
|
)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.padding([6, 16])
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut content = column![
|
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> {
|
fn help_text<'a>(value: String) -> Element<'a, Message> {
|
||||||
container(
|
inputs::card(
|
||||||
text(value)
|
text(value)
|
||||||
.size(14)
|
.size(14)
|
||||||
.shaping(Shaping::Advanced)
|
.shaping(Shaping::Advanced)
|
||||||
.color(Color::from_rgb(0.66, 0.66, 0.72)),
|
.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()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +162,7 @@ fn section<'a>(title: String, entries: &'a [String], accent: Color) -> Element<'
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
container(
|
inputs::card(
|
||||||
column![
|
column![
|
||||||
text(title.to_string())
|
text(title.to_string())
|
||||||
.size(16)
|
.size(16)
|
||||||
@@ -166,10 +172,5 @@ fn section<'a>(title: String, entries: &'a [String], accent: Color) -> Element<'
|
|||||||
]
|
]
|
||||||
.spacing(10),
|
.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()
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ fn airplane_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ pub fn dropdown_trigger(_theme: &Theme, status: button::Status) -> button::Style
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 3.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ pub fn dropdown_item(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 2.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ pub fn dropdown_bg(_theme: &Theme) -> container::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.30, 0.55, 0.90),
|
color: Color::from_rgb(0.30, 0.55, 0.90),
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::TRANSPARENT,
|
color: Color::TRANSPARENT,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..button::Style::default()
|
..button::Style::default()
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ fn tooltip_style(_theme: &Theme) -> container::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: Color::from_rgb(0.35, 0.35, 0.40),
|
color: Color::from_rgb(0.35, 0.35, 0.40),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 4.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ pub enum TagsMsg {
|
|||||||
|
|
||||||
/// Render the tags management view.
|
/// Render the tags management view.
|
||||||
pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||||
let section_nav = row![
|
let section_nav = inputs::card(
|
||||||
|
row![
|
||||||
section_tab(
|
section_tab(
|
||||||
&t(locale, "tags.nav.cloud"),
|
&t(locale, "tags.nav.cloud"),
|
||||||
state.section == TagsSection::Cloud,
|
state.section == TagsSection::Cloud,
|
||||||
@@ -139,8 +140,9 @@ pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Messa
|
|||||||
TagsSection::Discover
|
TagsSection::Discover
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(6),
|
||||||
.padding(8);
|
)
|
||||||
|
.padding(6);
|
||||||
|
|
||||||
let content: Element<'a, Message> = match state.section {
|
let content: Element<'a, Message> = match state.section {
|
||||||
TagsSection::Cloud => view_cloud(state, locale),
|
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]
|
column![section_nav, content]
|
||||||
|
.spacing(4)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
|
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
|
||||||
let color = if active {
|
button(text(label.to_string()).size(13))
|
||||||
Color::WHITE
|
|
||||||
} else {
|
|
||||||
Color::from_rgb(0.55, 0.58, 0.65)
|
|
||||||
};
|
|
||||||
button(text(label.to_string()).size(13).color(color))
|
|
||||||
.on_press(Message::Tags(TagsMsg::SetSection(section)))
|
.on_press(Message::Tags(TagsMsg::SetSection(section)))
|
||||||
.padding([6, 12])
|
.padding([6, 12])
|
||||||
.style(move |_theme: &Theme, _status| {
|
.style(if active {
|
||||||
if active {
|
inputs::primary_button
|
||||||
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 {
|
} else {
|
||||||
button::Style::default()
|
inputs::secondary_button
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -212,8 +201,8 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
|||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
const MIN_FONT: f32 = 11.0;
|
const MIN_FONT: f32 = 12.0;
|
||||||
const MAX_FONT: f32 = 24.0;
|
const MAX_FONT: f32 = 18.0;
|
||||||
|
|
||||||
let chips: Vec<Element<'a, Message>> = state
|
let chips: Vec<Element<'a, Message>> = state
|
||||||
.tags
|
.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())))
|
.on_press(Message::Tags(TagsMsg::ToggleTagSelection(tag.id.clone())))
|
||||||
.padding([vert_pad, 10])
|
.padding([vert_pad, 10])
|
||||||
.style(move |_: &Theme, _| button::Style {
|
.style(move |_: &Theme, status| button::Style {
|
||||||
background: Some(Background::Color(color)),
|
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 {
|
border: iced::Border {
|
||||||
radius: 12.0.into(),
|
radius: 12.0.into(),
|
||||||
width: if selected { 2.0 } else { 0.0 },
|
width: if selected { 2.0 } else { 1.0 },
|
||||||
color: if selected {
|
color,
|
||||||
Color::WHITE
|
|
||||||
} else {
|
|
||||||
Color::TRANSPARENT
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
..button::Style::default()
|
..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)),
|
.color(Color::from_rgb(0.75, 0.77, 0.82)),
|
||||||
button(text(t(locale, "tags.clearSelection")).size(12))
|
button(text(t(locale, "tags.clearSelection")).size(12))
|
||||||
.on_press(Message::Tags(TagsMsg::ClearSelection))
|
.on_press(Message::Tags(TagsMsg::ClearSelection))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([4, 8]),
|
.padding([4, 8]),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
@@ -285,13 +276,15 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
|||||||
};
|
};
|
||||||
|
|
||||||
scrollable(
|
scrollable(
|
||||||
|
column![inputs::card(
|
||||||
column![
|
column![
|
||||||
inputs::section_header(&t(locale, "tags.cloudSection")),
|
inputs::section_header(&t(locale, "tags.cloudSection")),
|
||||||
selection_summary,
|
selection_summary,
|
||||||
row(chips).spacing(6).wrap(),
|
row(chips).spacing(6).wrap(),
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill),
|
||||||
|
)]
|
||||||
.padding(16),
|
.padding(16),
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.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)
|
let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query)
|
||||||
.on_input(|value| Message::Tags(TagsMsg::SearchChanged(value)))
|
.on_input(|value| Message::Tags(TagsMsg::SearchChanged(value)))
|
||||||
.size(14);
|
.size(14)
|
||||||
|
.padding([8, 10])
|
||||||
|
.style(inputs::field_style);
|
||||||
|
|
||||||
let filtered: Vec<&Tag> = state
|
let filtered: Vec<&Tag> = state
|
||||||
.tags
|
.tags
|
||||||
@@ -446,13 +441,18 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|
|||||||
|
|
||||||
scrollable(
|
scrollable(
|
||||||
column![
|
column![
|
||||||
create_panel,
|
inputs::card(create_panel),
|
||||||
|
inputs::card(
|
||||||
|
column![
|
||||||
inputs::section_header(&t(locale, "tags.manageSection")),
|
inputs::section_header(&t(locale, "tags.manageSection")),
|
||||||
search,
|
search,
|
||||||
tag_list,
|
tag_list,
|
||||||
edit_panel,
|
edit_panel,
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(12)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
)
|
||||||
@@ -491,6 +491,7 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
container(inputs::card(
|
||||||
column![
|
column![
|
||||||
inputs::section_header(&t(locale, "tags.mergeSection")),
|
inputs::section_header(&t(locale, "tags.mergeSection")),
|
||||||
text(tw(
|
text(tw(
|
||||||
@@ -509,7 +510,9 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
|||||||
if merge_preview.is_empty() {
|
if merge_preview.is_empty() {
|
||||||
Element::from(Space::new(0, 0))
|
Element::from(Space::new(0, 0))
|
||||||
} else {
|
} else {
|
||||||
container(text(tw(locale, "tags.mergePreview", &[("tags", &merge_preview)])).size(12))
|
container(
|
||||||
|
text(tw(locale, "tags.mergePreview", &[("tags", &merge_preview)])).size(12),
|
||||||
|
)
|
||||||
.padding([4, 0])
|
.padding([4, 0])
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
@@ -523,13 +526,15 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
|||||||
.style(inputs::primary_button)
|
.style(inputs::primary_button)
|
||||||
.padding([6, 16]),
|
.padding([6, 16]),
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12),
|
||||||
|
))
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||||
|
container(inputs::card(
|
||||||
column![
|
column![
|
||||||
inputs::section_header(&t(locale, "tags.discoverSection")),
|
inputs::section_header(&t(locale, "tags.discoverSection")),
|
||||||
text(t(locale, "tags.discoverDescription"))
|
text(t(locale, "tags.discoverDescription"))
|
||||||
@@ -540,7 +545,8 @@ fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a,
|
|||||||
.style(inputs::primary_button)
|
.style(inputs::primary_button)
|
||||||
.padding([6, 16]),
|
.padding([6, 16]),
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12),
|
||||||
|
))
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
|
|||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "editor.validate")).size(13))
|
button(text(t(locale, "editor.validate")).size(13))
|
||||||
.on_press(Message::TemplateEditor(TemplateEditorMsg::Validate))
|
.on_press(Message::TemplateEditor(TemplateEditorMsg::Validate))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
.padding([6, 16])
|
.padding([6, 16])
|
||||||
.into(),
|
.into(),
|
||||||
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
|
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
|
||||||
@@ -168,10 +169,12 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
|
|||||||
});
|
});
|
||||||
let meta_row2 = row![kind_select, enabled_check]
|
let meta_row2 = row![kind_select, enabled_check]
|
||||||
.spacing(16)
|
.spacing(16)
|
||||||
|
.align_y(iced::Alignment::End)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// Content editor (CodeEditor with Liquid/HTML syntax highlighting)
|
// Content editor (CodeEditor with Liquid/HTML syntax highlighting)
|
||||||
let content_section: Element<'a, Message> = column![
|
let content_section: Element<'a, Message> = inputs::card(
|
||||||
|
column![
|
||||||
inputs::section_header(&t(locale, "editor.content")),
|
inputs::section_header(&t(locale, "editor.content")),
|
||||||
Element::from(
|
Element::from(
|
||||||
CodeEditor::new(&state.editor_buffer, highlighter(), "liquid",).on_change(|msg| {
|
CodeEditor::new(&state.editor_buffer, highlighter(), "liquid",).on_change(|msg| {
|
||||||
@@ -186,8 +189,10 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(10)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill),
|
||||||
|
)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
@@ -213,18 +218,18 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
|
|||||||
.padding(8);
|
.padding(8);
|
||||||
|
|
||||||
// Top pane: header + metadata (scrollable for overflow)
|
// Top pane: header + metadata (scrollable for overflow)
|
||||||
let top_pane = scrollable(
|
let metadata = inputs::card(
|
||||||
column![header, meta_row1, meta_row2,]
|
column![meta_row1, meta_row2]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.padding(16)
|
|
||||||
.width(Length::Fill),
|
.width(Length::Fill),
|
||||||
)
|
);
|
||||||
|
let top_pane = scrollable(column![header, metadata].spacing(12).width(Length::Fill))
|
||||||
.height(Length::Shrink);
|
.height(Length::Shrink);
|
||||||
|
|
||||||
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
|
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
|
||||||
column![top_pane, content_section, validation, footer,]
|
column![top_pane, content_section, validation, footer,]
|
||||||
.spacing(4)
|
.spacing(8)
|
||||||
.padding([0, 16])
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fn toast_style(level: ToastLevel) -> impl Fn(&Theme) -> container::Style {
|
|||||||
border: Border {
|
border: Border {
|
||||||
color: toast_border(level),
|
color: toast_border(level),
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 4.0.into(),
|
radius: 8.0.into(),
|
||||||
},
|
},
|
||||||
..container::Style::default()
|
..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