Skip to content

Commit e2ea15b

Browse files
committed
feat(examples): WinUI-style NavigationView + GridControl showcase apps
Add three full-screen example apps that compose NavigationView (the WinUI nav shell) with a Fill GridControl as each page's root: - GridDashboard — "Mission Control": live metric tiles + sparklines, a colSpan trend graph, a process table, splitter-resized network panes, an auto-scrolling log feed, and a settings form. - GridGallery — "Control Gallery": responsive 2x2 tile grids showing live instances of the control set by category, with a gridline toggle. - BizDashboard — "Business Analytics + POS": Sales KPIs, master-detail Customers, status-colored Inventory, a markdown Reports page, and an interactive Point of Sale page (tap a product -> live cart table + totals -> numeric keypad -> change due). All three use live-simulated, deterministic data (no System.Random), marshalled to the UI via the window's async thread. Enabling library change (additive, non-breaking): NavigationView can now host an arbitrary IWindowControl directly as page content (SetItemContent(item, IWindowControl) + builder AddItem overloads), bypassing the built-in scrollable content panel. This is required because a Fill GridControl collapses to height 1 inside that panel but fills correctly as a direct content-column child. The existing SetItemContent(item, Action<ScrollablePanelControl>) path is unchanged.
1 parent 16c4a17 commit e2ea15b

18 files changed

Lines changed: 2169 additions & 15 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<ProjectReference Include="..\..\SharpConsoleUI\SharpConsoleUI.csproj" />
10+
</ItemGroup>
11+
</Project>

