diff --git a/App.xaml b/App.xaml
index a26f4a708..4f824166e 100644
--- a/App.xaml
+++ b/App.xaml
@@ -189,33 +189,64 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -745,7 +784,8 @@
-
+
+
diff --git a/GridUxBehavior.cs b/GridUxBehavior.cs
index f3514172b..44127f65c 100644
--- a/GridUxBehavior.cs
+++ b/GridUxBehavior.cs
@@ -41,7 +41,9 @@ private sealed class GlobalRapidFilterState
public required ICollectionView View { get; init; }
public required DispatcherTimer RefreshTimer { get; init; }
public Dictionary Filters { get; } = new(StringComparer.OrdinalIgnoreCase);
+ public List HeaderRoots { get; } = new();
public string SearchQuery { get; set; } = string.Empty;
+ public bool FiltersExpanded { get; set; }
}
private static readonly ConditionalWeakTable MainWindows = new();
@@ -494,9 +496,8 @@ private static void InstallGlobalRapidFilters(MainWindow owner, DataGrid grid)
headerStyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Color.FromRgb(243, 246, 250))));
headerStyle.Setters.Add(new Setter(Control.BorderBrushProperty, new SolidColorBrush(Color.FromRgb(220, 227, 236))));
headerStyle.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0, 0, 1, 1)));
- headerStyle.Setters.Add(new Setter(FrameworkElement.HeightProperty, 74d));
grid.ColumnHeaderStyle = headerStyle;
- grid.ColumnHeaderHeight = 74;
+ grid.ColumnHeaderHeight = 34;
foreach (var column in grid.Columns)
{
@@ -515,6 +516,22 @@ internal static void SetGlobalRapidSearch(DataGrid grid, string? query)
state.RefreshTimer.Start();
}
+ internal static void SetGlobalRapidFiltersExpanded(DataGrid grid, bool expanded)
+ {
+ if (!GlobalGrids.TryGetValue(grid, out var state))
+ return;
+
+ state.FiltersExpanded = expanded;
+ foreach (var root in state.HeaderRoots)
+ {
+ if (root.RowDefinitions.Count < 2)
+ continue;
+ root.RowDefinitions[1].Height = expanded ? new GridLength(34) : new GridLength(0);
+ }
+ grid.ColumnHeaderHeight = expanded ? 68 : 34;
+ grid.UpdateLayout();
+ }
+
private static void ApplyGlobalColumnStretch(DataGrid grid)
{
var layout = new Dictionary(StringComparer.OrdinalIgnoreCase)
@@ -553,8 +570,8 @@ private static FrameworkElement BuildRapidFilterHeader(GlobalRapidFilterState st
Source = column,
Mode = BindingMode.OneWay
});
- root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(38) });
- root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(36) });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(34) });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(0) });
var title = new TextBlock
{
@@ -562,7 +579,7 @@ private static FrameworkElement BuildRapidFilterHeader(GlobalRapidFilterState st
Margin = new Thickness(10, 0, 8, 0),
Foreground = new SolidColorBrush(Color.FromRgb(52, 64, 84)),
FontWeight = FontWeights.SemiBold,
- FontSize = 12.5,
+ FontSize = 12.0,
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis
};
@@ -572,6 +589,7 @@ private static FrameworkElement BuildRapidFilterHeader(GlobalRapidFilterState st
var filterBox = CreateRapidFilterTextBox(state, caption);
Grid.SetRow(filterBox, 1);
root.Children.Add(filterBox);
+ state.HeaderRoots.Add(root);
return root;
}
@@ -581,9 +599,9 @@ private static TextBox CreateRapidFilterTextBox(GlobalRapidFilterState state, st
var box = new TextBox
{
Tag = key,
- Height = 36,
- Padding = new Thickness(10, 0, 7, 0),
- FontSize = 12.5,
+ Height = 34,
+ Padding = new Thickness(9, 0, 7, 0),
+ FontSize = 12.0,
FontWeight = FontWeights.Normal,
Foreground = new SolidColorBrush(Color.FromRgb(29, 41, 57)),
CaretBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235)),
@@ -667,7 +685,7 @@ private static ControlTemplate BuildRapidFilterTextBoxTemplate()
watermark.SetValue(TextBlock.TextProperty, "Filter…");
watermark.SetValue(FrameworkElement.MarginProperty, new Thickness(10, 0, 7, 0));
watermark.SetValue(TextBlock.ForegroundProperty, new SolidColorBrush(Color.FromRgb(152, 162, 179)));
- watermark.SetValue(TextBlock.FontSizeProperty, 12.5d);
+ watermark.SetValue(TextBlock.FontSizeProperty, 12.0d);
watermark.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
watermark.SetValue(UIElement.IsHitTestVisibleProperty, false);
watermark.SetValue(UIElement.VisibilityProperty, Visibility.Collapsed);
diff --git a/MainWindow.GlobalLiveSearch.cs b/MainWindow.GlobalLiveSearch.cs
index 1d80e4697..e8a531010 100644
--- a/MainWindow.GlobalLiveSearch.cs
+++ b/MainWindow.GlobalLiveSearch.cs
@@ -5,6 +5,8 @@ namespace ArIED61850Tester;
public partial class MainWindow
{
+ private bool _globalLiveFiltersExpanded;
+
private void GlobalLiveSearch_TextChanged(object sender, TextChangedEventArgs e)
=> GridUxBehavior.SetGlobalRapidSearch(GlobalLiveGrid, GlobalLiveSearchBox?.Text);
@@ -16,4 +18,12 @@ private void GlobalLiveSearchClear_Click(object sender, RoutedEventArgs e)
GlobalLiveSearchBox.Clear();
GlobalLiveSearchBox.Focus();
}
+
+ private void GlobalLiveFilters_Click(object sender, RoutedEventArgs e)
+ {
+ _globalLiveFiltersExpanded = !_globalLiveFiltersExpanded;
+ GridUxBehavior.SetGlobalRapidFiltersExpanded(GlobalLiveGrid, _globalLiveFiltersExpanded);
+ if (GlobalLiveFiltersLabel != null)
+ GlobalLiveFiltersLabel.Text = _globalLiveFiltersExpanded ? "Hide filters" : "Filters";
+ }
}
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 48aac43e5..10fd3b829 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -67,7 +67,7 @@
@@ -127,7 +127,7 @@
-
+
@@ -411,7 +411,7 @@
-
+
@@ -442,7 +442,7 @@
-
+
@@ -603,7 +603,7 @@
-
+
@@ -848,7 +848,7 @@
-
+
@@ -856,60 +856,75 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
-
-
+
+
@@ -987,7 +1001,7 @@
-
+
@@ -1007,9 +1021,9 @@
HorizontalAlignment="Center" VerticalAlignment="Center"/>
-
-
+
+
@@ -1304,7 +1318,7 @@
-
+
@@ -1332,7 +1346,7 @@
-
+
@@ -1423,7 +1437,7 @@
-
+
@@ -1486,7 +1500,7 @@
-
+
@@ -1570,14 +1584,13 @@
-
+
-
-
+
+
+ CornerRadius="10"/>
", Slice(app, "x:Key=\"WorkspaceTitle\"", ""), StringComparison.Ordinal);
+ Assert.Contains("", Slice(app, "x:Key=\"WorkspaceSubtitle\"", ""), StringComparison.Ordinal);
+ Assert.Contains("", Slice(app, "x:Key=\"WorkspaceCard\"", ""), StringComparison.Ordinal);
+ Assert.Contains("", Slice(app, "x:Key=\"WorkspaceCard\"", ""), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Navigation_UsesStableTypographyAndKeyboardOnlyInternalFocusRing()
+ {
+ var app = File.ReadAllText(FindRepoFile("App.xaml"));
+ var nav = Slice(app, "x:Key=\"SegmentedNavButton\"", "");
+
+ Assert.Contains("FontSize\" Value=\"12.5\"", nav, StringComparison.Ordinal);
+ Assert.Contains("FocusVisualStyle\" Value=\"{x:Null}\"", nav, StringComparison.Ordinal);
+ Assert.Contains("x:Name=\"KeyboardFocusRing\"", nav, StringComparison.Ordinal);
+ Assert.Contains("Property=\"IsKeyboardFocused\"", nav, StringComparison.Ordinal);
+ Assert.DoesNotContain("Property=\"FontSize\" Value=\"13.45\"", nav, StringComparison.Ordinal);
+ Assert.DoesNotContain("Property=\"FontSize\" Value=\"12.55\"", nav, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void MainWorkspaces_UseSharedWorkspaceSurfaceAndHeaderTypography()
+ {
+ var xaml = File.ReadAllText(FindRepoFile("MainWindow.xaml"));
+
+ Assert.Contains("Style=\"{StaticResource WorkspaceCard}\"", xaml, StringComparison.Ordinal);
+ Assert.Contains("Text=\"Global Multi-IED Live Monitor\" Style=\"{StaticResource WorkspaceTitle}\"", xaml, StringComparison.Ordinal);
+ Assert.Contains("Text=\"IEC 61850 Sequence of Events\" Style=\"{StaticResource WorkspaceTitle}\"", xaml, StringComparison.Ordinal);
+ Assert.Contains("Text=\"Diagnostics & Communication Journal\" Style=\"{StaticResource WorkspaceTitle}\"", xaml, StringComparison.Ordinal);
+ Assert.Contains("Text=\"Alarm Annunciator\" Style=\"{StaticResource WorkspaceTitle}\"", xaml, StringComparison.Ordinal);
+ Assert.DoesNotContain("Every IED keeps its own connection, report subscription, validated reads, and event-driven RCB state.", xaml, StringComparison.Ordinal);
+ Assert.DoesNotContain("SCADA/SAS SOE: state values use ARSAS blue for active", xaml, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void LiveMonitor_GlobalSearchIsPrimary_AndColumnFiltersAreProgressiveDisclosure()
+ {
+ var xaml = File.ReadAllText(FindRepoFile("MainWindow.xaml"));
+ var behavior = File.ReadAllText(FindRepoFile("GridUxBehavior.cs"));
+ var bridge = File.ReadAllText(FindRepoFile("MainWindow.GlobalLiveSearch.cs"));
+ var section = Slice(xaml, "", "");
+
+ Assert.Contains("x:Name=\"GlobalLiveSearchBox\"", section, StringComparison.Ordinal);
+ Assert.Contains("x:Name=\"GlobalLiveFiltersButton\"", section, StringComparison.Ordinal);
+ Assert.Contains("GlobalLiveFilters_Click", section, StringComparison.Ordinal);
+ Assert.Contains("SetGlobalRapidFiltersExpanded", behavior, StringComparison.Ordinal);
+ Assert.Contains("FiltersExpanded", behavior, StringComparison.Ordinal);
+ Assert.Contains("new GridLength(0)", behavior, StringComparison.Ordinal);
+ Assert.Contains("ColumnHeaderHeight = expanded ? 68 : 34", behavior, StringComparison.Ordinal);
+ Assert.Contains("SetGlobalRapidFiltersExpanded(GlobalLiveGrid", bridge, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void RuntimeNavigation_UsesStableP0Style_WhileResponsiveFixOwnsFinalWidth()
+ {
+ var runtime = File.ReadAllText(FindRepoFile("SasOperationalUiPolicy.cs"));
+ var responsive = File.ReadAllText(FindRepoFile("MainWindow.NavigationLayoutFix.cs"));
+
+ Assert.Contains("shell.Width = 760", runtime, StringComparison.Ordinal);
+ Assert.Contains("shell.CornerRadius = new CornerRadius(14)", runtime, StringComparison.Ordinal);
+ Assert.Contains("button.FontSize = 12.5", runtime, StringComparison.Ordinal);
+ Assert.Contains("button.FocusVisualStyle = null", runtime, StringComparison.Ordinal);
+ Assert.Contains("CornerRadius=\"10\"", runtime, StringComparison.Ordinal);
+ Assert.Contains("WideNavWidth = 990d", responsive, StringComparison.Ordinal);
+ Assert.Contains("MediumNavWidth = 900d", responsive, StringComparison.Ordinal);
+ Assert.Contains("CompactNavWidth = 720d", responsive, StringComparison.Ordinal);
+ Assert.Contains("shell.Width = shellWidth", responsive, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void P0_KeepsTheReviewedAriecEnginePinUntouched()
+ {
+ var testDirectory = Path.GetDirectoryName(FindRepoFile("MainWindow.xaml"))!;
+ var engineLock = File.ReadAllText(Path.Combine(testDirectory, "engines", "ARIEC61850.lock.json"));
+
+ Assert.Contains("becda399b4a3ae34831215fc915798b4f846c1be", engineLock, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("\"sourcePullRequest\": 81", engineLock, StringComparison.Ordinal);
+ }
+
+ private static string Slice(string source, string start, string end)
+ {
+ var a = source.IndexOf(start, StringComparison.Ordinal);
+ Assert.True(a >= 0, $"Start marker not found: {start}");
+ var b = source.IndexOf(end, a + start.Length, StringComparison.Ordinal);
+ Assert.True(b > a, $"End marker not found: {end}");
+ return source[a..b];
+ }
+
+ private static string FindRepoFile(string relativePath)
+ {
+ DirectoryInfo? directory = new(AppContext.BaseDirectory);
+ while (directory != null)
+ {
+ var candidate = Path.Combine(directory.FullName, relativePath);
+ if (File.Exists(candidate))
+ return candidate;
+ directory = directory.Parent;
+ }
+ throw new FileNotFoundException(relativePath);
+ }
+}