feat(topbar): the segmented mode switcher, to the mock
Implements the creator's mock (2026-11-12) for the world segment: pill track (#1E222B, radius 12, padding 4) holding two equal halves; the active half sits on a lighter pill (#2E3440, radius 8) with #E2E8F0 bold text and a coral (#F26D6D) icon; the inactive half is #8892A4 on a one-alpha black face so the whole area hit-tests; hover tints #252A34 on the inactive half only, so the active one never looks clickable-but-dim. Labels are "Build scenes" / "Record / go live". Two deliberate deviations from the pasted recipe, both to honour this repo: - State is the existing BarWorld binding plus a command, not a code-behind SetActiveMode — one source of truth, and the change still raises the same property-changed notification a mode event would. - Styles live in Themes/Controls.xaml (SegmentHalf, SegmentIcon) with colours as tokens in Tokens.xaml, rather than inline properties on the elements: AGENTS requires shared styles, and one DataTrigger on each element's own Tag serves both halves. DesignTokensTests caught the one raw literal (#01000000) and it is now Brush.SegmentHitArea, which is the rule working as intended. 680 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check.
This commit is contained in:
+19
-22
@@ -66,43 +66,40 @@
|
||||
question "which world am I in?" and "can I edit?" cannot disagree. Clicking the
|
||||
highlighted half re-enters it, which is how a creator gets back to the layers
|
||||
during a session. -->
|
||||
<Border Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="4"
|
||||
Padding="2" Height="32" VerticalAlignment="Center" Margin="0,0,12,0"
|
||||
<!-- The segmented mode switcher (creator mock 2026-11-12): pill track, two equal halves,
|
||||
active half on a lighter pill with off-white bold text and a coral icon. Hover tints
|
||||
only the inactive half. -->
|
||||
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0"
|
||||
Visibility="{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!-- Build Scenes carries a LAYOUT glyph while it is the active world and a PADLOCK
|
||||
once Record / Go Live is: the door is still there, it is just behind the
|
||||
session. Clicking it swaps the glyph back and takes the creator into the
|
||||
layers (creator ruling 2026-11-12). -->
|
||||
<Button Style="{StaticResource WorldSegmentButton}" Tag="{Binding IsBuildWorld}"
|
||||
Command="{Binding EnterBuildWorldCommand}" Padding="12,0"
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Style="{StaticResource SegmentHalf}"
|
||||
Tag="{Binding IsBuildWorld}" Command="{Binding EnterBuildWorldCommand}"
|
||||
ToolTip="Edit layers, scenes and webcam placement">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center"
|
||||
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"
|
||||
Visibility="{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}"
|
||||
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsBuildWorld}"
|
||||
Data="M3,4h8v6H3V4zM13,4h8v6h-8V4zM3,12h8v8H3V12zM13,12h8v8h-8V12z"/>
|
||||
|
||||
<TextBlock Text="Build Scenes" FontSize="12" FontWeight="Bold" Margin="6,0,0,0"
|
||||
<TextBlock Text="Build scenes" FontSize="12" Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<!-- Record / Go Live: a record dot in a ring, the same "armed" language the REC sign
|
||||
uses, so the two halves are read apart at a glance. -->
|
||||
<Button Style="{StaticResource WorldSegmentButton}" Tag="{Binding IsOutputWorld}"
|
||||
Command="{Binding EnterOutputWorldCommand}" Padding="12,0"
|
||||
<Button Grid.Column="1" Style="{StaticResource SegmentHalf}"
|
||||
Tag="{Binding IsOutputWorld}" Command="{Binding EnterOutputWorldCommand}"
|
||||
ToolTip="Choose the output and start the session">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center"
|
||||
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"
|
||||
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsOutputWorld}"
|
||||
Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
|
||||
<TextBlock Text="Record / Go Live" FontSize="12" FontWeight="Bold" Margin="6,0,0,0"
|
||||
<TextBlock Text="Record / go live" FontSize="12" Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- IDLE: the mode switch + the action(s) of the armed world. The three
|
||||
controls share one height (build 77 feedback) so they read as one
|
||||
|
||||
@@ -1002,6 +1002,56 @@
|
||||
<!-- REC / ON-AIR segment pills: an armed session control reads red, because red is what
|
||||
"live or recording" means (DESIGN.md). Kept separate from SegmentToggle so the Rect|Circle
|
||||
choice in the properties panel stays the interactive blue. -->
|
||||
<!-- The segmented mode switcher's icon: muted when its segment is inactive, coral when active.
|
||||
Tag carries the segment's own flag so one style serves both halves (creator mock 2026-11-12). -->
|
||||
<Style x:Key="SegmentIcon" TargetType="Path">
|
||||
<Setter Property="Fill" Value="{StaticResource Brush.SegmentTextInactive}"/>
|
||||
<Setter Property="Width" Value="15"/>
|
||||
<Setter Property="Height" Value="15"/>
|
||||
<Setter Property="Stretch" Value="Uniform"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter Property="Fill" Value="{StaticResource Brush.SegmentIconActive}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- One segment of the switcher: pill container is the host Border, this is a half of it. -->
|
||||
<Style x:Key="SegmentHalf" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.SegmentHitArea}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.SegmentTextInactive}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Face" Background="{TemplateBinding Background}"
|
||||
CornerRadius="8" Padding="14,6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<!-- Active: lighter inner pill, off-white bold text. -->
|
||||
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource Brush.SegmentActive}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Brush.SegmentTextActive}"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</DataTrigger>
|
||||
<!-- Hover tints ONLY the inactive half, so the active one never looks clickable-but-dim. -->
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True"/>
|
||||
<Condition Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="False"/>
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter Property="Background" Value="{StaticResource Brush.SegmentHover}"/>
|
||||
</MultiDataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- The top bar's world segment (creator ruling 2026-11-12): [ Build Scenes ][ Record / Go Live ].
|
||||
A Button, not a ToggleButton — the highlight follows the view model, so a click must never be
|
||||
what sets it. Each button passes its own world flag in Tag and one DataTrigger paints it. -->
|
||||
|
||||
@@ -35,6 +35,16 @@
|
||||
<SolidColorBrush x:Key="Brush.Tertiary" Color="#38bdf8"/>
|
||||
<SolidColorBrush x:Key="Brush.OnTertiary" Color="#0b1326"/>
|
||||
<SolidColorBrush x:Key="Brush.OnAccent" Color="#ffffff"/>
|
||||
<!-- Segmented mode switcher (creator mock, 2026-11-12) -->
|
||||
<SolidColorBrush x:Key="Brush.SegmentTrack" Color="#1E222B"/>
|
||||
<SolidColorBrush x:Key="Brush.SegmentActive" Color="#2E3440"/>
|
||||
<SolidColorBrush x:Key="Brush.SegmentHover" Color="#252A34"/>
|
||||
<SolidColorBrush x:Key="Brush.SegmentTextActive" Color="#E2E8F0"/>
|
||||
<SolidColorBrush x:Key="Brush.SegmentTextInactive" Color="#8892A4"/>
|
||||
<SolidColorBrush x:Key="Brush.SegmentIconActive" Color="#F26D6D"/>
|
||||
<!-- One-alpha black: transparent to the eye, opaque to hit-testing, so the whole segment is clickable. -->
|
||||
<SolidColorBrush x:Key="Brush.SegmentHitArea" Color="#01000000"/>
|
||||
|
||||
<SolidColorBrush x:Key="Brush.Primary" Color="#ff5451"/>
|
||||
<SolidColorBrush x:Key="Brush.PrimaryContainer" Color="#b91a24"/>
|
||||
<SolidColorBrush x:Key="Brush.Secondary" Color="#ffb95f"/>
|
||||
|
||||
@@ -29,8 +29,8 @@ public class UiShellTests
|
||||
|
||||
// 2. The world segment replaced the EDIT toggle (creator ruling 2026-11-12): both halves are
|
||||
// present, each lit by its own world flag through the shared style.
|
||||
Assert.Contains("Text=\"Build Scenes\"", topBar);
|
||||
Assert.Contains("Text=\"Record / Go Live\"", topBar);
|
||||
Assert.Contains("Text=\"Build scenes\"", topBar);
|
||||
Assert.Contains("Text=\"Record / go live\"", topBar);
|
||||
Assert.Contains("Tag=\"{Binding IsBuildWorld}\"", topBar);
|
||||
Assert.Contains("Tag=\"{Binding IsOutputWorld}\"", topBar);
|
||||
Assert.DoesNotContain("x:Name=\"EditBody\"", topBar);
|
||||
@@ -38,6 +38,10 @@ public class UiShellTests
|
||||
// 2b. The segment is Build-world only: choosing Record / Go Live REPLACES it with the output
|
||||
// line (creator ruling 2026-11-12), and a chevron is the way back.
|
||||
Assert.Contains("Data=\"M3,4h8v6H3V4zM13,4h8v6h-8V4zM3,12h8v8H3V12zM13,12h8v8h-8V12z\"", topBar);
|
||||
// Segmented switcher (creator mock 2026-11-12): pill track, two equal halves, coral active icon.
|
||||
Assert.Contains("Brush.SegmentTrack", topBar);
|
||||
Assert.Contains("Style=\"{StaticResource SegmentHalf}\"", topBar);
|
||||
Assert.Contains("Style=\"{StaticResource SegmentIcon}\"", topBar);
|
||||
Assert.Contains("Visibility=\"{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}\"", topBar);
|
||||
Assert.Contains("Text=\"Output:\"", topBar);
|
||||
Assert.Contains("Text=\"press <esc> to return\"", topBar);
|
||||
@@ -116,7 +120,7 @@ public class UiShellTests
|
||||
|
||||
// The world segment leads the centre cluster; the brand column carries no edit control.
|
||||
var centre = topBar.IndexOf("<StackPanel Grid.Column=\"1\"", System.StringComparison.Ordinal);
|
||||
var segment = topBar.IndexOf("Text=\"Build Scenes\"", System.StringComparison.Ordinal);
|
||||
var segment = topBar.IndexOf("Text=\"Build scenes\"", System.StringComparison.Ordinal);
|
||||
Assert.True(centre > 0 && segment > centre,
|
||||
"the [ Build Scenes ][ Record / Go Live ] segment must lead the centre cluster");
|
||||
Assert.DoesNotContain("x:Name=\"EditBody\"", topBar);
|
||||
|
||||
Reference in New Issue
Block a user