feat(ui): left-panel alert stack — licence indicator pinned on top, events LIFO below

Creator design (2026-11-05). A stack of red rows at the top of the left panel, above both mode grids
so it shows while editing and while live, pushing the panel's content down.

- Row 1 is the **Steam licence indicator** and nothing displaces it. It reads as status, not a
  warning: "Entitlement 30 fps · Maximum 60 fps", both numbers derived from QualityOptions and
  HasProOutput. While higher tiers are unavailable it shows a **pin** (standing indicator, no close
  control); once ownership is verified the pin becomes a **close control** and the dismissal is
  persisted (`Steam.LicenceIndicatorDismissed`), permanent while ownership holds. Losing ownership
  restores it as pinned — a change of state, not a preference. Click → About.
- **Event rows stack beneath, newest first**, one row per episode (an episode advancing its state
  updates its own row rather than adding another). Each is dismissible. Click → the Logs tab opens
  with that event's sequence range painted red (`HighlightedLogFrom/To` + `LogInRangeConverter`; two
  numbers rather than a collection because the log list is virtualised and realised rows do not
  re-evaluate on membership changes).
- Replaces the old red panel under the quality picker, which is removed.
- The Steam diagnostic text loses "Contact the creator or support" for a neutral
  "Steam licensing is not configured in this build."

Tests: `AttentionAlertStackTests` — pinned first with the status text; owned → closable and the close
survives a reload but ownership loss restores it pinned; pinned cannot be dismissed; event rows LIFO
with in-place episode updates; clicking an event opens Logs and sets its range. AboutHub + Entitlement
tests updated for the removed panel and the new wording. FAST gate: 532 passed, 0 failed, 0 warnings.
This commit is contained in:
2026-11-05 11:41:59 -08:00
parent adf9883ae9
commit a80108dd96
15 changed files with 501 additions and 49 deletions
+82 -8
View File
@@ -29,6 +29,7 @@
Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<Helpers:LogInRangeConverter x:Key="LogInRange"/>
<Helpers:ClosedBoxTemplateSelector x:Key="BorderColorTemplates"
ClosedTemplate="{StaticResource BorderColorClosedTemplate}"
RowTemplate="{StaticResource BorderColorRowTemplate}"/>
@@ -37,11 +38,60 @@
<Border Background="{StaticResource Brush.SurfaceContainer}" CornerRadius="6" Padding="12">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ALERT STACK (creator design 2026-11-05): standing state on top, transient
events beneath, newest first, pushing the panel's content down. Only red
rows live here. Row 1 is the Steam licence indicator and is never displaced;
it shows a pin while higher tiers are unavailable, and a close control once
ownership is verified (that close is permanent). Clicking a row runs its
action: the licence indicator opens About, an event opens the Logs tab with
that event's lines painted. -->
<ItemsControl Grid.Row="0" ItemsSource="{Binding AttentionAlerts}" Margin="0,0,0,6"
Visibility="{Binding HasAttentionAlerts, Converter={StaticResource BoolToVis}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,4" Padding="8,6" CornerRadius="4" Cursor="Hand"
Background="{StaticResource Brush.ErrorContainer}"
BorderBrush="{StaticResource Brush.Error}" BorderThickness="1">
<Border.InputBindings>
<MouseBinding MouseAction="LeftClick"
Command="{Binding DataContext.ActivateAlertCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"/>
</Border.InputBindings>
<DockPanel LastChildFill="True">
<Button DockPanel.Dock="Right" VerticalAlignment="Top" Margin="6,0,0,0"
Style="{StaticResource IconButton}" Width="20" Height="20"
ToolTip="Dismiss"
Command="{Binding DataContext.DismissAlertCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Visibility="{Binding CanDismiss, Converter={StaticResource BoolToVis}}">
<Path Data="M4,4 L16,16 M16,4 L4,16" Stroke="{StaticResource Brush.Error}"
StrokeThickness="2" Width="12" Height="12" Stretch="Uniform"/>
</Button>
<!-- Pinned rows show the pin INSTEAD of a close control: the icon is
the state ("standing indicator"), not a disabled button. -->
<Path DockPanel.Dock="Right" Data="M12,2 L12,9 M8,9 H16 L14,15 H10 Z M10,15 L10,21 M14,15 L14,21"
Stroke="{StaticResource Brush.Error}" StrokeThickness="1.6"
Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Top"
Margin="6,2,2,0" ToolTip="Standing indicator"
Visibility="{Binding ShowsPin, Converter={StaticResource BoolToVis}}"/>
<StackPanel>
<TextBlock Text="{Binding Label}" Style="{StaticResource AppLogEntryText}"
FontWeight="Bold" Foreground="{StaticResource Brush.Error}"/>
<TextBlock Text="{Binding Text}" Style="{StaticResource AppLogEntryText}"
TextWrapping="Wrap" Margin="0,2,0,0"/>
</StackPanel>
</DockPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Edit mode (offline): layers + properties -->
<Grid Grid.Row="0" Visibility="{Binding IsEditMode, Converter={StaticResource BoolToVis}}">
<Grid Grid.Row="1" Visibility="{Binding IsEditMode, Converter={StaticResource BoolToVis}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@@ -1072,7 +1122,7 @@
</Grid>
<!-- Live mode: YouTube Live Chat (entire left panel replaced) -->
<Grid Grid.Row="0" Visibility="{Binding IsEditMode, Converter={StaticResource InverseBoolToVis}}">
<Grid Grid.Row="1" Visibility="{Binding IsEditMode, Converter={StaticResource InverseBoolToVis}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
@@ -1194,12 +1244,36 @@
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel Margin="6,4">
<TextBlock DockPanel.Dock="Left" Text="{Binding TypeLabel}"
Style="{StaticResource AppLogEntryText}" FontWeight="Bold"
Margin="0,0,8,0"/>
<TextBlock Text="{Binding Text}" Style="{StaticResource AppLogEntryText}"/>
</DockPanel>
<!-- An alert row is an entry point to its detail: clicking it opens
this tab and paints the sequence range that event produced. -->
<Border Padding="2,1" CornerRadius="3">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource LogInRange}">
<Binding Path="Sequence"/>
<Binding Path="DataContext.HighlightedLogFrom"
RelativeSource="{RelativeSource AncestorType=UserControl}"/>
<Binding Path="DataContext.HighlightedLogTo"
RelativeSource="{RelativeSource AncestorType=UserControl}"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="{StaticResource Brush.ErrorContainer}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<DockPanel Margin="4,3">
<TextBlock DockPanel.Dock="Left" Text="{Binding TypeLabel}"
Style="{StaticResource AppLogEntryText}" FontWeight="Bold"
Margin="0,0,8,0"/>
<TextBlock Text="{Binding Text}" Style="{StaticResource AppLogEntryText}"
TextWrapping="Wrap"/>
</DockPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>