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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ DiagramForge currently supports more than a dozen diagram types across Mermaid,

<h3>Built-in Themes</h3>

This theme gallery is also a representative sample rather than the full catalog. DiagramForge ships with 25 built-in themes: `default`, `zinc-light`, `zinc-dark`, `dark`, `neutral`, `forest`, `presentation`, `prism`, `angled-light`, `angled-dark`, `github-light`, `github-dark`, `nord`, `nord-light`, `dracula`, `tokyo-night`, `tokyo-night-storm`, `tokyo-night-light`, `catppuccin-latte`, `catppuccin-mocha`, `solarized-light`, `solarized-dark`, `one-dark`, `cyberpunk`, and `synthwave`. See [With a custom theme](#with-a-custom-theme), [doc/theming.md](doc/theming.md), and [doc/frontmatter.md](doc/frontmatter.md) for the full styling surface.
This theme gallery is also a representative sample rather than the full catalog. DiagramForge ships with 28 built-in themes: `default`, `zinc-light`, `zinc-dark`, `dark`, `neutral`, `forest`, `presentation`, `prism`, `angled-light`, `angled-dark`, `github-light`, `github-dark`, `nord`, `nord-light`, `dracula`, `tokyo-night`, `tokyo-night-storm`, `tokyo-night-light`, `catppuccin-latte`, `catppuccin-mocha`, `solarized-light`, `solarized-dark`, `one-dark`, `cyberpunk`, `synthwave`, `glass`, `neumorphic`, and `neon`. See [With a custom theme](#with-a-custom-theme), [doc/theming.md](doc/theming.md), and [doc/frontmatter.md](doc/frontmatter.md) for the full styling surface.

<table cellpadding="16" width="100%">
<tr>
Expand Down Expand Up @@ -272,6 +272,32 @@ This theme gallery is also a representative sample rather than the full catalog.
<sub>Synthwave</sub>
</td>
</tr>
<tr>
<td align="center" valign="top" width="50%">
<a href="https://github.com/jongalloway/DiagramForge/blob/main/tests/DiagramForge.E2ETests/Fixtures/mermaid-theme-glass.expected.svg">
<img src="https://raw.githubusercontent.com/jongalloway/DiagramForge/main/tests/DiagramForge.E2ETests/Fixtures/mermaid-theme-glass.expected.svg" alt="Glass theme" width="400" />
</a>
<br />
<sub>Glass</sub>
</td>
<td align="center" valign="top" width="50%">
<a href="https://github.com/jongalloway/DiagramForge/blob/main/tests/DiagramForge.E2ETests/Fixtures/mermaid-theme-neumorphic.expected.svg">
<img src="https://raw.githubusercontent.com/jongalloway/DiagramForge/main/tests/DiagramForge.E2ETests/Fixtures/mermaid-theme-neumorphic.expected.svg" alt="Neumorphic theme" width="400" />
</a>
<br />
<sub>Neumorphic</sub>
</td>
</tr>
<tr>
<td align="center" valign="top" width="50%">
<a href="https://github.com/jongalloway/DiagramForge/blob/main/tests/DiagramForge.E2ETests/Fixtures/mermaid-theme-neon.expected.svg">
<img src="https://raw.githubusercontent.com/jongalloway/DiagramForge/main/tests/DiagramForge.E2ETests/Fixtures/mermaid-theme-neon.expected.svg" alt="Neon theme" width="400" />
</a>
<br />
<sub>Neon</sub>
</td>
<td></td>
</tr>
</table>
<!-- markdownlint-enable MD033 -->

Expand Down
36 changes: 35 additions & 1 deletion src/DiagramForge/DiagramRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,42 @@ private static void ApplyShadowStyle(Theme theme, string shadowStyle)
theme.ShadowOffsetX = 0;
theme.ShadowOffsetY = 0;
break;
case "neumorphic":
theme.ShadowStyle = "neumorphic";
theme.UseNodeShadows = true;
theme.ShadowOpacity = Math.Clamp(theme.ShadowOpacity <= 0 ? 0.50 : theme.ShadowOpacity, 0.30, 0.70);
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 4.00 : theme.ShadowBlur, 2.00, 6.00);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shadowStyle: neumorphic does not reset ShadowOffsetX/ShadowOffsetY, but the neumorphic filter definition adds/subtracts fixed offsets (+/-5). If the base theme already has offsets (e.g., a theme with ShadowOffsetY=1.4), the resulting dark/light shadows become asymmetric and shifted. Consider explicitly setting ShadowOffsetX = 0 and ShadowOffsetY = 0 for the neumorphic frontmatter style to make it deterministic and consistent with the built-in Neumorphic preset.

Suggested change
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 4.00 : theme.ShadowBlur, 2.00, 6.00);
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 4.00 : theme.ShadowBlur, 2.00, 6.00);
theme.ShadowOffsetX = 0;
theme.ShadowOffsetY = 0;

