diff --git a/crates/bds-core/src/engine/preview.rs b/crates/bds-core/src/engine/preview.rs index 4a98603..1e1d973 100644 --- a/crates/bds-core/src/engine/preview.rs +++ b/crates/bds-core/src/engine/preview.rs @@ -178,7 +178,7 @@ async fn handle_style_preview( let language = escape_html(metadata.main_language.as_deref().unwrap_or("en")); let description = escape_html(metadata.description.as_deref().unwrap_or(&metadata.name)); let html = format!( - "{project_name}

{project_name}

{description}

" + "{project_name}

{description}

" ); Html(html).into_response() } @@ -1083,14 +1083,35 @@ mod tests { .unwrap(); assert!(stylesheet.status().is_success()); let stylesheet = stylesheet.text().unwrap(); + let light_body = client + .get(format!( + "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__style-preview?theme=amber&mode=light" + )) + .send() + .unwrap() + .text() + .unwrap(); + let auto_body = client + .get(format!( + "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__style-preview?theme=amber&mode=auto" + )) + .send() + .unwrap() + .text() + .unwrap(); server.stop().unwrap(); assert!(body.contains("
")); assert!(body.contains("Blog")); + assert!(body.find("").unwrap()); assert!(body.contains("href=\"/assets/pico.amber.min.css\"")); assert!(body.contains("data-theme=\"dark\"")); assert!(body.contains("data-mode=\"dark\"")); assert!(!body.contains("data-theme=\"amber\"")); + assert!(light_body.contains("data-theme=\"light\"")); + assert!(light_body.contains("data-mode=\"light\"")); + assert!(!auto_body.contains("data-theme=")); + assert!(!auto_body.contains("data-mode=")); assert!(stylesheet.contains("--pico-primary-background:#ffbf00")); } diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index ca4416c..81ad49f 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -637,6 +637,11 @@ struct PreviewSession { struct EmbeddedPreviewState { controller: WebViewController, current_url: Option, + creation_pending: bool, +} + +fn should_start_embedded_preview_creation(active: bool, pending: bool) -> bool { + !active && !pending } fn persist_media_editor_state_impl( @@ -8712,6 +8717,7 @@ impl BdsApp { self.embedded_style_preview = Some(EmbeddedPreviewState { controller: WebViewController::new(WebViewConfig::default().url(url.clone())), current_url: Some(url.clone()), + creation_pending: false, }); } @@ -8719,9 +8725,13 @@ impl BdsApp { return window::get_oldest().map(Message::MainWindowLoaded); }; if let Some(preview) = &mut self.embedded_style_preview - && !preview.controller.is_active() + && should_start_embedded_preview_creation( + preview.controller.is_active(), + preview.creation_pending, + ) { preview.controller = WebViewController::new(WebViewConfig::default().url(url)); + preview.creation_pending = true; return preview .controller .create_task(window_id, Message::EmbeddedStylePreviewReady); @@ -8762,6 +8772,7 @@ impl BdsApp { self.embedded_preview = Some(EmbeddedPreviewState { controller: WebViewController::new(WebViewConfig::default().url(url.clone())), current_url: Some(url.clone()), + creation_pending: false, }); } @@ -8770,9 +8781,13 @@ impl BdsApp { }; if let Some(preview) = &mut self.embedded_preview - && !preview.controller.is_active() + && should_start_embedded_preview_creation( + preview.controller.is_active(), + preview.creation_pending, + ) { preview.controller = WebViewController::new(WebViewConfig::default().url(url)); + preview.creation_pending = true; return preview .controller .create_task(window_id, Message::EmbeddedPreviewReady); @@ -9426,6 +9441,7 @@ mod tests { persist_post_editor_preview_state_impl, persist_post_editor_state_impl, remote_error_closes_connection, save_editor_settings_state_impl, save_script_editor_state_impl, save_template_editor_state_impl, + should_start_embedded_preview_creation, }; use crate::i18n::t; use crate::platform::menu::MenuAction; @@ -10954,6 +10970,13 @@ mod tests { assert!(!state.can_apply()); } + #[test] + fn embedded_preview_creation_starts_only_when_idle() { + assert!(should_start_embedded_preview_creation(false, false)); + assert!(!should_start_embedded_preview_creation(false, true)); + assert!(!should_start_embedded_preview_creation(true, false)); + } + #[test] fn applying_style_persists_project_metadata_emits_event_and_updates_badge() { let (db, project, tmp) = setup(); diff --git a/crates/bds-ui/src/app/preview_handlers.rs b/crates/bds-ui/src/app/preview_handlers.rs index 5f2e7d1..58a5331 100644 --- a/crates/bds-ui/src/app/preview_handlers.rs +++ b/crates/bds-ui/src/app/preview_handlers.rs @@ -11,6 +11,9 @@ impl BdsApp { ]) } Message::EmbeddedPreviewReady(result) => { + if let Some(preview) = &mut self.embedded_preview { + preview.creation_pending = false; + } match result { Ok(()) => { let visible = self.active_post_uses_embedded_preview(); @@ -29,6 +32,9 @@ impl BdsApp { Task::none() } Message::EmbeddedStylePreviewReady(result) => { + if let Some(preview) = &mut self.embedded_style_preview { + preview.creation_pending = false; + } match result { Ok(()) => { let visible = self.active_style_uses_embedded_preview(); diff --git a/crates/bds-ui/src/views/style_view.rs b/crates/bds-ui/src/views/style_view.rs index 212a8ce..d45ebe7 100644 --- a/crates/bds-ui/src/views/style_view.rs +++ b/crates/bds-ui/src/views/style_view.rs @@ -1,6 +1,6 @@ use iced::widget::text::Shaping; use iced::widget::{Column, Space, button, column, container, row, scrollable, text}; -use iced::{Background, Border, Color, Element, Length, Theme}; +use iced::{Background, Border, Color, Element, Length, Radians, Theme, gradient}; use bds_core::i18n::UiLocale; @@ -194,11 +194,24 @@ fn theme_button_style(selected: bool, status: button::Status) -> button::Style { } } -fn swatch(color: &'static str) -> Element<'static, Message> { - container(Space::new(Length::Fill, Length::Fixed(16.0))) - .width(Length::Fill) +fn theme_accent_background(theme: &StyleTheme) -> Background { + gradient::Linear::new(Radians(3.0 * std::f32::consts::FRAC_PI_4)) + .add_stop(0.0, parse_hex_color(theme.accent_color)) + .add_stop(1.0, parse_hex_color(theme.dark_bg_color)) + .into() +} + +fn swatch(background: Background, width_portion: u16) -> Element<'static, Message> { + container(Space::new(Length::Fill, Length::Fill)) + .width(Length::FillPortion(width_portion)) + .height(Length::Fixed(30.0)) .style(move |_: &Theme| container::Style { - background: Some(Background::Color(parse_hex_color(color))), + background: Some(background), + border: Border { + color: Color::from_rgba8(0xff, 0xff, 0xff, 0.08), + width: 1.0, + radius: 5.0.into(), + }, ..container::Style::default() }) .into() @@ -210,11 +223,11 @@ fn theme_button<'a>(theme: &StyleTheme, selected_theme: &str) -> Element<'a, Mes button( column![ row![ - swatch(theme.accent_color), - swatch(theme.light_bg_color), - swatch(theme.dark_bg_color), + swatch(theme_accent_background(theme), 2), + swatch(Background::Color(parse_hex_color(theme.light_bg_color)), 1), + swatch(Background::Color(parse_hex_color(theme.dark_bg_color)), 1), ] - .spacing(2), + .spacing(4), text(display_theme_name(theme.name)) .size(12) .shaping(Shaping::Advanced), @@ -382,15 +395,42 @@ mod tests { })); } + #[test] + fn theme_swatches_use_distinct_accent_gradients() { + let backgrounds = THEMES + .iter() + .map(theme_accent_background) + .collect::>(); + + assert!( + backgrounds + .iter() + .all(|background| matches!(background, Background::Gradient(_))) + ); + for (index, background) in backgrounds.iter().enumerate() { + assert!(!backgrounds[..index].contains(background)); + } + } + #[test] fn preview_url_tracks_selection_and_local_mode() { let mut state = StyleViewState::new(Some("blue")); state.select_theme("pumpkin"); + + assert_eq!( + state.preview_url(), + "http://127.0.0.1:4123/__style-preview?theme=pumpkin&mode=auto" + ); state.set_preview_mode(PreviewMode::Light); assert_eq!( state.preview_url(), "http://127.0.0.1:4123/__style-preview?theme=pumpkin&mode=light" ); + state.set_preview_mode(PreviewMode::Dark); + assert_eq!( + state.preview_url(), + "http://127.0.0.1:4123/__style-preview?theme=pumpkin&mode=dark" + ); assert_eq!(state.applied_theme, "blue"); }