//! Notebook create/edit form component. use dioxus::prelude::*; use weaver_api::sh_weaver::notebook::book::Book; use weaver_common::slugify; use crate::components::button::{Button, ButtonVariant}; use crate::components::inline_theme_editor::{InlineThemeEditor, InlineThemeValues}; const NOTEBOOK_EDITOR_CSS: Asset = asset!("/assets/styling/notebook-editor.css"); /// Mode for the notebook editor. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NotebookEditorMode { Create, Edit, } /// Form state for notebook editing. #[derive(Debug, Clone, PartialEq, Default)] pub struct NotebookFormState { pub title: String, pub path: String, pub publish_global: bool, pub tags: Vec, pub tags_input: String, pub content_warnings: Vec, pub content_warnings_input: String, pub rating: Option, pub theme: InlineThemeValues, } impl NotebookFormState { /// Create form state from an existing Book record. pub fn from_book(book: &Book<'_>) -> Self { Self::from_book_with_theme(book, None) } /// Create form state with pre-loaded theme values. pub fn from_book_with_theme(book: &Book<'_>, theme: Option) -> Self { Self { title: book.title.as_ref().map(|t| t.to_string()).unwrap_or_default(), path: book.path.as_ref().map(|p| p.to_string()).unwrap_or_default(), publish_global: book.publish_global.unwrap_or(false), tags: book .tags .as_ref() .map(|t| t.iter().map(|s| s.to_string()).collect()) .unwrap_or_default(), tags_input: String::new(), content_warnings: book .content_warnings .as_ref() .map(|cw| cw.iter().map(|s| s.to_string()).collect()) .unwrap_or_default(), content_warnings_input: String::new(), rating: book.rating.as_ref().map(|r| r.to_string()), theme: theme.unwrap_or_default(), } } } /// Props for NotebookEditor. #[derive(Props, Clone, PartialEq)] pub struct NotebookEditorProps { /// Create or edit mode. pub mode: NotebookEditorMode, /// Initial form state (for edit mode). #[props(default)] pub initial_state: Option, /// Callback on successful save. pub on_save: EventHandler, /// Callback on cancel. pub on_cancel: EventHandler<()>, /// Whether save is in progress. #[props(default = false)] pub saving: bool, /// Error message to display. #[props(default)] pub error: Option, /// Control advanced theme options visibility. /// None = auto (container query), Some(true) = always show, Some(false) = always hide. #[props(default)] pub show_advanced_theme: Option, /// Show content settings (content warnings, rating). #[props(default = false)] pub show_content_settings: bool, } /// Notebook create/edit form. #[component] pub fn NotebookEditor(props: NotebookEditorProps) -> Element { let mut state = use_signal(|| props.initial_state.clone().unwrap_or_default()); // Separate signal for theme editor (InlineThemeEditor writes directly to this). let theme_values = use_signal(|| state.read().theme.clone()); let save_label = match props.mode { NotebookEditorMode::Create => "Create", NotebookEditorMode::Edit => "Save", }; // Generate path from title if empty. let auto_path = { let s = state.read(); if s.path.is_empty() && !s.title.is_empty() { slugify(&s.title) } else { s.path.clone() } }; let can_save = { let s = state.read(); !s.title.trim().is_empty() }; rsx! { document::Stylesheet { href: NOTEBOOK_EDITOR_CSS } div { class: "notebook-editor", div { class: "notebook-editor-form", // Top section - two columns when wide. div { class: "notebook-editor-top", // Left column: title, path. div { class: "notebook-editor-top-left", // Title field. div { class: "notebook-editor-field", label { "Title" } input { r#type: "text", value: "{state.read().title}", placeholder: "My Notebook", oninput: move |e| { state.write().title = e.value(); }, } } // Path field. div { class: "notebook-editor-field", label { "Path" } input { r#type: "text", value: "{auto_path}", placeholder: "my-notebook", oninput: move |e| { state.write().path = e.value(); }, } span { class: "notebook-editor-hint", "URL-friendly identifier. Auto-generated from title if empty." } } } // Right column: publish globally, tags. div { class: "notebook-editor-top-right", // Publish global toggle. div { class: "notebook-editor-field notebook-editor-toggle", label { input { r#type: "checkbox", checked: state.read().publish_global, onchange: move |e| { state.write().publish_global = e.checked(); }, } " Publish globally" } span { class: "notebook-editor-hint", "Enable site.standard.* records for cross-platform discovery." } } // Tags field. div { class: "notebook-editor-field", label { "Tags" } div { class: "notebook-editor-tags", for (i, tag) in state.read().tags.iter().enumerate() { span { key: "{i}", class: "notebook-editor-tag", "{tag}" button { class: "notebook-editor-tag-remove", onclick: move |_| { state.write().tags.remove(i); }, "×" } } } input { r#type: "text", class: "notebook-editor-tags-input", value: "{state.read().tags_input}", placeholder: "Add tag...", oninput: move |e| { state.write().tags_input = e.value(); }, onkeydown: move |e| { if e.key() == Key::Enter || e.key() == Key::Character(",".to_string()) { e.prevent_default(); let tag = state.read().tags_input.trim().to_string(); if !tag.is_empty() { let mut s = state.write(); if !s.tags.contains(&tag) { s.tags.push(tag); } s.tags_input.clear(); } } }, } } } } } // Theme editor. InlineThemeEditor { values: theme_values, show_advanced: props.show_advanced_theme, show_preview: true, } // Content settings (optional). if props.show_content_settings { div { class: "notebook-editor-content-settings", h4 { "Content Settings" } // Content warnings field. div { class: "notebook-editor-field", label { "Content Warnings" } div { class: "notebook-editor-tags", for (i, warning) in state.read().content_warnings.iter().enumerate() { span { key: "{i}", class: "notebook-editor-tag notebook-editor-warning", "{warning}" button { class: "notebook-editor-tag-remove", onclick: move |_| { state.write().content_warnings.remove(i); }, "×" } } } input { r#type: "text", class: "notebook-editor-tags-input", value: "{state.read().content_warnings_input}", placeholder: "Add warning...", oninput: move |e| { state.write().content_warnings_input = e.value(); }, onkeydown: move |e| { if e.key() == Key::Enter || e.key() == Key::Character(",".to_string()) { e.prevent_default(); let warning = state.read().content_warnings_input.trim().to_string(); if !warning.is_empty() { let mut s = state.write(); if !s.content_warnings.contains(&warning) { s.content_warnings.push(warning); } s.content_warnings_input.clear(); } } }, } } span { class: "notebook-editor-hint", "Add content warnings for sensitive material (e.g., violence, adult themes)." } } // Rating field. div { class: "notebook-editor-field", label { "Content Rating" } select { value: state.read().rating.clone().unwrap_or_default(), onchange: move |e: Event| { let val = e.value(); state.write().rating = if val.is_empty() { None } else { Some(val) }; }, option { value: "", "None" } option { value: "general", "General" } option { value: "teen", "Teen" } option { value: "mature", "Mature" } option { value: "adult", "Adult" } } span { class: "notebook-editor-hint", "Age-appropriateness rating for your notebook's content." } } } } // Error display. if let Some(ref err) = props.error { div { class: "notebook-editor-error", "{err}" } } // Actions. div { class: "notebook-editor-actions", Button { variant: ButtonVariant::Primary, onclick: move |_| { let mut form_state = state.read().clone(); form_state.theme = theme_values(); props.on_save.call(form_state); }, disabled: !can_save || props.saving, if props.saving { "Saving..." } else { "{save_label}" } } Button { variant: ButtonVariant::Ghost, onclick: move |_| { props.on_cancel.call(()); }, disabled: props.saving, "Cancel" } } } } } }