Copilot uses AI. Check for mistakes.
theme.ShadowOffsetX = 0;
theme.ShadowOffsetY = 0;
break;
case "frosted-glass":
theme.ShadowStyle = "frosted-glass";
theme.UseNodeShadows = true;
theme.ShadowOpacity = Math.Clamp(theme.ShadowOpacity <= 0 ? 0.10 : theme.ShadowOpacity, 0.06, 0.18);
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 2.80 : theme.ShadowBlur, 1.50, 5.00);
theme.ShadowOffsetY = theme.ShadowOffsetY == 0 ? 3.00 : theme.ShadowOffsetY;
break;
case "glass-glow":
theme.ShadowStyle = "glass-glow";
theme.UseNodeShadows = true;
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 2.00 : theme.ShadowBlur, 1.00, 5.00);
theme.ShadowOffsetX = 0;
theme.ShadowOffsetY = 0;
break;
case "inner-glass":
theme.ShadowStyle = "inner-glass";
theme.UseNodeShadows = true;
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 1.20 : theme.ShadowBlur, 0.50, 2.50);
break;
case "ambient-shadow":
theme.ShadowStyle = "ambient-shadow";
theme.UseNodeShadows = true;
theme.ShadowOpacity = Math.Clamp(theme.ShadowOpacity <= 0 ? 0.10 : theme.ShadowOpacity, 0.04, 0.20);
theme.ShadowBlur = Math.Clamp(theme.ShadowBlur <= 0 ? 3.00 : theme.ShadowBlur, 1.50, 6.00);
theme.ShadowOffsetY = theme.ShadowOffsetY == 0 ? 3.00 : theme.ShadowOffsetY;
break;
default:
throw new ArgumentException($"Unknown shadow style in frontmatter: '{shadowStyle}'. Expected none, soft, or glow.", nameof(shadowStyle));
throw new ArgumentException($"Unknown shadow style in frontmatter: '{shadowStyle}'. Expected none, soft, glow, neumorphic, frosted-glass, glass-glow, inner-glass, or ambient-shadow.", nameof(shadowStyle));
}
}

Expand Down
126 changes: 126 additions & 0 deletions src/DiagramForge/Models/Theme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public class Theme
"one-dark",
"cyberpunk",
"synthwave",
"glass",
"neumorphic",
"neon",
];

// ── Built-in named presets ────────────────────────────────────────────────
Expand Down Expand Up @@ -390,6 +393,15 @@ public class Theme
/// <summary>Dark synthwave theme with warm sunset gradients and analog glow.</summary>
public static Theme Synthwave => CreateSynthwaveTheme();

/// <summary>Light frosted-glass theme with translucent sheen and specular highlights.</summary>
public static Theme Glass => CreateGlassTheme();

/// <summary>Clean neumorphic theme with paired light and dark soft shadows for tactile depth.</summary>
public static Theme Neumorphic => CreateNeumorphicTheme();

/// <summary>Dark neon theme with vivid glass-glow halos around nodes.</summary>
public static Theme Neon => CreateNeonTheme();

// ── Palette lookup ────────────────────────────────────────────────────────

/// <summary>All built-in theme names supported by <see cref="GetByName(string?)"/>.</summary>
Expand Down Expand Up @@ -428,6 +440,9 @@ public class Theme
"one-dark" => OneDark,
"cyberpunk" => Cyberpunk,
"synthwave" => Synthwave,
"glass" => Glass,
"neumorphic" => Neumorphic,
"neon" => Neon,
_ => null,
};

Expand Down Expand Up @@ -891,6 +906,117 @@ private static Theme CreateSynthwaveTheme()
return theme;
}

