diff --git a/src/TextForge.Core/Engine/DocumentEngine.cs b/src/TextForge.Core/Engine/DocumentEngine.cs
index 9285337..4b67577 100644
--- a/src/TextForge.Core/Engine/DocumentEngine.cs
+++ b/src/TextForge.Core/Engine/DocumentEngine.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using TextForge.Core.Documents;
using TextForge.Core.Modules;
using TextForge.Core.Templates;
@@ -9,6 +6,8 @@ namespace TextForge.Core.Engine;
public class DocumentEngine : IDocumentEngine
{
+ private readonly DefaultTemplate _defaultTemplate = new();
+
public RenderTree Evaluate(Document document, DefaultTemplate template)
{
ArgumentNullException.ThrowIfNull(document);
diff --git a/src/TextForge.Core/Templates/DefaultTemblate.cs b/src/TextForge.Core/Templates/DefaultTemblate.cs
index 9eb2e64..917be84 100644
--- a/src/TextForge.Core/Templates/DefaultTemblate.cs
+++ b/src/TextForge.Core/Templates/DefaultTemblate.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using TextForge.Core.Engine;
using TextForge.Core.Modules;
@@ -8,10 +9,13 @@ public class DefaultTemplate
{
public string Name { get; init; } = "Default";
+ ///
+ /// Global template baseline defaults. Default text color is standard blue.
+ ///
public ModuleFeatures DocumentDefaults { get; init; } = new()
{
Font = "Segoe UI",
- Color = "#1F2937",
+ Color = "#2563EB", // Standard Template Blue
FontWeight = ModuleFontWeight.Normal,
LineSpacing = 1.2
};
@@ -71,7 +75,7 @@ public class DefaultTemplate
}
};
- public Dictionary NamedStyles { get; init; } = new()
+ public Dictionary NamedStyles { get; init; } = new(StringComparer.OrdinalIgnoreCase)
{
["Title"] = new ModuleFeatures
{
@@ -95,7 +99,7 @@ public class DefaultTemplate
}
};
- public Dictionary NamedLayoutStyles { get; init; } = new()
+ public Dictionary NamedLayoutStyles { get; init; } = new(StringComparer.OrdinalIgnoreCase)
{
["Title"] = new LayoutProperties
{
@@ -120,6 +124,8 @@ public class DefaultTemplate
public ModuleFeatures ResolveFeatures(Module module)
{
+ ArgumentNullException.ThrowIfNull(module);
+
var resolved = DocumentDefaults;
if (TypeDefaults.TryGetValue(module.Type, out var typeStyle))
@@ -127,7 +133,8 @@ public ModuleFeatures ResolveFeatures(Module module)
resolved = typeStyle.MergeWith(resolved);
}
- if (module.StyleKey is not null && NamedStyles.TryGetValue(module.StyleKey, out var namedStyle))
+ var key = module.StyleKey ?? module.Name;
+ if (!string.IsNullOrWhiteSpace(key) && NamedStyles.TryGetValue(key, out var namedStyle))
{
resolved = namedStyle.MergeWith(resolved);
}
@@ -137,6 +144,8 @@ public ModuleFeatures ResolveFeatures(Module module)
public LayoutProperties ResolveLayout(Module module)
{
+ ArgumentNullException.ThrowIfNull(module);
+
var resolved = DocumentLayoutDefaults;
if (TypeLayoutDefaults.TryGetValue(module.Type, out var typeLayout))
@@ -144,7 +153,8 @@ public LayoutProperties ResolveLayout(Module module)
resolved = typeLayout.MergeWith(resolved);
}
- if (module.StyleKey is not null && NamedLayoutStyles.TryGetValue(module.StyleKey, out var namedLayout))
+ var key = module.StyleKey ?? module.Name;
+ if (!string.IsNullOrWhiteSpace(key) && NamedLayoutStyles.TryGetValue(key, out var namedLayout))
{
resolved = namedLayout.MergeWith(resolved);
}
diff --git a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs
index 5fad44b..b378ecd 100644
--- a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs
+++ b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs
@@ -1,12 +1,14 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
+using TextForge.Core.Documents;
using TextForge.Core.Modules;
namespace TextForge.Desktop.Views.Components;
public partial class ModuleEditorView : UserControl
{
+ private Document? _document;
public event EventHandler? ModuleMoveUpRequested;
public event EventHandler? ModuleMoveDownRequested;
public event EventHandler? ModuleDeleteRequested;
@@ -39,4 +41,18 @@ private void DeleteModuleButton_Click(object? sender, RoutedEventArgs e)
ModuleDeleteRequested?.Invoke(this, module);
}
}
+ ///
+ /// Binds the editor to a document instance.
+ /// Done once per document load.
+ ///
+ public void SetDocument(Document document)
+ {
+ if (_document == document) return;
+
+ _document = document;
+
+ // Assign the collection directly ONCE.
+ // ObservableCollection will handle all subsequent Add/Remove operations automatically.
+ ModuleListBox.ItemsSource = _document.Modules;
+ }
}
diff --git a/tests/TextForge.Tests/DefaultTemplateTests.cs b/tests/TextForge.Tests/DefaultTemplateTests.cs
index 8c74e2a..dd58ee4 100644
--- a/tests/TextForge.Tests/DefaultTemplateTests.cs
+++ b/tests/TextForge.Tests/DefaultTemplateTests.cs
@@ -19,7 +19,7 @@ public void ResolveFeatures_StandardTextModule_InheritsDocumentAndTypeDefaults()
// Assert: gets baseline font and color from document defaults + type line spacing
Assert.Equal("Segoe UI", features.Font);
- Assert.Equal("#1F2937", features.Color);
+ Assert.Equal("#2563EB", features.Color);
Assert.Equal(ModuleFontWeight.Normal, features.FontWeight);
Assert.Equal(1.2, features.LineSpacing);
Assert.False(features.Italic);
diff --git a/tests/TextForge.Tests/TemplateResolutionTests.cs b/tests/TextForge.Tests/TemplateResolutionTests.cs
new file mode 100644
index 0000000..1f02c9f
--- /dev/null
+++ b/tests/TextForge.Tests/TemplateResolutionTests.cs
@@ -0,0 +1,62 @@
+using Xunit;
+using TextForge.Core.Documents;
+using TextForge.Core.Engine;
+using TextForge.Core.Modules;
+using TextForge.Core.Templates;
+
+namespace TextForge.Tests;
+
+public class TemplateResolutionTests
+{
+ private readonly DocumentEngine _engine = new();
+ private readonly DefaultTemplate _template = new();
+
+ [Fact]
+ public void Evaluate_DefaultDocument_AppliesTemplateBaselineBlueColor()
+ {
+ // Arrange
+ var doc = new Document("Test Showcase");
+ doc.AddModule(Module.CreateTitle("Welcome to TextForge"));
+ doc.AddModule(Module.CreateParagraph("Standard text body."));
+
+ // Act
+ var renderTree = _engine.Evaluate(doc, _template);
+
+ // Assert
+ Assert.Equal(2, renderTree.RootNodes.Count);
+ Assert.Equal("#2563EB", renderTree.RootNodes[0].Features.Color);
+ Assert.Equal("#2563EB", renderTree.RootNodes[1].Features.Color);
+ }
+
+ [Fact]
+ public void Evaluate_ModuleExplicitColorOverride_TakesPrecedenceOverTemplate()
+ {
+ // Arrange
+ var doc = new Document("Alert Test");
+ var alertModule = Module.CreateAlert("Warning Message", color: "#DC2626");
+ doc.AddModule(alertModule);
+
+ // Act
+ var renderTree = _engine.Evaluate(doc, _template);
+
+ // Assert
+ Assert.Single(renderTree.RootNodes);
+ Assert.Equal("#DC2626", renderTree.RootNodes[0].Features.Color);
+ }
+
+ [Fact]
+ public void Evaluate_UnknownOrCustomModule_FallsBackSafelyToBaselineColor()
+ {
+ // Arrange
+ var doc = new Document("Fallback Test");
+ var unknownModule = new Module("Custom content", ModuleType.Custom, name: "unsupported-custom-archetype");
+ doc.AddModule(unknownModule);
+
+ // Act
+ var renderTree = _engine.Evaluate(doc, _template);
+
+ // Assert
+ Assert.Single(renderTree.RootNodes);
+ Assert.Equal("#2563EB", renderTree.RootNodes[0].Features.Color);
+ }
+}