Examples/BizDashboard/Pages.cs

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
// -----------------------------------------------------------------------
2+
// BizDashboard — analytics page builders.
3+
//
4+
// Each returns a Fill+Stretch GridControl that becomes the direct root of a
5+
// NavigationView page (via nav.SetItemContent). The Sales grid's live KPI
6+
// controls are stored in BizRefs so the deterministic Simulation walk can
7+
// touch them each tick.
8+
//
9+
// NOTE on track sizing: never use GridLength.Auto() for a track whose content
10+
// might measure 0 (Markup/Dropdown/Checkbox collapse it to blank). Use
11+
// GridLength.Cells(N) for fixed rows and GridLength.Star(1) for proportional.
12+
// -----------------------------------------------------------------------
13+
14+
using SharpConsoleUI;
15+
using SharpConsoleUI.Builders;
16+
using SharpConsoleUI.Controls;
17+
using SharpConsoleUI.Layout;
18+
using SharpConsoleUI.Themes;
19+
20+
namespace BizDashboard;
21+
22+
/// <summary>
23+
/// Holds the live Sales-page controls the <see cref="Simulation"/> updates each tick.
24+
/// </summary>
25+
internal sealed class BizRefs
26+
{
27+
// Sales KPI tiles (big number + delta line each).
28+
public MarkupControl RevenueTile { get; set; } = null!;
29+
public MarkupControl OrdersTile { get; set; } = null!;
30+
public MarkupControl ChurnTile { get; set; } = null!;
31+
32+
// Sales trend chart.
33+
public LineGraphControl RevenueGraph { get; set; } = null!;
34+
}
35+
36+
internal static class Pages
37+
{
38+
private static readonly Color BgSlate = new(28, 32, 48);
39+
private static readonly Color TileBg = new(34, 40, 60);
40+
41+
/// <summary>Splits a '\n'-joined markup block into per-row markup lines.</summary>
42+
private static string[] SplitLines(string s) => s.Split('\n');
43+
44+
private static GridBuilder FillGrid() =>
45+
Controls.Grid()
46+
.WithColorRole(ColorRole.Primary)
47+
.WithVerticalAlignment(VerticalAlignment.Fill)
48+
.WithAlignment(HorizontalAlignment.Stretch);
49+
50+
#region Sales
51+
52+
/// <summary>
53+
/// Three KPI tiles (Revenue / Orders / Churn) across the top, a wide
54+
/// revenue trend line graph below. KPIs + chart are driven live by the sim.
55+
/// </summary>
56+
public static GridControl BuildSalesGrid(BizRefs refs)
57+
{
58+
var grid = FillGrid()
59+
.Columns(GridLength.Star(1), GridLength.Star(1), GridLength.Star(1))
60+
.Rows(GridLength.Cells(8), GridLength.Star(1))
61+
.RowGap(1)
62+
.ColumnGap(2)
63+
.WithPadding(1, 1, 1, 1)
64+
.Build();
65+
66+
// Seed each tile from a SPLIT line list (one markup line per row) so the
67+
// big-number value is populated and visible immediately on launch — and
68+
// so the build-time content matches exactly what the sim writes each tick
69+
// via SetContent(List<string>). A single '\n'-joined string would land as
70+
// one logical line and hide the value line.
71+
var revenue = Controls.Markup()
72+
.AddLines(SplitLines(FormatRevenue(184_200, +4.2)))
73+
.WithVerticalAlignment(VerticalAlignment.Top)
74+
.WithMargin(2, 0, 1, 0)
75+
.Build();
76+
var orders = Controls.Markup()
77+
.AddLines(SplitLines(FormatOrders(1_284, +2.8)))
78+
.WithVerticalAlignment(VerticalAlignment.Top)
79+
.WithMargin(2, 0, 1, 0)
80+
.Build();
81+
var churn = Controls.Markup()
82+
.AddLines(SplitLines(FormatChurn(2.4, -0.3)))
83+
.WithVerticalAlignment(VerticalAlignment.Top)
84+
.WithMargin(2, 0, 1, 0)
85+
.Build();
86+
87+
grid.Place(revenue, 0, 0);
88+
grid.Place(orders, 0, 1);
89+
grid.Place(churn, 0, 2);
90+
grid.Cell(0, 0).Border = BorderStyle.Rounded;
91+
grid.Cell(0, 1).Border = BorderStyle.Rounded;
92+
grid.Cell(0, 2).Border = BorderStyle.Rounded;
93+
grid.Cell(0, 0).Background = TileBg;
94+
grid.Cell(0, 1).Background = TileBg;
95+
grid.Cell(0, 2).Background = TileBg;
96+
97+
var graph = Controls.LineGraph()
98+
.WithTitle("Revenue (last 60 ticks)")
99+
.WithMode(LineGraphMode.Braille)
100+
.WithColorRole(ColorRole.Success)
101+
.WithMinValue(0)
102+
.WithMaxValue(100)
103+
.WithData(new double[] { 0 })
104+
.WithVerticalAlignment(VerticalAlignment.Fill)
105+
.WithMargin(1, 1, 1, 1)
106+
.Build();
107+
graph.MaxDataPoints = Simulation.MaxPoints;
108+
109+
grid.Place(graph, 1, 0, colSpan: 3);
110+
grid.Cell(1, 0).Border = BorderStyle.Rounded;
111+
grid.Cell(1, 0).Background = BgSlate;
112+
113+
refs.RevenueTile = revenue;
114+
refs.OrdersTile = orders;
115+
refs.ChurnTile = churn;
116+
refs.RevenueGraph = graph;
117+
return grid;
118+
}
119+
120+
internal static string FormatRevenue(int dollars, double deltaPct)
121+
{
122+
string arrow = deltaPct >= 0 ? "▲" : "▼";
123+
string clr = deltaPct >= 0 ? "rgb(120,220,160)" : "rgb(230,120,110)";
124+
return string.Join('\n', new[]
125+
{
126+
"[dim]REVENUE (MTD)[/]",
127+
$"[bold rgb(120,220,160)] ${dollars:N0}[/]",
128+
$"[{clr}]{arrow} {Math.Abs(deltaPct):0.0}% vs last month[/]",
129+
});
130+
}
131+
132+
internal static string FormatOrders(int count, double deltaPct)
133+
{
134+
string arrow = deltaPct >= 0 ? "▲" : "▼";
135+
string clr = deltaPct >= 0 ? "rgb(120,220,160)" : "rgb(230,120,110)";
136+
return string.Join('\n', new[]
137+
{
138+
"[dim]ORDERS (MTD)[/]",
139+
$"[bold rgb(120,190,255)] {count:N0}[/]",
140+
$"[{clr}]{arrow} {Math.Abs(deltaPct):0.0}% vs last month[/]",
141+
});
142+
}
143+
144+
internal static string FormatChurn(double pct, double deltaPct)
145+
{
146+
// For churn, DOWN is good — invert the color logic.
147+
string arrow = deltaPct >= 0 ? "▲" : "▼";
148+
string clr = deltaPct <= 0 ? "rgb(120,220,160)" : "rgb(230,120,110)";
149+
return string.Join('\n', new[]
150+
{
151+
"[dim]CHURN (30d)[/]",
152+
$"[bold rgb(230,180,90)] {pct:0.0}%[/]",
153+
$"[{clr}]{arrow} {Math.Abs(deltaPct):0.0} pts vs last month[/]",
154+
});
155+
}
156+
157+
#endregion
158+
159+
#region Customers
160+
161+
/// <summary>
162+
/// Left: a customer table (Name / Plan / MRR). Right: a detail markup that
163+
/// updates as the selected row changes via SelectedRowChanged.
164+
/// </summary>
165+
public static GridControl BuildCustomersGrid()
166+
{
167+
var grid = FillGrid()
168+
.Columns(GridLength.Star(1), GridLength.Star(1))
169+
.Rows(GridLength.Star(1))
170+
.ColumnGap(2)
171+
.ColumnSplitterAfter(0)
172+
.WithPadding(1, 1, 1, 1)
173+
.Build();
174+
175+
var table = Controls.Table()
176+
.WithColorRole(ColorRole.Primary)
177+
.AddColumn("Customer")
178+
.AddColumn("Plan")
179+
.AddColumn("MRR", TextJustification.Right)
180+
.WithVerticalAlignment(VerticalAlignment.Fill)
181+
.WithMargin(1, 1, 1, 1)
182+
.Build();
183+
184+
(string name, string plan, string mrr, string detail)[] seed =
185+
{
186+
("Acme Corp", "Enterprise", "$4,200", "12 seats · renews Sep · CSM: Dana"),
187+
("Globex", "Business", "$1,150", "5 seats · renews Jul · trial→paid"),
188+
("Initech", "Business", "$980", "4 seats · at-risk · low usage"),
189+
("Umbrella", "Enterprise", "$6,400", "30 seats · expanding · upsell ready"),
190+
("Hooli", "Starter", "$290", "2 seats · self-serve · healthy"),
191+
("Stark Ind.", "Enterprise", "$8,900", "44 seats · advocate · case study"),
192+
("Wayne Ent.", "Business", "$1,320", "6 seats · renews Aug · stable"),
193+
("Soylent", "Starter", "$190", "1 seat · churned trial last Q"),
194+
};
195+
196+
string[] details = new string[seed.Length];
197+
for (int i = 0; i < seed.Length; i++)
198+
{
199+
table.AddRow(seed[i].name, seed[i].plan, seed[i].mrr);
200+
details[i] = seed[i].detail;
201+
}
202+
203+
var detail = Controls.Markup(BuildCustomerDetail(seed[0].name, seed[0].plan, seed[0].mrr, details[0]))
204+
.WithVerticalAlignment(VerticalAlignment.Fill)
205+
.WithMargin(2, 1, 2, 1)
206+
.Build();
207+
208+
table.SelectedRowChanged += (s, idx) =>
209+
{
210+
if (idx >= 0 && idx < seed.Length)
211+
{
212+
var c = seed[idx];
213+
detail.SetContent(BuildCustomerDetailLines(c.name, c.plan, c.mrr, details[idx]));
214+
}
215+
};
216+
217+
grid.Place(table, 0, 0);
218+
grid.Place(detail, 0, 1);
219+
grid.Cell(0, 0).Border = BorderStyle.Rounded;
220+
grid.Cell(0, 1).Border = BorderStyle.Rounded;
221+
grid.Cell(0, 0).Background = BgSlate;
222+
grid.Cell(0, 1).Background = BgSlate;
223+
return grid;
224+
}
225+
226+
private static List<string> BuildCustomerDetailLines(string name, string plan, string mrr, string detail) =>
227+
new()
228+
{
229+
$"[bold rgb(120,200,255)]{name}[/]",
230+
"",
231+
$"[dim]Plan[/] {plan}",
232+
$"[dim]MRR [/] [bold rgb(120,220,160)]{mrr}[/]",
233+
"",
234+
$"[dim]{detail}[/]",
235+
};
236+
237+
private static string BuildCustomerDetail(string name, string plan, string mrr, string detail) =>
238+
string.Join('\n', BuildCustomerDetailLines(name, plan, mrr, detail));
239+
240+
#endregion
241+
242+
#region Inventory
243+
244+
/// <summary>A single Fill table with status colored via inline markup.</summary>
245+
public static GridControl BuildInventoryGrid()
246+
{
247+
var grid = FillGrid()
248+
.Columns(GridLength.Star(1))
249+
.Rows(GridLength.Star(1))
250+
.WithPadding(1, 1, 1, 1)
251+
.Build();
252+
253+
var table = Controls.Table()
254+
.WithColorRole(ColorRole.Primary)
255+
.AddColumn("SKU")
256+
.AddColumn("Item")
257+
.AddColumn("Stock", TextJustification.Right)
258+
.AddColumn("Status")
259+
.WithVerticalAlignment(VerticalAlignment.Fill)
260+
.WithMargin(1, 1, 1, 1)
261+
.Build();
262+
263+
(string sku, string item, int stock)[] seed =
264+
{
265+
("CFE-001", "House Blend Coffee", 142),
266+
("TEA-014", "Green Tea", 6),
267+
("BAK-220", "Blueberry Muffin", 0),
268+
("BAK-118", "Sesame Bagel", 38),
269+
("JUI-007", "Orange Juice", 21),
270+
("BAK-330", "Choc Chip Cookie", 4),
271+
("CFE-009", "Decaf Beans", 75),
272+
("SYR-002", "Vanilla Syrup", 0),
273+
("CUP-016", "12oz Paper Cups", 410),
274+
("LID-016", "12oz Lids", 12),
275+
};
276+
277+
foreach (var (sku, item, stock) in seed)
278+
table.AddRow(sku, item, stock.ToString(), StatusMarkup(stock));
279+
280+
grid.Place(table, 0, 0);
281+
grid.Cell(0, 0).Border = BorderStyle.Rounded;
282+
grid.Cell(0, 0).Background = BgSlate;
283+
return grid;
284+
}
285+
286+
private static string StatusMarkup(int stock) => stock switch
287+
{
288+
0 => "[red]Out[/]",
289+
<= 10 => "[yellow]Low[/]",
290+
_ => "[green]In stock[/]",
291+
};
292+
293+
#endregion
294+
295+
#region Reports
296+
297+
/// <summary>A single Fill markdown pane — an executive business summary.</summary>
298+
public static GridControl BuildReportsGrid()
299+
{
300+
var grid = FillGrid()
301+
.Columns(GridLength.Star(1))
302+
.Rows(GridLength.Star(1))
303+
.WithPadding(1, 1, 1, 1)
304+
.Build();
305+
306+
string md = "[markdown]" + string.Join('\n', new[]
307+
{
308+
"# Q2 Executive Summary",
309+
"",
310+
"Revenue is **up 4.2%** month-over-month, driven by enterprise expansion",
311+
"and a healthy trial-to-paid conversion rate.",
312+
"",
313+
"## Highlights",
314+
"",
315+
"- **Revenue (MTD):** $184,200",
316+
"- **New logos:** 14 (best quarter on record)",
317+
"- **Churn (30d):** 2.4% — *down* 0.3 pts",
318+
"- **NRR:** 118%",
319+
"",
320+
"## Watch items",
321+
"",
322+
"- *Initech* flagged at-risk — low usage, renews this month",
323+
"- Two SKUs out of stock (Blueberry Muffin, Vanilla Syrup)",
324+
"- 12oz Lids running low — reorder before weekend",
325+
"",
326+
"## Top accounts by MRR",
327+
"",
328+
"1. Stark Industries — $8,900",
329+
"2. Umbrella — $6,400",
330+
"3. Acme Corp — $4,200",
331+
"",
332+
"> Net: a strong quarter. Prioritize the Initech save and clear the",
333+
"> two stockouts before the weekend rush.",
334+
}) + "[/]";
335+
336+
var report = Controls.Markup(md)
337+
.WithVerticalAlignment(VerticalAlignment.Fill)
338+
.WithMargin(2, 1, 2, 1)
339+
.Build();
340+
341+
grid.Place(report, 0, 0);
342+
grid.Cell(0, 0).Border = BorderStyle.Rounded;
343+
grid.Cell(0, 0).Background = BgSlate;
344+
return grid;
345+
}
346+
347+
#endregion
348+
}

0 commit comments

Comments
 (0)