diff --git a/src/Junction.App/App.axaml b/src/Junction.App/App.axaml
index 433d2b2..297511a 100644
--- a/src/Junction.App/App.axaml
+++ b/src/Junction.App/App.axaml
@@ -2,15 +2,113 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Junction.App"
xmlns:conv="clr-namespace:Junction.App.Converters"
- x:Class="Junction.App.App">
+ x:Class="Junction.App.App"
+ RequestedThemeVariant="Default">
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Junction.App/Converters/ProtocolIdToIconConverter.cs b/src/Junction.App/Converters/ProtocolIdToIconConverter.cs
new file mode 100644
index 0000000..5eb7758
--- /dev/null
+++ b/src/Junction.App/Converters/ProtocolIdToIconConverter.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace Junction.App.Converters
+{
+ ///
+ /// Maps a machine's ProtocolId ("mtconnect", future "opcua"/"fanuc", …) to a vector
+ /// glyph () for a PathIcon. Keeps view-models framework-agnostic:
+ /// the icon decision lives entirely in the view layer, mirroring
+ /// . Unknown/empty ids fall back to a generic glyph.
+ /// Glyphs are line-only 24×24 path data (no arcs/curves) so they always parse and read
+ /// as distinct filled shapes. No external image assets — net48 packaging stays a single exe set.
+ ///
+ public sealed class ProtocolIdToIconConverter : IValueConverter
+ {
+ public static readonly ProtocolIdToIconConverter Instance = new ProtocolIdToIconConverter();
+
+ // Signal-strength bars — a live telemetry feed.
+ private const string MtconnectData = "M4,14 H7 V20 H4 Z M10,10 H13 V20 H10 Z M16,5 H19 V20 H16 Z";
+
+ // Stacked diamonds — the OPC UA layered address space.
+ private const string OpcuaData = "M12,2 L20,7 L12,12 L4,7 Z M12,13 L20,18 L12,23 L4,18 Z";
+
+ // Plus/cross — an industrial CNC controller marker.
+ private const string FanucData = "M10,3 H14 V10 H21 V14 H14 V21 H10 V14 H3 V10 H10 Z";
+
+ // Octagon node — generic / unknown protocol.
+ private const string FallbackData = "M8,4 H16 L20,8 V16 L16,20 H8 L4,16 V8 Z";
+
+ private static Geometry? _mtconnect;
+ private static Geometry? _opcua;
+ private static Geometry? _fanuc;
+ private static Geometry? _fallback;
+
+ ///
+ /// Pure lookup of the raw path-data string for a protocol id. View-framework-free and
+ /// side-effect-free so it is unit-testable without an Avalonia platform. Known ids return a
+ /// distinct non-empty string; anything else returns the generic fallback. Never throws.
+ ///
+ public static string PathDataFor(string? protocolId)
+ {
+ switch (protocolId == null ? null : protocolId.Trim().ToLowerInvariant())
+ {
+ case "mtconnect":
+ return MtconnectData;
+ case "opcua":
+ return OpcuaData;
+ case "fanuc":
+ return FanucData;
+ default:
+ return FallbackData;
+ }
+ }
+
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ switch (PathDataFor(value as string))
+ {
+ case MtconnectData:
+ return _mtconnect ?? (_mtconnect = Geometry.Parse(MtconnectData));
+ case OpcuaData:
+ return _opcua ?? (_opcua = Geometry.Parse(OpcuaData));
+ case FanucData:
+ return _fanuc ?? (_fanuc = Geometry.Parse(FanucData));
+ default:
+ return _fallback ?? (_fallback = Geometry.Parse(FallbackData));
+ }
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+ }
+}
diff --git a/src/Junction.App/MainWindow.axaml b/src/Junction.App/MainWindow.axaml
index e2299d6..000f366 100644
--- a/src/Junction.App/MainWindow.axaml
+++ b/src/Junction.App/MainWindow.axaml
@@ -5,7 +5,8 @@
x:DataType="vm:MainWindowViewModel"
x:CompileBindings="True"
Title="Junction"
- Width="800" Height="600">
+ Width="900" Height="640"
+ Background="{DynamicResource AppBackgroundBrush}">
diff --git a/src/Junction.App/Views/DashboardView.axaml b/src/Junction.App/Views/DashboardView.axaml
index f2705cb..4f92f7d 100644
--- a/src/Junction.App/Views/DashboardView.axaml
+++ b/src/Junction.App/Views/DashboardView.axaml
@@ -8,66 +8,80 @@
x:DataType="vm:DashboardViewModel"
x:CompileBindings="True">
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Junction.App/Views/MachineConfigView.axaml b/src/Junction.App/Views/MachineConfigView.axaml
index 1238c30..5ccee4c 100644
--- a/src/Junction.App/Views/MachineConfigView.axaml
+++ b/src/Junction.App/Views/MachineConfigView.axaml
@@ -8,17 +8,16 @@
x:DataType="vm:MachineConfigViewModel"
x:CompileBindings="True">
-
+
-
-
+
@@ -26,47 +25,46 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
@@ -82,22 +80,22 @@
-
+ TextWrapping="Wrap" Margin="0,0,0,8" />
-
-
-
-
+
+
+
+
@@ -112,7 +110,7 @@
VerticalAlignment="Center" />
-
+
diff --git a/src/Junction.App/Views/MachineDetailView.axaml b/src/Junction.App/Views/MachineDetailView.axaml
index 8fe4de9..c1b727d 100644
--- a/src/Junction.App/Views/MachineDetailView.axaml
+++ b/src/Junction.App/Views/MachineDetailView.axaml
@@ -9,71 +9,70 @@
x:CompileBindings="True">
-
+
-
-
+
+
-
-
+
+
-
-
-
+
-
-
-
-
+
+
+
+
-
+
-
-
+ FontWeight="SemiBold" Margin="0,0,24,6" />
+
+
-
-
-
-
+
+
+
-
+
-
+ Margin="0,0,0,8" />
-
-
-
-
+
+
+
+
@@ -86,7 +85,7 @@
-
+
@@ -101,11 +100,10 @@
-
-
+
@@ -113,19 +111,16 @@
-
-
-
+
+
-
+
diff --git a/tests/Junction.Tests/Junction.Tests.csproj b/tests/Junction.Tests/Junction.Tests.csproj
index cd57b89..43b5e21 100644
--- a/tests/Junction.Tests/Junction.Tests.csproj
+++ b/tests/Junction.Tests/Junction.Tests.csproj
@@ -26,6 +26,8 @@
+
+
diff --git a/tests/Junction.Tests/Unit/ProtocolIdToIconConverterTests.cs b/tests/Junction.Tests/Unit/ProtocolIdToIconConverterTests.cs
new file mode 100644
index 0000000..597abda
--- /dev/null
+++ b/tests/Junction.Tests/Unit/ProtocolIdToIconConverterTests.cs
@@ -0,0 +1,78 @@
+using Junction.App.Converters;
+using Xunit;
+
+namespace Junction.Tests.Unit
+{
+ ///
+ /// Data-level tests for the protocol → glyph mapping. Targets the pure
+ /// so no Avalonia platform is required
+ /// (the geometry parse in Convert needs a render backend). Mirrors the intent of the
+ /// StatusKind converter: known ids → distinct non-empty data, unknown → fallback, never throws.
+ ///
+ public class ProtocolIdToIconConverterTests
+ {
+ [Theory]
+ [InlineData("mtconnect")]
+ [InlineData("opcua")]
+ [InlineData("fanuc")]
+ public void KnownProtocol_ReturnsNonEmptyData(string protocolId)
+ {
+ var data = ProtocolIdToIconConverter.PathDataFor(protocolId);
+
+ Assert.False(string.IsNullOrWhiteSpace(data));
+ }
+
+ [Fact]
+ public void KnownProtocols_AreMutuallyDistinct()
+ {
+ var mtc = ProtocolIdToIconConverter.PathDataFor("mtconnect");
+ var opc = ProtocolIdToIconConverter.PathDataFor("opcua");
+ var fan = ProtocolIdToIconConverter.PathDataFor("fanuc");
+
+ Assert.NotEqual(mtc, opc);
+ Assert.NotEqual(mtc, fan);
+ Assert.NotEqual(opc, fan);
+ }
+
+ [Fact]
+ public void Lookup_IsCaseAndWhitespaceInsensitive()
+ {
+ var canonical = ProtocolIdToIconConverter.PathDataFor("mtconnect");
+
+ Assert.Equal(canonical, ProtocolIdToIconConverter.PathDataFor(" MtConnect "));
+ }
+
+ [Theory]
+ [InlineData("unknown")]
+ [InlineData("modbus")]
+ [InlineData("")]
+ public void UnknownOrEmpty_ReturnsFallback(string protocolId)
+ {
+ var fallback = ProtocolIdToIconConverter.PathDataFor("definitely-not-a-protocol");
+ var data = ProtocolIdToIconConverter.PathDataFor(protocolId);
+
+ Assert.False(string.IsNullOrWhiteSpace(data));
+ Assert.Equal(fallback, data);
+ }
+
+ [Fact]
+ public void Null_ReturnsFallback_DoesNotThrow()
+ {
+ var fallback = ProtocolIdToIconConverter.PathDataFor("definitely-not-a-protocol");
+ var data = ProtocolIdToIconConverter.PathDataFor(null);
+
+ Assert.False(string.IsNullOrWhiteSpace(data));
+ Assert.Equal(fallback, data);
+ }
+
+ [Fact]
+ public void Fallback_DiffersFromEveryKnownProtocol()
+ {
+ var fallback = ProtocolIdToIconConverter.PathDataFor("unknown");
+
+ Assert.NotEqual(fallback, ProtocolIdToIconConverter.PathDataFor("mtconnect"));
+ Assert.NotEqual(fallback, ProtocolIdToIconConverter.PathDataFor("opcua"));
+ Assert.NotEqual(fallback, ProtocolIdToIconConverter.PathDataFor("fanuc"));
+ }
+ }
+}