From db95ec7dffda47edcc9088e59a23765288e70349 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:18:49 +0200 Subject: [PATCH 01/33] feat(themes): theme colors record --- HellionChat/Themes/ThemeColors.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 HellionChat/Themes/ThemeColors.cs diff --git a/HellionChat/Themes/ThemeColors.cs b/HellionChat/Themes/ThemeColors.cs new file mode 100644 index 0000000..192ff71 --- /dev/null +++ b/HellionChat/Themes/ThemeColors.cs @@ -0,0 +1,31 @@ +namespace HellionChat.Themes; + +// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui. +public sealed record ThemeColors( + uint PrimaryDark, + uint Primary, + uint PrimaryLight, + uint PrimaryGlow, + + uint AccentDark, + uint Accent, + uint AccentLight, + + uint Identity, + + uint WindowBg, + uint ChildBg, + uint FrameBg, + uint Surface, + uint SurfaceHover, + uint Border, + + uint TextPrimary, + uint TextMuted, + uint TextDim, + + uint StatusSuccess, + uint StatusDanger, + uint StatusWarning, + uint StatusInfo +); From 990edd8300580d9c38e73c6c53e1f3448d054b7c Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:19:03 +0200 Subject: [PATCH 02/33] feat(themes): theme layout record --- HellionChat/Themes/ThemeLayout.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 HellionChat/Themes/ThemeLayout.cs diff --git a/HellionChat/Themes/ThemeLayout.cs b/HellionChat/Themes/ThemeLayout.cs new file mode 100644 index 0000000..116c2a3 --- /dev/null +++ b/HellionChat/Themes/ThemeLayout.cs @@ -0,0 +1,14 @@ +namespace HellionChat.Themes; + +// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht. +public sealed record ThemeLayout( + float WindowRounding, + float ChildRounding, + float PopupRounding, + float FrameRounding, + float GrabRounding, + float TabRounding, + float ScrollbarRounding, + float WindowBorderSize, + float FrameBorderSize +); From fe9e66b0ff3c53cf52e5e49012a975fb575fc239 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:19:18 +0200 Subject: [PATCH 03/33] feat(themes): theme typography record --- HellionChat/Themes/ThemeTypography.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 HellionChat/Themes/ThemeTypography.cs diff --git a/HellionChat/Themes/ThemeTypography.cs b/HellionChat/Themes/ThemeTypography.cs new file mode 100644 index 0000000..b1fbf3a --- /dev/null +++ b/HellionChat/Themes/ThemeTypography.cs @@ -0,0 +1,8 @@ +namespace HellionChat.Themes; + +// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt +// für zukünftige Theme-Slots vorbereitet. +public sealed record ThemeTypography( + float? OverrideGlobalFontSizePt = null, + float? OverrideSymbolsFontSizePt = null +); From 289fe2eb78bb0fae690ab3e3d3e34e7b4aad9f86 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:19:33 +0200 Subject: [PATCH 04/33] feat(themes): theme top-level record --- HellionChat/Themes/Theme.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 HellionChat/Themes/Theme.cs diff --git a/HellionChat/Themes/Theme.cs b/HellionChat/Themes/Theme.cs new file mode 100644 index 0000000..591bf49 --- /dev/null +++ b/HellionChat/Themes/Theme.cs @@ -0,0 +1,12 @@ +namespace HellionChat.Themes; + +public sealed record Theme( + string Slug, + string Name, + string Author, + string Description, + ThemeColors Colors, + ThemeLayout Layout, + ThemeTypography Typography, + bool IsBuiltIn +); From 0b13efd0b5e4c2d6c591955b4b328290e100e148 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:21:46 +0200 Subject: [PATCH 05/33] feat(util): add HexToRgba parser for theme JSON --- HellionChat/Util/ColourUtil.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/HellionChat/Util/ColourUtil.cs b/HellionChat/Util/ColourUtil.cs index 63214f0..3373700 100755 --- a/HellionChat/Util/ColourUtil.cs +++ b/HellionChat/Util/ColourUtil.cs @@ -62,4 +62,20 @@ internal static class ColourUtil { return ((uint) a << 24) | ((uint) nb << 16) | ((uint) ng << 8) | nr; } + + public static uint HexToRgba(string hex) + { + ArgumentNullException.ThrowIfNull(hex); + var s = hex.StartsWith('#') ? hex[1..] : hex; + if (s.Length != 6 && s.Length != 8) + throw new FormatException($"Hex colour must be 6 or 8 hex digits, got {s.Length}: '{hex}'"); + + if (!uint.TryParse(s, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var value)) + throw new FormatException($"Hex colour '{hex}' is not a valid hexadecimal value"); + + if (s.Length == 6) + value = (value << 8) | 0xFFu; // RRGGBB → RRGGBBFF + + return value; + } } From 48f1fb5ba1ef5f4262ea42b0bf13d135e00031b2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:23:51 +0200 Subject: [PATCH 06/33] feat(themes): hellion-arctic built-in theme --- HellionChat/Themes/Builtin/HellionArctic.cs | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/HellionArctic.cs diff --git a/HellionChat/Themes/Builtin/HellionArctic.cs b/HellionChat/Themes/Builtin/HellionArctic.cs new file mode 100644 index 0000000..38ed5b6 --- /dev/null +++ b/HellionChat/Themes/Builtin/HellionArctic.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class HellionArctic +{ + public const string Slug = "hellion-arctic"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Hellion Arctic", + Author: "Hellion Online Media", + Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#0097A7"), + Primary: ColourUtil.HexToRgba("#00BED2"), + PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"), + PrimaryGlow: ColourUtil.HexToRgba("#00BED299"), + + AccentDark: ColourUtil.HexToRgba("#E85D04"), + Accent: ColourUtil.HexToRgba("#F97316"), + AccentLight: ColourUtil.HexToRgba("#FB923C"), + + Identity: ColourUtil.HexToRgba("#0097A7"), + + WindowBg: ColourUtil.HexToRgba("#070B12"), + ChildBg: ColourUtil.HexToRgba("#0C1220"), + FrameBg: ColourUtil.HexToRgba("#141E30"), + Surface: ColourUtil.HexToRgba("#1A2538"), + SurfaceHover: ColourUtil.HexToRgba("#22303F"), + Border: ColourUtil.HexToRgba("#00BED266"), + + TextPrimary: ColourUtil.HexToRgba("#E6F4F1"), + TextMuted: ColourUtil.HexToRgba("#8FA3B5"), + TextDim: ColourUtil.HexToRgba("#566273"), + + StatusSuccess: ColourUtil.HexToRgba("#5CB85C"), + StatusDanger: ColourUtil.HexToRgba("#D9534F"), + StatusWarning: ColourUtil.HexToRgba("#F0AD4E"), + StatusInfo: ColourUtil.HexToRgba("#00BED2") + ), + Layout: new ThemeLayout( + WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f, + FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f, + ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} From d3d28924e64a383a85e9b351099da68d34540ebd Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:24:17 +0200 Subject: [PATCH 07/33] feat(themes): chat2-classic built-in theme --- HellionChat/Themes/Builtin/Chat2Classic.cs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/Chat2Classic.cs diff --git a/HellionChat/Themes/Builtin/Chat2Classic.cs b/HellionChat/Themes/Builtin/Chat2Classic.cs new file mode 100644 index 0000000..f0ba9e8 --- /dev/null +++ b/HellionChat/Themes/Builtin/Chat2Classic.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class Chat2Classic +{ + public const string Slug = "chat2-classic"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Chat 2 Klassik", + Author: "Upstream (Infi & Anna)", + Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#3D6E92"), + Primary: ColourUtil.HexToRgba("#4682B4"), + PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"), + PrimaryGlow: ColourUtil.HexToRgba("#4682B466"), + + AccentDark: ColourUtil.HexToRgba("#3D6E92"), + Accent: ColourUtil.HexToRgba("#4682B4"), + AccentLight: ColourUtil.HexToRgba("#5C9DC8"), + + Identity: ColourUtil.HexToRgba("#4682B4"), + + WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"), + ChildBg: ColourUtil.HexToRgba("#141414"), + FrameBg: ColourUtil.HexToRgba("#1A1A1A"), + Surface: ColourUtil.HexToRgba("#202020"), + SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"), + Border: ColourUtil.HexToRgba("#404040"), + + TextPrimary: ColourUtil.HexToRgba("#E6E6E6"), + TextMuted: ColourUtil.HexToRgba("#999999"), + TextDim: ColourUtil.HexToRgba("#666666"), + + StatusSuccess: ColourUtil.HexToRgba("#5CB85C"), + StatusDanger: ColourUtil.HexToRgba("#D9534F"), + StatusWarning: ColourUtil.HexToRgba("#F0AD4E"), + StatusInfo: ColourUtil.HexToRgba("#4682B4") + ), + Layout: new ThemeLayout( + WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f, + FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f, + ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} From 537b96c79f639375894186d49c76cf40dcdf1e95 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:24:39 +0200 Subject: [PATCH 08/33] feat(themes): event-horizon built-in theme --- HellionChat/Themes/Builtin/EventHorizon.cs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/EventHorizon.cs diff --git a/HellionChat/Themes/Builtin/EventHorizon.cs b/HellionChat/Themes/Builtin/EventHorizon.cs new file mode 100644 index 0000000..ac3b625 --- /dev/null +++ b/HellionChat/Themes/Builtin/EventHorizon.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class EventHorizon +{ + public const string Slug = "event-horizon"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Event Horizon", + Author: "Hellion Online Media", + Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"), + Primary: ColourUtil.HexToRgba("#9D5CFF"), + PrimaryLight: ColourUtil.HexToRgba("#B585FF"), + PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"), + + AccentDark: ColourUtil.HexToRgba("#C9982E"), + Accent: ColourUtil.HexToRgba("#E0AB36"), + AccentLight: ColourUtil.HexToRgba("#F2C25C"), + + Identity: ColourUtil.HexToRgba("#9D5CFF"), + + WindowBg: ColourUtil.HexToRgba("#040308"), + ChildBg: ColourUtil.HexToRgba("#0A081A"), + FrameBg: ColourUtil.HexToRgba("#140F23"), + Surface: ColourUtil.HexToRgba("#1B1530"), + SurfaceHover: ColourUtil.HexToRgba("#251D40"), + Border: ColourUtil.HexToRgba("#9D5CFF44"), + + TextPrimary: ColourUtil.HexToRgba("#E6E0F5"), + TextMuted: ColourUtil.HexToRgba("#9890B5"), + TextDim: ColourUtil.HexToRgba("#5A5570"), + + StatusSuccess: ColourUtil.HexToRgba("#26A269"), + StatusDanger: ColourUtil.HexToRgba("#ED333B"), + StatusWarning: ColourUtil.HexToRgba("#E0AB36"), + StatusInfo: ColourUtil.HexToRgba("#9D5CFF") + ), + Layout: new ThemeLayout( + WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f, + FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f, + ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} From cbfdfe35bedfae01c3831f287f24c1f11487c78d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:25:00 +0200 Subject: [PATCH 09/33] feat(themes): moonlit-bloom built-in theme --- HellionChat/Themes/Builtin/MoonlitBloom.cs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/MoonlitBloom.cs diff --git a/HellionChat/Themes/Builtin/MoonlitBloom.cs b/HellionChat/Themes/Builtin/MoonlitBloom.cs new file mode 100644 index 0000000..07b700d --- /dev/null +++ b/HellionChat/Themes/Builtin/MoonlitBloom.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class MoonlitBloom +{ + public const string Slug = "moonlit-bloom"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Moonlit Bloom", + Author: "Hellion Online Media", + Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#C957D0"), + Primary: ColourUtil.HexToRgba("#E374E8"), + PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"), + PrimaryGlow: ColourUtil.HexToRgba("#E374E899"), + + AccentDark: ColourUtil.HexToRgba("#7AAC5C"), + Accent: ColourUtil.HexToRgba("#9CCB7C"), + AccentLight: ColourUtil.HexToRgba("#B6E297"), + + Identity: ColourUtil.HexToRgba("#E374E8"), + + WindowBg: ColourUtil.HexToRgba("#0E0C1F"), + ChildBg: ColourUtil.HexToRgba("#15122B"), + FrameBg: ColourUtil.HexToRgba("#1F1A38"), + Surface: ColourUtil.HexToRgba("#28224A"), + SurfaceHover: ColourUtil.HexToRgba("#332B5B"), + Border: ColourUtil.HexToRgba("#E374E844"), + + TextPrimary: ColourUtil.HexToRgba("#ECE6F5"), + TextMuted: ColourUtil.HexToRgba("#9A8BB0"), + TextDim: ColourUtil.HexToRgba("#554B6E"), + + StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"), + StatusDanger: ColourUtil.HexToRgba("#E85C6A"), + StatusWarning: ColourUtil.HexToRgba("#E8B590"), + StatusInfo: ColourUtil.HexToRgba("#6278FF") + ), + Layout: new ThemeLayout( + WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f, + FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f, + ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} From 4c6d52e6520293a4d5a573c0265dde0b4fd19ed1 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:25:22 +0200 Subject: [PATCH 10/33] feat(themes): mint-grove built-in theme --- HellionChat/Themes/Builtin/MintGrove.cs | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HellionChat/Themes/Builtin/MintGrove.cs diff --git a/HellionChat/Themes/Builtin/MintGrove.cs b/HellionChat/Themes/Builtin/MintGrove.cs new file mode 100644 index 0000000..727d30c --- /dev/null +++ b/HellionChat/Themes/Builtin/MintGrove.cs @@ -0,0 +1,50 @@ +using HellionChat.Util; + +namespace HellionChat.Themes.Builtin; + +internal static class MintGrove +{ + public const string Slug = "mint-grove"; + + public static Theme Build() => new( + Slug: Slug, + Name: "Mint Grove", + Author: "Hellion Online Media", + Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.", + Colors: new ThemeColors( + PrimaryDark: ColourUtil.HexToRgba("#3CB371"), + Primary: ColourUtil.HexToRgba("#5DD39E"), + PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"), + PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"), + + AccentDark: ColourUtil.HexToRgba("#F4C870"), + Accent: ColourUtil.HexToRgba("#F9D580"), + AccentLight: ColourUtil.HexToRgba("#FCDD93"), + + Identity: ColourUtil.HexToRgba("#5DD39E"), + + WindowBg: ColourUtil.HexToRgba("#0A1410"), + ChildBg: ColourUtil.HexToRgba("#10201A"), + FrameBg: ColourUtil.HexToRgba("#162B22"), + Surface: ColourUtil.HexToRgba("#1E372B"), + SurfaceHover: ColourUtil.HexToRgba("#284335"), + Border: ColourUtil.HexToRgba("#5DD39E55"), + + TextPrimary: ColourUtil.HexToRgba("#E8F5EA"), + TextMuted: ColourUtil.HexToRgba("#9BB5A5"), + TextDim: ColourUtil.HexToRgba("#5C6F65"), + + StatusSuccess: ColourUtil.HexToRgba("#5DD39E"), + StatusDanger: ColourUtil.HexToRgba("#D9534F"), + StatusWarning: ColourUtil.HexToRgba("#E8B590"), + StatusInfo: ColourUtil.HexToRgba("#5DA9C7") + ), + Layout: new ThemeLayout( + WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f, + FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f, + ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f + ), + Typography: new ThemeTypography(), + IsBuiltIn: true + ); +} From cae7d762069c437c7c93d6bf84344beaf0bd7350 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:27:50 +0200 Subject: [PATCH 11/33] feat(themes): theme registry with built-in lookup and fallback --- HellionChat/Themes/ThemeRegistry.cs | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 HellionChat/Themes/ThemeRegistry.cs diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs new file mode 100644 index 0000000..efb1768 --- /dev/null +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -0,0 +1,42 @@ +using HellionChat.Themes.Builtin; + +namespace HellionChat.Themes; + +public sealed class ThemeRegistry +{ + public const string DefaultSlug = HellionArctic.Slug; + + private readonly Dictionary _builtIns; + private Theme _active; + + public ThemeRegistry() + { + _builtIns = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { HellionArctic.Slug, HellionArctic.Build() }, + { Chat2Classic.Slug, Chat2Classic.Build() }, + { EventHorizon.Slug, EventHorizon.Build() }, + { MoonlitBloom.Slug, MoonlitBloom.Build() }, + { MintGrove.Slug, MintGrove.Build() }, + }; + _active = _builtIns[DefaultSlug]; + } + + public Theme Active => _active; + + // Active-Lookup: liefert das angeforderte Theme oder fällt auf Hellion Arctic + // zurück, wenn der Slug unbekannt ist. + public Theme Get(string slug) + { + return _builtIns.TryGetValue(slug, out var theme) + ? theme + : _builtIns[DefaultSlug]; + } + + public IEnumerable AllBuiltIns() => _builtIns.Values; + + public void Switch(string slug) + { + _active = Get(slug); + } +} From b85db2460106827e5e216e70def31fa0f82754ab Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 10:28:08 +0200 Subject: [PATCH 12/33] test(themes): sanity tests for all built-in themes From 2378ce6bf2de9bf37d3d666391de3d0dfbfac96e Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 13:42:40 +0200 Subject: [PATCH 13/33] feat(themes): json loader with schema validation --- HellionChat/Themes/ThemeJsonLoader.cs | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 HellionChat/Themes/ThemeJsonLoader.cs diff --git a/HellionChat/Themes/ThemeJsonLoader.cs b/HellionChat/Themes/ThemeJsonLoader.cs new file mode 100644 index 0000000..d73200f --- /dev/null +++ b/HellionChat/Themes/ThemeJsonLoader.cs @@ -0,0 +1,106 @@ +using System.Text.Json; +using HellionChat.Util; + +namespace HellionChat.Themes; + +internal static class ThemeJsonLoader +{ + public const int SupportedSchemaVersion = 1; + + public static Theme LoadFromString(string json) + { + if (string.IsNullOrWhiteSpace(json)) + throw new FormatException("Theme JSON is empty"); + + JsonDocument doc; + try { doc = JsonDocument.Parse(json); } + catch (JsonException ex) { throw new FormatException("Theme JSON is not valid JSON", ex); } + + using (doc) + { + var root = doc.RootElement; + + var schemaVersion = ReadInt(root, "schemaVersion"); + if (schemaVersion != SupportedSchemaVersion) + throw new FormatException($"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}"); + + var slug = ReadString(root, "slug"); + var name = ReadString(root, "name"); + var author = ReadString(root, "author"); + var description = ReadString(root, "description"); + + var colors = ReadColors(root.GetProperty("colors")); + var layout = ReadLayout(root.GetProperty("layout")); + + return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false); + } + } + + public static Theme LoadFromFile(string path) + { + var json = File.ReadAllText(path); + return LoadFromString(json); + } + + private static ThemeColors ReadColors(JsonElement el) => new( + PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")), + Primary: ColourUtil.HexToRgba(ReadString(el, "primary")), + PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")), + PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")), + + AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")), + Accent: ColourUtil.HexToRgba(ReadString(el, "accent")), + AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")), + + Identity: ColourUtil.HexToRgba(ReadString(el, "identity")), + + WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")), + ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")), + FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")), + Surface: ColourUtil.HexToRgba(ReadString(el, "surface")), + SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")), + Border: ColourUtil.HexToRgba(ReadString(el, "border")), + + TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")), + TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")), + TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")), + + StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")), + StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")), + StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")), + StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo")) + ); + + private static ThemeLayout ReadLayout(JsonElement el) => new( + WindowRounding: ReadFloat(el, "windowRounding"), + ChildRounding: ReadFloat(el, "childRounding"), + PopupRounding: ReadFloat(el, "popupRounding"), + FrameRounding: ReadFloat(el, "frameRounding"), + GrabRounding: ReadFloat(el, "grabRounding"), + TabRounding: ReadFloat(el, "tabRounding"), + ScrollbarRounding: ReadFloat(el, "scrollbarRounding"), + WindowBorderSize: ReadFloat(el, "windowBorderSize"), + FrameBorderSize: ReadFloat(el, "frameBorderSize") + ); + + private static string ReadString(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.String) + throw new FormatException($"Theme JSON missing string property '{name}'"); + return v.GetString() ?? throw new FormatException($"Theme JSON property '{name}' is null"); + } + + private static int ReadInt(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number) + throw new FormatException($"Theme JSON missing number property '{name}'"); + return v.GetInt32(); + } + + private static float ReadFloat(JsonElement el, string name) + { + if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number) + throw new FormatException($"Theme JSON missing number property '{name}'"); + return (float)v.GetDouble(); + } +} From 4bf6c3ef1f6276e994df5bfd85c2143755feeb53 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 13:44:15 +0200 Subject: [PATCH 14/33] feat(themes): custom theme loading with file-stamp cache --- HellionChat/Themes/ThemeRegistry.cs | 70 +++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/HellionChat/Themes/ThemeRegistry.cs b/HellionChat/Themes/ThemeRegistry.cs index efb1768..3e14c84 100644 --- a/HellionChat/Themes/ThemeRegistry.cs +++ b/HellionChat/Themes/ThemeRegistry.cs @@ -7,9 +7,11 @@ public sealed class ThemeRegistry public const string DefaultSlug = HellionArctic.Slug; private readonly Dictionary _builtIns; + private readonly Dictionary _customCache = new(StringComparer.OrdinalIgnoreCase); + private readonly string? _customThemesDir; private Theme _active; - public ThemeRegistry() + public ThemeRegistry(string? customThemesDir = null) { _builtIns = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -20,23 +22,75 @@ public sealed class ThemeRegistry { MintGrove.Slug, MintGrove.Build() }, }; _active = _builtIns[DefaultSlug]; + _customThemesDir = customThemesDir; } public Theme Active => _active; - // Active-Lookup: liefert das angeforderte Theme oder fällt auf Hellion Arctic - // zurück, wenn der Slug unbekannt ist. public Theme Get(string slug) { - return _builtIns.TryGetValue(slug, out var theme) - ? theme - : _builtIns[DefaultSlug]; + if (_builtIns.TryGetValue(slug, out var b)) return b; + + var custom = LoadCustomBySlug(slug); + if (custom != null) return custom; + + return _builtIns[DefaultSlug]; } public IEnumerable AllBuiltIns() => _builtIns.Values; - public void Switch(string slug) + public IEnumerable AllCustom() => RefreshCustomCache(); + + public void Switch(string slug) => _active = Get(slug); + + // Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit + // LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup + // neu eingelesen. + private Theme? LoadCustomBySlug(string slug) { - _active = Get(slug); + if (_customThemesDir is null) return null; + if (!Directory.Exists(_customThemesDir)) return null; + + foreach (var theme in RefreshCustomCache()) + if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase)) + return theme; + return null; + } + + private IEnumerable RefreshCustomCache() + { + if (_customThemesDir is null || !Directory.Exists(_customThemesDir)) + yield break; + + var seenSlugs = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var path in Directory.EnumerateFiles(_customThemesDir, "*.json")) + { + Theme? theme = null; + var stamp = File.GetLastWriteTimeUtc(path); + var key = path; + if (_customCache.TryGetValue(key, out var cached) && cached.Stamp == stamp) + { + theme = cached.Theme; + } + else + { + try + { + theme = ThemeJsonLoader.LoadFromFile(path); + _customCache[key] = (theme, stamp); + } + catch (Exception ex) + { + // Logging passiert in Plugin.cs durch den Aufrufer; hier still + // ignorieren, damit ein einzelnes kaputtes JSON nicht alle + // Custom-Themes blockt. + _ = ex; + continue; + } + } + + if (theme is not null && seenSlugs.Add(theme.Slug)) + yield return theme; + } } } From dd3a0ea0692a0ac8382dd4260566ba256b0dec3f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 13:51:31 +0200 Subject: [PATCH 15/33] =?UTF-8?q?feat(themes):=20wire=20theme=20engine=20i?= =?UTF-8?q?nto=20plugin=20draw=20pipeline=20+=20migrate=20v13=E2=86=92v14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HellionStyle.PushGlobal nimmt jetzt eine Theme-Instance + Window-Opacity und liest alle Color- und Style-Slots aus dem aktiven Theme statt aus einer fixen Konstanten-Tabelle. Plugin hält die ThemeRegistry und schaltet beim Init auf das in Config.Theme gespeicherte Slug. Configuration v13 → v14: - Neue Felder Theme (slug), WindowOpacity, ReduceMotion, UseCompactDensity, ShowThemeQuickPicker - HellionThemeEnabled und HellionThemeWindowOpacity sind ab v14 [Obsolete] und bleiben bis v1.2.0 als JSON-Safety-Net erhalten - Migration setzt alle Bestandsuser auf hellion-arctic; chat2-classic bleibt im Themes-Tab als Upstream-Look wählbar - WindowOpacity übernimmt den Wert von HellionThemeWindowOpacity, alte HellionThemeEnabled-Flag entfällt funktional (Theme-Engine ist immer aktiv) Konsumenten der alten Felder (ChatLogWindow.BgAlpha, Popout.BgAlpha) lesen jetzt das neue WindowOpacity. Die Settings-UI in Appearance.cs schreibt übergangsweise weiter in die Obsolete-Felder; Phase J ersetzt diesen Block durch den dedizierten Themes-Tab. CS0612/CS0618 sind dort gezielt mit pragma gekapselt. --- HellionChat/Configuration.cs | 26 ++- HellionChat/Plugin.cs | 39 +++- HellionChat/Ui/ChatLogWindow.cs | 4 +- HellionChat/Ui/HellionStyle.cs | 259 +++++++--------------- HellionChat/Ui/Popout.cs | 4 +- HellionChat/Ui/SettingsTabs/Appearance.cs | 8 + 6 files changed, 153 insertions(+), 187 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index fbe7fdc..da3b4d6 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -34,10 +34,23 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 13; + private const int LatestVersion = 14; public int Version { get; set; } = LatestVersion; + // v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt. + public string Theme = "hellion-arctic"; + + // v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus + // HellionThemeWindowOpacity beim Bump v13 → v14. + public float WindowOpacity = 0.85f; + + // v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden + // vorab angelegt, damit später keine Migration nötig ist. + public bool ReduceMotion; + public bool UseCompactDensity; + public bool ShowThemeQuickPicker; + // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default). // Master-switch defaults to true; set false to restore upstream behavior. public bool PrivacyFilterEnabled = true; @@ -70,12 +83,14 @@ public class Configuration : IPluginConfiguration // Hellion Chat global ImGui theme — applied to every plugin window in // Plugin.Draw. Default ON; users who prefer the upstream Dalamud look // can flip this off in the Privacy tab. + [Obsolete("Replaced by Theme slug + WindowOpacity in v14")] public bool HellionThemeEnabled = true; // Window background opacity, 0.5–1.0. Lower values make the plugin // panes more glass-like so the game shines through. Default 0.5 // matches the maintainer's daily-driver preference; users who want // a less translucent look bump it up in Aussehen → Theme. + [Obsolete("Replaced by WindowOpacity in v14")] public float HellionThemeWindowOpacity = 0.5f; // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font @@ -321,10 +336,19 @@ public class Configuration : IPluginConfiguration RetentionLastRunAt = other.RetentionLastRunAt; FirstRunCompleted = other.FirstRunCompleted; +#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten HellionThemeEnabled = other.HellionThemeEnabled; HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; +#pragma warning restore CS0612, CS0618 UseHellionFont = other.UseHellionFont; + // v1.1.0 theme engine fields + Theme = other.Theme; + WindowOpacity = other.WindowOpacity; + ReduceMotion = other.ReduceMotion; + UseCompactDensity = other.UseCompactDensity; + ShowThemeQuickPicker = other.ShowThemeQuickPicker; + EnableAutoTellTabs = other.EnableAutoTellTabs; AutoTellTabsLimit = other.AutoTellTabsLimit; AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 992600a..e5e7770 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -63,6 +63,7 @@ public sealed class Plugin : IDalamudPlugin internal ExtraChat ExtraChat { get; } internal TypingIpc TypingIpc { get; } internal FontManager FontManager { get; } + internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!; internal int DeferredSaveFrames = -1; @@ -237,6 +238,27 @@ public sealed class Plugin : IDalamudPlugin }); } + // Hellion Chat v13 → v14 — theme-engine migration. Alle User landen + // auf "hellion-arctic" als neues Default-Theme; die alte + // HellionThemeEnabled-Flag wird deprecated und nur noch ein Release + // als Safety-Net im JSON behalten. Window-Opacity wandert von + // HellionThemeWindowOpacity in das neue WindowOpacity-Feld. + if (Config.Version < 14) + { + Config.Theme = "hellion-arctic"; + #pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0 + Config.WindowOpacity = Config.HellionThemeWindowOpacity; + #pragma warning restore CS0612, CS0618 + Config.ReduceMotion = false; + Config.UseCompactDensity = false; + Config.ShowThemeQuickPicker = false; + Config.Version = 14; + SaveConfig(); + Log.Information( + "Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " + + "pick chat2-classic in Settings → Themes for the upstream look"); + } + // Hellion v1.0.0 default tab layout. Five thematically separated // tabs: General catches the immediate-surroundings public chat // (Say/Yell/Shout) only; System absorbs the rest of the technical @@ -266,6 +288,12 @@ public sealed class Plugin : IDalamudPlugin ExtraChat = new ExtraChat(); FontManager = new FontManager(); + // v1.1.0 — Theme-Engine init. Custom-Themes liegen in + // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. + var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); + ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); + ThemeRegistry.Switch(Config.Theme); + MessageManager = new MessageManager(this); // Does it require UI? // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the @@ -559,13 +587,10 @@ public sealed class Plugin : IDalamudPlugin private void Draw() { - // Hellion theme is pushed once per frame here so every plugin window - // (chat log, settings, viewers, wizard, file dialog) renders with - // the same palette. Skipping the push leaves the upstream Dalamud - // look untouched for users who flipped the toggle off. - using IDisposable? _style = Config.HellionThemeEnabled - ? HellionStyle.PushGlobal(Config.HellionThemeWindowOpacity) - : null; + // Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes + // Theme statt einem deaktivierten Hellion-Theme. Active wird einmal + // pro Frame aus der Registry gelesen. + using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity); ChatLogWindow.BeginFrame(); diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index cd7c8db..f5fda68 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -494,9 +494,7 @@ public sealed class ChatLogWindow : Window Flags |= ImGuiWindowFlags.NoTitleBar; if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked) - BgAlpha = Plugin.Config.HellionThemeEnabled - ? Plugin.Config.HellionThemeWindowOpacity - : Plugin.Config.WindowAlpha / 100f; + BgAlpha = Plugin.Config.WindowOpacity; LastViewport = ImGui.GetWindowViewport().Handle; WasDocked = ImGui.IsWindowDocked(); diff --git a/HellionChat/Ui/HellionStyle.cs b/HellionChat/Ui/HellionStyle.cs index 026c464..39d839b 100644 --- a/HellionChat/Ui/HellionStyle.cs +++ b/HellionChat/Ui/HellionStyle.cs @@ -1,3 +1,4 @@ +using HellionChat.Themes; using HellionChat.Util; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; @@ -5,207 +6,119 @@ using Dalamud.Interface.Utility.Raii; namespace HellionChat.Ui; /// -/// ImGui style override for Hellion Chat. Industrial HUD palette with three -/// distinct accents — cyan-teal as the primary action color, industrial -/// amber for active state highlights, slate-violet for title bars and -/// active tabs — on a deep-slate frame background with steel borders. -/// -/// Two entry points: -/// Push — local color stack, scoped via using-block. Use inside -/// Hellion-only surfaces (Privacy tab, first-run wizard). -/// PushGlobal — full color + style variable stack. Pushed once per frame -/// in Plugin.Draw so every Hellion-rendered window inherits -/// the look. Cheap to pop because ImGui keeps its own stack. +/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine +/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window- +/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme +/// gelesen statt aus einer fixen Konstanten-Tabelle. /// internal static class HellionStyle { - // Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs - // Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui - // expects. Hex values are sourced from the Hellion Online Media brand - // guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo). - - // Primary — Arctic Cyan, used for every interactive control (buttons, - // checks, sliders, separators when hovered). Three brand stages plus a - // hover that lifts to brand-color-light and a press that drops to - // brand-color-dark. - private const uint PrimaryRgba = 0x00BED2FF; // brand-color - private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light - private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark - - // Identity — brand-color-dark teal for window title bars and the - // active tab. Sits visibly below the primary cyan on buttons so the - // user sees "where am I" (deep teal) versus "what can I click" - // (brand cyan) without leaving the cyan family. - private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark - private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light - private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab - - // Accent — Ember Orange for warm highlights on grips and scrollbar - // pulls. Replaces the previous industrial amber so the plugin matches - // the website's CTA palette. AccentActive is reserved for any future - // pressed-state on accent surfaces; the current slots only need - // AccentRgba and AccentHoverRgba. - private const uint AccentRgba = 0xF97316FF; // accent-color - private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light - - // Surfaces — Hellion brand background ladder. Window darkest, frame - // hover ladder climbs into surface tones. Matches the website's - // background / background-medium / background-light / surface vars. - private const uint WindowBgRgba = 0x070B12FF; // background - private const uint ChildBgRgba = 0x0C1220FF; // background-medium - private const uint PopupBgRgba = 0x0C1220FF; // background-medium - private const uint FrameBgRgba = 0x141E30FF; // background-light - private const uint FrameBgHoverRgba = 0x1A2538FF; // surface - private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover - // Cyan-tinted border — matches website --border-brand (cyan @ 40% α). - private const uint BorderRgba = 0x00BED266; - private const uint BorderShadowRgba = 0x00000000; - - // Headers / collapsing-headers / tree nodes / selectables — same - // surface ladder as frames so panels feel consistent. - private const uint HeaderRgba = 0x141E30FF; - private const uint HeaderHoverRgba = 0x1A2538FF; - private const uint HeaderActiveRgba = 0x22303FFF; - - // Title bars — Identity teal on active so the focused window reads - // as "yours" without using accent or primary slots. - private const uint TitleBgRgba = 0x070B12FF; - private const uint TitleBgActiveRgba = IdentityRgba; - private const uint TitleBgCollapsedRgba = 0x05080EFF; - - // Tabs — neutral inactive, Identity-light on hover, Identity teal on - // active. Unfocused-active uses the deeper Identity stage so an - // unfocused window's active tab still reads but does not pull focus. - private const uint TabRgba = 0x141E30FF; - private const uint TabHoveredRgba = IdentityHoverRgba; - private const uint TabActiveRgba = IdentityRgba; - private const uint TabUnfocusedRgba = 0x0C1220FF; - private const uint TabUnfocusedActiveRgba = IdentityDeepRgba; - - // Scrollbar — Ember on grab so the pull stands out without competing - // with the cyan action buttons. Idle grab is a subtle surface tone, - // hover/active climb into accent. - private const uint ScrollbarBgRgba = 0x070B12FF; - private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover - private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba; - private const uint ScrollbarGrabActiveRgba = AccentRgba; - - // Resize grip — same Ember treatment as the scrollbar. - private const uint ResizeGripRgba = 0x141E30FF; - private const uint ResizeGripHoveredRgba = AccentHoverRgba; - private const uint ResizeGripActiveRgba = AccentRgba; - - // Separator and check mark / slider follow the primary cyan. - /// - /// Local color stack for Hellion-only surfaces. Cheap. Use inside a - /// `using var _ = HellionStyle.Push();` block. + /// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a + /// `using var _ = HellionStyle.Push(theme);` block. /// - internal static IDisposable Push() + internal static IDisposable Push(Theme theme) { + var c = theme.Colors; var stack = new StackHandle(); - stack.PushColor(ImGuiCol.Button, PrimaryRgba); - stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba); - stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba); - stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba); - stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba); - stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba); - stack.PushColor(ImGuiCol.Border, BorderRgba); - stack.PushColor(ImGuiCol.Header, HeaderRgba); - stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba); - stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba); - stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba); + stack.PushColor(ImGuiCol.Button, c.Primary); + stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark); + stack.PushColor(ImGuiCol.FrameBg, c.FrameBg); + stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.FrameBgActive, c.Surface); + stack.PushColor(ImGuiCol.Border, c.Border); + stack.PushColor(ImGuiCol.Header, c.Surface); + stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.HeaderActive, c.Identity); + stack.PushColor(ImGuiCol.CheckMark, c.Primary); + stack.PushColor(ImGuiCol.SliderGrab, c.Primary); + stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight); return stack; } /// /// Global color and style-variable stack pushed once per frame in - /// Plugin.Draw. Covers every ImGui surface the plugin renders so the - /// Hellion look is consistent across upstream and Hellion tabs. + /// Plugin.Draw. Drives every Hellion-rendered window from the active + /// theme's palette and layout values. /// - /// Window background alpha (0.5–1.0). Lower - /// values let the game shine through the plugin panes. - internal static IDisposable PushGlobal(float windowOpacity = 1.0f) + /// Active theme from ThemeRegistry. + /// Window background alpha (0.5–1.0). + internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f) { + var c = theme.Colors; + var l = theme.Layout; var stack = new StackHandle(); - // Mix the configured opacity into both the outer window and the - // inner content child backgrounds — without ChildBg following the - // slider the chat log stays opaque inside even when the user - // wants to see the game behind it during combat. Form fields and - // popups (FrameBg, PopupBg) still stay opaque so input is readable. var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF); - var windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte; - var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte; + var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte; + var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | alphaByte; - // Layout — geometric edges, modest rounding, single-pixel borders. - stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f); - stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f); - stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f); - stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f); - stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f); - stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); + // Layout + stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding); + stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding); + stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding); + stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding); + stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding); + stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding); + stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding); + stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize); + stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize); - // Surfaces. - stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha); - stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha); - stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba); - stack.PushColor(ImGuiCol.Border, BorderRgba); - stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba); + // Surfaces + stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha); + stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha); + stack.PushColor(ImGuiCol.PopupBg, c.ChildBg); + stack.PushColor(ImGuiCol.Border, c.Border); + stack.PushColor(ImGuiCol.BorderShadow, 0u); - // Frames (input fields, combos, sliders). - stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba); - stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba); - stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba); + // Frames + stack.PushColor(ImGuiCol.FrameBg, c.FrameBg); + stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.FrameBgActive, c.Surface); - // Title bars — tertiary identity on active. - stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba); - stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba); - stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba); + // Title bars + stack.PushColor(ImGuiCol.TitleBg, c.WindowBg); + stack.PushColor(ImGuiCol.TitleBgActive, c.Identity); + stack.PushColor(ImGuiCol.TitleBgCollapsed, c.WindowBg); - // Buttons — primary cyan. - stack.PushColor(ImGuiCol.Button, PrimaryRgba); - stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba); - stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba); + // Buttons + stack.PushColor(ImGuiCol.Button, c.Primary); + stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark); - // Headers / selectables — slate with subtle steps. - stack.PushColor(ImGuiCol.Header, HeaderRgba); - stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba); - stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba); + // Headers / selectables + stack.PushColor(ImGuiCol.Header, c.Surface); + stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover); + stack.PushColor(ImGuiCol.HeaderActive, c.Identity); - // Tabs — tertiary identity for the active tab. - stack.PushColor(ImGuiCol.Tab, TabRgba); - stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba); - stack.PushColor(ImGuiCol.TabActive, TabActiveRgba); - stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba); - stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba); + // Tabs + stack.PushColor(ImGuiCol.Tab, c.FrameBg); + stack.PushColor(ImGuiCol.TabHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.TabActive, c.Identity); + stack.PushColor(ImGuiCol.TabUnfocused, c.ChildBg); + stack.PushColor(ImGuiCol.TabUnfocusedActive, c.PrimaryDark); - // Scrollbar. - stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba); - stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba); - stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba); - stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba); + // Scrollbar + stack.PushColor(ImGuiCol.ScrollbarBg, c.WindowBg); + stack.PushColor(ImGuiCol.ScrollbarGrab, c.Surface); + stack.PushColor(ImGuiCol.ScrollbarGrabHovered, c.AccentLight); + stack.PushColor(ImGuiCol.ScrollbarGrabActive, c.Accent); - // Resize grip — secondary amber on active. - stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba); - stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba); - stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba); + // Resize grip + stack.PushColor(ImGuiCol.ResizeGrip, c.FrameBg); + stack.PushColor(ImGuiCol.ResizeGripHovered, c.AccentLight); + stack.PushColor(ImGuiCol.ResizeGripActive, c.Accent); - // Check mark + slider grab — primary cyan. - stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba); - stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba); + // Check mark + slider grab + stack.PushColor(ImGuiCol.CheckMark, c.Primary); + stack.PushColor(ImGuiCol.SliderGrab, c.Primary); + stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight); - // Separator — primary cyan when hovered/active so the eye - // immediately sees that splitters are interactive. - stack.PushColor(ImGuiCol.Separator, BorderRgba); - stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba); - stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba); + // Separator + stack.PushColor(ImGuiCol.Separator, c.Border); + stack.PushColor(ImGuiCol.SeparatorHovered, c.PrimaryLight); + stack.PushColor(ImGuiCol.SeparatorActive, c.Primary); return stack; } diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index a3944ff..f86c980 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -103,9 +103,7 @@ internal class Popout : Window } else { - BgAlpha = Plugin.Config.HellionThemeEnabled - ? Plugin.Config.HellionThemeWindowOpacity - : Plugin.Config.WindowAlpha / 100f; + BgAlpha = Plugin.Config.WindowOpacity; } } } diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index 6517f74..ce09f4c 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -45,6 +45,13 @@ internal sealed class Appearance : ISettingsTab using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { + // v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten + // Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten, + // damit die Settings-Seite kompiliert; sie schreiben in die mit + // [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net + // bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen + // gezielt für diesen Übergangs-Block. +#pragma warning disable CS0612, CS0618 ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled); ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description); @@ -81,6 +88,7 @@ internal sealed class Appearance : ISettingsTab { ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); } +#pragma warning restore CS0612, CS0618 } } From cb5c940a84e9f5ef2c29e0042f8bc8b182298b38 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:02:13 +0200 Subject: [PATCH 16/33] feat(settings): card-grid overview router --- HellionChat/Ui/Settings.cs | 42 +++++++++++++- HellionChat/Ui/SettingsOverview.cs | 90 ++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 HellionChat/Ui/SettingsOverview.cs diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 6abf6a6..538fc9f 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -9,13 +9,21 @@ using Dalamud.Bindings.ImGui; namespace HellionChat.Ui; +internal enum SettingsView +{ + Overview, + Detail, +} + public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window { - private readonly Plugin Plugin; + internal readonly Plugin Plugin; private Configuration Mutable { get; } private List Tabs { get; } private int CurrentTab; + private SettingsView View = SettingsView.Overview; + private readonly SettingsOverview Overview; internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings") { @@ -31,6 +39,8 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window Plugin = plugin; Mutable = new Configuration(); + Overview = new SettingsOverview(this); + Tabs = [ new General(Plugin, Mutable), @@ -72,8 +82,33 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window public override void Draw() { if (ImGui.IsWindowAppearing()) + { Initialise(); + View = SettingsView.Overview; + } + if (View == SettingsView.Overview) + Overview.Draw(); + else + DrawDetail(); + + ImGui.Separator(); + DrawSaveButtons(); + } + + internal void OpenSection(int tabIndex) + { + CurrentTab = tabIndex; + View = SettingsView.Detail; + } + + internal void OpenOverview() + { + View = SettingsView.Overview; + } + + private void DrawDetail() + { using (var table = ImRaii.Table("##chat2-settings-table", 2)) { if (table.Success) @@ -103,9 +138,10 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window Tabs[CurrentTab].Draw(changed); } } + } - ImGui.Separator(); - + private void DrawSaveButtons() + { var save = ImGui.Button(Language.Settings_Save); ImGui.SameLine(); diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs new file mode 100644 index 0000000..0574021 --- /dev/null +++ b/HellionChat/Ui/SettingsOverview.cs @@ -0,0 +1,90 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Resources; +using HellionChat.Util; + +namespace HellionChat.Ui; + +internal sealed class SettingsOverview +{ + private readonly SettingsWindow _window; + + // Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow. + // Themes (Phase J) wird später als Card 2 zwischen Appearance und Window + // eingeschoben — dabei muss diese Liste neu gemappt werden. + private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs = + [ + (FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"), + (FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"), + (FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"), + (FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"), + (FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"), + (FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"), + (FontAwesomeIcon.Database, "Settings_Card_Database_Title", "Settings_Card_Database_Subtext"), + (FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"), + ]; + + public SettingsOverview(SettingsWindow window) + { + _window = window; + } + + public void Draw() + { + var avail = ImGui.GetContentRegionAvail(); + var columns = avail.X >= 700f ? 3 : 2; + var cardWidth = (avail.X - (columns - 1) * 8f) / columns; + var cardHeight = 96f; + + for (var i = 0; i < CardDefs.Length; i++) + { + var (icon, titleKey, subtextKey) = CardDefs[i]; + var title = HellionStrings.ResourceManager.GetString(titleKey) ?? titleKey; + var subtext = HellionStrings.ResourceManager.GetString(subtextKey) ?? subtextKey; + DrawCard(i, icon, title, subtext, cardWidth, cardHeight); + + if ((i + 1) % columns != 0 && i != CardDefs.Length - 1) + ImGui.SameLine(); + } + } + + private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h) + { + var cursorBefore = ImGui.GetCursorScreenPos(); + var clicked = ImGui.InvisibleButton($"##settings-card-{index}", new Vector2(w, h)); + var hovered = ImGui.IsItemHovered(); + var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u; + + var draw = ImGui.GetWindowDrawList(); + draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); + + var textPos = cursorBefore + new Vector2(16f, 12f); + ImGui.SetCursorScreenPos(textPos); + // Plugin ist hier Instanz, nicht Static-Type — daher über _window + // referenzieren (Codebase-Konvention, siehe ImGuiUtil.cs:22 für die + // alternative Static-Init-Pattern, das wir hier nicht nutzen). + using (_window.Plugin.FontManager.FontAwesome.Push()) + { + ImGui.Text(icon.ToIconString()); + } + + ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 28f)); + ImGui.TextUnformatted(title); + + ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 50f)); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) + { + ImGui.TextUnformatted(subtext); + } + + // Cursor unter die Card setzen für nächsten Item-Pass + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + + if (clicked) + { + _window.OpenSection(index); + } + } +} From c878d24d11cb89c694d7c0b09ae07974b49d3be2 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:05:59 +0200 Subject: [PATCH 17/33] feat(themes): settings tab with built-in and custom theme grids --- HellionChat/Ui/Settings.cs | 1 + HellionChat/Ui/SettingsOverview.cs | 4 +- HellionChat/Ui/SettingsTabs/Themes.cs | 147 ++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 HellionChat/Ui/SettingsTabs/Themes.cs diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 538fc9f..cec9ea5 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -45,6 +45,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window [ new General(Plugin, Mutable), new Appearance(Plugin, Mutable), + new SettingsTabs.Themes(Plugin, Mutable), new SettingsTabs.Window(Plugin, Mutable), new Chat(Plugin, Mutable), new SettingsTabs.Tabs(Plugin, Mutable), diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 0574021..9715bbe 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -12,12 +12,12 @@ internal sealed class SettingsOverview private readonly SettingsWindow _window; // Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow. - // Themes (Phase J) wird später als Card 2 zwischen Appearance und Window - // eingeschoben — dabei muss diese Liste neu gemappt werden. + // Themes ist Card-Index 2, eingeschoben zwischen Appearance und Window. private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs = [ (FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"), (FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"), + (FontAwesomeIcon.Swatchbook, "Settings_Card_Themes_Title", "Settings_Card_Themes_Subtext"), (FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"), (FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"), (FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"), diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs new file mode 100644 index 0000000..0fe85af --- /dev/null +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -0,0 +1,147 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Resources; +using HellionChat.Themes; +using HellionChat.Util; + +namespace HellionChat.Ui.SettingsTabs; + +internal sealed class Themes : ISettingsTab +{ + private readonly Plugin Plugin; + private readonly Configuration Mutable; + + public string Name => HellionStrings.ResourceManager.GetString("Settings_Tab_Themes") ?? "Themes" + "###tabs-themes"; + + internal Themes(Plugin plugin, Configuration mutable) + { + Plugin = plugin; + Mutable = mutable; + } + + public void Draw(bool changed) + { + var registry = Plugin.ThemeRegistry; + var active = registry.Get(Mutable.Theme); + + var activeLabelTemplate = HellionStrings.ResourceManager.GetString("Settings_Themes_Active") ?? "Active: {0}"; + ImGui.TextUnformatted(string.Format(activeLabelTemplate, active.Name)); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) + ImGui.TextUnformatted(active.Author); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var builtInsLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_BuiltIns") ?? "Built-in themes"; + ImGui.TextUnformatted(builtInsLabel); + ImGui.Spacing(); + DrawThemeGrid(registry.AllBuiltIns(), active.Slug); + + var customs = registry.AllCustom().ToList(); + if (customs.Count > 0) + { + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + var customLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_Custom") ?? "Custom themes"; + ImGui.TextUnformatted(customLabel); + ImGui.Spacing(); + DrawThemeGrid(customs, active.Slug); + } + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var openFolderLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_OpenFolder") ?? "Open themes folder"; + if (ImGui.Button(openFolderLabel)) + { + var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(dir); + Dalamud.Utility.Util.OpenLink(dir); + } + + ImGui.SameLine(); + var exportLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ExportActive") ?? "Export active..."; + if (ImGui.Button(exportLabel)) + { + // Export-Logik wird in Phase L (Task 21) ergänzt — Stub belassen, Button bleibt sichtbar. + } + } + + private void DrawThemeGrid(IEnumerable themes, string activeSlug) + { + var avail = ImGui.GetContentRegionAvail(); + var columns = avail.X >= 700f ? 3 : 2; + var cardWidth = (avail.X - (columns - 1) * 8f) / columns; + var cardHeight = 110f; + var i = 0; + foreach (var theme in themes) + { + DrawThemeCard(theme, activeSlug, cardWidth, cardHeight); + i++; + if (i % columns != 0) + ImGui.SameLine(); + else + ImGui.NewLine(); + } + } + + private void DrawThemeCard(Theme theme, string activeSlug, float w, float h) + { + var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase); + var cursorBefore = ImGui.GetCursorScreenPos(); + var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h)); + var hovered = ImGui.IsItemHovered(); + + var draw = ImGui.GetWindowDrawList(); + var bg = ColourUtil.RgbaToAbgr(theme.Colors.WindowBg | 0xFFu); + draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bg, 4f); + + if (isActive) + { + var border = ColourUtil.RgbaToAbgr(theme.Colors.Primary); + draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 2f); + } + else if (hovered) + { + var border = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight & 0xFFFFFF99u); + draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f); + } + + // Akzent-Swatch links oben + var swatchPos = cursorBefore + new Vector2(12f, 12f); + var swatchSize = new Vector2(20f, 20f); + draw.AddRectFilled(swatchPos, swatchPos + swatchSize, ColourUtil.RgbaToAbgr(theme.Colors.Primary), 3f); + + // Name + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 12f)); + var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + using (ImRaii.PushColor(ImGuiCol.Text, textColor)) + ImGui.TextUnformatted(theme.Name); + + // Author + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 32f)); + var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); + using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) + ImGui.TextUnformatted(theme.Author); + + // Description (wrapped, falls zu lang) + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 60f)); + ImGui.PushTextWrapPos(cursorBefore.X + w - 12f); + using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) + ImGui.TextUnformatted(theme.Description); + ImGui.PopTextWrapPos(); + + // Cursor unter die Card setzen + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + + if (clicked) + { + Mutable.Theme = theme.Slug; + Plugin.ThemeRegistry.Switch(theme.Slug); + } + } +} From 485dc4e1b4dbbed225d74c73f2589a5257af28d5 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:09:02 +0200 Subject: [PATCH 18/33] i18n(themes): localize theme settings card grid (en/de) --- .../Resources/HellionStrings.Designer.cs | 28 ++++++++ HellionChat/Resources/HellionStrings.de.resx | 72 +++++++++++++++++++ HellionChat/Resources/HellionStrings.resx | 72 +++++++++++++++++++ 3 files changed, 172 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index fb80313..deb2ea1 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -203,6 +203,34 @@ internal class HellionStrings internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database)); internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information)); + // v1.1.0 — Settings card-grid overview + internal static string Settings_Card_General_Title => Get(nameof(Settings_Card_General_Title)); + internal static string Settings_Card_General_Subtext => Get(nameof(Settings_Card_General_Subtext)); + internal static string Settings_Card_Appearance_Title => Get(nameof(Settings_Card_Appearance_Title)); + internal static string Settings_Card_Appearance_Subtext => Get(nameof(Settings_Card_Appearance_Subtext)); + internal static string Settings_Card_Themes_Title => Get(nameof(Settings_Card_Themes_Title)); + internal static string Settings_Card_Themes_Subtext => Get(nameof(Settings_Card_Themes_Subtext)); + internal static string Settings_Card_Window_Title => Get(nameof(Settings_Card_Window_Title)); + internal static string Settings_Card_Window_Subtext => Get(nameof(Settings_Card_Window_Subtext)); + internal static string Settings_Card_Chat_Title => Get(nameof(Settings_Card_Chat_Title)); + internal static string Settings_Card_Chat_Subtext => Get(nameof(Settings_Card_Chat_Subtext)); + internal static string Settings_Card_Tabs_Title => Get(nameof(Settings_Card_Tabs_Title)); + internal static string Settings_Card_Tabs_Subtext => Get(nameof(Settings_Card_Tabs_Subtext)); + internal static string Settings_Card_Privacy_Title => Get(nameof(Settings_Card_Privacy_Title)); + internal static string Settings_Card_Privacy_Subtext => Get(nameof(Settings_Card_Privacy_Subtext)); + internal static string Settings_Card_Database_Title => Get(nameof(Settings_Card_Database_Title)); + internal static string Settings_Card_Database_Subtext => Get(nameof(Settings_Card_Database_Subtext)); + internal static string Settings_Card_Information_Title => Get(nameof(Settings_Card_Information_Title)); + internal static string Settings_Card_Information_Subtext => Get(nameof(Settings_Card_Information_Subtext)); + + // v1.1.0 — Themes-Settings-Tab + internal static string Settings_Tab_Themes => Get(nameof(Settings_Tab_Themes)); + internal static string Settings_Themes_Active => Get(nameof(Settings_Themes_Active)); + internal static string Settings_Themes_BuiltIns => Get(nameof(Settings_Themes_BuiltIns)); + internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom)); + internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder)); + internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive)); + // Hellion Chat — General-Tab section headings internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading)); internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 9f88ea0..207bb35 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -624,4 +624,76 @@ Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren. + + Allgemein + + + Sprache und grundlegendes Verhalten + + + Erscheinungsbild + + + Fensterdeckkraft, Schriften, Bewegung + + + Themes + + + Theme wählen oder eigenes importieren + + + Fenster + + + Fensterposition, Rahmen, Hide-Zustände + + + Chat + + + Chat-Verhalten, Emotes, Auto-Tells + + + Tabs + + + Tab-Layout, Kanäle, eigene Tabs + + + Datenschutz + + + Filter, Aufbewahrung, Bereinigung, Export + + + Datenbank + + + Speicher, Migration, alte Bereinigung + + + Information + + + Über, Mitwirkende, Support + + + Themes + + + Aktiv: {0} + + + Eingebaute Themes + + + Eigene Themes + + + Themes-Ordner öffnen + + + Aktives exportieren... + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index aefa6c6..b34833f 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -624,4 +624,76 @@ Disable Chat 2 in /xlplugins, then re-enable Hellion Chat. + + General + + + Language and basic behaviour + + + Appearance + + + Window opacity, fonts, motion + + + Themes + + + Choose a theme or import your own + + + Window + + + Window position, frame, hide states + + + Chat + + + Chat behaviour, emotes, auto-tells + + + Tabs + + + Tab layout, channels, custom tabs + + + Privacy + + + Filter, retention, cleanup, export + + + Database + + + Storage, migration, legacy cleanup + + + Information + + + About, credits, support + + + Themes + + + Active: {0} + + + Built-in themes + + + Custom themes + + + Open themes folder + + + Export active... + From af4651b37eb31d8b610ae5875f6ae3047b7e1319 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:11:14 +0200 Subject: [PATCH 19/33] feat(themes): export active theme to json --- HellionChat/Themes/ThemeJsonWriter.cs | 65 +++++++++++++++++++++++++++ HellionChat/Ui/SettingsTabs/Themes.cs | 8 +++- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 HellionChat/Themes/ThemeJsonWriter.cs diff --git a/HellionChat/Themes/ThemeJsonWriter.cs b/HellionChat/Themes/ThemeJsonWriter.cs new file mode 100644 index 0000000..97523f1 --- /dev/null +++ b/HellionChat/Themes/ThemeJsonWriter.cs @@ -0,0 +1,65 @@ +using System.Text.Json; + +namespace HellionChat.Themes; + +internal static class ThemeJsonWriter +{ + public static string Serialize(Theme theme) + { + using var ms = new MemoryStream(); + using (var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + writer.WriteNumber("schemaVersion", ThemeJsonLoader.SupportedSchemaVersion); + writer.WriteString("slug", theme.Slug); + writer.WriteString("name", theme.Name); + writer.WriteString("author", theme.Author); + writer.WriteString("description", theme.Description); + + writer.WriteStartObject("colors"); + WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark); + WriteColor(writer, "primary", theme.Colors.Primary); + WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight); + WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow); + WriteColor(writer, "accentDark", theme.Colors.AccentDark); + WriteColor(writer, "accent", theme.Colors.Accent); + WriteColor(writer, "accentLight", theme.Colors.AccentLight); + WriteColor(writer, "identity", theme.Colors.Identity); + WriteColor(writer, "windowBg", theme.Colors.WindowBg); + WriteColor(writer, "childBg", theme.Colors.ChildBg); + WriteColor(writer, "frameBg", theme.Colors.FrameBg); + WriteColor(writer, "surface", theme.Colors.Surface); + WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover); + WriteColor(writer, "border", theme.Colors.Border); + WriteColor(writer, "textPrimary", theme.Colors.TextPrimary); + WriteColor(writer, "textMuted", theme.Colors.TextMuted); + WriteColor(writer, "textDim", theme.Colors.TextDim); + WriteColor(writer, "statusSuccess", theme.Colors.StatusSuccess); + WriteColor(writer, "statusDanger", theme.Colors.StatusDanger); + WriteColor(writer, "statusWarning", theme.Colors.StatusWarning); + WriteColor(writer, "statusInfo", theme.Colors.StatusInfo); + writer.WriteEndObject(); + + writer.WriteStartObject("layout"); + writer.WriteNumber("windowRounding", theme.Layout.WindowRounding); + writer.WriteNumber("childRounding", theme.Layout.ChildRounding); + writer.WriteNumber("popupRounding", theme.Layout.PopupRounding); + writer.WriteNumber("frameRounding", theme.Layout.FrameRounding); + writer.WriteNumber("grabRounding", theme.Layout.GrabRounding); + writer.WriteNumber("tabRounding", theme.Layout.TabRounding); + writer.WriteNumber("scrollbarRounding", theme.Layout.ScrollbarRounding); + writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize); + writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize); + writer.WriteEndObject(); + + writer.WriteEndObject(); + } + + return System.Text.Encoding.UTF8.GetString(ms.ToArray()); + } + + private static void WriteColor(Utf8JsonWriter writer, string key, uint rgba) + { + writer.WriteString(key, $"#{rgba:X8}"); + } +} diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index 0fe85af..f04a5de 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -67,7 +67,13 @@ internal sealed class Themes : ISettingsTab var exportLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ExportActive") ?? "Export active..."; if (ImGui.Button(exportLabel)) { - // Export-Logik wird in Phase L (Task 21) ergänzt — Stub belassen, Button bleibt sichtbar. + var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(dir); + var fileName = $"{active.Slug}.export.json"; + var path = Path.Combine(dir, fileName); + var json = ThemeJsonWriter.Serialize(active); + File.WriteAllText(path, json); + Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}"); } } From 8f9c01d3226ec30197d3de8600fc03b9ebd8520f Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:13:19 +0200 Subject: [PATCH 20/33] feat(themes): mini-mockup preview in theme cards --- HellionChat/Ui/SettingsTabs/ThemeMockup.cs | 70 ++++++++++++++++++++++ HellionChat/Ui/SettingsTabs/Themes.cs | 25 +++----- 2 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 HellionChat/Ui/SettingsTabs/ThemeMockup.cs diff --git a/HellionChat/Ui/SettingsTabs/ThemeMockup.cs b/HellionChat/Ui/SettingsTabs/ThemeMockup.cs new file mode 100644 index 0000000..3fcb382 --- /dev/null +++ b/HellionChat/Ui/SettingsTabs/ThemeMockup.cs @@ -0,0 +1,70 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using HellionChat.Themes; +using HellionChat.Util; + +namespace HellionChat.Ui.SettingsTabs; + +internal static class ThemeMockup +{ + // Zeichnet ein Mini-Chat-Window-Mockup mit den Theme-Werten direkt + // ins WindowDrawList. Keine Texture, keine Allocation pro Frame — + // alles via DrawList.AddRectFilled / AddText. + public static void Draw(Vector2 origin, Vector2 size, Theme theme) + { + var draw = ImGui.GetWindowDrawList(); + var c = theme.Colors; + + // Window-Bg + draw.AddRectFilled(origin, origin + size, ColourUtil.RgbaToAbgr(c.WindowBg | 0xFFu), theme.Layout.WindowRounding); + + // Title-Bar + var titleHeight = 14f; + draw.AddRectFilled( + origin, + new Vector2(origin.X + size.X, origin.Y + titleHeight), + ColourUtil.RgbaToAbgr(c.Identity), theme.Layout.WindowRounding); + + // Tab-Bar — 3 Mini-Tabs + var tabY = origin.Y + titleHeight + 4f; + var tabHeight = 12f; + for (var i = 0; i < 3; i++) + { + var tabX = origin.X + 6f + i * 28f; + var color = i == 0 ? c.FrameBg : c.ChildBg; + draw.AddRectFilled( + new Vector2(tabX, tabY), + new Vector2(tabX + 26f, tabY + tabHeight), + ColourUtil.RgbaToAbgr(color), theme.Layout.TabRounding); + + if (i == 0) // Active-Pill + { + draw.AddRectFilled( + new Vector2(tabX, tabY + tabHeight - 2f), + new Vector2(tabX + 26f, tabY + tabHeight), + ColourUtil.RgbaToAbgr(c.Primary)); + } + } + + // Card-Row mit Mock-Sender + Text + var rowY = tabY + tabHeight + 6f; + var rowHeight = 18f; + draw.AddRectFilled( + new Vector2(origin.X + 6f, rowY), + new Vector2(origin.X + size.X - 6f, rowY + rowHeight), + ColourUtil.RgbaToAbgr(c.Surface), 2f); + + // Akzent-Button rechts unten + var btnW = 28f; + var btnH = 10f; + var btnX = origin.X + size.X - btnW - 6f; + var btnY = origin.Y + size.Y - btnH - 6f; + draw.AddRectFilled( + new Vector2(btnX, btnY), + new Vector2(btnX + btnW, btnY + btnH), + ColourUtil.RgbaToAbgr(c.Accent), theme.Layout.FrameRounding); + + // Border um das gesamte Mockup + draw.AddRect(origin, origin + size, ColourUtil.RgbaToAbgr(c.Border), theme.Layout.WindowRounding); + } +} diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index f04a5de..f501b6b 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -82,7 +82,7 @@ internal sealed class Themes : ISettingsTab var avail = ImGui.GetContentRegionAvail(); var columns = avail.X >= 700f ? 3 : 2; var cardWidth = (avail.X - (columns - 1) * 8f) / columns; - var cardHeight = 110f; + var cardHeight = 140f; // war 110f — Mockup braucht mehr Platz var i = 0; foreach (var theme in themes) { @@ -117,30 +117,23 @@ internal sealed class Themes : ISettingsTab draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f); } - // Akzent-Swatch links oben - var swatchPos = cursorBefore + new Vector2(12f, 12f); - var swatchSize = new Vector2(20f, 20f); - draw.AddRectFilled(swatchPos, swatchPos + swatchSize, ColourUtil.RgbaToAbgr(theme.Colors.Primary), 3f); + // Mini-Mockup statt Akzent-Swatch — visualisiert das Theme im Mini-Chat-Window + var mockupOrigin = cursorBefore + new Vector2(12f, 12f); + var mockupSize = new Vector2(w - 24f, 60f); + ThemeMockup.Draw(mockupOrigin, mockupSize, theme); - // Name - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 12f)); + // Name unter dem Mockup + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 78f)); var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); using (ImRaii.PushColor(ImGuiCol.Text, textColor)) ImGui.TextUnformatted(theme.Name); - // Author - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(40f, 32f)); + // Author dahinter dezent + ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 96f)); var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) ImGui.TextUnformatted(theme.Author); - // Description (wrapped, falls zu lang) - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 60f)); - ImGui.PushTextWrapPos(cursorBefore.X + w - 12f); - using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) - ImGui.TextUnformatted(theme.Description); - ImGui.PopTextWrapPos(); - // Cursor unter die Card setzen ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); From 9103bbb8928fb5954acaf5a5bada805a77c1dd28 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:15:12 +0200 Subject: [PATCH 21/33] feat(settings): breadcrumb header and esc to return to overview --- HellionChat/Ui/Settings.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index cec9ea5..62cf690 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -88,6 +88,18 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window View = SettingsView.Overview; } + // ESC im Detail-View kehrt zur Overview zurück. Window-Focus-Check ist + // Pflicht — sonst triggert ESC auch wenn der User ein anderes Fenster + // fokussiert hat und ESC fürs Game-Menü drückt (Codebase-Pattern siehe + // Util/SearchSelector.cs:37). + if (View == SettingsView.Detail + && ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) + && ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + View = SettingsView.Overview; + return; + } + if (View == SettingsView.Overview) Overview.Draw(); else @@ -110,6 +122,28 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window private void DrawDetail() { + // Breadcrumb-Header — Akzent-Cyan, klickbar, führt zurück zur Overview + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF00BED2u)) + using (ImRaii.PushColor(ImGuiCol.Button, 0u)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, 0x33FFFFFFu)) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, 0x55FFFFFFu)) + { + if (ImGui.SmallButton("← Settings")) + { + View = SettingsView.Overview; + return; + } + } + ImGui.SameLine(); + ImGui.TextUnformatted("·"); + ImGui.SameLine(); + ImGui.TextUnformatted(Tabs[CurrentTab].Name.Split("###")[0]); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + // Tab-Liste + Content (wie vorher) using (var table = ImRaii.Table("##chat2-settings-table", 2)) { if (table.Success) From 2f52cbb7d4340e3e23a187971c6dd9a96b5dbc44 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:17:22 +0200 Subject: [PATCH 22/33] feat(themes): seed example custom theme on first start --- HellionChat/HellionChat.csproj | 3 ++ HellionChat/Plugin.cs | 34 +++++++++++++++ HellionChat/Themes/Builtin/example-theme.json | 41 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 HellionChat/Themes/Builtin/example-theme.json diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 0d048e8..cfe8103 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -75,6 +75,9 @@ HellionFont-OFL.txt + + HellionChat.Themes.Builtin.example-theme.json + diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index e5e7770..6cb47f5 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -291,6 +291,8 @@ public sealed class Plugin : IDalamudPlugin // v1.1.0 — Theme-Engine init. Custom-Themes liegen in // pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get. var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); + Directory.CreateDirectory(customThemesDir); + SeedExampleThemeIfEmpty(customThemesDir); ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); ThemeRegistry.Switch(Config.Theme); @@ -675,4 +677,36 @@ public sealed class Plugin : IDalamudPlugin public static bool InBattle => Condition[ConditionFlag.InCombat]; public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene]; public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78]; + + // v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded + // example-theme.json als Vorlage rein. Bestehende User-Customs werden + // nicht angefasst (existing JSONs lassen den Block überspringen). + private static void SeedExampleThemeIfEmpty(string dir) + { + if (Directory.EnumerateFiles(dir, "*.json").Any()) + return; + + var examplePath = Path.Combine(dir, "example-theme.json"); + var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream("HellionChat.Themes.Builtin.example-theme.json"); + if (resourceStream is null) + { + Log.Warning("Themes example template not found in assembly resources; skipping seed."); + return; + } + + try + { + using var fileStream = File.Create(examplePath); + resourceStream.CopyTo(fileStream); + Log.Information($"Seeded example-theme.json into {dir}"); + } + catch (IOException ex) + { + Log.Warning(ex, "Failed to seed example-theme.json; user can create custom themes manually."); + } + finally + { + resourceStream.Dispose(); + } + } } diff --git a/HellionChat/Themes/Builtin/example-theme.json b/HellionChat/Themes/Builtin/example-theme.json new file mode 100644 index 0000000..1f547f7 --- /dev/null +++ b/HellionChat/Themes/Builtin/example-theme.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "slug": "example-custom", + "name": "Example Custom", + "author": "You", + "description": "Starting template — duplicate, rename, edit colors and reload.", + "colors": { + "primaryDark": "#0097A7", + "primary": "#00BED2", + "primaryLight": "#4DD9E8", + "primaryGlow": "#00BED299", + "accentDark": "#E85D04", + "accent": "#F97316", + "accentLight": "#FB923C", + "identity": "#0097A7", + "windowBg": "#070B12", + "childBg": "#0C1220", + "frameBg": "#141E30", + "surface": "#1A2538", + "surfaceHover": "#22303F", + "border": "#00BED266", + "textPrimary": "#E6F4F1", + "textMuted": "#8FA3B5", + "textDim": "#566273", + "statusSuccess": "#5CB85C", + "statusDanger": "#D9534F", + "statusWarning": "#F0AD4E", + "statusInfo": "#00BED2" + }, + "layout": { + "windowRounding": 4, + "childRounding": 3, + "popupRounding": 3, + "frameRounding": 2, + "grabRounding": 2, + "tabRounding": 2, + "scrollbarRounding": 2, + "windowBorderSize": 1, + "frameBorderSize": 1 + } +} From abcd0847ef6fdb2d0f85e10abbbf5c28b8dced94 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:22:23 +0200 Subject: [PATCH 23/33] fix(settings): restore cursor after card draw to keep grid layout intact --- HellionChat/Ui/SettingsOverview.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 9715bbe..9837b53 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -60,6 +60,13 @@ internal sealed class SettingsOverview var draw = ImGui.GetWindowDrawList(); draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); + // ImGui hat den Cursor nach dem InvisibleButton bereits korrekt + // weitergesetzt (für die Layout-Engine). Wir merken uns die Position, + // verschieben den Cursor temporär für die Inhalts-Beschriftung und + // restoren ihn am Ende — sonst kollidieren manuelle Cursor-Sets mit + // SameLine im Caller und die Cards stapeln sich diagonal. + var cursorAfterButton = ImGui.GetCursorScreenPos(); + var textPos = cursorBefore + new Vector2(16f, 12f); ImGui.SetCursorScreenPos(textPos); // Plugin ist hier Instanz, nicht Static-Type — daher über _window @@ -79,8 +86,9 @@ internal sealed class SettingsOverview ImGui.TextUnformatted(subtext); } - // Cursor unter die Card setzen für nächsten Item-Pass - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + // Restore cursor to post-button position, so SameLine + Wrap im Caller + // wieder mit dem ImGui-Layout-Flow arbeiten. + ImGui.SetCursorScreenPos(cursorAfterButton); if (clicked) { From c943a2cff3b8612ac5f2b46931e9fa76822ca834 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:23:41 +0200 Subject: [PATCH 24/33] fix(themes): drop legacy StyleModel push from chat log and pop-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-engine StyleModel override in ChatLogWindow.PreDraw and Popout.PreDraw was layering an extra Dalamud style on top of the Hellion theme, locally tinting the chat window back to a non-Hellion look while every other plugin window rendered correctly. Theme is now the single source of truth — pick chat2-classic for the upstream flavour. --- HellionChat/Ui/ChatLogWindow.cs | 28 +++++----------------------- HellionChat/Ui/Popout.cs | 25 ++++--------------------- 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index f5fda68..5949104 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -521,28 +521,16 @@ public sealed class ChatLogWindow : Window return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } - // Tracks the style instance pushed in PreDraw so PostDraw can pop the same - // one even if the user toggled OverrideStyle / ChosenStyle mid-frame. - // Without this, a config change between PreDraw and PostDraw could either - // leak a Push (no matching Pop) or pop nothing while we still have a frame - // pushed onto the ImGui stack. - private StyleModel? _pushedStyle; - public override void PreDraw() { if (Plugin.Config.KeepInputFocus && Activate) ImGui.SetWindowFocus(WindowName); - _pushedStyle = null; - if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - { - var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle); - if (style != null) - { - style.Push(); - _pushedStyle = style; - } - } + // Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein + // zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw + // pusht das aktive Hellion-Theme global; ChatLogWindow zeichnet sich + // damit konsistent zu Settings/Pop-Out/Wizard. Wer den Upstream-Look + // will, wählt das Built-In-Theme "Chat 2 Klassik" in Settings → Themes. } public override void PostDraw() @@ -553,12 +541,6 @@ public sealed class ChatLogWindow : Window // doesn't get called if the input is disabled. if (Plugin.CurrentTab.InputDisabled) Activate = false; - - if (_pushedStyle != null) - { - _pushedStyle.Pop(); - _pushedStyle = null; - } } public override void OnClose() diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index f86c980..dc10cd2 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -65,23 +65,12 @@ internal class Popout : Window return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } - // Tracks the style instance pushed in PreDraw so PostDraw pops the same - // one even if config changes mid-frame. See AUDIT-2026-05-05 [CR-UI-5]. - private StyleModel? _pushedStyle; - public override void PreDraw() { - _pushedStyle = null; - if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) - { - var style = StyleModel.GetConfiguredStyles()?.FirstOrDefault(s => s.Name == Plugin.Config.ChosenStyle); - if (style != null) - { - style.Push(); - _pushedStyle = style; - } - } - + // Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein + // zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw + // pusht das aktive Hellion-Theme global; Pop-Out zeichnet sich damit + // konsistent zum Haupt-Chat-Window. Flags = ImGuiWindowFlags.None; if (!Plugin.Config.ShowPopOutTitleBar) Flags |= ImGuiWindowFlags.NoTitleBar; @@ -210,12 +199,6 @@ internal class Popout : Window { if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count) ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); - - if (_pushedStyle != null) - { - _pushedStyle.Pop(); - _pushedStyle = null; - } } public override void OnClose() From d41cea00314c34eec7108e424393b9d21b1e5066 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:28:24 +0200 Subject: [PATCH 25/33] fix(settings): card grid wraps correctly, detail view drops legacy tab list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SettingsOverview now wraps each card in BeginGroup/EndGroup so SameLine in the loop can wrap rows. The card content is drawn directly into the DrawList (icon, title, subtext) without cursor hopping that broke the flow. DrawDetail no longer renders the second-column tab list — the user has already picked a section from the overview, the redundant column made the detail view feel like the old vanilla settings layout. Section content now uses the full width. --- HellionChat/Ui/Settings.cs | 39 +++++++-------------------- HellionChat/Ui/SettingsOverview.cs | 43 ++++++++++++++---------------- 2 files changed, 30 insertions(+), 52 deletions(-) diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index 62cf690..2a61094 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -143,36 +143,17 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window ImGui.Separator(); ImGui.Spacing(); - // Tab-Liste + Content (wie vorher) - using (var table = ImRaii.Table("##chat2-settings-table", 2)) - { - if (table.Success) - { - ImGui.TableSetupColumn("tab", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("settings", ImGuiTableColumnFlags.WidthStretch); + // Section-Content in voller Breite. Die Tab-Liste links ist überholt: + // der User ist bereits über die Card-Übersicht navigiert, eine zweite + // Tab-Liste daneben würde nur den Vanilla-Look zurückbringen. Falls + // der User in eine andere Section will, geht er zurück zur Overview + // (Breadcrumb / ESC). + var style = ImGui.GetStyle(); + var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y; - ImGui.TableNextColumn(); - - var changed = false; - for (var i = 0; i < Tabs.Count; i++) - { - if (!ImGui.Selectable($"{Tabs[i].Name}###tab-{i}", CurrentTab == i)) - continue; - - CurrentTab = i; - changed = true; - } - - ImGui.TableNextColumn(); - - var style = ImGui.GetStyle(); - var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y; - - using var child = ImRaii.Child("##chat2-settings", new Vector2(-1, height)); - if (child.Success) - Tabs[CurrentTab].Draw(changed); - } - } + using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height)); + if (child.Success) + Tabs[CurrentTab].Draw(false); } private void DrawSaveButtons() diff --git a/HellionChat/Ui/SettingsOverview.cs b/HellionChat/Ui/SettingsOverview.cs index 9837b53..b0f8a96 100644 --- a/HellionChat/Ui/SettingsOverview.cs +++ b/HellionChat/Ui/SettingsOverview.cs @@ -52,6 +52,11 @@ internal sealed class SettingsOverview private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h) { + // BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item. + // Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die + // einzelnen InvisibleButton/Text-Items separat und das Wrapping bricht. + ImGui.BeginGroup(); + var cursorBefore = ImGui.GetCursorScreenPos(); var clicked = ImGui.InvisibleButton($"##settings-card-{index}", new Vector2(w, h)); var hovered = ImGui.IsItemHovered(); @@ -60,35 +65,27 @@ internal sealed class SettingsOverview var draw = ImGui.GetWindowDrawList(); draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f); - // ImGui hat den Cursor nach dem InvisibleButton bereits korrekt - // weitergesetzt (für die Layout-Engine). Wir merken uns die Position, - // verschieben den Cursor temporär für die Inhalts-Beschriftung und - // restoren ihn am Ende — sonst kollidieren manuelle Cursor-Sets mit - // SameLine im Caller und die Cards stapeln sich diagonal. - var cursorAfterButton = ImGui.GetCursorScreenPos(); + // Inhalts-Overlay: Icon + Title + Subtext direkt mit DrawList in den + // Card-Bereich zeichnen, statt Cursor-Hopping mit SetCursorScreenPos. + // DrawList-Overlays ändern den Cursor nicht, BeginGroup/EndGroup + // hält den Layout-Anker stabil für SameLine. + var iconPos = cursorBefore + new Vector2(16f, 12f); + var titlePos = cursorBefore + new Vector2(16f, 40f); + var subtextPos = cursorBefore + new Vector2(16f, 62f); - var textPos = cursorBefore + new Vector2(16f, 12f); - ImGui.SetCursorScreenPos(textPos); - // Plugin ist hier Instanz, nicht Static-Type — daher über _window - // referenzieren (Codebase-Konvention, siehe ImGuiUtil.cs:22 für die - // alternative Static-Init-Pattern, das wir hier nicht nutzen). + var titleColor = ColourUtil.RgbaToAbgr(0xE6F4F1FFu); + var subtextColor = ColourUtil.RgbaToAbgr(0x8FA3B5FFu); + + // Icon via FontAwesome — temporär den Font pushen, mit DrawList zeichnen using (_window.Plugin.FontManager.FontAwesome.Push()) { - ImGui.Text(icon.ToIconString()); + draw.AddText(iconPos, titleColor, icon.ToIconString()); } - ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 28f)); - ImGui.TextUnformatted(title); + draw.AddText(titlePos, titleColor, title); + draw.AddText(subtextPos, subtextColor, subtext); - ImGui.SetCursorScreenPos(textPos + new Vector2(0f, 50f)); - using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) - { - ImGui.TextUnformatted(subtext); - } - - // Restore cursor to post-button position, so SameLine + Wrap im Caller - // wieder mit dem ImGui-Layout-Flow arbeiten. - ImGui.SetCursorScreenPos(cursorAfterButton); + ImGui.EndGroup(); if (clicked) { From fcbbd174b6b9a20a200644114e2523826218baed Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:31:35 +0200 Subject: [PATCH 26/33] fix(themes): wrap theme cards in begin/end group so the grid wraps Theme-card grid was stacking diagonally for the same reason the settings overview did: SetCursorScreenPos plus SameLine in the caller loop don't compose. Wrap each card in BeginGroup/EndGroup, draw name and author via DrawList instead of cursor hops, and let ImGui handle row wrapping naturally. --- HellionChat/Ui/SettingsTabs/Themes.cs | 42 ++++++++++++++------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index f501b6b..564322d 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -82,21 +82,27 @@ internal sealed class Themes : ISettingsTab var avail = ImGui.GetContentRegionAvail(); var columns = avail.X >= 700f ? 3 : 2; var cardWidth = (avail.X - (columns - 1) * 8f) / columns; - var cardHeight = 140f; // war 110f — Mockup braucht mehr Platz - var i = 0; - foreach (var theme in themes) + var cardHeight = 140f; // Mockup + Name + Author brauchen den Platz + + var list = themes.ToList(); + for (var i = 0; i < list.Count; i++) { - DrawThemeCard(theme, activeSlug, cardWidth, cardHeight); - i++; - if (i % columns != 0) + DrawThemeCard(list[i], activeSlug, cardWidth, cardHeight); + + // SameLine zwischen den Cards einer Reihe; am Spalten-Ende kein + // SameLine, dann beginnt automatisch eine neue Zeile. + if ((i + 1) % columns != 0 && i != list.Count - 1) ImGui.SameLine(); - else - ImGui.NewLine(); } } private void DrawThemeCard(Theme theme, string activeSlug, float w, float h) { + // BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item. + // Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die + // einzelnen InvisibleButton-Items separat und das Wrapping bricht. + ImGui.BeginGroup(); + var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase); var cursorBefore = ImGui.GetCursorScreenPos(); var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h)); @@ -117,25 +123,21 @@ internal sealed class Themes : ISettingsTab draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f); } - // Mini-Mockup statt Akzent-Swatch — visualisiert das Theme im Mini-Chat-Window + // Mini-Mockup oben — DrawList-Operation, kein Cursor-Hopping var mockupOrigin = cursorBefore + new Vector2(12f, 12f); var mockupSize = new Vector2(w - 24f, 60f); ThemeMockup.Draw(mockupOrigin, mockupSize, theme); - // Name unter dem Mockup - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 78f)); + // Name + Author direkt via DrawList (statt SetCursorScreenPos + + // TextUnformatted), damit der ImGui-Layout-Cursor stabil bleibt + // und die BeginGroup/EndGroup-Klammer den Card-Bereich als ein + // Layout-Item führt. var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); - using (ImRaii.PushColor(ImGuiCol.Text, textColor)) - ImGui.TextUnformatted(theme.Name); - - // Author dahinter dezent - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(12f, 96f)); var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); - using (ImRaii.PushColor(ImGuiCol.Text, mutedColor)) - ImGui.TextUnformatted(theme.Author); + draw.AddText(cursorBefore + new Vector2(12f, 80f), textColor, theme.Name); + draw.AddText(cursorBefore + new Vector2(12f, 100f), mutedColor, theme.Author); - // Cursor unter die Card setzen - ImGui.SetCursorScreenPos(cursorBefore + new Vector2(0f, h + 8f)); + ImGui.EndGroup(); if (clicked) { From 53952717c0838ec470f425ad226a60615a588519 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:44:59 +0200 Subject: [PATCH 27/33] feat(themes): optional chat channel colors in theme schema --- HellionChat/Themes/Theme.cs | 3 ++- HellionChat/Themes/ThemeChatColors.cs | 11 +++++++++++ HellionChat/Themes/ThemeJsonLoader.cs | 27 ++++++++++++++++++++++++++- HellionChat/Themes/ThemeJsonWriter.cs | 8 ++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 HellionChat/Themes/ThemeChatColors.cs diff --git a/HellionChat/Themes/Theme.cs b/HellionChat/Themes/Theme.cs index 591bf49..18eee74 100644 --- a/HellionChat/Themes/Theme.cs +++ b/HellionChat/Themes/Theme.cs @@ -8,5 +8,6 @@ public sealed record Theme( ThemeColors Colors, ThemeLayout Layout, ThemeTypography Typography, - bool IsBuiltIn + bool IsBuiltIn, + ThemeChatColors? ChatColors = null ); diff --git a/HellionChat/Themes/ThemeChatColors.cs b/HellionChat/Themes/ThemeChatColors.cs new file mode 100644 index 0000000..3c2ba1b --- /dev/null +++ b/HellionChat/Themes/ThemeChatColors.cs @@ -0,0 +1,11 @@ +using HellionChat.Code; + +namespace HellionChat.Themes; + +// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der +// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden. +// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel- +// Farben unverändert. +public sealed record ThemeChatColors( + IReadOnlyDictionary Channels +); diff --git a/HellionChat/Themes/ThemeJsonLoader.cs b/HellionChat/Themes/ThemeJsonLoader.cs index d73200f..e340165 100644 --- a/HellionChat/Themes/ThemeJsonLoader.cs +++ b/HellionChat/Themes/ThemeJsonLoader.cs @@ -32,10 +32,35 @@ internal static class ThemeJsonLoader var colors = ReadColors(root.GetProperty("colors")); var layout = ReadLayout(root.GetProperty("layout")); - return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false); + ThemeChatColors? chatColors = null; + if (root.TryGetProperty("chatChannels", out var ch) && ch.ValueKind == JsonValueKind.Object) + chatColors = ReadChatColors(ch); + + return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false, ChatColors: chatColors); } } + private static ThemeChatColors ReadChatColors(JsonElement el) + { + var dict = new Dictionary(); + foreach (var prop in el.EnumerateObject()) + { + // Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"), + // Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names + // werden still übersprungen — Forward-Compat falls SE neue Channels + // einführt. + if (!Enum.TryParse(prop.Name, ignoreCase: true, out var channel)) + continue; + if (prop.Value.ValueKind != JsonValueKind.String) + continue; + var hex = prop.Value.GetString(); + if (string.IsNullOrWhiteSpace(hex)) + continue; + dict[channel] = HellionChat.Util.ColourUtil.HexToRgba(hex); + } + return new ThemeChatColors(dict); + } + public static Theme LoadFromFile(string path) { var json = File.ReadAllText(path); diff --git a/HellionChat/Themes/ThemeJsonWriter.cs b/HellionChat/Themes/ThemeJsonWriter.cs index 97523f1..9cf197a 100644 --- a/HellionChat/Themes/ThemeJsonWriter.cs +++ b/HellionChat/Themes/ThemeJsonWriter.cs @@ -52,6 +52,14 @@ internal static class ThemeJsonWriter writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize); writer.WriteEndObject(); + if (theme.ChatColors is { Channels.Count: > 0 } cc) + { + writer.WriteStartObject("chatChannels"); + foreach (var kvp in cc.Channels) + writer.WriteString(kvp.Key.ToString(), $"#{kvp.Value:X8}"); + writer.WriteEndObject(); + } + writer.WriteEndObject(); } From 15a89dd6e74766b7cbe61e165f22fe94cea53c52 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:48:34 +0200 Subject: [PATCH 28/33] feat(themes): chat channel color sets for four built-in themes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hellion Arctic, Event Horizon, Moonlit Bloom and Mint Grove each ship a distinct chat-channel palette tinted toward their brand family while preserving the FFXIV channel identity (Say light, Yell yellow, Shout orange, Tell pink-magenta, Party blue, FC cyan, NN green). Chat 2 Klassik intentionally ships without — users picking that theme keep their existing channel colours. --- HellionChat/Themes/Builtin/EventHorizon.cs | 29 ++++++++++++++++++++- HellionChat/Themes/Builtin/HellionArctic.cs | 28 +++++++++++++++++++- HellionChat/Themes/Builtin/MintGrove.cs | 29 ++++++++++++++++++++- HellionChat/Themes/Builtin/MoonlitBloom.cs | 28 +++++++++++++++++++- 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/HellionChat/Themes/Builtin/EventHorizon.cs b/HellionChat/Themes/Builtin/EventHorizon.cs index ac3b625..c436ec6 100644 --- a/HellionChat/Themes/Builtin/EventHorizon.cs +++ b/HellionChat/Themes/Builtin/EventHorizon.cs @@ -45,6 +45,33 @@ internal static class EventHorizon ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen + // Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta- + // Lila. Channel-Identität bleibt klar erkennbar. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"), + }) ); } diff --git a/HellionChat/Themes/Builtin/HellionArctic.cs b/HellionChat/Themes/Builtin/HellionArctic.cs index 38ed5b6..481084f 100644 --- a/HellionChat/Themes/Builtin/HellionArctic.cs +++ b/HellionChat/Themes/Builtin/HellionArctic.cs @@ -45,6 +45,32 @@ internal static class HellionArctic ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den + // blauen Channels (Party/FC). Channel-Identität bleibt klar. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"), + }) ); } diff --git a/HellionChat/Themes/Builtin/MintGrove.cs b/HellionChat/Themes/Builtin/MintGrove.cs index 727d30c..eab1f5c 100644 --- a/HellionChat/Themes/Builtin/MintGrove.cs +++ b/HellionChat/Themes/Builtin/MintGrove.cs @@ -45,6 +45,33 @@ internal static class MintGrove ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie, + // Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität + // bleibt erhalten für Erkennbarkeit. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"), + }) ); } diff --git a/HellionChat/Themes/Builtin/MoonlitBloom.cs b/HellionChat/Themes/Builtin/MoonlitBloom.cs index 07b700d..3da16b3 100644 --- a/HellionChat/Themes/Builtin/MoonlitBloom.cs +++ b/HellionChat/Themes/Builtin/MoonlitBloom.cs @@ -45,6 +45,32 @@ internal static class MoonlitBloom ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f ), Typography: new ThemeTypography(), - IsBuiltIn: true + IsBuiltIn: true, + ChatColors: new ThemeChatColors(new Dictionary + { + // Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork + // und Linkshell4. Tell-Pink-Identität bleibt sichtbar. + [HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"), + [HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"), + [HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"), + [HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"), + [HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"), + [HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"), + [HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"), + [HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"), + [HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"), + [HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"), + [HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"), + [HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"), + [HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"), + [HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"), + [HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"), + [HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"), + [HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"), + [HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"), + [HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"), + [HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"), + [HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"), + }) ); } From f2086865cec69ed6cd47e0e1e2a795aad41a5435 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:51:16 +0200 Subject: [PATCH 29/33] feat(themes): opt-in chat color apply banner in themes tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a theme defines its own chat channel colours and the current Configuration.ChatColours don't match, a dezent banner offers Apply / Keep — opt-in, never auto-overwriting user picks. Switching themes re-arms the banner so each theme can be evaluated separately. --- .../Resources/HellionStrings.Designer.cs | 3 + HellionChat/Resources/HellionStrings.de.resx | 9 +++ HellionChat/Resources/HellionStrings.resx | 9 +++ HellionChat/Ui/SettingsTabs/Themes.cs | 75 +++++++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index deb2ea1..1402a1d 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -230,6 +230,9 @@ internal class HellionStrings internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom)); internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder)); internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive)); + internal static string Settings_Themes_ApplyChatColors_Hint => Get(nameof(Settings_Themes_ApplyChatColors_Hint)); + internal static string Settings_Themes_ApplyChatColors_Apply => Get(nameof(Settings_Themes_ApplyChatColors_Apply)); + internal static string Settings_Themes_ApplyChatColors_Keep => Get(nameof(Settings_Themes_ApplyChatColors_Keep)); // Hellion Chat — General-Tab section headings internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 207bb35..9b9c9bf 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -696,4 +696,13 @@ Aktives exportieren... + + Dieses Theme schlägt eigene Channel-Farben vor. + + + Übernehmen + + + Behalten + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index b34833f..edba1d7 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -696,4 +696,13 @@ Export active... + + This theme suggests its own chat channel colours. + + + Apply + + + Keep current + diff --git a/HellionChat/Ui/SettingsTabs/Themes.cs b/HellionChat/Ui/SettingsTabs/Themes.cs index 564322d..be66c5d 100644 --- a/HellionChat/Ui/SettingsTabs/Themes.cs +++ b/HellionChat/Ui/SettingsTabs/Themes.cs @@ -12,6 +12,12 @@ internal sealed class Themes : ISettingsTab private readonly Plugin Plugin; private readonly Configuration Mutable; + // Tracks ob der User die Apply-Frage für das aktive Theme bereits + // beantwortet hat. Banner wird nur angezeigt wenn das Theme ein + // ChatColors-Set hat UND noch keine Antwort vorliegt UND die aktuellen + // Mutable.ChatColours davon abweichen. + private string? _applyDismissedFor; + public string Name => HellionStrings.ResourceManager.GetString("Settings_Tab_Themes") ?? "Themes" + "###tabs-themes"; internal Themes(Plugin plugin, Configuration mutable) @@ -30,6 +36,8 @@ internal sealed class Themes : ISettingsTab using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u)) ImGui.TextUnformatted(active.Author); + DrawChatColorsApplyBanner(active); + ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); @@ -143,6 +151,73 @@ internal sealed class Themes : ISettingsTab { Mutable.Theme = theme.Slug; Plugin.ThemeRegistry.Switch(theme.Slug); + _applyDismissedFor = null; // Banner für neues Theme wieder zeigen } } + + private void DrawChatColorsApplyBanner(Theme active) + { + // Klassik hat per Design keine ChatColors — kein Banner. + if (active.ChatColors is not { Channels.Count: > 0 } themeChatColors) + return; + + // User hat die Frage bereits für genau dieses Theme beantwortet. + if (_applyDismissedFor == active.Slug) + return; + + // Wenn die aktuellen Channel-Colors bereits exakt mit dem Theme-Vorschlag + // übereinstimmen, gibt's nichts zu tun. + var alreadyMatching = themeChatColors.Channels.All(kvp => + Mutable.ChatColours.TryGetValue(kvp.Key, out var current) && current == kvp.Value); + if (alreadyMatching) + return; + + ImGui.Spacing(); + + // Dezent-Akzent-Banner mit Border in Theme-Primary + var border = ColourUtil.RgbaToAbgr(active.Colors.Primary); + var bgFill = ColourUtil.RgbaToAbgr((active.Colors.Surface & 0xFFFFFF00u) | 0xCCu); + var origin = ImGui.GetCursorScreenPos(); + var width = ImGui.GetContentRegionAvail().X; + var height = 64f; + var draw = ImGui.GetWindowDrawList(); + draw.AddRectFilled(origin, origin + new Vector2(width, height), bgFill, 4f); + draw.AddRect(origin, origin + new Vector2(width, height), border, 4f, ImDrawFlags.None, 1f); + + var hint = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Hint") + ?? "This theme suggests its own chat channel colours."; + var applyLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Apply") + ?? "Apply"; + var keepLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Keep") + ?? "Keep current"; + + var textColor = ColourUtil.RgbaToAbgr(active.Colors.TextPrimary); + draw.AddText(origin + new Vector2(12f, 10f), textColor, hint); + + // Buttons als InvisibleButton + DrawList-Overlay, damit sie konsistent + // zum Banner-Look bleiben. + using (ImRaii.PushColor(ImGuiCol.Button, active.Colors.Primary)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, active.Colors.PrimaryLight)) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, active.Colors.PrimaryDark)) + { + ImGui.SetCursorScreenPos(origin + new Vector2(12f, 32f)); + if (ImGui.Button(applyLabel)) + { + foreach (var kvp in themeChatColors.Channels) + Mutable.ChatColours[kvp.Key] = kvp.Value; + _applyDismissedFor = active.Slug; + } + } + + ImGui.SameLine(); + if (ImGui.Button(keepLabel)) + { + _applyDismissedFor = active.Slug; + } + + // Cursor unter den Banner setzen + ImGui.SetCursorScreenPos(origin + new Vector2(0f, height + 8f)); + + ImGui.Spacing(); + } } From feeb1df4eb0890856291bc024b4961716af0f503 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 14:54:58 +0200 Subject: [PATCH 30/33] docs(themes): theme authoring guide with hellion forge branding --- README.md | 8 ++ docs/THEME-AUTHORING.md | 185 ++++++++++++++++++++++++++++++++++ docs/images/hellion-forge.png | Bin 0 -> 37850 bytes 3 files changed, 193 insertions(+) create mode 100644 docs/THEME-AUTHORING.md create mode 100644 docs/images/hellion-forge.png diff --git a/README.md b/README.md index f6a898b..f257792 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,13 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf - **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font. - **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste. +#### Custom Themes (v1.1.0) + +HellionChat 1.1.0 bringt eine Theme-Engine mit fünf eingebauten Themes +(Hellion Arctic, Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove) +und ein JSON-basiertes Authoring-Format für eigene Themes. Schema und +Schritt-für-Schritt-Anleitung in [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md). + ### Pop-Out Convenience (v0.6.0) - **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort. @@ -302,6 +309,7 @@ Dokumentation lebt unter [`docs/`](docs/). | [`docs/CONTRIBUTORS.md`](docs/CONTRIBUTORS.md) | Tester, Übersetzer und Code-Beiträger der Hellion-Seite. | | [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. | | [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. | +| [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md) | Theme-Engine-Authoring-Guide (EN): JSON-Schema, Color-/Layout-Slots, Channel-Identity-Regeln, Validierung. | | [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. | | [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. | | [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. | diff --git a/docs/THEME-AUTHORING.md b/docs/THEME-AUTHORING.md new file mode 100644 index 0000000..0f7376a --- /dev/null +++ b/docs/THEME-AUTHORING.md @@ -0,0 +1,185 @@ +

