#[allow(unused_imports)] use crate::fetch; #[allow(unused_imports)] use dioxus::{CapturedError, prelude::*}; #[cfg(feature = "fullstack-server")] use dioxus::fullstack::response::Response; use jacquard::smol_str::SmolStr; #[allow(unused_imports)] use std::sync::Arc; #[allow(unused_imports)] use weaver_renderer::theme::{ResolvedTheme, Theme}; #[cfg(feature = "server")] use axum::{extract::Extension, response::IntoResponse}; #[cfg(feature = "fullstack-server")] #[component] pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { use dioxus::fullstack::get_server_url; rsx! { document::Stylesheet { href: "{get_server_url()}/css/{ident}/{notebook}" } } } #[cfg(not(feature = "fullstack-server"))] #[component] pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { use jacquard::client::AgentSessionExt; use jacquard::types::ident::AtIdentifier; use jacquard::{CowStr, from_data}; use weaver_api::sh_weaver::notebook::book::Book; use weaver_renderer::css::{generate_base_css, generate_syntax_css}; use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; let fetcher = use_context::(); let css_content = use_resource(move || { let ident = ident.clone(); let notebook = notebook.clone(); let fetcher = fetcher.clone(); async move { let ident = AtIdentifier::new_owned(ident).ok()?; let resolved_theme = if let Some(notebook) = fetcher.get_notebook(ident, notebook).await.ok()? { let book: Book = from_data(¬ebook.0.record).ok()?; if let Some(theme_ref) = book.theme { if let Ok(theme_response) = fetcher.client.get_record::(&theme_ref.uri).await { if let Ok(theme_output) = theme_response.into_output() { let theme: Theme = theme_output.into(); resolve_theme(fetcher.client.as_ref(), &theme) .await .unwrap_or_else(|_| default_resolved_theme()) } else { default_resolved_theme() } } else { default_resolved_theme() } } else { default_resolved_theme() } } else { default_resolved_theme() }; let mut css = generate_base_css(&resolved_theme); css.push_str( &generate_syntax_css(&resolved_theme) .await .unwrap_or_default(), ); Some(css) } }); match css_content() { Some(Some(css)) => rsx! { document::Style { {css} } }, _ => rsx! {}, } } #[component] pub fn DefaultNotebookCss() -> Element { rsx! { document::Stylesheet { href: asset!("/assets/styling/theme-defaults.css") } document::Stylesheet { href: asset!("/assets/styling/notebook-defaults.css") } } } #[cfg(feature = "fullstack-server")] #[get("/css/{ident}/{notebook}", fetcher: Extension>)] pub async fn css(ident: SmolStr, notebook: SmolStr) -> Result { use dioxus::fullstack::http::header::CONTENT_TYPE; use jacquard::client::AgentSessionExt; use jacquard::types::ident::AtIdentifier; use jacquard::{CowStr, from_data}; use weaver_api::sh_weaver::notebook::book::Book; use weaver_renderer::css::{generate_base_css, generate_syntax_css}; use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; let ident = AtIdentifier::new_owned(ident)?; let resolved_theme = if let Some(notebook) = fetcher.get_notebook(ident, notebook).await? { let book: Book = from_data(¬ebook.0.record).unwrap(); if let Some(theme_ref) = book.theme { if let Ok(theme_response) = fetcher.client.get_record::(&theme_ref.uri).await { if let Ok(theme_output) = theme_response.into_output() { let theme: Theme = theme_output.into(); let client = fetcher.get_client(); // Try to resolve the theme (fetch colour schemes from PDS) resolve_theme(client.as_ref(), &theme) .await .unwrap_or_else(|_| default_resolved_theme()) } else { default_resolved_theme() } } else { default_resolved_theme() } } else { default_resolved_theme() } } else { default_resolved_theme() }; let mut css = generate_base_css(&resolved_theme); css.push_str( &generate_syntax_css(&resolved_theme) .await .map_err(|e| CapturedError::from_display(e)) .unwrap_or_default(), ); let css = minify_css(&css).unwrap_or(css); Ok(([(CONTENT_TYPE, "text/css")], css).into_response()) } /// Input for generating theme preview data from 4 base colours. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ThemePreviewInput { pub background: String, pub text: String, pub primary: String, pub link: String, pub light_background: Option, pub light_text: Option, pub light_primary: Option, pub light_link: Option, pub dark_background: Option, pub dark_text: Option, pub dark_primary: Option, pub dark_link: Option, pub light_code_theme: String, pub dark_code_theme: String, } /// Generated 16-colour palette for a single variant. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct ColourPalette { pub base: String, pub surface: String, pub overlay: String, pub text: String, pub muted: String, pub subtle: String, pub emphasis: String, pub primary: String, pub secondary: String, pub tertiary: String, pub error: String, pub warning: String, pub success: String, pub border: String, pub link: String, pub highlight: String, } impl ColourPalette { pub fn to_css_vars(&self) -> String { // Ensure all colours have # prefix. let fmt = |c: &str| if c.starts_with('#') { c.to_string() } else { format!("#{}", c) }; format!( "--color-base: {}; --color-surface: {}; --color-overlay: {}; \ --color-text: {}; --color-muted: {}; --color-subtle: {}; \ --color-emphasis: {}; --color-primary: {}; --color-secondary: {}; \ --color-tertiary: {}; --color-error: {}; --color-warning: {}; \ --color-success: {}; --color-border: {}; --color-link: {}; \ --color-highlight: {};", fmt(&self.base), fmt(&self.surface), fmt(&self.overlay), fmt(&self.text), fmt(&self.muted), fmt(&self.subtle), fmt(&self.emphasis), fmt(&self.primary), fmt(&self.secondary), fmt(&self.tertiary), fmt(&self.error), fmt(&self.warning), fmt(&self.success), fmt(&self.border), fmt(&self.link), fmt(&self.highlight) ) } } /// Generated theme preview data: palettes + syntax CSS. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ThemePreviewOutput { pub light_palette: ColourPalette, pub dark_palette: ColourPalette, pub syntax_css: String, } /// Generate theme preview data from 4 base colours. #[post("/api/theme-preview")] pub async fn generate_theme_preview( input: ThemePreviewInput, ) -> Result { use weaver_renderer::colour_gen::{ThemeInputs, ThemeVariant, detect_variant, generate_palette, generate_counterpart_palette}; use weaver_renderer::css::generate_syntax_css; use weaver_renderer::theme::{ResolvedTheme, ThemeDefault, default_fonts, default_spacing, ColourSchemeColours}; let light_inputs = ThemeInputs { background: input.light_background.unwrap_or_else(|| input.background.clone()), text: input.light_text.unwrap_or_else(|| input.text.clone()), primary: input.light_primary.unwrap_or_else(|| input.primary.clone()), link: input.light_link.unwrap_or_else(|| input.link.clone()), }; let dark_inputs = ThemeInputs { background: input.dark_background.unwrap_or_else(|| input.background.clone()), text: input.dark_text.unwrap_or_else(|| input.text.clone()), primary: input.dark_primary.unwrap_or_else(|| input.primary.clone()), link: input.dark_link.unwrap_or_else(|| input.link.clone()), }; let primary_variant = detect_variant(&input.background) .map_err(|e| ServerFnError::new(format!("Invalid background colour: {}", e)))?; let (light_scheme, dark_scheme): (ColourSchemeColours, ColourSchemeColours) = match primary_variant { ThemeVariant::Light => { let light = generate_palette(&light_inputs, ThemeVariant::Light) .map_err(|e| ServerFnError::new(format!("Failed to generate light palette: {}", e)))?; let dark = generate_counterpart_palette(&dark_inputs, ThemeVariant::Light) .map_err(|e| ServerFnError::new(format!("Failed to generate dark palette: {}", e)))?; (light, dark) } ThemeVariant::Dark => { let dark = generate_palette(&dark_inputs, ThemeVariant::Dark) .map_err(|e| ServerFnError::new(format!("Failed to generate dark palette: {}", e)))?; let light = generate_counterpart_palette(&light_inputs, ThemeVariant::Dark) .map_err(|e| ServerFnError::new(format!("Failed to generate light palette: {}", e)))?; (light, dark) } }; let light_code_theme = weaver_renderer::theme::ThemeLightCodeTheme::CodeThemeName(Box::new(input.light_code_theme.into())); let dark_code_theme = weaver_renderer::theme::ThemeDarkCodeTheme::CodeThemeName(Box::new(input.dark_code_theme.into())); let resolved = ResolvedTheme { default: ThemeDefault::Auto, dark_scheme: dark_scheme.clone(), light_scheme: light_scheme.clone(), fonts: default_fonts(), spacing: default_spacing(), dark_code_theme, light_code_theme, }; let syntax_css = generate_syntax_css(&resolved).await.unwrap_or_default(); let light_palette = ColourPalette { base: light_scheme.base.to_string(), surface: light_scheme.surface.to_string(), overlay: light_scheme.overlay.to_string(), text: light_scheme.text.to_string(), muted: light_scheme.muted.to_string(), subtle: light_scheme.subtle.to_string(), emphasis: light_scheme.emphasis.to_string(), primary: light_scheme.primary.to_string(), secondary: light_scheme.secondary.to_string(), tertiary: light_scheme.tertiary.to_string(), error: light_scheme.error.to_string(), warning: light_scheme.warning.to_string(), success: light_scheme.success.to_string(), border: light_scheme.border.to_string(), link: light_scheme.link.to_string(), highlight: light_scheme.highlight.to_string(), }; let dark_palette = ColourPalette { base: dark_scheme.base.to_string(), surface: dark_scheme.surface.to_string(), overlay: dark_scheme.overlay.to_string(), text: dark_scheme.text.to_string(), muted: dark_scheme.muted.to_string(), subtle: dark_scheme.subtle.to_string(), emphasis: dark_scheme.emphasis.to_string(), primary: dark_scheme.primary.to_string(), secondary: dark_scheme.secondary.to_string(), tertiary: dark_scheme.tertiary.to_string(), error: dark_scheme.error.to_string(), warning: dark_scheme.warning.to_string(), success: dark_scheme.success.to_string(), border: dark_scheme.border.to_string(), link: dark_scheme.link.to_string(), highlight: dark_scheme.highlight.to_string(), }; Ok(ThemePreviewOutput { light_palette, dark_palette, syntax_css, }) } #[cfg(feature = "server")] fn minify_css(css: &str) -> Option { use lightningcss::printer::PrinterOptions; use lightningcss::stylesheet::{MinifyOptions, ParserOptions, StyleSheet}; let stylesheet = match StyleSheet::parse(css, ParserOptions::default()) { Ok(s) => s, Err(e) => { tracing::warn!("CSS parse error: {:?}", e); return None; } }; let mut stylesheet = stylesheet; if let Err(e) = stylesheet.minify(MinifyOptions::default()) { tracing::warn!("CSS minify error: {:?}", e); return None; } let result = match stylesheet.to_css(PrinterOptions { minify: true, ..Default::default() }) { Ok(r) => r, Err(e) => { tracing::warn!("CSS print error: {:?}", e); return None; } }; Some(result.code) }