diff --git a/crates/weaver-app/assets/styling/inline-theme-editor.css b/crates/weaver-app/assets/styling/inline-theme-editor.css new file mode 100644 index 0000000..9aa5c01 --- /dev/null +++ b/crates/weaver-app/assets/styling/inline-theme-editor.css @@ -0,0 +1,89 @@ +/* Inline theme editor for notebook settings */ + +.inline-theme-editor { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + background: var(--color-surface); + border: 1px solid var(--color-border); +} + +.inline-theme-editor-heading { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--color-text); +} + +.inline-theme-editor-presets { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.inline-theme-editor-presets label { + font-size: 0.875rem; + color: var(--color-muted); +} + +.inline-theme-editor-presets select { + padding: 0.5rem; + border: 1px solid var(--color-border); + background: var(--color-base); + color: var(--color-text); + font-family: var(--font-ui); + font-size: 0.875rem; +} + +.inline-theme-editor-presets select:focus { + outline: none; + border-color: var(--color-primary); +} + +.inline-theme-editor-colours { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; +} + +.inline-theme-editor-code-theme, +.inline-theme-editor-mode { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.inline-theme-editor-code-theme label, +.inline-theme-editor-mode label { + font-size: 0.875rem; + color: var(--color-muted); +} + +.inline-theme-editor-code-theme select { + padding: 0.5rem; + border: 1px solid var(--color-border); + background: var(--color-base); + color: var(--color-text); + font-family: var(--font-ui); + font-size: 0.875rem; +} + +.inline-theme-editor-code-theme select:focus { + outline: none; + border-color: var(--color-primary); +} + +.inline-theme-editor-mode { + gap: 0.5rem; +} + +.inline-theme-editor-full-link { + font-size: 0.875rem; + color: var(--color-link); + text-decoration: none; +} + +.inline-theme-editor-full-link:hover { + text-decoration: underline; +} diff --git a/crates/weaver-app/src/components/inline_theme_editor.rs b/crates/weaver-app/src/components/inline_theme_editor.rs new file mode 100644 index 0000000..d2147d1 --- /dev/null +++ b/crates/weaver-app/src/components/inline_theme_editor.rs @@ -0,0 +1,219 @@ +//! Inline theme editor for notebook settings. +//! +//! Provides core 4 colour pickers (background, text, primary, link) plus code theme selector. + +use dioxus::prelude::*; +use weaver_renderer::themes::{BUILTIN_CODE_THEMES, BUILTIN_COLOUR_SCHEMES}; + +use crate::components::toggle_group::{ToggleGroup, ToggleItem}; +use crate::components::HexColourInput; + +/// Strip leading # or 0x from hex colour strings. +fn strip_hex_prefix(s: &str) -> String { + s.trim_start_matches('#') + .trim_start_matches("0x") + .trim_start_matches("0X") + .to_uppercase() +} + +/// Convert mode string to toggle index. +fn mode_to_index(mode: &str) -> std::collections::HashSet { + let idx = match mode { + "light" => 1, + "dark" => 2, + _ => 0, // "auto" or default + }; + std::collections::HashSet::from([idx]) +} + +/// Convert toggle index to mode string. +fn index_to_mode(idx: usize) -> &'static str { + match idx { + 1 => "light", + 2 => "dark", + _ => "auto", + } +} + +const INLINE_THEME_EDITOR_CSS: Asset = asset!("/assets/styling/inline-theme-editor.css"); + +/// Theme values being edited. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct InlineThemeValues { + pub background: String, + pub text: String, + pub primary: String, + pub link: String, + pub code_theme: String, + pub default_mode: String, // "light", "dark", or "auto" +} + +/// Props for InlineThemeEditor. +#[derive(Props, Clone, PartialEq)] +pub struct InlineThemeEditorProps { + /// Current theme values. + pub values: InlineThemeValues, + /// Callback when any value changes. + pub onchange: EventHandler, +} + +/// Inline theme editor with core colour pickers and code theme selector. +#[component] +pub fn InlineThemeEditor(props: InlineThemeEditorProps) -> Element { + let values = props.values.clone(); + + // Filter code themes by current mode for the selector. + let mode = &values.default_mode; + let relevant_code_themes: Vec<_> = BUILTIN_CODE_THEMES + .iter() + .filter(|t| mode == "auto" || t.variant == mode) + .collect(); + + rsx! { + document::Stylesheet { href: INLINE_THEME_EDITOR_CSS } + + div { class: "inline-theme-editor", + h4 { class: "inline-theme-editor-heading", "Theme" } + + // Preset selector. + div { class: "inline-theme-editor-presets", + label { "Start from preset:" } + select { + onchange: { + let onchange = props.onchange.clone(); + let code_theme = props.values.code_theme.clone(); + move |e: Event| { + let preset_id = e.value(); + if let Some(scheme) = BUILTIN_COLOUR_SCHEMES.iter().find(|s| s.id == preset_id) { + onchange.call(InlineThemeValues { + background: strip_hex_prefix(&scheme.colours.base), + text: strip_hex_prefix(&scheme.colours.text), + primary: strip_hex_prefix(&scheme.colours.primary), + link: strip_hex_prefix(&scheme.colours.link), + code_theme: code_theme.clone(), + default_mode: scheme.variant.to_string(), + }); + } + } + }, + option { value: "", "Custom" } + for scheme in BUILTIN_COLOUR_SCHEMES.iter() { + option { + value: "{scheme.id}", + "{scheme.name}" + } + } + } + } + + // Colour pickers. + div { class: "inline-theme-editor-colours", + HexColourInput { + label: Some("Background".to_string()), + value: values.background.clone(), + onchange: { + let values = props.values.clone(); + let onchange = props.onchange.clone(); + move |val| { + let mut v = values.clone(); + v.background = val; + onchange.call(v); + } + }, + } + HexColourInput { + label: Some("Text".to_string()), + value: values.text.clone(), + onchange: { + let values = props.values.clone(); + let onchange = props.onchange.clone(); + move |val| { + let mut v = values.clone(); + v.text = val; + onchange.call(v); + } + }, + } + HexColourInput { + label: Some("Primary".to_string()), + value: values.primary.clone(), + onchange: { + let values = props.values.clone(); + let onchange = props.onchange.clone(); + move |val| { + let mut v = values.clone(); + v.primary = val; + onchange.call(v); + } + }, + } + HexColourInput { + label: Some("Link".to_string()), + value: values.link.clone(), + onchange: { + let values = props.values.clone(); + let onchange = props.onchange.clone(); + move |val| { + let mut v = values.clone(); + v.link = val; + onchange.call(v); + } + }, + } + } + + // Code theme selector. + div { class: "inline-theme-editor-code-theme", + label { "Code theme:" } + select { + value: "{values.code_theme}", + onchange: { + let values = props.values.clone(); + let onchange = props.onchange.clone(); + move |e: Event| { + let mut v = values.clone(); + v.code_theme = e.value(); + onchange.call(v); + } + }, + for theme in relevant_code_themes { + option { + value: "{theme.id}", + "{theme.name}" + } + } + } + } + + // Default mode toggle group. + // Indexes: 0 = auto, 1 = light, 2 = dark + div { class: "inline-theme-editor-mode", + label { "Default mode:" } + ToggleGroup { + horizontal: true, + default_pressed: mode_to_index(&values.default_mode), + on_pressed_change: { + let values = props.values.clone(); + let onchange = props.onchange.clone(); + move |pressed: std::collections::HashSet| { + if let Some(&idx) = pressed.iter().next() { + let mut v = values.clone(); + v.default_mode = index_to_mode(idx).to_string(); + onchange.call(v); + } + } + }, + ToggleItem { index: 0usize, "Auto" } + ToggleItem { index: 1usize, "Light" } + ToggleItem { index: 2usize, "Dark" } + } + } + + // Link to full editor. + // TODO: Enable once theme page exists + // a { href: "/{ident}/themes", class: "inline-theme-editor-full-link", + // "Edit full theme ->" + // } + } + } +} diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index ea3da4e..c74756b 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -362,3 +362,6 @@ pub mod tooltip; mod hex_colour_input; pub use hex_colour_input::HexColourInput; + +mod inline_theme_editor; +pub use inline_theme_editor::{InlineThemeEditor, InlineThemeValues};