atproto blogging
1//! Theme preview component with sample markdown rendering.
2
3use dioxus::prelude::*;
4
5use crate::components::css::{ThemePreviewInput, generate_theme_preview};
6use crate::components::inline_theme_editor::InlineThemeValues;
7
8const NOTEBOOK_DEFAULTS_CSS: Asset = asset!("/assets/styling/notebook-defaults.css");
9
10/// Sample markdown to render for theme preview.
11const SAMPLE_MARKDOWN: &str = r#"# Heading 1
12
13Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
14
15## Heading 2
16
17Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Here's a [link to somewhere](#) and some **bold text** with *italics*.
18
19### Heading 3
20
21> A blockquote for emphasis. This tests the border and muted text colours.
22
23Here's a list of items:
24
25- First item with some text
26- Second item with `inline code`
27- Third item
28
29And some numbered steps:
30
311. Do the first thing
322. Then the second
333. Finally the third
34
35```rust
36fn main() {
37 // A code block to test syntax highlighting
38 let message = "Hello, world!";
39 println!("{}", message);
40}
41```
42
43---
44
45That's the end of the preview.
46"#;
47
48/// Render markdown to HTML using the full pipeline with syntax highlighting.
49fn render_markdown(markdown: &str) -> String {
50 use markdown_weaver::Parser;
51 use weaver_renderer::atproto::ClientWriter;
52 use weaver_renderer::default_md_options;
53
54 let parser = Parser::new_ext(markdown, default_md_options()).into_offset_iter();
55 let mut html = String::new();
56 ClientWriter::<_, _, ()>::new(parser, &mut html, markdown)
57 .run()
58 .ok();
59 html
60}
61
62/// Props for ThemePreview with signal for reactivity.
63#[derive(Props, Clone, PartialEq)]
64pub struct ThemePreviewProps {
65 /// Theme values to preview (signal for reactivity).
66 pub values: Signal<InlineThemeValues>,
67 /// Whether to show dark variant (false = light).
68 #[props(default = false)]
69 pub dark: bool,
70}
71
72/// Theme preview component that renders sample markdown with theme applied.
73#[component]
74pub fn ThemePreview(props: ThemePreviewProps) -> Element {
75 let dark = props.dark;
76 let values = props.values;
77
78 let mut preview_resource = use_resource(move || {
79 let values = values();
80 async move {
81 // Skip if background is empty (invalid).
82 if values.background.is_empty() {
83 return Err(ServerFnError::new("No theme values set"));
84 }
85
86 let input = ThemePreviewInput {
87 background: values.background.clone(),
88 text: values.text.clone(),
89 primary: values.primary.clone(),
90 link: values.link.clone(),
91 light_background: values.light_background.clone(),
92 light_text: values.light_text.clone(),
93 light_primary: values.light_primary.clone(),
94 light_link: values.light_link.clone(),
95 dark_background: values.dark_background.clone(),
96 dark_text: values.dark_text.clone(),
97 dark_primary: values.dark_primary.clone(),
98 dark_link: values.dark_link.clone(),
99 light_code_theme: values.light_code_theme.clone(),
100 dark_code_theme: values.dark_code_theme.clone(),
101 };
102 generate_theme_preview(input).await
103 }
104 });
105
106 // Restart resource when values change.
107 use_effect(move || {
108 let _ = values();
109 preview_resource.restart();
110 });
111
112 let rendered_html = render_markdown(SAMPLE_MARKDOWN);
113
114 match preview_resource() {
115 Some(Ok(output)) => {
116 let palette = if dark { &output.dark_palette } else { &output.light_palette };
117 let css_vars = palette.to_css_vars();
118
119 // Scoped CSS: variables -> notebook defaults -> syntax highlighting
120 let scoped_css = format!(".theme-preview {{ {} }}", css_vars);
121
122 rsx! {
123 // 1. CSS variables scoped to .theme-preview
124 style { dangerous_inner_html: "{scoped_css}" }
125
126 // 2. Notebook content styles (uses the variables)
127 document::Stylesheet { href: NOTEBOOK_DEFAULTS_CSS }
128
129 // 3. Syntax highlighting CSS
130 style { dangerous_inner_html: "{output.syntax_css}" }
131
132 div {
133 class: "theme-preview notebook-content",
134 dangerous_inner_html: "{rendered_html}"
135 }
136 }
137 }
138 Some(Err(e)) => rsx! {
139 div { class: "theme-preview theme-preview--error",
140 "Failed to generate theme preview: {e}"
141 }
142 },
143 None => rsx! {
144 div { class: "theme-preview theme-preview--loading",
145 "Loading preview..."
146 }
147 },
148 }
149}