Forge
csharp3407750f
1namespace AgentForge.Plugin.View.Components.Layout;
2
3/// <summary>Sectioned list for Human-layer catalog views (Group by axis).</summary>
4public static class ForgeViewGroupedList
5{
6 public readonly record struct Section(string Key, string Label);
7
8 public static string Render<T>(
9 IReadOnlyList<T> items,
10 Func<T, string> groupKey,
11 IReadOnlyList<Section> sections,
12 Func<T, string> renderItem,
13 string listClass = "plain view-group-items",
14 string sectionDetailsClass = "view-group-section",
15 string sectionHeadClass = "view-group-head")
16 {
17 if (items.Count == 0)
18 return "";
19
20 var buckets = items
21 .GroupBy(groupKey, StringComparer.OrdinalIgnoreCase)
22 .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
23
24 var sectionNodes = new List<string>();
25 foreach (var section in sections)
26 {
27 if (!buckets.TryGetValue(section.Key, out var bucket) || bucket.Count == 0)
28 continue;
29
30 var summary = ForgeHtml.Fragment(
31 ForgeHtml.Strong(section.Label),
32 ForgeHtml.Text(" "),
33 ForgeHtml.Span("meta", ForgeHtml.Text($"{bucket.Count} repos")));
34
35 var body = ForgeHtml.Ul(listClass, [.. bucket.Select(renderItem)]);
36 sectionNodes.Add(ForgeViewDisclosure.Render(
37 summary,
38 body,
39 open: false,
40 sectionDetailsClass,
41 sectionHeadClass));
42 }
43
44 return sectionNodes.Count == 0
45 ? ForgeHtml.Ul(listClass, [.. items.Select(renderItem)])
46 : ForgeHtml.Div("view-grouped-list", string.Concat(sectionNodes));
47 }
48
49 public static string RenderNested<T>(
50 IReadOnlyList<T> items,
51 IReadOnlyList<(Func<T, string> KeySelector, IReadOnlyList<Section> Sections)> axes,
52 Func<T, string> renderItem,
53 string listClass = "plain view-group-items",
54 string sectionDetailsClass = "view-group-section",
55 string sectionHeadClass = "view-group-head")
56 {
57 if (items.Count == 0)
58 return "";
59
60 if (axes.Count == 0)
61 return ForgeHtml.Ul(listClass, [.. items.Select(renderItem)]);
62
63 return RenderNestedLevel(
64 items,
65 axes,
66 0,
67 renderItem,
68 listClass,
69 sectionDetailsClass,
70 sectionHeadClass);
71 }
72
73 private static string RenderNestedLevel<T>(
74 IReadOnlyList<T> items,
75 IReadOnlyList<(Func<T, string> KeySelector, IReadOnlyList<Section> Sections)> axes,
76 int depth,
77 Func<T, string> renderItem,
78 string listClass,
79 string sectionDetailsClass,
80 string sectionHeadClass)
81 {
82 if (depth >= axes.Count)
83 return ForgeHtml.Ul(listClass, [.. items.Select(renderItem)]);
84
85 var (keySelector, sections) = axes[depth];
86 var buckets = items
87 .GroupBy(keySelector, StringComparer.OrdinalIgnoreCase)
88 .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
89
90 var sectionNodes = new List<string>();
91 foreach (var section in sections)
92 {
93 if (!buckets.TryGetValue(section.Key, out var bucket) || bucket.Count == 0)
94 continue;
95
96 var summary = ForgeHtml.Fragment(
97 ForgeHtml.Strong(section.Label),
98 ForgeHtml.Text(" "),
99 ForgeHtml.Span("meta", ForgeHtml.Text($"{bucket.Count} repos")));
100
101 var body = depth == axes.Count - 1
102 ? ForgeHtml.Ul(listClass, [.. bucket.Select(renderItem)])
103 : RenderNestedLevel(bucket, axes, depth + 1, renderItem, listClass, sectionDetailsClass, sectionHeadClass);
104
105 sectionNodes.Add(ForgeViewDisclosure.Render(
106 summary,
107 body,
108 open: false,
109 sectionDetailsClass,
110 sectionHeadClass));
111 }
112
113 return sectionNodes.Count == 0
114 ? ForgeHtml.Ul(listClass, [.. items.Select(renderItem)])
115 : ForgeHtml.Div("view-grouped-list", string.Concat(sectionNodes));
116 }
117}
118
View only · write via MCP/CIDE