feat: redesign dashboard with Fluent KPI tiles, connection strip, and devices column

- Replace LCD-style readings with a 3×2 KPI tile grid (Fluent card surfaces, 52pt values)
- Add persistent top connection strip with horizontal chips + pump name badge
- Add elapsed test timer (DispatcherTimer, mm:ss) to Test Summary card
- Restyle Test Summary and Active Alarms with Fluent brushes/iconography
- Add Devices column (CAN / K-Line / Bench tiles) between KPI grid and test/alarms
  - Enumerates attached PCAN USB channels via PCAN_ATTACHED_CHANNELS API
  - Enumerates FTDI K-Line adapters via existing FtdiInterface helpers
  - Click to connect/disconnect; confirmation dialog when session active or test running
  - Hover tint: blue = will connect, red = will disconnect; Bench row is read-only stub
- Extend ICanService with SelectedChannel + EnumerateAttachedChannels()
- Expose IKwpService.ConnectedPort for active session device tracking
- Add DeviceRow button style with MultiDataTrigger hover colour logic
- Add 30+ new localization keys (ES + EN) for KPI labels, devices, confirmations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 22:25:00 +02:00
parent 0280a2fad1
commit 197e9d1775
26 changed files with 1638 additions and 515 deletions

View File

@@ -42,6 +42,40 @@
<sys:String x:Key="Dashboard.Action.Stop">Stop</sys:String>
<sys:String x:Key="Dashboard.Action.EmergencyStop">EMERGENCY STOP</sys:String>
<!-- ── Dashboard KPI tile labels ──────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Kpi.Rpm">Bench RPM</sys:String>
<sys:String x:Key="Dashboard.Kpi.Qdelivery">Q delivery</sys:String>
<sys:String x:Key="Dashboard.Kpi.P1">Pressure P1</sys:String>
<sys:String x:Key="Dashboard.Kpi.P2">Pressure P2</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tin">Oil in T</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tout">Oil out T</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Rpm">rpm</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.CcS">cc/s</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Bar">bar</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Celsius">°C</sys:String>
<!-- Dashboard test summary extras -->
<sys:String x:Key="Dashboard.TestSummary.Elapsed">Elapsed:</sys:String>
<!-- Dashboard connection chip extras -->
<sys:String x:Key="Dashboard.Conn.Pump.Label">Pump:</sys:String>
<sys:String x:Key="Dashboard.Conn.NoPump">No pump</sys:String>
<!-- ── Dashboard Devices column ─────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Devices">Devices</sys:String>
<sys:String x:Key="Dashboard.Devices.Can">CAN</sys:String>
<sys:String x:Key="Dashboard.Devices.Kline">K-Line</sys:String>
<sys:String x:Key="Dashboard.Devices.Bench">Bench</sys:String>
<sys:String x:Key="Dashboard.Devices.Refresh">Refresh</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Idle">Available</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Connected">Connected</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Active">Session active</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Failed">Error</sys:String>
<sys:String x:Key="Dashboard.Devices.None">No devices found</sys:String>
<sys:String x:Key="Dashboard.Devices.BenchRow">Bench controller</sys:String>
<!-- Confirmation dialogs -->
<sys:String x:Key="Devices.Confirm.Title">Confirm device change</sys:String>
<sys:String x:Key="Devices.Confirm.Body.Active">The {0} session is active. Disconnect?</sys:String>
<sys:String x:Key="Devices.Confirm.Body.TestRunning">A test is running. Changing device state may abort it. Continue?</sys:String>
<!-- ── Status bar / connection indicators ───────────────────────────── -->
<sys:String x:Key="Status.Label">Status:</sys:String>
<sys:String x:Key="Status.Can">CAN</sys:String>

View File

