[weaver-app] add InlineThemeEditor component

Orual 7e01a8b3 88bb15d1

+311
+89
crates/weaver-app/assets/styling/inline-theme-editor.css
··· 1 + /* Inline theme editor for notebook settings */ 2 + 3 + .inline-theme-editor { 4 + display: flex; 5 + flex-direction: column; 6 + gap: 1rem; 7 + padding: 1rem; 8 + background: var(--color-surface); 9 + border: 1px solid var(--color-border); 10 + } 11 + 12 + .inline-theme-editor-heading { 13 + margin: 0; 14 + font-size: 1rem; 15 + font-weight: 600; 16 + color: var(--color-text); 17 + } 18 + 19 + .inline-theme-editor-presets { 20 + display: flex; 21 + flex-direction: column; 22 + gap: 0.25rem; 23 + } 24 + 25 + .inline-theme-editor-presets label { 26 + font-size: 0.875rem; 27 + color: var(--color-muted); 28 + } 29 + 30 + .inline-theme-editor-presets select { 31 + padding: 0.5rem; 32 + border: 1px solid var(--color-border); 33 + background: var(--color-base); 34 + color: var(--color-text); 35 + font-family: var(--font-ui); 36 + font-size: 0.875rem; 37 + } 38 + 39 + .inline-theme-editor-presets select:focus { 40 + outline: none; 41 + border-color: var(--color-primary); 42 + } 43 + 44 + .inline-theme-editor-colours { 45 + display: grid; 46 + grid-template-columns: repeat(2, 1fr); 47 + gap: 0.75rem; 48 + } 49 + 50 + .inline-theme-editor-code-theme, 51 + .inline-theme-editor-mode { 52 + display: flex; 53 + flex-direction: column; 54 + gap: 0.25rem; 55 + } 56 + 57 + .inline-theme-editor-code-theme label, 58 + .inline-theme-editor-mode label { 59 + font-size: 0.875rem; 60 + color: var(--color-muted); 61 + } 62 + 63 + .inline-theme-editor-code-theme select { 64 + padding: 0.5rem; 65 + border: 1px solid var(--color-border); 66 + background: var(--color-base); 67 + color: var(--color-text); 68 + font-family: var(--font-ui); 69 + font-size: 0.875rem; 70 + } 71 + 72 + .inline-theme-editor-code-theme select:focus { 73 + outline: none; 74 + border-color: var(--color-primary); 75 + } 76 + 77 + .inline-theme-editor-mode { 78 + gap: 0.5rem; 79 + } 80 + 81 + .inline-theme-editor-full-link { 82 + font-size: 0.875rem; 83 + color: var(--color-link); 84 + text-decoration: none; 85 + } 86 + 87 + .inline-theme-editor-full-link:hover { 88 + text-decoration: underline; 89 + }
+219
crates/weaver-app/src/components/inline_theme_editor.rs
··· 1 + //! Inline theme editor for notebook settings. 2 + //! 3 + //! Provides core 4 colour pickers (background, text, primary, link) plus code theme selector. 4 + 5 + use dioxus::prelude::*; 6 + use weaver_renderer::themes::{BUILTIN_CODE_THEMES, BUILTIN_COLOUR_SCHEMES}; 7 + 8 + use crate::components::toggle_group::{ToggleGroup, ToggleItem}; 9 + use crate::components::HexColourInput; 10 + 11 + /// Strip leading # or 0x from hex colour strings. 12 + fn strip_hex_prefix(s: &str) -> String { 13 + s.trim_start_matches('#') 14 + .trim_start_matches("0x") 15 + .trim_start_matches("0X") 16 + .to_uppercase() 17 + } 18 + 19 + /// Convert mode string to toggle index. 20 + fn mode_to_index(mode: &str) -> std::collections::HashSet<usize> { 21 + let idx = match mode { 22 + "light" => 1, 23 + "dark" => 2, 24 + _ => 0, // "auto" or default 25 + }; 26 + std::collections::HashSet::from([idx]) 27 + } 28 + 29 + /// Convert toggle index to mode string. 30 + fn index_to_mode(idx: usize) -> &'static str { 31 + match idx { 32 + 1 => "light", 33 + 2 => "dark", 34 + _ => "auto", 35 + } 36 + } 37 + 38 + const INLINE_THEME_EDITOR_CSS: Asset = asset!("/assets/styling/inline-theme-editor.css"); 39 + 40 + /// Theme values being edited. 41 + #[derive(Debug, Clone, PartialEq, Default)] 42 + pub struct InlineThemeValues { 43 + pub background: String, 44 + pub text: String, 45 + pub primary: String, 46 + pub link: String, 47 + pub code_theme: String, 48 + pub default_mode: String, // "light", "dark", or "auto" 49 + } 50 + 51 + /// Props for InlineThemeEditor. 52 + #[derive(Props, Clone, PartialEq)] 53 + pub struct InlineThemeEditorProps { 54 + /// Current theme values. 55 + pub values: InlineThemeValues, 56 + /// Callback when any value changes. 57 + pub onchange: EventHandler<InlineThemeValues>, 58 + } 59 + 60 + /// Inline theme editor with core colour pickers and code theme selector. 61 + #[component] 62 + pub fn InlineThemeEditor(props: InlineThemeEditorProps) -> Element { 63 + let values = props.values.clone(); 64 + 65 + // Filter code themes by current mode for the selector. 66 + let mode = &values.default_mode; 67 + let relevant_code_themes: Vec<_> = BUILTIN_CODE_THEMES 68 + .iter() 69 + .filter(|t| mode == "auto" || t.variant == mode) 70 + .collect(); 71 + 72 + rsx! { 73 + document::Stylesheet { href: INLINE_THEME_EDITOR_CSS } 74 + 75 + div { class: "inline-theme-editor", 76 + h4 { class: "inline-theme-editor-heading", "Theme" } 77 + 78 + // Preset selector. 79 + div { class: "inline-theme-editor-presets", 80 + label { "Start from preset:" } 81 + select { 82 + onchange: { 83 + let onchange = props.onchange.clone(); 84 + let code_theme = props.values.code_theme.clone(); 85 + move |e: Event<FormData>| { 86 + let preset_id = e.value(); 87 + if let Some(scheme) = BUILTIN_COLOUR_SCHEMES.iter().find(|s| s.id == preset_id) { 88 + onchange.call(InlineThemeValues { 89 + background: strip_hex_prefix(&scheme.colours.base), 90 + text: strip_hex_prefix(&scheme.colours.text), 91 + primary: strip_hex_prefix(&scheme.colours.primary), 92 + link: strip_hex_prefix(&scheme.colours.link), 93 + code_theme: code_theme.clone(), 94 + default_mode: scheme.variant.to_string(), 95 + }); 96 + } 97 + } 98 + }, 99 + option { value: "", "Custom" } 100 + for scheme in BUILTIN_COLOUR_SCHEMES.iter() { 101 + option { 102 + value: "{scheme.id}", 103 + "{scheme.name}" 104 + } 105 + } 106 + } 107 + } 108 + 109 + // Colour pickers. 110 + div { class: "inline-theme-editor-colours", 111 + HexColourInput { 112 + label: Some("Background".to_string()), 113 + value: values.background.clone(), 114 + onchange: { 115 + let values = props.values.clone(); 116 + let onchange = props.onchange.clone(); 117 + move |val| { 118 + let mut v = values.clone(); 119 + v.background = val; 120 + onchange.call(v); 121 + } 122 + }, 123 + } 124 + HexColourInput { 125 + label: Some("Text".to_string()), 126 + value: values.text.clone(), 127 + onchange: { 128 + let values = props.values.clone(); 129 + let onchange = props.onchange.clone(); 130 + move |val| { 131 + let mut v = values.clone(); 132 + v.text = val; 133 + onchange.call(v); 134 + } 135 + }, 136 + } 137 + HexColourInput { 138 + label: Some("Primary".to_string()), 139 + value: values.primary.clone(), 140 + onchange: { 141 + let values = props.values.clone(); 142 + let onchange = props.onchange.clone(); 143 + move |val| { 144 + let mut v = values.clone(); 145 + v.primary = val; 146 + onchange.call(v); 147 + } 148 + }, 149 + } 150 + HexColourInput { 151 + label: Some("Link".to_string()), 152 + value: values.link.clone(), 153 + onchange: { 154 + let values = props.values.clone(); 155 + let onchange = props.onchange.clone(); 156 + move |val| { 157 + let mut v = values.clone(); 158 + v.link = val; 159 + onchange.call(v); 160 + } 161 + }, 162 + } 163 + } 164 + 165 + // Code theme selector. 166 + div { class: "inline-theme-editor-code-theme", 167 + label { "Code theme:" } 168 + select { 169 + value: "{values.code_theme}", 170 + onchange: { 171 + let values = props.values.clone(); 172 + let onchange = props.onchange.clone(); 173 + move |e: Event<FormData>| { 174 + let mut v = values.clone(); 175 + v.code_theme = e.value(); 176 + onchange.call(v); 177 + } 178 + }, 179 + for theme in relevant_code_themes { 180 + option { 181 + value: "{theme.id}", 182 + "{theme.name}" 183 + } 184 + } 185 + } 186 + } 187 + 188 + // Default mode toggle group. 189 + // Indexes: 0 = auto, 1 = light, 2 = dark 190 + div { class: "inline-theme-editor-mode", 191 + label { "Default mode:" } 192 + ToggleGroup { 193 + horizontal: true, 194 + default_pressed: mode_to_index(&values.default_mode), 195 + on_pressed_change: { 196 + let values = props.values.clone(); 197 + let onchange = props.onchange.clone(); 198 + move |pressed: std::collections::HashSet<usize>| { 199 + if let Some(&idx) = pressed.iter().next() { 200 + let mut v = values.clone(); 201 + v.default_mode = index_to_mode(idx).to_string(); 202 + onchange.call(v); 203 + } 204 + } 205 + }, 206 + ToggleItem { index: 0usize, "Auto" } 207 + ToggleItem { index: 1usize, "Light" } 208 + ToggleItem { index: 2usize, "Dark" } 209 + } 210 + } 211 + 212 + // Link to full editor. 213 + // TODO: Enable once theme page exists 214 + // a { href: "/{ident}/themes", class: "inline-theme-editor-full-link", 215 + // "Edit full theme ->" 216 + // } 217 + } 218 + } 219 + }
+3
crates/weaver-app/src/components/mod.rs
··· 362 362 363 363 mod hex_colour_input; 364 364 pub use hex_colour_input::HexColourInput; 365 + 366 + mod inline_theme_editor; 367 + pub use inline_theme_editor::{InlineThemeEditor, InlineThemeValues};