//! Theme preview component with sample markdown rendering. use dioxus::prelude::*; use crate::components::css::{ThemePreviewInput, generate_theme_preview}; use crate::components::inline_theme_editor::InlineThemeValues; const NOTEBOOK_DEFAULTS_CSS: Asset = asset!("/assets/styling/notebook-defaults.css"); /// Sample markdown to render for theme preview. const SAMPLE_MARKDOWN: &str = r#"# Heading 1 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ## Heading 2 Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Here's a [link to somewhere](#) and some **bold text** with *italics*. ### Heading 3 > A blockquote for emphasis. This tests the border and muted text colours. Here's a list of items: - First item with some text - Second item with `inline code` - Third item And some numbered steps: 1. Do the first thing 2. Then the second 3. Finally the third ```rust fn main() { // A code block to test syntax highlighting let message = "Hello, world!"; println!("{}", message); } ``` --- That's the end of the preview. "#; /// Render markdown to HTML using the full pipeline with syntax highlighting. fn render_markdown(markdown: &str) -> String { use markdown_weaver::Parser; use weaver_renderer::atproto::ClientWriter; use weaver_renderer::default_md_options; let parser = Parser::new_ext(markdown, default_md_options()).into_offset_iter(); let mut html = String::new(); ClientWriter::<_, _, ()>::new(parser, &mut html, markdown) .run() .ok(); html } /// Props for ThemePreview with signal for reactivity. #[derive(Props, Clone, PartialEq)] pub struct ThemePreviewProps { /// Theme values to preview (signal for reactivity). pub values: Signal, /// Whether to show dark variant (false = light). #[props(default = false)] pub dark: bool, } /// Theme preview component that renders sample markdown with theme applied. #[component] pub fn ThemePreview(props: ThemePreviewProps) -> Element { let dark = props.dark; let values = props.values; let mut preview_resource = use_resource(move || { let values = values(); async move { // Skip if background is empty (invalid). if values.background.is_empty() { return Err(ServerFnError::new("No theme values set")); } let input = ThemePreviewInput { background: values.background.clone(), text: values.text.clone(), primary: values.primary.clone(), link: values.link.clone(), light_background: values.light_background.clone(), light_text: values.light_text.clone(), light_primary: values.light_primary.clone(), light_link: values.light_link.clone(), dark_background: values.dark_background.clone(), dark_text: values.dark_text.clone(), dark_primary: values.dark_primary.clone(), dark_link: values.dark_link.clone(), light_code_theme: values.light_code_theme.clone(), dark_code_theme: values.dark_code_theme.clone(), }; generate_theme_preview(input).await } }); // Restart resource when values change. use_effect(move || { let _ = values(); preview_resource.restart(); }); let rendered_html = render_markdown(SAMPLE_MARKDOWN); match preview_resource() { Some(Ok(output)) => { let palette = if dark { &output.dark_palette } else { &output.light_palette }; let css_vars = palette.to_css_vars(); // Scoped CSS: variables -> notebook defaults -> syntax highlighting let scoped_css = format!(".theme-preview {{ {} }}", css_vars); rsx! { // 1. CSS variables scoped to .theme-preview style { dangerous_inner_html: "{scoped_css}" } // 2. Notebook content styles (uses the variables) document::Stylesheet { href: NOTEBOOK_DEFAULTS_CSS } // 3. Syntax highlighting CSS style { dangerous_inner_html: "{output.syntax_css}" } div { class: "theme-preview notebook-content", dangerous_inner_html: "{rendered_html}" } } } Some(Err(e)) => rsx! { div { class: "theme-preview theme-preview--error", "Failed to generate theme preview: {e}" } }, None => rsx! { div { class: "theme-preview theme-preview--loading", "Loading preview..." } }, } }