+ Hellion Forge +

+ +# Theme Authoring Guide + +> Built by **Hellion Forge** — the plugin workshop arm of [Hellion Online Media](https://hellion-media.de). HellionChat ships with five built-in themes; this guide walks you through writing your own. + +## TL;DR + +1. Open Settings → Themes → **Open themes folder** +2. Copy `example-theme.json` to `.json` in the same folder +3. Edit the file with any text editor +4. Reload the plugin (toggle off/on in `/xlplugins`) +5. Your theme appears in the Custom-Themes section in Settings → Themes + +That's the whole loop. The rest of this document is reference. + +## File location + +``` +%APPDATA%\XIVLauncher\pluginConfigs\HellionChat\themes\ +``` + +(or the equivalent path on Linux/macOS — Settings → Themes → "Open themes folder" opens it directly). + +Each `*.json` file in this folder is loaded as one theme. The `example-theme.json` that HellionChat seeds on first launch is your starting template. + +## File format + +Theme JSON has four blocks: + +```json +{ + "schemaVersion": 1, + "slug": "your-slug", + "name": "Your Theme Name", + "author": "You", + "description": "One-line description shown under the theme name.", + "colors": { ... 21 color slots ... }, + "layout": { ... 9 layout values ... }, + "chatChannels": { ... optional, channel-name → hex ... } +} +``` + +### Top-level fields + +| Field | Type | Required | Notes | +|---|---|---|---| +| `schemaVersion` | int | yes | Always `1` for HellionChat 1.1.0. The plugin warns and skips themes with a different number. | +| `slug` | string | yes | Lowercase, hyphenated. Must be unique across all themes (built-in slugs are reserved). | +| `name` | string | yes | Display name in the picker. | +| `author` | string | yes | Shown small under the theme name. | +| `description` | string | yes | One short sentence. | +| `colors` | object | yes | All 21 slots required (see below). | +| `layout` | object | yes | All 9 slots required (see below). | +| `chatChannels` | object | no | Optional channel-name → hex map (see below). | + +### Color slots + +All values are 6-digit `#RRGGBB` or 8-digit `#RRGGBBAA` hex strings. Six-digit values get an implicit `FF` alpha. + +| Slot | Role | +|---|---| +| `primary` | Brand color — used on buttons, sliders, check marks, highlighted separators. | +| `primaryDark` | Pressed-button stage. | +| `primaryLight` | Hovered-button / link-text stage. | +| `primaryGlow` | Glow / subtle accent (typically primary with ~60% alpha). | +| `accent` | Counter-accent — scrollbar grab on hover/active, resize grip, optional CTA. | +| `accentDark` / `accentLight` | Dark/light siblings of accent. | +| `identity` | Title-bar active color and active-tab color. Often equals `primaryDark`. | +| `windowBg` | Outermost window background. | +| `childBg` | Inner panel / popup background. | +| `frameBg` | Input fields, sliders, combos. | +| `surface` | Card surfaces, headers, selectables. | +| `surfaceHover` | Hovered card / header step. | +| `border` | Panel borders. Typically primary with ~40% alpha for a brand-tinted edge. | +| `textPrimary` | Body text. Soft off-white reads better than pure `#FFFFFF` on dark backgrounds. | +| `textMuted` | Captions, secondary lines. | +| `textDim` | Disabled / hint text, separators. | +| `statusSuccess` | Green-ish for success notifications. | +| `statusDanger` | Red for errors. | +| `statusWarning` | Amber for warnings. | +| `statusInfo` | Cyan-ish info. Often equals primary. | + +### Layout slots + +All values are floats in pixels. `BorderSize` is 0 or 1 (no thicker borders look right with ImGui's edge anti-aliasing). + +| Slot | Typical range | Notes | +|---|---|---| +| `windowRounding` | 0–8 | 0 = sharp upstream look; 4–6 = softer "app" feel. | +| `childRounding` | 0–6 | Usually 1 less than `windowRounding`. | +| `popupRounding` | 0–6 | Same as `childRounding`. | +| `frameRounding` | 0–4 | For inputs, sliders. | +| `grabRounding` | 0–4 | Slider grab dot. | +| `tabRounding` | 0–4 | Tab corners. | +| `scrollbarRounding` | 0–4 | Scrollbar grab. | +| `windowBorderSize` | 0 or 1 | 1 reads better in dark themes. | +| `frameBorderSize` | 0 or 1 | Usually matches windowBorderSize. | + +### Optional `chatChannels` + +If present, your theme proposes its own chat-channel colors. Property names are `ChatType` enum values (case-insensitive). Unknown names are skipped silently — safe for forward-compat. + +```json +"chatChannels": { + "Say": "#FFFFFF", + "Yell": "#FFE066", + "Shout": "#FFA040", + "TellIncoming": "#FF99CC", + "TellOutgoing": "#FF99CC", + "Party": "#80C0E8", + "FreeCompany": "#4DD9E8", + "NoviceNetwork": "#A8E060", + "Linkshell1": "#A8E060" +} +``` + +The user is asked **once per theme switch** whether to apply these colors — never auto-overwriting existing picks. The banner shows up only if your suggested colors differ from the user's current `Configuration.ChatColours`. + +#### Channel-identity rule + +**Don't break FFXIV channel identity.** Players have used these conventions for over a decade: + +| Channel | Convention | Why | +|---|---|---| +| Say | white / off-white | Default-readable speech. | +| Yell | yellow | Urgent broadcast. | +| Shout | orange | Local urgent. | +| Tell | pink-magenta | Whisper, must stand out. | +| Party | light blue | Group ops. | +| FreeCompany | cyan-teal | Guild ops. | +| NoviceNetwork | lime-green | Mentor channel. | + +A theme can tint these toward its brand family (e.g., a purple theme can shift Tell from `#FF99CC` to `#E090FF`), but **don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual hierarchy. + +The four colored built-in themes (Hellion Arctic, Event Horizon, Moonlit Bloom, Mint Grove) all follow this rule — read their JSON for reference. Chat 2 Klassik intentionally ships without `chatChannels` so the user keeps their existing picks. + +## Theme families + +Naming convention `-` is recommended for theme families. The first member of a family is the lightest/brightest: + +- `mint-grove` (current built-in, light mint) +- `forest-grove` (planned, dark emerald) +- `moss-grove` (planned, mid muted) + +Code-wise families have no special handling — only the slug naming hints at the relationship. The picker may group families later, but that's not required. + +## Validation and errors + +When HellionChat loads your theme: + +- **Schema mismatch** (`schemaVersion != 1`): theme is skipped, warning written to `/xllog`. +- **Missing required field** (e.g., no `slug`): theme is skipped, warning written. +- **Invalid hex** (e.g., `#GGHHII`): theme is skipped, warning written. +- **Unknown channel name** in `chatChannels`: that one channel is skipped silently, the rest of the theme loads normally. + +Check `/xllog` after a plugin reload to see what loaded and what didn't. + +## Testing your theme + +1. Edit the JSON, save the file. +2. Reload the plugin: `/xlplugins` → toggle HellionChat off, then on. +3. Settings → Themes → click your theme card. +4. Watch every plugin window (chat, settings, pop-out) and pick something to fix. +5. Tweak. Reload. Repeat. + +Tip: the **Settings → Themes** picker shows a mini-mockup per theme — your colors are visible before you switch. + +## Sharing themes + +Themes are JSON, so sharing is just a file. Drop it into someone's `pluginConfigs/HellionChat/themes/` folder and their plugin picks it up on next reload. + +A community theme repository is on the Hellion Forge roadmap. Until then: share via Discord or any pastebin. + +## Reference + +- `docs/example-theme.json` (seeded automatically on first launch into `pluginConfigs/HellionChat/themes/`) — minimal valid theme. +- The five built-in themes live in source under `HellionChat/Themes/Builtin/`. They are a good reference for Color choices that work. +- [Hellion Online Media branding](https://hellion-media.de) — the Arctic Cyan + Ember Glow palette that drives the default Hellion Arctic theme. + +--- + +

