Skip to content

Commit 9053be6

Browse files
NetdocsCopilot
andcommitted
Add nav icons and reference badges (mkdocs-material style)
Nav items can now show a Material-style glyph before their label via an icon on the nav entry or a page's icon: front matter — the same "Reference" look as mkdocs-material. - MaterialIcons: curated 24x24 glyph set, prefix-tolerant name lookup - nav_icon Scriban helper: override -> curated -> brand -> emoji/text - NavItem.Icon / NavNode.Icon + JsonConfigLoader + NavigationBuilder (front-matter icon, nav icon wins) - Nav template renders .nd-nav-icon in all three link forms + CSS - Badge macros: version(), flag(), badge() render .nd-badge pills - Dogfood: section + reference nav icons in docs-site config - Docs: theme.md Icons section, macros.md badge section - 16 new unit tests (badges + nav-icon resolution) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ec703e5 commit 9053be6

14 files changed

Lines changed: 447 additions & 10 deletions

File tree

docs-site/appsettings.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@
4747
},
4848

4949
"nav": [
50-
{ "title": "Home", "path": "index.md" },
50+
{ "title": "Home", "path": "index.md", "icon": "home" },
5151
{
5252
"title": "Setup",
53+
"icon": "rocket-launch",
5354
"children": [
5455
{ "path": "getting-started/index.md" },
5556
{ "path": "getting-started/installation.md" },
@@ -64,13 +65,15 @@
6465
},
6566
{
6667
"title": "Writing docs",
68+
"icon": "book-open-variant",
6769
"children": [
68-
{ "path": "reference/markdown-extensions.md" },
69-
{ "path": "reference/code-blocks.md" },
70-
{ "path": "reference/mermaid.md" },
71-
{ "path": "reference/admonitions-and-tabs.md" },
70+
{ "path": "reference/markdown-extensions.md", "icon": "format-size" },
71+
{ "path": "reference/code-blocks.md", "icon": "code-braces" },
72+
{ "path": "reference/mermaid.md", "icon": "sitemap" },
73+
{ "path": "reference/admonitions-and-tabs.md", "icon": "alert" },
7274
{
7375
"title": "Plugins",
76+
"icon": "puzzle",
7477
"children": [
7578
{ "path": "plugins/index.md" },
7679
{ "path": "plugins/abbreviations.md" },
@@ -98,6 +101,7 @@
98101
},
99102
{
100103
"title": "Deployment",
104+
"icon": "cloud-upload",
101105
"children": [
102106
{ "path": "setup/publishing.md" },
103107
{ "path": "setup/docker.md" },
@@ -107,6 +111,7 @@
107111
},
108112
{
109113
"title": "Development",
114+
"icon": "cog-outline",
110115
"children": [
111116
{ "path": "development/lifecycle.md" },
112117
{ "path": "development/events-and-callbacks.md" },
@@ -117,6 +122,7 @@
117122
},
118123
{
119124
"title": "About",
125+
"icon": "information",
120126
"children": [
121127
{ "path": "about/netdocs-vs-mkdocs.md" },
122128
{ "path": "about/benchmarks.md" },

docs-site/docs/plugins/macros.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@ offering a one-click download of the same file.
8888

8989
If the file can't be found, an HTML comment is emitted instead of a broken link.
9090

91+
### `version("x.y.z"[, "url"])`, `flag("name"[, "text"])`, `badge("icon", "text")`
92+
93+
Render mkdocs-material style **reference badges** — small pills that mark when a feature landed
94+
or its support status. They're handy at the top of a reference page.
95+
96+
```markdown
97+
{{ version("1.2.0") }} {{ flag("experimental") }}
98+
```
99+
100+
renders a version pill (tag icon + `1.2.0`) next to an "Experimental" status pill.
101+
102+
- **`version`** shows a tag icon + version. Pass a second argument to link it (e.g. to a changelog
103+
entry): `{{ version("1.2.0", "../about/changelog/") }}`.
104+
- **`flag`** shows a status pill. Known names (`experimental`, `required`, `default`, `optional`,
105+
`beta`, `new`, `deprecated`) get a matching icon and label; a second argument overrides the text.
106+
- **`badge`** is the generic form — the first argument is any
107+
[curated icon name](../reference/theme.md#icons) and the second is the label:
108+
`{{ badge("shield-check", "Stable") }}`.
109+
110+
Badges are styled by the theme (`.nd-badge`) and adapt to light/dark automatically.
111+
91112
## Writing your own macros
92113

93114
### Option 1 — Variables (no code)

docs-site/docs/reference/theme.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,45 @@ precedence over the built-ins:
281281
}
282282
```
283283

284+
## Icons
285+
286+
Nav items can show a small glyph before their label — the same "reference" look as
287+
mkdocs-material. Set an icon in **either** place:
288+
289+
- **Front matter** on a page (mkdocs-material compatible):
290+
291+
```yaml
292+
---
293+
title: Code blocks
294+
icon: code-braces
295+
---
296+
```
297+
298+
- **Nav config** (`icon` on any nav entry, including sections):
299+
300+
```json
301+
"nav": [
302+
{ "title": "Setup", "icon": "rocket-launch", "children": [ /* … */ ] },
303+
{ "path": "reference/code-blocks.md", "icon": "code-braces" }
304+
]
305+
```
306+
307+
An icon name is resolved in this order: a custom `extra.social_icons` override → a curated
308+
Material glyph → a brand glyph (`social_icon`) → a literal emoji/short string rendered as text
309+
(so `icon: "🚀"` works too). Fully-qualified names like `material/rocket-launch` also match —
310+
only the last path segment is used.
311+
312+
### Curated icon names
313+
314+
| Category | Names |
315+
|---|---|
316+
| Callouts | `alert`, `alert-circle`, `information`, `plus-circle` |
317+
| Content | `code-braces`, `tab`, `table`, `format-size`, `grid`, `image`, `format-list-bulleted`, `sigma`, `math-integral`, `tooltip-text` |
318+
| Diagrams | `sitemap`, `graph` |
319+
| Sections | `book`, `book-open-variant`, `puzzle`, `cog`, `cog-outline`, `rocket`, `rocket-launch`, `home`, `flask`, `heart` |
320+
| Files & actions | `file-document`, `cloud-upload`, `download`, `content-save`, `magnify`, `tag`, `shield-check` |
321+
| Fun | `emoticon-happy`, `cursor-default-click` |
322+
323+
Need one that isn't listed? Add it via [`extra.social_icons`](#custom-icons) — the override map
324+
feeds nav icons and [badges](../plugins/macros.md) too.
325+

src/Netdocs.Abstractions/SiteConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ public sealed class NavItem
223223
{
224224
public string? Title { get; init; }
225225
public string? Path { get; init; }
226+
public string? Icon { get; init; }
226227
public IReadOnlyList<NavItem> Children { get; init; } = [];
227228
public bool IsSection => Children.Count > 0 || Path is null;
228229
}

src/Netdocs.Abstractions/SiteContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public sealed class NavNode
5656
public required string Title { get; init; }
5757
public Page? Page { get; init; }
5858
public string? Url => Page?.Url;
59+
public string? Icon { get; set; }
5960
public IReadOnlyList<NavNode> Children { get; set; } = [];
6061
public bool IsSection => Page is null;
6162

src/Netdocs.Core/Configuration/JsonConfigLoader.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,11 @@ private static List<NavItem> ParseNav(object? node)
120120
var m = entry.AsMap();
121121
var title = m.Get("title").AsString();
122122
var path = m.Get("path").AsString();
123+
var icon = m.Get("icon").AsString();
123124
var children = m.Get("children");
124125
result.Add(children is not null
125-
? new NavItem { Title = title, Children = ParseNav(children) }
126-
: new NavItem { Title = title, Path = path });
126+
? new NavItem { Title = title, Icon = icon, Children = ParseNav(children) }
127+
: new NavItem { Title = title, Path = path, Icon = icon });
127128
}
128129
return result;
129130
}

src/Netdocs.Core/Content/NavigationBuilder.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static IReadOnlyList<NavNode> Build(SiteConfig config, IReadOnlyList<Page
2020
private static NavNode? Resolve(NavItem item, IReadOnlyDictionary<string, Page> byRelative)
2121
{
2222
if (item.Path is not null && byRelative.TryGetValue(item.Path, out var page))
23-
return new NavNode { Title = item.Title ?? page.Title, Page = page };
23+
return new NavNode { Title = item.Title ?? page.Title, Page = page, Icon = item.Icon ?? PageIcon(page) };
2424

2525
if (item.Children.Count > 0)
2626
{
@@ -35,14 +35,18 @@ public static IReadOnlyList<NavNode> Build(SiteConfig config, IReadOnlyList<Page
3535
children.RemoveAt(0);
3636
}
3737

38-
return new NavNode { Title = item.Title ?? "", Children = children, SectionIndex = sectionIndex };
38+
return new NavNode { Title = item.Title ?? "", Children = children, SectionIndex = sectionIndex, Icon = item.Icon ?? (sectionIndex is not null ? PageIcon(sectionIndex) : null) };
3939
}
4040

4141
return item.Path is null && item.Title is not null
42-
? new NavNode { Title = item.Title }
42+
? new NavNode { Title = item.Title, Icon = item.Icon }
4343
: null;
4444
}
4545

46+
/// <summary>A page can declare a nav icon via <c>icon:</c> front matter (mkdocs-material style).</summary>
47+
private static string? PageIcon(Page page) =>
48+
page.FrontMatter.TryGetValue("icon", out var v) && v is string s && s.Length > 0 ? s : null;
49+
4650
private static bool IsIndexPage(string relativePath)
4751
{
4852
var name = Path.GetFileNameWithoutExtension(relativePath);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Collections.Frozen;
2+
3+
namespace Netdocs.Core.Templating;
4+
5+
/// <summary>
6+
/// A small, curated set of Material Design Icons (24×24 path data) used for nav and badge
7+
/// glyphs. Keys are the short icon name; callers may also pass fully-qualified names such as
8+
/// <c>material/alert</c> or <c>octicons/tag-16</c> — only the final path segment is matched.
9+
/// Sites can add or override any glyph via <c>extra.social_icons</c> in their config.
10+
/// </summary>
11+
public static class MaterialIcons
12+
{
13+
/// <summary>Resolve an icon name to its SVG path data, or <c>null</c> if unknown.</summary>
14+
public static string? Path(string name)
15+
{
16+
if (string.IsNullOrWhiteSpace(name)) return null;
17+
var key = name.Trim();
18+
var slash = key.LastIndexOf('/');
19+
if (slash >= 0) key = key[(slash + 1)..];
20+
key = key.Trim(':'); // allow :material-alert: style
21+
key = key.Replace("material-", "").Replace("octicons-", "");
22+
return Map.TryGetValue(key, out var d) ? d : null;
23+
}
24+
25+
private static readonly FrozenDictionary<string, string> Map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
26+
{
27+
["alert"] = "M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z",
28+
["alert-circle"] = "M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
29+
["information"] = "M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
30+
["plus-circle"] = "M17,13H13V17H11V13H7V11H11V7H13V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
31+
["cursor-default-click"] = "M13.64 21.97C13.14 22.21 12.54 22 12.31 21.5L10.13 16.76L7.62 18.78C7.45 18.92 7.24 19 7 19A1 1 0 0 1 6 18V3A1 1 0 0 1 7 2C7.24 2 7.47 2.09 7.64 2.23L7.65 2.22L19.14 11.86C19.57 12.22 19.62 12.85 19.27 13.27C19.12 13.45 18.91 13.57 18.7 13.61L15.54 14.23L17.74 18.96C18 19.46 17.76 20.05 17.26 20.28L13.64 21.97Z",
32+
["code-braces"] = "M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z",
33+
["tab"] = "M21,3H3C1.9,3 1,3.9 1,5V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19V5C23,3.9 22.1,3 21,3M21,19H3V5H13V9H21V19Z",
34+
["table"] = "M5,4H19A2,2 0 0,1 21,6V18A2,2 0 0,1 19,20H5A2,2 0 0,1 3,18V6A2,2 0 0,1 5,4M5,8V12H11V8H5M13,8V12H19V8H13M5,14V18H11V14H5M13,14V18H19V14H13Z",
35+
["sitemap"] = "M9,2V8H11V11H5C3.89,11 3,11.89 3,13V16H1V22H7V16H5V13H19V16H17V22H23V16H21V13C21,11.89 20.11,11 19,11H13V8H15V2H9Z",
36+
["graph"] = "M9,2V8H11V11H5C3.89,11 3,11.89 3,13V16H1V22H7V16H5V13H19V16H17V22H23V16H21V13C21,11.89 20.11,11 19,11H13V8H15V2H9Z",
37+
["format-size"] = "M2,4V7H7V19H10V7H15V4H2M21,9H14V12H16.5V19H18.5V12H21V9Z",
38+
["grid"] = "M2,2H22V22H2V2M4,4V8H8V4H4M10,4V8H14V4H10M16,4V8H20V4H16M4,10V14H8V10H4M10,10V14H14V10H10M16,10V14H20V10H16M4,16V20H8V16H4M10,16V20H14V16H10M16,16V20H20V16H16Z",
39+
["emoticon-happy"] = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M8.5,8A1.5,1.5 0 0,1 10,9.5A1.5,1.5 0 0,1 8.5,11A1.5,1.5 0 0,1 7,9.5A1.5,1.5 0 0,1 8.5,8M15.5,8A1.5,1.5 0 0,1 17,9.5A1.5,1.5 0 0,1 15.5,11A1.5,1.5 0 0,1 14,9.5A1.5,1.5 0 0,1 15.5,8M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z",
40+
["image"] = "M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z",
41+
["format-list-bulleted"] = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z",
42+
["sigma"] = "M18,6H8.83L14.83,12L8.83,18H18V20H6V18L12,12L6,6V4H18V6Z",
43+
["math-integral"] = "M18,6H8.83L14.83,12L8.83,18H18V20H6V18L12,12L6,6V4H18V6Z",
44+
["tooltip-text"] = "M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M4,4V16H8.83L12,19.17L15.17,16H20V4H4M6,7H18V9H6V7M6,11H16V13H6V11Z",
45+
["book-open-variant"] = "M12,21.5C10.65,20.65 8.2,20 6.5,20C4.85,20 3.15,20.3 1.75,21.05C1.65,21.1 1.6,21.1 1.5,21.1C1.25,21.1 1,20.85 1,20.6V6C1.6,5.55 2.25,5.25 3,5C4.11,4.65 5.33,4.5 6.5,4.5C8.45,4.5 10.55,4.9 12,6C13.45,4.9 15.55,4.5 17.5,4.5C18.67,4.5 19.89,4.65 21,5C21.75,5.25 22.4,5.55 23,6V20.6C23,20.85 22.75,21.1 22.5,21.1C22.4,21.1 22.35,21.1 22.25,21.05C20.85,20.3 19.15,20 17.5,20C15.8,20 13.35,20.65 12,21.5Z",
46+
["book"] = "M18,22A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18M9,4H11V9L9.5,7.75L8,9V4H9Z",
47+
["puzzle"] = "M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5,10.8 6.2,12 6.2,13.5C6.2,15 5,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,19 9,17.8 10.5,17.8C12,17.8 13.2,19 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z",
48+
["cog"] = "M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z",
49+
["cog-outline"] = "M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z",
50+
["rocket-launch"] = "M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M21.61 2.39C21.61 2.39 16.66 .269 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.79 17.44 10.61 17.63 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63 18.15 8.68 17.37 9.46 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.46 5.64 14.12L2 17.76V19.17Z",
51+
["rocket"] = "M2.81,14.12L5.64,11.29L8.17,10.79C11.39,6.41 17.55,4.22 19.78,4.22C19.78,6.45 17.59,12.61 13.21,15.83L12.71,18.36L9.88,21.19L9.53,16.85C9.53,16.85 8.62,17.76 8.62,17.76M14.14,11.15C14.72,11.73 15.66,11.73 16.24,11.15C16.82,10.57 16.82,9.63 16.24,9.05C15.66,8.47 14.72,8.47 14.14,9.05C13.56,9.63 13.56,10.57 14.14,11.15Z",
52+
["file-document"] = "M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z",
53+
["cloud-upload"] = "M14,13V17H10V13H7L12,8L17,13M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z",
54+
["magnify"] = "M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z",
55+
["tag"] = "M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z",
56+
["content-save"] = "M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z",
57+
["home"] = "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z",
58+
["flask"] = "M5,19A1,1 0 0,0 6,20H18A1,1 0 0,0 19,19C19,18.79 18.93,18.59 18.82,18.43L13,8.35V4H11V8.35L5.18,18.43C5.07,18.59 5,18.79 5,19M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6Z",
59+
["heart"] = "M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z",
60+
["download"] = "M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z",
61+
["shield-check"] = "M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9L10,17Z",
62+
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
63+
}

src/Netdocs.Core/Templating/ThemeTemplateLoader.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static void Register(ScriptObject globals, IDictionary<string, object?>?
4444

4545
var overrides = ExtractIconOverrides(model);
4646
globals.Import("social_icon", (string? name) => SocialIcon(name ?? "", overrides));
47+
globals.Import("nav_icon", (string? name) => NavIcon(name ?? "", overrides));
4748

4849
var versioner = (model is not null && model.TryGetValue("asset_versioner", out var v) ? v as AssetVersioner : null)
4950
?? AssetVersioner.NoOp;
@@ -98,4 +99,28 @@ private static string SocialIcon(string name, IReadOnlyDictionary<string, string
9899

99100
private static string Svg(string path) =>
100101
$"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"{path}\"/></svg>";
102+
103+
/// <summary>
104+
/// Resolves a nav/badge icon to inline markup. Resolution order: custom <c>extra.social_icons</c>
105+
/// override → curated Material glyph (<see cref="MaterialIcons"/>) → brand glyph
106+
/// (<see cref="SocialIcon"/>) → a short literal string/emoji rendered as text. Returns an empty
107+
/// string for a blank name so templates can call it unconditionally.
108+
/// </summary>
109+
private static string NavIcon(string name, IReadOnlyDictionary<string, string> overrides)
110+
{
111+
if (string.IsNullOrWhiteSpace(name)) return string.Empty;
112+
113+
if (overrides.TryGetValue(name, out var exact))
114+
return Svg(exact);
115+
116+
if (MaterialIcons.Path(name) is { } d)
117+
return Svg(d);
118+
119+
// A short token with no slash is treated as an emoji/text glyph rather than an icon name.
120+
if (!name.Contains('/') && name.Length <= 4)
121+
return $"<span class=\"nd-emoji\">{System.Net.WebUtility.HtmlEncode(name)}</span>";
122+
123+
// Fall back to the brand-icon resolver (github, rss, mail, …); it returns a globe otherwise.
124+
return SocialIcon(name, overrides);
125+
}
101126
}

0 commit comments

Comments
 (0)