From d90c5ae7c1742a76c9086c9bc1ef8f822d51afd7 Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Fri, 28 Aug 2026 03:17:41 +0300 Subject: [PATCH 1/2] feat(templates): implement standard template with blue baseline color and archetype fallback resolution --- src/TextForge.Core/Engine/DocumentEngine.cs | 5 +- .../TemplateResolutionTests.cs | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 tests/TextForge.Tests/TemplateResolutionTests.cs 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/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); + } +} From 4a0f9ff83ba676c40ea8b9849a06a3533d8e996b Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Fri, 28 Aug 2026 03:17:49 +0300 Subject: [PATCH 2/2] feat(templates): implement standard template with blue baseline color and archetype fallback resolution --- .../Templates/DefaultTemblate.cs | 20 ++++++++++++++----- .../Components/ModuleEditorView.axaml.cs | 16 +++++++++++++++ tests/TextForge.Tests/DefaultTemplateTests.cs | 2 +- 3 files changed, 32 insertions(+), 6 deletions(-) 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);