@@ -42,6 +42,40 @@
<sys:String x:Key="Dashboard.Action.Stop">Detener</sys:String>
<sys:String x:Key="Dashboard.Action.EmergencyStop">PARADA DE EMERGENCIA</sys:String>
<!-- ── Dashboard KPI tile labels ──────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Kpi.Rpm">RPM del banco</sys:String>
<sys:String x:Key="Dashboard.Kpi.Qdelivery">Q. caudal</sys:String>
<sys:String x:Key="Dashboard.Kpi.P1">Presión P1</sys:String>
<sys:String x:Key="Dashboard.Kpi.P2">Presión P2</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tin">T. entrada</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tout">T. salida</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Rpm">rpm</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.CcS">cc/s</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Bar">bar</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Celsius">°C</sys:String>
<!-- Dashboard test summary extras -->
<sys:String x:Key="Dashboard.TestSummary.Elapsed">Duración:</sys:String>
<!-- Dashboard connection chip extras -->
<sys:String x:Key="Dashboard.Conn.Pump.Label">Bomba:</sys:String>
<sys:String x:Key="Dashboard.Conn.NoPump">Sin bomba</sys:String>
<!-- ── Dashboard Devices column ─────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Devices">Dispositivos</sys:String>
<sys:String x:Key="Dashboard.Devices.Can">CAN</sys:String>
<sys:String x:Key="Dashboard.Devices.Kline">K-Line</sys:String>
<sys:String x:Key="Dashboard.Devices.Bench">Banco</sys:String>
<sys:String x:Key="Dashboard.Devices.Refresh">Actualizar</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Idle">Disponible</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Connected">Conectado</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Active">Sesión activa</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Failed">Error</sys:String>
<sys:String x:Key="Dashboard.Devices.None">Sin dispositivos</sys:String>
<sys:String x:Key="Dashboard.Devices.BenchRow">Controlador del banco</sys:String>
<!-- Confirmation dialogs -->
<sys:String x:Key="Devices.Confirm.Title">Confirmar cambio de dispositivo</sys:String>
<sys:String x:Key="Devices.Confirm.Body.Active">La sesión {0} está activa. ¿Desea desconectar?</sys:String>
<sys:String x:Key="Devices.Confirm.Body.TestRunning">Hay una prueba en ejecución. Cambiar el estado del dispositivo puede interrumpirla. ¿Continuar?</sys:String>
<!-- ── Status bar / connection indicators ───────────────────────────── -->
<sys:String x:Key="Status.Label">Estado:</sys:String>
<sys:String x:Key="Status.Can">CAN</sys:String>

View File

@@ -42,11 +42,169 @@
<Setter Property="Margin" Value="2,4"/>
</Style>
<!-- Relay toggle button style -->
<Style x:Key="RelayButton" TargetType="Button">
<!-- Relay toggle button style — inherits WPF-UI Button appearance -->
<Style x:Key="RelayButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="6,3"/>
<Setter Property="Margin" Value="3,2"/>
<Setter Property="FontSize" Value="11"/>
</Style>
<!--
Base style for state-indicating toggle buttons (on/off with colour feedback).
Uses a custom ControlTemplate so that TemplateBinding Background is honoured
in ALL states — including IsChecked=True. Derived styles add IsChecked triggers
with a custom Background colour (green, amber, blue, etc.) and those colours are
guaranteed to propagate, unlike WPF-UI's own ToggleButton template which has an
internal IsChecked trigger that would override a simple BasedOn + Background setter.
Hover/pressed feedback is applied via a transparent overlay so it works on any
background colour without conflicting with the checked state.
-->
<Style x:Key="FluentStateToggle" TargetType="ToggleButton">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Border x:Name="Root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="True"/>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
TextElement.Foreground="{TemplateBinding Foreground}"/>
<!-- Transparent overlay — darkens on hover/pressed over any background colour -->
<Border x:Name="HoverOverlay" CornerRadius="4" Background="Transparent"
IsHitTestVisible="False"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="HoverOverlay" Property="Background" Value="#18000000"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="HoverOverlay" Property="Background" Value="#30000000"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Dashboard KPI tile styles ─────────────────────────────────────── -->
<Style x:Key="KpiTile" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="6"/>
<Setter Property="MinHeight" Value="140"/>
</Style>
<Style x:Key="KpiHeaderText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style x:Key="KpiValueText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="52"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
<Setter Property="TextAlignment" Value="Left"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
</Style>
<Style x:Key="KpiUnitText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Margin" Value="6,0,0,6"/>
</Style>
<!-- Connection strip chip -->
<Style x:Key="ConnChip" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="14"/>
<Setter Property="Padding" Value="14,6"/>
<Setter Property="Margin" Value="0,0,8,0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<!-- Status dot (10 px ellipse; Fill overridden by DataTrigger for state colour) -->
<Style x:Key="StatusDot" TargetType="Ellipse">
<Setter Property="Width" Value="10"/>
<Setter Property="Height" Value="10"/>
<Setter Property="Fill" Value="{DynamicResource SystemFillColorNeutralBrush}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="6,0,4,0"/>
</Style>
<!-- ── Device row button — hover tint indicates intent (connect=blue, disconnect=red) -->
<Style x:Key="DeviceRow" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="Margin" Value="0,0,0,4"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="6">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<!-- Hover + disconnected → accent blue (will connect) -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsMouseOver}" Value="True"/>
<Condition Binding="{Binding IsConnected}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentFillColorSecondaryBrush}"/>
</MultiDataTrigger>
<!-- Hover + connected → critical red (will disconnect) -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsMouseOver}" Value="True"/>
<Condition Binding="{Binding IsConnected}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SystemFillColorCriticalBackgroundBrush}"/>
</MultiDataTrigger>
<!-- Disabled → muted (bench placeholder) -->
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Opacity" Value="0.5"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>