private static Theme CreateGlassTheme()
{
var theme = CreatePreset(
backgroundColor: "#E2E8F0",
foregroundColor: "#1A2B42",
accentColor: "#2563EB",
mutedColor: "#64748B",
surfaceColor: "#F8FAFC",
borderColor: "#94A3B8",
lineColor: "#475569",
nodePalette:
[
"#C7D8EED9", "#C7E8D5D9", "#E8D5C7D9", "#D5C7E8D9",
"#C7E8E2D9", "#E8C7D2D9", "#D5DFEAD9", "#E8E2C7D9",
],
useGradients: true,
useBorderGradients: false,
gradientStrength: 0.08);

theme.NodeFillColor = "#C7D8EED9";
theme.NodeStrokeColor = "#94A3B8";
theme.GroupFillColor = "#CBD5E1B3";
theme.GroupStrokeColor = "#94A3B8";
theme.BorderRadius = 14;
theme.StrokeWidth = 1.4;
theme.ShadowStyle = "frosted-glass";
theme.UseNodeShadows = true;
theme.ShadowColor = "#0F172A";
theme.ShadowOpacity = 0.18;
theme.ShadowBlur = 3.50;
theme.ShadowOffsetY = 3.00;
return theme;
}

private static Theme CreateNeumorphicTheme()
{
var theme = CreatePreset(
backgroundColor: "#E4E9F0",
foregroundColor: "#2D3748",
accentColor: "#4299E1",
mutedColor: "#8896A6",
surfaceColor: "#E4E9F0",
borderColor: "#D2D8E0",
lineColor: "#8896A6",
nodePalette:
[
"#E4E9F0", "#E4E9F0", "#E4E9F0", "#E4E9F0",
"#E4E9F0", "#E4E9F0", "#E4E9F0", "#E4E9F0",
],
useGradients: false,
useBorderGradients: false,
gradientStrength: 0.0);

theme.NodeFillColor = "#E4E9F0";
theme.NodeStrokeColor = "#D2D8E0";
theme.GroupFillColor = "#E4E9F0";
theme.GroupStrokeColor = "#D2D8E0";
theme.BorderRadius = 16;
theme.StrokeWidth = 0.8;
theme.ShadowStyle = "neumorphic";
theme.UseNodeShadows = true;
theme.ShadowColor = "#8B98AC";
theme.ShadowOpacity = 0.55;
theme.ShadowBlur = 4.00;
theme.ShadowOffsetX = 0;
theme.ShadowOffsetY = 0;
return theme;
}

private static Theme CreateNeonTheme()
{
var theme = CreatePreset(
backgroundColor: "#0D0D1A",
foregroundColor: "#E0E0F0",
accentColor: "#00FF88",
mutedColor: "#7A7A9E",
surfaceColor: "#141428",
borderColor: "#00FF88",
lineColor: "#00CCFF",
nodePalette:
[
"#101028", "#121230", "#0E1828", "#141432",
"#161636", "#0E1E30", "#121228", "#101830",
],
useGradients: true,
useBorderGradients: true,
gradientStrength: 0.14);

theme.NodeFillColor = "#101028";
theme.NodeStrokeColor = "#00FF88";
theme.GroupFillColor = "#141428E0";
theme.GroupStrokeColor = "#00CCFF";
theme.TitleTextColor = "#00FF88";
theme.EdgeColor = "#00CCFF";
theme.BorderGradientStops =
[
"#00FF88", // neon green
"#00CCFF", // electric blue
"#FF00FF", // magenta
"#FFD700", // gold
];
theme.BorderRadius = 8;
theme.StrokeWidth = 1.6;
theme.ShadowStyle = "glass-glow";
theme.UseNodeShadows = true;
theme.ShadowBlur = 4.00;
theme.ShadowOffsetX = 0;
theme.ShadowOffsetY = 0;
return theme;
}

private static List<string> CreateStrokePalette(IEnumerable<string> palette, double darkenAmount) =>
[.. palette.Select(color => ColorUtils.Darken(color, darkenAmount))];

Expand Down
4 changes: 2 additions & 2 deletions src/DiagramForge/Rendering/SvgNodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ internal static void AppendNode(StringBuilder sb, Node node, Theme theme, int no
&& textOnlyObj is bool isTextOnly
&& isTextOnly;
bool applyNodeShadow = theme.UseNodeShadows
&& (string.Equals(theme.ShadowStyle, "soft", StringComparison.OrdinalIgnoreCase)
|| string.Equals(theme.ShadowStyle, "glow", StringComparison.OrdinalIgnoreCase))
&& !string.IsNullOrWhiteSpace(theme.ShadowStyle)
&& !string.Equals(theme.ShadowStyle.Trim(), "none", StringComparison.OrdinalIgnoreCase)
&& !textOnly;

sb.AppendLine($""" <g transform="translate({SvgRenderSupport.F(node.X)},{SvgRenderSupport.F(node.Y)})">""");
Expand Down
Loading
Loading