diff --git a/src/TextForge.Core/Documents/Document.cs b/src/TextForge.Core/Documents/Document.cs index fecbc66..1363477 100644 --- a/src/TextForge.Core/Documents/Document.cs +++ b/src/TextForge.Core/Documents/Document.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using TextForge.Core.Modules; namespace TextForge.Core.Documents; @@ -12,7 +13,7 @@ public class Document public Guid Id { get; init; } = Guid.NewGuid(); public DocumentMetadata Metadata { get; set; } = new(); public string TemplateName { get; set; } = "Default"; - public List Modules { get; init; } = []; + public ObservableCollection Modules { get; init; } = []; /// /// Currently selected module in the active editing session, if any. @@ -39,7 +40,10 @@ public Document(DocumentMetadata metadata, IEnumerable? modules = null, TemplateName = templateName; if (modules is not null) { - Modules.AddRange(modules); + foreach (var module in modules) + { + Modules.Add(module); + } } } @@ -55,7 +59,8 @@ public void NotifyChanged() /// /// Adds a module to the document hierarchy. - /// If parent is provided, appends to parent.SubModules; otherwise appends to root Modules. + /// If parent is provided, appends to parent.SubModules, expands parent, and keeps parent selected. + /// If parent is null, appends to root Modules and selects the new module. /// /// The module to insert. /// The target parent module, or null to append to the document root. @@ -66,19 +71,23 @@ public Document AddModule(Module newModule, Module? parent = null) if (parent is not null) { + // Auto-expand the parent so the newly added child is visible + parent.IsExpanded = true; parent.SubModules.Add(newModule); + + // Retain selection on the existing parent + SelectModule(parent); } else { Modules.Add(newModule); + // Root addition: select the newly created root module + SelectModule(newModule); } - // Keep the newly added module active for selection/toolbar - SelectModule(newModule); NotifyChanged(); return this; } - /// /// Updates the active module selection, updating all node flags across the tree. /// diff --git a/src/TextForge.Core/Modules/Module.cs b/src/TextForge.Core/Modules/Module.cs index d9fac76..81ac634 100644 --- a/src/TextForge.Core/Modules/Module.cs +++ b/src/TextForge.Core/Modules/Module.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace TextForge.Core.Modules; @@ -8,40 +10,64 @@ namespace TextForge.Core.Modules; /// Represents an independent content element within a document. /// Modules can be nested hierarchically to form compound structures. /// -public class Module +public class Module : INotifyPropertyChanged { + private string _content = string.Empty; + private bool _isSelected; + private bool _isExpanded; + + public event PropertyChangedEventHandler? PropertyChanged; + public Guid Id { get; init; } = Guid.NewGuid(); - /// - /// Archetype identifier used for preset styling and UI representation (e.g., "default-title"). - /// public string Name { get; set; } = "default-text"; - /// - /// Optional key corresponding to a template style rule (e.g., "Title", "Heading", "Body"). - /// public string? StyleKey { get; init; } public ModuleType Type { get; init; } = ModuleType.Text; - public string Content { get; set; } = string.Empty; - + public string Content + { + get => _content; + set + { + if (_content != value) + { + _content = value; + OnPropertyChanged(); + } + } + } - /// - /// Direct visual overrides that take precedence over the template style. - /// public ModuleFeatures Features { get; set; } = ModuleFeatures.Default; - /// - /// Nested child modules (e.g., list items under a section heading). - /// public ObservableCollection SubModules { get; set; } = []; + public bool IsSelected + { + get => _isSelected; + set + { + if (_isSelected != value) + { + _isSelected = value; + OnPropertyChanged(); + } + } + } - /// - /// Ephemeral UI selection state. Retained in-memory during active sessions. - /// - public bool IsSelected { get; set; } + public bool IsExpanded + { + get => _isExpanded; + set + { + if (_isExpanded != value) + { + _isExpanded = value; + OnPropertyChanged(); + } + } + } public Module() { } @@ -59,41 +85,28 @@ public Module( Name = name; } + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + #region Archetype Presets - /// - /// Creates a top-level document title module. - /// public static Module CreateTitle(string text) => new(text, ModuleType.Section, styleKey: "Title", name: "default-title"); - /// - /// Creates a section header module. - /// public static Module CreateHeading(string text) => new(text, ModuleType.Section, styleKey: "Heading", name: "default-heading"); - /// - /// Creates a standard body paragraph module. - /// public static Module CreateParagraph(string text) => new(text, ModuleType.Text, styleKey: "Body", name: "default-paragraph"); - /// - /// Creates a bullet item module with prefixed bullet character. - /// public static Module CreateBulletItem(string text) => new($"• {text.TrimStart('•', ' ')}", ModuleType.Text, styleKey: "Body", name: "default-bullet"); - /// - /// Creates an accent callout banner module. - /// public static Module CreateCallout(string text) => new(text, ModuleType.Text, styleKey: "Callout", name: "default-callout"); - /// - /// Creates an alert module with explicit color and weight overrides. - /// public static Module CreateAlert(string text, string color = "#DC2626") => new(text, ModuleType.Text, features: new ModuleFeatures { diff --git a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml index 84d5682..700349d 100644 --- a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml +++ b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml @@ -27,7 +27,7 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + - - + + - - -