Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/TextForge.Core/Documents/Document.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using TextForge.Core.Modules;

namespace TextForge.Core.Documents;
Expand All @@ -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<Module> Modules { get; init; } = [];
public ObservableCollection<Module> Modules { get; init; } = [];

/// <summary>
/// Currently selected module in the active editing session, if any.
Expand All @@ -39,7 +40,10 @@ public Document(DocumentMetadata metadata, IEnumerable<Module>? modules = null,
TemplateName = templateName;
if (modules is not null)
{
Modules.AddRange(modules);
foreach (var module in modules)
{
Modules.Add(module);
}
}
}

Expand All @@ -55,7 +59,8 @@ public void NotifyChanged()

/// <summary>
/// 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.
/// </summary>
/// <param name="newModule">The module to insert.</param>
/// <param name="parent">The target parent module, or null to append to the document root.</param>
Expand All @@ -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;
}

/// <summary>
/// Updates the active module selection, updating all node flags across the tree.
/// </summary>
Expand Down
87 changes: 50 additions & 37 deletions src/TextForge.Core/Modules/Module.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,73 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace TextForge.Core.Modules;

/// <summary>
/// Represents an independent content element within a document.
/// Modules can be nested hierarchically to form compound structures.
/// </summary>
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();

/// <summary>
/// Archetype identifier used for preset styling and UI representation (e.g., "default-title").
/// </summary>
public string Name { get; set; } = "default-text";

/// <summary>
/// Optional key corresponding to a template style rule (e.g., "Title", "Heading", "Body").
/// </summary>
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();
}
}
}

/// <summary>
/// Direct visual overrides that take precedence over the template style.
/// </summary>
public ModuleFeatures Features { get; set; } = ModuleFeatures.Default;

/// <summary>
/// Nested child modules (e.g., list items under a section heading).
/// </summary>
public ObservableCollection<Module> SubModules { get; set; } = [];

public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged();
}
}
}

/// <summary>
/// Ephemeral UI selection state. Retained in-memory during active sessions.
/// </summary>
public bool IsSelected { get; set; }
public bool IsExpanded
{
get => _isExpanded;
set
{
if (_isExpanded != value)
{
_isExpanded = value;
OnPropertyChanged();
}
}
}

public Module() { }

Expand All @@ -59,41 +85,28 @@ public Module(
Name = name;
}

protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

#region Archetype Presets

/// <summary>
/// Creates a top-level document title module.
/// </summary>
public static Module CreateTitle(string text) =>
new(text, ModuleType.Section, styleKey: "Title", name: "default-title");

/// <summary>
/// Creates a section header module.
/// </summary>
public static Module CreateHeading(string text) =>
new(text, ModuleType.Section, styleKey: "Heading", name: "default-heading");

/// <summary>
/// Creates a standard body paragraph module.
/// </summary>
public static Module CreateParagraph(string text) =>
new(text, ModuleType.Text, styleKey: "Body", name: "default-paragraph");

/// <summary>
/// Creates a bullet item module with prefixed bullet character.
/// </summary>
public static Module CreateBulletItem(string text) =>
new($"• {text.TrimStart('•', ' ')}", ModuleType.Text, styleKey: "Body", name: "default-bullet");

/// <summary>
/// Creates an accent callout banner module.
/// </summary>
public static Module CreateCallout(string text) =>
new(text, ModuleType.Text, styleKey: "Callout", name: "default-callout");

/// <summary>
/// Creates an alert module with explicit color and weight overrides.
/// </summary>
public static Module CreateAlert(string text, string color = "#DC2626") =>
new(text, ModuleType.Text, features: new ModuleFeatures
{
Expand Down
Loading
Loading