HellionChat is a privacy-focused fork of Chat 2, distributed under the EUPL-1.2.
Theme engine and authoring guide are part of Hellion Forge.

diff --git a/docs/images/hellion-forge.png b/docs/images/hellion-forge.png new file mode 100644 index 0000000000000000000000000000000000000000..f4b169b48cb67e7ee8dc95607068a30043f7db3b GIT binary patch literal 37850 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 LNkvXXu0mjfa({cg literal 0 HcmV?d00001 From 916640fb60fb313c604ed4a7f3637911ec7005f3 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 15:00:08 +0200 Subject: [PATCH 31/33] feat(brand): swap plugin icon to hellion forge hammer The chat plugin now ships under the Hellion Forge plugin-workshop brand. Icon is the 512x512 hammer mark from the Forge logo set (was 256x256 ChatTwo derivative). --- HellionChat/images/icon.png | Bin 54069 -> 36142 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/HellionChat/images/icon.png b/HellionChat/images/icon.png index fce75cddc38bb8dda29f6a8cc592896661f557aa..8c2430cb76a7b4ae9b214ebcc9bb744b000d6274 100644 GIT binary patch literal 36142 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

    XZQT7rfO(uMe}vh3_!KQRbj>Pz=k>&MSR$ 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�jFl@^V$0SoYqU~-BS^e7P1Ai@n#+q{f9)0c;Kx3Lo<<`R zQob{e<7TB?xmpH6JxMzErdj&Wzw?Uge(>H8eiE}sW{!G49N^|8Yv)Q#P0a})zZ^w3x zM)L$s;Y)Oailwx?CSN4@aH0E8WojPR!0Qw`Sv7~=%fNFAfShBIlbqn0dZA5=9Kh+G zA6e%f24SIE;hbz>yCsdhyRfoeL@+GPFhUpwzMVYHl&Rj>vMUPw1?j2`_TIY%V8B8> zZ>WtMRLTXg#0ldhX@ZR#HekojodED|F#bQSb&J9H@$KIR0DSaQUqEQ5LtXitOYprr z?iy5?DhVkj_Uzf)nx39P5J->WO0|kGbhYz*z8bm47CWrdb^!TY6CSh? ze)fP91$ER|Ct8w=Opqsm3@kHW~TD|@e!Bt=w;oMN%BKsfW?#Z^+$%=0LUDU>sJ-`(#RhbPeK zn9jk&hYxpH^PgzS^xD@;Q`IDdh2&b<-8<#%)Nsnr; z9-F}+FMAcHr>8O7oE-tCeTkbR7lfe~hHyV~9ehz3 zdZEeuB$JlBVoGuOxU)P5cTs>FX5kiCW@ih!)5?|mI_-|Hofk`KZjc2mkAUrogaT2= zh5W8A@6?(+kDrQy9r^tQ&2Pjh0-8l}Ruk$YALc=1%Ve;d%TbOk0BD_DKGrL5PIADQ z``&5ddh&$_Oxa=yuw~vrnznE!#sUk9y1d>^ekzqB-{YUyGIXgFc_v@0)rOQ3<#GvW zn%R(F+w{c@lSP4G)#)Ujnd!h+2l}DF_i)bEb;b%QF*z}bb?erlIoCW$L^rVJn@Ipt z1o**szkV#X=R8|*4B(Ic^v&3~aU+5t=nsSNU7b$nN>hg|pkNTsJUr4X7Vmuz{f{j1^30sEFZqeT+1@!oed)~*A^%WPr6E%V~V+9BCcbv$T=58 zP&S8kB9dm{WKR45=elmL1f;#*o2!unO69~SH~lB%z!QLEAPaz`@S>4X6q*hGumLGW z;)xp*R#aDJpx`+2P@UP8;%vjSLFVjTWiN(mqAgeI)cN>!qjTq-gMv%Dw4ZATn_@Tz z<0!v|!RIgpBe5P@g=E-g?Vf?3bZkLS2PQ^Vr$I`yFpN-aUX^e<5pJTdwgXY!lqn}4 z=Dv@Ud$XX6cQ&q;5Br_W8iFswy3(^r8MS9=0)QyC68q<7{-?jc4=0>(0@gpUeke)O zcL3NfguJg-tse8ue@q;JQVR8Y9YGLW7RT}Pf*`<>B}-7LR3HO$N)mEeXY;VB&^-t% z!Nm3^M+IRJ_%PAsrNzM^K3sKw*EvJWh=>*@r8Vm+_K=Tvc>rX5@v(5e9E z5e2N%$$Xiw7+*P235PsurI4gLmqfGwmZYgEYuU{l*w4}aWF}88Dvm)D$BC{n&$95ECPEok@gB+bX%c~L^$e{di*3?!+qEckn;e4fPS5`!!+ppMp zsB9@3a5|O(aVhpVQ$EWAzLMlpu%e{8Dm;_n;UUb8cFH6$o=#jnS z=>TfAno+T>R!(8&pwVccTCI9UmGEGQMv^-6h+CZ6pTcZ6byk@0o9Ckzd~)cnYeZ!| zFFcTeC^WDL55U3gduYF)0YHQEDZ!MZ;!c?ske+Hv_A{M4lOXcQFKijgg?%oscVV#; zpNzuLH|m%sC+zpH(AN-NJdV1}<6D6qkA zT#tjj)-=oP_;}^#6^xFIU}k0pNz%DEj^lG!^S6I39Y7q%SiO22TI&hd<~!GFfvxnn zL%dC0RJszmm2=MZ?aa^#L)tJi?b!|qasAiu$h9bx%Zh(z2veR>b}_oVxiEz{SFDA1 zDsC}&O?*3C()bng`b;U1&8dYDfD|CIgcK<~7$!u1Jq~LSLLjsY%z${XVS5h3X=O<) zYJL}A-t+_CiKjUS+UCilyV(aq%yUK#3qaxDiv%DPrWm=@r>F?zKttyMlqDV!$kS#W zH^M{++S8K>fOHaKg*1klktV4f%ql^`MB4%yTCv^829!nNv_^qm9@;drC~p>q}wTSr=(Ln%oT(~xC1G8cx2I84gp8nz|dZFmzIn7J5wUa3?Atf6%- zX0meS3bb48n1~j3(8FWt0Gt?t*7_UudVOoHUSDYt>eQB;@|z}P-~lAS0ACL9b0=$P zfwx>-vcM&G!$k^hsh*Bo%#FeGMt$W@Bp(6pI_%s+Z~~-1{iG{LR@%n zlxl-{D7EX*g9R2dn^&q#c}Hg!uSA+=b|7WwH?T90>Z{BLJWk!nA?;ZqLU2EW1>$s} zr05!9=1&{WQyQoE2th8TD0BrDRfFbNA#`EW=jua6p%Dk*N>38fo}%nJ1Z6(y z*pG^H$i5rY;#7j%Ko!TK2nrkm7ZrJ>>cWsxZJ)b4+2ew2S(;^Nwc4mek&P-vLC67d^LDtNmH2 z6rI4D167cU)>;!tATSh45=4|{iUkpoqp=zgjI?Eu<)=4+?CZ$2&sq@11aHL>RPLDE zdMliE`r6l*6CSvdMTz&i&g_wCeq+(vc?yJPyP0?6hfH#eT$I0(LkC>VK{2{WMm&^n}IAIJa0@AF>Zg8@02h7_1zF8RLSKLXdSdlqn?&dUV2-t%Q(71hU)(Vuk>ltQCOBLw6y( zv(qG45LgR9Kq#dQTtXlLVVW1r!Y8dY*%Ow?B;gQt$D7ETNZ7>OZVZxAk_B`{Ud|+9 z);_3`Od^jQED_Q^Q|GjWVe^o!|5G3XvMtZX-wSiUq!1*C$nrEu2qBE)kg)%0Hnsh` zby7~K9*QA?Fd%7h89_q$vGNSkTKVUolp-e~PC^J*Ng|NQROT=VBBV$-gD2 ztB>f&$D5U^Vp?>f3V&v0^InkbV=E_`(H3Wn| zbsU{F6oIA68lJrr@ajX-Zj8K9se^JkHk-`~U}h>q9K^oyNF|vCISn&&aA*L#_w2^h z#1tBF8IyP1g{M~f@tLoF8BhB6w_$q!VT=q9L54A9S%!A21tNiZe-GB(_aG{PL?8k* z=i1cM+dvpaPyptdbLi=5KxY}IC#O(v)FFhVdfk-JoYQQ#n#eL`hQ5?yBuPr;N(`le zPLiN(QG844p(IT#aIMKiQbw}ATB%Z!C9cPUK#o?di2>8Lohd4nN>9LZ%ctoz6U5 zhU^%!R;z9J2X>rUv5%w3TujrwfVsU48?w@ov^RIJ#%Vwh}Uaz2l6t^FKw##!TaNeF>DvE66ASPj|7R^O#Te=z+}po+Y`QYg z|4$vATw+_{fD5k5U--0Z4NlNWfMzqCnVfk3(Ae0!7cW~rxaM)^VxYhOcz=~&T^l%U z{-3Tbs6FPIfWJw`N@1qcOlrM7Kkgq}^z7!rL+@gxZiLd`gCO7uF8~@WAF6giyrBhB80Bs&L+fj|OA=rIED zLMZ*Yp(RUSd-7RlP4^EE{o9_ILs_TY#!p^%^qOwJ$(GE{;>g~;q}uJ1bdr?S+#G@| zgPxp(y8El7lnOH|!QHi^v#tYp6xLX?0wR!rpa;WJY2VO_7+Hw&p7>fU*@m1bwR*KVY_bmG{&Cs0s!cM`Qo@<8_&`db5m1` zA;hVWq8CJe1K{%B(a~t_dFP?g+xwta>W^F7_fLo@#Fwt=Qa*o?z5Bj9aL>A}sPy&@ zr*m_E(?&T(l8n?>ta#VrlTWHGU%U2$aiyB>`ffJ`I9lss0FTmik^;KccwRr8*Ex^p>$!B$O#|Ydr7LPQh3Q!v zQmwW%k(v+!!EhQE7|AkO?`Le_j}Y#zkwzYy$(8E*`tb=4D$Hu^<=_Fv6o>fEaa?6k z52y7htXIA{Z(xzJy}f}SK|G*e<&w|H$DCJ(CS|L&N{koGs$6(kC}u0w;_n|C&GabA z7eS^xRc}C_2E5F=-eRn-4f%T3u9fV4&&V~Zz~<(5$Xw&Dy4BWnu)?3olD#WtFn<8+ z*==Kf5l1}Y#j}Vwk$dA6xlR`F*$ZHjxP2u+tBjH**Udi*>u3o=4#}n3@jwlZKO|72 z5eK#T5qIK?>l+J%RSo_a29p}E+W+0*FbUzja_{SD34W)P3-GT*YuZGeA9L4XMlqQA?`5DnQ)h?TPR{?e zWLl#yexDBQJ5>8^N_R&YRV@9TMiLfv*KLnng;nb=lt^3a#|A}=lbfa74O0fkwu@A6 z$-#6`K5gXn;mm-Idw9lp?UxkSPxcJ>{TYe5=aYfC;L@)v?pzjMUKJ#}tTqprJyi>g zB8Rrxab5ZYV)4T=gE`eE$q!69P?X=Jjn=4S{8m0&06f=SnLyH;Q`V6kUr6_eJ_|Am z1mJk;N7e%!>R6hVbsG<`-A_AJc)Z^lFju^L{q2C=?GJwElJzS@dE&FLT%{r#gM^I`kn5tZ>7`HZ>2k$?FzJY_x_aP8dIvR21O5 zucRd90S!);N&g}NGUhoM+L~TP5^7Y9l`UFVy}*{#_4?Of@}qqD%Y-ryVKgfnyZ0i+ z0N2D{Rxy~ zTb7y!2QET}QEOkHOWzre)At&mdHq_GX5B2}v)mcQ*6Ic20KT z&OHWxm6o+NpH`6erNZI-8IK5G|NAQ-<-6hn@%RPju&$ifXTCjEgykY zEvavrJ6IY-WM)UUNrxk35Zrc}j^8`+ReUg6LNTM8hUPY^-~N^xoKa#QKHygdu*-Lu zq>T6GX?2IYwa}uWWfvXpz0!w=Uov#ryiTUyD&-_|EkhX6XAGl!zP+$x&dvGA^d$%- z8DD#^pr{9dh&N3IS;-U_m%8-st%L%7DJw!>_mI(8?+-AOT?lcHS+6dKA&FE^ z-zj(e$0XUE!coog_3kJuJX|R-SAz9Cq;KWmjbvh8vI-%*eo$$?O)zMt_)X{`cCB-0 zcAt;))}Sq?v$&xKym!7T#1rIS824ujbGqJZzrvwbUPf9NaQv|(ubvdY$9K<9O{H2o zPdfzJvpqJZS0!Pu+_gPsUs*f*vC{6t=Vl9U&qo2}p_?Rs2N5RhHn|~?8Xoy@_A&8O z2%0KuQo8xv0>OMv%!kGCI9be{Q%dU{{zVv5vK~Ds=+qS`7}_r%%k?!c2FGV%ACU2+mO396D9m zJ^tH=i&`35yw#aK9j=#U0MSOWln+`u#Ju>p6r=Q5ExjRQ*x++X7<$_JnBg z^xmEsYc}njWADn=l}=iPgATvfqYS&1R7XKd?C1|H%NzJ=db;!18AlTHX8E)D1j``B)!J0ZCJ?l(BxDR%5 zB7moaw+X^IqJZ8#Eos+#xyfBUW|8kJnN$a@un+B%MnsM=WfA*_o%3eH=frX?Uz3`P z`4_%#J@+HrOb@rr5frYHgEQB~-f7vt@|s8zy%$O4dvdNr_*%$`ww5?otQs#l=$!|r zG{5sMffG568ol+kg`K5d7g=59h`3W>Ut6)eBeGrXoG>yh<^s?39D=eugNF)GS?4fM z{)$DEoUd%aFWl6qk1(iPD(gGkc)YsgED3ft7Pn1p3qGh_4lf8Fhn`#D1tyf`*8aHk zB6#z@SE#^g^Eo>pq_(3RvvaJrT3E<1_ECVdh^T0Bh^LsMV0|q8l$DLuJ3_5!F&I2K zGo`{Z+M9dQC@!u{pYP=)lSr5{3-4zJvSugAY=U^eE_3=XM$>4(09BsT{?Yz`bCB}X zM$WBWx16;*sFgRcIsfh08Mj70|NJ=pNW<;n(+2YB^`1M2DHI(YtsR!X5%pI-?)Soj zOjhwS`AS{Qf9E%ytNKwJZ{1jE$|u+vRcxW;odaEAd9)(6UiFsEdA@++fe~wf&%;o4 zUF}G?+ohb3*`KTQjH1`)q>OWPGc_6lPFV2qhi;OaUmHCZFp!J(^f?5JwzJpQW^4)! z(0f;+IWsUh^mhlTR)$>_;8w`#8>%7FQ*l8(BaMRx^UuU>(B>UwWhITO1<mR-d0}83X$BRc(eWPcB^6;21tyI|G`N`7+l+AD;4xHor z-ccX@_i%54SW~w_(S|4kY~id7jRr0qoY#2!O*d^)Dwx3Dr987Ng(#Pm5!ka6N4xTI zw3-McH8ZF*M>*eqHkuN}TvAA13HHhEI*2KssF zbhisN6A;+g5^!SNI0)4YH=RAh$sTwkUA^#UCaftCOkO3jd=+ZXs{;~#GuWrCK@9qc zDi70Z@Hys!+U4nBvp3dxy_WP!tTTl~i*b%g7jhaR+q9;F z2L@JDr{LFa1AGW<*fHH9@U%_8Xx%S)4anNw1~zmE_IqOk1#(ASBUW>(*12y-$v+k1 z7P$DHkvtjFu~xwwy~H6|vy%u3j-&uF3rf0Wsg{r%Z_&_k#iM_N2vJr>TTPT$m1GGe zFERYvyPSNxPcbaQ(Xs}o#@$tyZUH*}kaE!T0Q+O9!7jksN@m4Xv!x)l0 Date: Tue, 5 May 2026 15:04:52 +0200 Subject: [PATCH 32/33] =?UTF-8?q?chore(release):=20bump=20to=20v1.1.0=20?= =?UTF-8?q?=E2=80=94=20theme=20foundation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 63 ++++++++++++++++++++++++++++++++++ PRIVACY.md | 2 +- docs/CHANGELOG.md | 52 ++++++++++++++++++++++++++++ docs/ROADMAP.md | 37 ++++++++++++++------ docs/THIRD_PARTY_NOTICES.md | 4 +-- repo.json | 14 ++++---- 7 files changed, 153 insertions(+), 21 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index cfe8103..1771783 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.0.3 + 1.1.0 enable enable