TASK 58: the properties panel wears the theme (no more light-system chrome)

The creator asked why the Stream Alerts "Display" drop-down was not in the same
style as the other drop-downs. It was never given the theme's YtComboBox style -
only a hand-painted Background/Foreground. Setting colours changes the paint, not
the template, so WPF still drew the light system ComboBox (grey border, system
arrow, white popup list) on top of the dark panel. The chat Font drop-down was a
copy of the same mistake, and every check box was the light system one.

The sweep of the whole properties panel (then the app):
  - ComboBox x2 (chat Font, alert Display) -> YtComboBox, hand-painted colours gone
  - TextBox x16 in the properties panel -> YtTextBoxCompact (new, BasedOn YtTextBox:
    11px / padding 4,2 - the panel is dense, the full 13px box would double it)
  - CheckBox x2 (chat) + x3 (preview broadcast) + x1 (socials dialog) -> YtCheckBox
    (new: a 14x14 rounded box + tick path, accent fill when checked)
  - the layer-rename field -> YtTextBoxCompact via BasedOn, still borderless
  - buttons, pills, sliders: already themed, untouched

The sweep also killed a typo'd foreground (#e0d0e0 for #e0e0e0) and the only
remaining hand-painted colour is the deliberate dimming of the two read-only path
fields.

Guard: ytLive.Tests/PropertiesStyleTests.cs (the ONE Good Dog test) parses the app
XAML and fails if an unstyled drop-down, check box or properties field comes back.
431/431 green, 0 warnings.
This commit is contained in:
2026-10-21 19:46:25 -07:00
parent 17aa0273a9
commit 2273d20160
12 changed files with 262 additions and 51 deletions
+24 -39
View File
@@ -155,12 +155,11 @@
</TextBlock>
<TextBox Grid.Column="0" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="0"
Padding="4,2" VerticalContentAlignment="Center"
BorderThickness="0" FontSize="12"
IsVisibleChanged="SourceNameBox_IsVisibleChanged"
KeyDown="SourceNameBox_KeyDown" LostFocus="SourceNameBox_LostFocus">
<TextBox.Style>
<Style TargetType="TextBox">
<Style TargetType="TextBox" BasedOn="{StaticResource YtTextBoxCompact}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
@@ -264,22 +263,18 @@
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="X" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="16"/>
<TextBox Text="{Binding SelectedElement.X, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
<TextBlock DockPanel.Dock="Left" Text=" Y" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="20" Margin="8,0,0,0"/>
<TextBox Text="{Binding SelectedElement.Y, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
</DockPanel>
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="W" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="16"/>
<TextBox Text="{Binding SelectedElement.Width, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
<TextBlock DockPanel.Dock="Left" Text=" H" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="20" Margin="8,0,0,0"/>
<TextBox Text="{Binding SelectedElement.Height, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
</DockPanel>
</StackPanel>
@@ -291,8 +286,7 @@
Click="WebcamBorderColorPicker_Click" Margin="4,0,0,0"
ToolTip="Pick color"/>
<TextBox Text="{Binding SelectedElement.BorderColor, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"
Style="{StaticResource YtTextBoxCompact}"
MaxLength="7" ToolTip="#RRGGBB"/>
</DockPanel>
<TextBlock Text="Border Thickness" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
@@ -378,22 +372,18 @@
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="X" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="16"/>
<TextBox Text="{Binding SelectedElement.X, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
<TextBlock DockPanel.Dock="Left" Text=" Y" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="20" Margin="8,0,0,0"/>
<TextBox Text="{Binding SelectedElement.Y, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
</DockPanel>
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="W" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="16"/>
<TextBox Text="{Binding SelectedElement.Width, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
<TextBlock DockPanel.Dock="Left" Text=" H" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="20" Margin="8,0,0,0"/>
<TextBox Text="{Binding SelectedElement.Height, UpdateSourceTrigger=PropertyChanged, StringFormat=F0}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"/>
Style="{StaticResource YtTextBoxCompact}"/>
</DockPanel>
<TextBlock Text="URI" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
<DockPanel>
@@ -410,8 +400,7 @@
Width="14" Height="14" Stretch="Uniform"/>
</Button>
<TextBox Text="{Binding SelectedElement.WebUri, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0d0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="12"
Style="{StaticResource YtTextBoxCompact}"
ToolTip="https://example.com"/>
</DockPanel>
</StackPanel>
@@ -434,7 +423,7 @@
<TextBlock DockPanel.Dock="Left" Text="Font" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
<ComboBox SelectedValue="{Binding SelectedElement.ChatFontFamily, Mode=TwoWay}"
SelectedValuePath="Content"
Background="#0f3460" Foreground="#e0e0e0" FontSize="11"
Style="{StaticResource YtComboBox}"
Width="140" VerticalAlignment="Center">
<ComboBoxItem Content="Segoe UI"/>
<ComboBoxItem Content="Arial"/>
@@ -449,24 +438,21 @@
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="Font Color" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
<TextBox Text="{Binding SelectedElement.ChatFontColor, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
Style="{StaticResource YtTextBoxCompact}" Width="80"/>
</DockPanel>
<!-- Member Color -->
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="Member" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
<TextBox Text="{Binding SelectedElement.ChatMemberColor, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
Style="{StaticResource YtTextBoxCompact}" Width="80"/>
</DockPanel>
<!-- Super Chat Color -->
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="Super Chat" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
<TextBox Text="{Binding SelectedElement.ChatSuperChatColor, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
Style="{StaticResource YtTextBoxCompact}" Width="80"/>
</DockPanel>
<!-- Max Messages -->
@@ -482,8 +468,7 @@
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="Background" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
<TextBox Text="{Binding SelectedElement.ChatBackgroundColor, UpdateSourceTrigger=PropertyChanged}"
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
Style="{StaticResource YtTextBoxCompact}" Width="80"/>
</DockPanel>
<!-- Background Opacity -->
@@ -505,9 +490,9 @@
</DockPanel>
<!-- Toggles -->
<CheckBox Content="Show timestamps" Foreground="#a0a0b0" FontSize="11" Margin="0,4,0,2"
<CheckBox Content="Show timestamps" Style="{StaticResource YtCheckBox}" Margin="0,4,0,2"
IsChecked="{Binding SelectedElement.ChatShowTimestamps, Mode=TwoWay}"/>
<CheckBox Content="Show badges" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"
<CheckBox Content="Show badges" Style="{StaticResource YtCheckBox}" Margin="0,0,0,2"
IsChecked="{Binding SelectedElement.ChatShowBadges, Mode=TwoWay}"/>
</StackPanel>
@@ -531,8 +516,8 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding SelectedElement.AlertVideoPath, Mode=OneWay}"
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
FontSize="11" VerticalAlignment="Center"/>
Style="{StaticResource YtTextBoxCompact}"
IsReadOnly="True" Foreground="#a0a0b0" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseAlertVideoCommand}"
CommandParameter="{Binding SelectedElement}" Style="{StaticResource YtButtonSecondary}"
Margin="4,0,0,0" Padding="8,2"/>
@@ -558,7 +543,7 @@
<TextBlock DockPanel.Dock="Left" Text="Display" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
<ComboBox SelectedValue="{Binding SelectedElement.AlertDisplayMethod, Mode=TwoWay}"
SelectedValuePath="Tag"
Background="#0f3460" Foreground="#e0e0e0" FontSize="11"
Style="{StaticResource YtComboBox}"
Width="140" VerticalAlignment="Center">
<ComboBoxItem Content="Scroll" Tag="TickerScroll"/>
<ComboBoxItem Content="Flash" Tag="Flash"/>
@@ -593,8 +578,8 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding SelectedElement.CustomBackgroundPath, Mode=OneWay}"
IsReadOnly="True" Background="#0f3460" Foreground="#a0a0b0"
FontSize="11" VerticalAlignment="Center"/>
Style="{StaticResource YtTextBoxCompact}"
IsReadOnly="True" Foreground="#a0a0b0" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Browse…" Command="{Binding BrowseBackgroundCommand}"
Style="{StaticResource YtButtonSecondary}" Margin="4,0,0,0" Padding="8,2"/>
</Grid>
+6 -6
View File
@@ -542,8 +542,8 @@
<ComboBox Style="{StaticResource YtComboBox}" Margin="0,0,0,10"
ItemsSource="{Binding BroadcastForm.VisibilityOptions}"
SelectedItem="{Binding BroadcastForm.Visibility}"/>
<CheckBox Content="Made for kids" Foreground="#e0e0e0" FontSize="12"
Margin="0,2,0,10"
<CheckBox Content="Made for kids" Style="{StaticResource YtCheckBox}"
FontSize="12" Margin="0,2,0,10"
IsChecked="{Binding BroadcastForm.MadeForKids}"/>
<TextBlock Text="SCHEDULED START" Style="{StaticResource YtLabel}" Margin="0,0,0,3"/>
<TextBlock Text="{Binding BroadcastForm.ScheduledStartText}"
@@ -655,8 +655,8 @@
<TextBlock Text="CHANNEL STANDING" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
<TextBlock Text="{Binding Ypp.StandingDetailText}" Foreground="#888899" FontSize="11"
TextWrapping="Wrap" Margin="0,0,0,8"/>
<CheckBox Content="Two-Step Verification switched on" Foreground="#e0e0e0" FontSize="12"
Margin="0,2,0,0"
<CheckBox Content="Two-Step Verification switched on" Style="{StaticResource YtCheckBox}"
FontSize="12" Margin="0,2,0,0"
IsChecked="{Binding Ypp.TwoFactorEnabled}"/>
<Button Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Left"
Command="{Binding Ypp.OpenTwoStepCommand}" ToolTip="Open Google's 2-Step Verification page">
@@ -667,8 +667,8 @@
</Button.Template>
<TextBlock Text="Set up on Google →" Foreground="#5aa7ff" FontSize="12" Margin="0,0,0,6"/>
</Button>
<CheckBox Content="AdSense account linked" Foreground="#e0e0e0" FontSize="12"
Margin="0,2,0,0"
<CheckBox Content="AdSense account linked" Style="{StaticResource YtCheckBox}"
FontSize="12" Margin="0,2,0,0"
IsChecked="{Binding Ypp.AdSenseLinked}"/>
<Button Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Left"
Command="{Binding Ypp.OpenEarnCommand}" ToolTip="Open the YouTube Earn page">
+1 -1
View File
@@ -12,7 +12,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
| `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` |
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials). **TASK 57:** dragging a webcam under the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` (the six-spot snap inside the cropped band) instead of free-floating over the master, and a resize re-snaps the tile onto its spot (TASK 57 Unit 2: the resize routes through `MainViewModel.ResizeWebcamInVerticalTier` — the Chat camera's two sizes, then the snap) |
| `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight |
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout |
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs` |
| `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here |
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
+10 -2
View File
@@ -18,6 +18,13 @@
are `ytLive.Tests/VerticalWebcamLayoutTests.cs` (Unit 1) and
`ytLive.Tests/VerticalChatCameraLayoutTests.cs` (Unit 2). **Only the creator's hands-on check is
left** — see the vv section.
- **TASK 58 — the properties panel now wears the theme** (shipped 2026-10-21). The creator's
"why is the drop-down not in the same style as the others?" was a real defect: those drop-downs
(and every check box, and every properties field) had been hand-painted with a `Background`/
`Foreground` instead of a style, so WPF drew the **light system template** on top of the dark
panel. Now: `ComboBox` → `YtComboBox`, `CheckBox` → `YtCheckBox` (new), properties fields →
`YtTextBoxCompact` (new, BasedOn `YtTextBox`). Guard:
`ytLive.Tests/PropertiesStyleTests.cs`.
- **The previous session (HA lights) produced NO ytLive code** — it was spent on the creator's Home
Assistant light alert system (off-repo, `192.168.50.145`). See `MyMistakes.md` → 2026-10-21: the plan
was one tweak to a verified-working script; the script was rewritten twice, the alert path broke, and
@@ -186,8 +193,9 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
- TASK 55 disk hygiene (`startup.log` uncapped — 23MB and growing every 5s).
## Tests
- Last full run: **429/430 passed, 0 warnings** (2026-10-21, this session — the vv Unit 2 gate run).
The one failure was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
- Last full run: **431/431 passed, 0 warnings** (2026-10-21, this session — after TASK 58's
`PropertiesStyleTests`). The previous run was 429/430: the one failure was
`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression.
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
`MasterLimiter_CapsTheLiveMix_OnThePipe`, `LayerReorderPersistenceTests.RealMouseDrag…`,
+22
View File
@@ -2078,3 +2078,25 @@ that does not contain a dash (e.g. the `if (...) return false;` line instead of
it), and put the new prose in the `newString` where any character is fine. When a whole stale block
must go (e.g. half-overwritten comments), a three-line `python3` script that deletes the exact line
range is faster and safer than fighting the matcher.
## 2026-10-21 — A WPF CONTROL WITH NO STYLE DRAWS THE LIGHT SYSTEM CHROME (RECIPE — TASK 58)
The creator asked why the Stream Alerts "Display" drop-down looked like it belonged to another
app. The cause is not a colour: **setting `Background`/`Foreground`/`FontSize` on a WPF control
changes its paint, not its TEMPLATE.** An unstyled `ComboBox` still renders the system template —
grey border, system arrow, **white popup list** — so a dark blue box sat on the dark panel
wearing light-system chrome. Unstyled `CheckBox` is the same story.
**The recipe (audit + fix + guard):**
1. **Audit by grep, not by eye:** `grep -rn "<ComboBox\|<CheckBox" --include="*.xaml"` and look at
each one's `Style` attribute. Hand-painted colours are the smell — they mean "no style".
2. **Fix by style, never by more painting:** put the look in `Themes/Controls.xaml` once
(`YtComboBox`, `YtCheckBox`, a `BasedOn` compact variant when the panel is dense) and delete
the per-control colours. A dense panel needs a *variant* of the style, not a re-painted control.
3. **Guard it with a test** that parses the XAML and fails when a control of that type has no
style — otherwise the next hand-painted control walks the look back.
4. **Check the inherited defaults while you are there:** a compact `BasedOn` variant must set the
metrics it overrides (`FontSize`, `Padding`, `MinHeight`) or the panel silently changes density.
**Side effect worth naming:** the sweep also exposed a typo'd foreground (`#e0d0e0` instead of
`#e0e0e0`) that only a single source of truth can prevent from recurring.
+1 -1
View File
@@ -25,7 +25,7 @@
Foreground="#e0e0e0" Margin="0,0,0,12"/>
<!-- Show/hide the bar on the stream -->
<CheckBox Grid.Row="1" IsChecked="{Binding BarEnabled}" Margin="0,0,0,10">
<CheckBox Grid.Row="1" IsChecked="{Binding BarEnabled}" Style="{StaticResource YtCheckBox}" Margin="0,0,0,10">
<TextBlock Text="Show the social bar on the stream" Foreground="#e0e0e0" FontSize="13"/>
</CheckBox>
+1
View File
@@ -73,6 +73,7 @@
| 55 | **Disk hygiene: log rotation + low-disk guard** | ☐ Queued (2026-10-09) — `startup.log` appears uncapped; also temp alert-clip cleanup + free-space check before recording | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
| 56 | **Pre-publication secret scan (full git history)** | ☐ Queued (2026-10-09) — gate before the Gitea repo goes public; report commit/path only, never values | [`TASKS/task-56-secret-scan.md`](TASKS/task-56-secret-scan.md) |
| 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | ✅ **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only — the circle is not offered — the tile or one whole 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) |
| 58 | **Properties-panel style consistency (the theme owns the look)** | ✅ Done — 2026-10-21 (the creator's "why is the drop-down not in the same style as the others?" — the drop-downs/fields/check boxes had been hand-painted instead of themed, so WPF drew the light system template; now every one carries a theme style, guarded by a test) | [`TASKS/task-58-properties-style-consistency.md`](TASKS/task-58-properties-style-consistency.md) |
---
@@ -0,0 +1,45 @@
# TASK 58 — Properties-panel style consistency (the theme owns the look)
**Status:** ✅ Done — shipped 2026-10-21.
## What it is
The creator asked (2026-10-21), pointing at the Live scene → Stream Alerts layer →
Properties → **Display**:
> why is the drop-down box not in the same style as the others? Can you run through the
> existing elements in properties to ensure that we're maintaining a consistent style?
## Why the drop-down looked alien
`Controls/LeftPanel.xaml` gave the drop-down a hand-painted `Background`/`Foreground`/
`FontSize` instead of the theme's `YtComboBox` style. Setting colours does **not** change a
control's template — WPF still drew the **light system ComboBox** (grey border, system
arrow, white popup list) on top of the dark panel. The code even said so: the alert
drop-down's comment says it was built "same shape as the chat Font selector above" — and the
chat Font drop-down was unstyled too, so the inconsistency was copied.
## The sweep (the whole properties panel, then the app)
| Control | Before | After |
|---|---|---|
| ComboBox ×2 (chat Font, alert Display) | hand-painted colours → light system template | `YtComboBox` (the same style as the bottom-bar/zoom drop-downs) |
| TextBox ×16 in the properties panel | hand-painted colours, no template, no focus/hover, one typo'd foreground `#e0d0e0` | `YtTextBoxCompact` (BasedOn `YtTextBox`: 11px, padding 4,2) |
| CheckBox ×2 (chat) | default light system check box | `YtCheckBox` (new dark style) |
| CheckBox ×3 (preview broadcast panel) + ×1 (Socials dialog) | default light system check box | `YtCheckBox` |
| TextBox (layer rename) | hand-painted colours | `YtTextBoxCompact` via `BasedOn`, borderless as before |
| Buttons, pills, sliders | already themed | unchanged |
New theme styles (`Themes/Controls.xaml`):
- `YtTextBoxCompact` — `BasedOn YtTextBox`, 11px + `Padding 4,2` + `MinHeight 22`. The
properties panel is dense (11px labels); the full `YtTextBox` (13px, 8,6) would have
doubled the panel's height.
- `YtCheckBox` — a 14×14 rounded box + tick path, accent fill when checked, hover/focus
border triggers. The default WPF check box is the light system one.
## The guard
`ytLive.Tests/PropertiesStyleTests.cs` — the ONE test for this change:
1. the theme defines `YtComboBox`/`YtCheckBox`/`YtTextBox`/`YtTextBoxCompact`;
2. every `ComboBox` and `CheckBox` in the app's XAML carries the matching theme style;
3. every field in the properties panel (`StackPanel x:Name="ElementsPanel"`) carries
`YtTextBoxCompact` and sets no `Background`/`BorderBrush` of its own.
## Tests
`ytLive.Tests/PropertiesStyleTests.cs` — 431/431 green at ship time.
+59
View File
@@ -122,6 +122,65 @@
</Setter>
</Style>
<!-- Dense variant of YtTextBox for the properties panel (11px labels, tight
padding). Same template + colours as YtTextBox — only the metrics differ,
so the panel keeps its rhythm without hand-painting every field. -->
<Style x:Key="YtTextBoxCompact" TargetType="TextBox" BasedOn="{StaticResource YtTextBox}">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="MinHeight" Value="22"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<!-- Dark check box. The default WPF template is the light system one — it
sticks out of the dark panels, so every check box carries this style
(the guard test in ytLive.Tests/PropertiesStyleTests.cs enforces it). -->
<Style x:Key="YtCheckBox" TargetType="CheckBox">
<Setter Property="Foreground" Value="#e0e0e0"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Box" Grid.Column="0" Width="14" Height="14" CornerRadius="3"
Background="#0f3460" BorderBrush="#2a3a5e" BorderThickness="1"
VerticalAlignment="Center">
<Path x:Name="Tick" Data="M2.5,7.2 L5.8,10.5 L11.5,3.8"
Stroke="#ffffff" StrokeThickness="2"
StrokeStartLineCap="Round" StrokeEndLineCap="Round"
Visibility="Collapsed"/>
</Border>
<ContentPresenter Grid.Column="1" Margin="7,0,0,0" VerticalAlignment="Center"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Box" Property="BorderBrush" Value="#3d5076"/>
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter TargetName="Box" Property="BorderBrush" Value="#e94560"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Box" Property="Background" Value="#e94560"/>
<Setter TargetName="Box" Property="BorderBrush" Value="#e94560"/>
<Setter TargetName="Tick" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.45"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="YtComboBox" TargetType="ComboBox">
<Setter Property="Foreground" Value="#e0e0e0"/>
<Setter Property="FontSize" Value="13"/>
+8 -1
View File
@@ -5,7 +5,7 @@ memory-map conventions.
| File | Purpose |
|------|---------|
| `Controls.xaml` | All custom control styles/templates: `YtButton`, `YtButtonSecondary`, `IconButton`, `YtTextBox`, `YtComboBox`, scrollbar/slider/tooltip/context-menu/menu-item/separator/list-item (implicit where app-wide), `YtListBoxItem`, `YtLabel`, `SectionHeader` |
| `Controls.xaml` | All custom control styles/templates: `YtButton`, `YtButtonSecondary`, `IconButton`, `YtTextBox`, `YtTextBoxCompact` (the dense variant the properties panel uses), `YtComboBox`, `YtCheckBox`, scrollbar/slider/tooltip/context-menu/menu-item/separator/list-item (implicit where app-wide), `YtListBoxItem`, `YtLabel`, `SectionHeader` |
How it's wired:
- Merged once in [`App.xaml`](../App.xaml) — app-wide. New windows get the theme
@@ -19,6 +19,13 @@ Gotchas learned the hard way:
- A custom `ComboBox` template must put the `ContentPresenter` (`SelectionBoxItem`)
**inside the ComboBox template, not the ToggleButton template** — the latter
resolves against the wrong TemplatedParent and renders nothing.
- **A control with no style renders with the LIGHT WPF system template** — a
hand-painted `Background`/`Foreground` colours the box but leaves the system
border, the system arrow and the white popup list. That is why the Stream
Alerts "Display" drop-down looked like it belonged to another app (TASK 58,
2026-10-21). **Rule: every control carries a theme style** — `YtComboBox`,
`YtCheckBox`, `YtTextBox`/`YtTextBoxCompact` — and paints no colour of its
own. `ytLive.Tests/PropertiesStyleTests.cs` fails if that drifts back.
Related: [`Helpers/index.md`](../Helpers/index.md) (custom controls the theme
styles), the XAML files at repo root that consume these styles.
+1 -1
View File
@@ -276,7 +276,7 @@ C# / WPF (.NET 8) following MVVM:
- Layout persists to SQLite (`Microsoft.Data.Sqlite`); scenes/sources/asset bytes stored in the DB, asset identity is a SHA-256 content hash (1:M reuse, no file paths — assets are always available). Loaded sources always derive `IsBackground` from `Type` (OR'd with the persisted column, so legacy DisplayCapture backdrops keep their flag) — pre-derivation rows with `IsBackground=0` heal on load
- **Element z-order is a unified space, not per-table (2026-09-20):** `Source` and `WebcamSceneConfig` live in different tables, and a webcam can be interleaved between sources in a scene. Both tables' `SortOrder` is stamped from the SAME counter — the element's index within `scene.Elements` — and `Load` merges the two tables' rows per scene by that shared z (tie-break sources-first, so legacy per-table rows read back stacked the way they were saved). Never re-introduce per-type sort counters + "append all Sources then all configs", or a webcam slotted mid-stack reverts on reload (creator-reproduced bug).
- **Five-scene catalog (`Models/SceneCatalog.cs`):** the product is exactly Starting/Live/BRB/Chat/Ending — work with less, never more (the escape hatch for "more" is OBS). Scenes are matched **by name** (`SceneCatalog.Is`, case-insensitive trim). Empty DBs seed all five; the scenes-header "+" (`ShowAddScene`/`MissingScenes` on `MainViewModel`) only appears while ≥1 canonical scene is missing and its menu lists only the missing ones, re-adding them by name (`AddSceneCommand`). Renaming a canonical scene makes it missing again; `AddScene` rejects non-canonical names.
- Theming: all custom styles live in `Themes/Controls.xaml`, merged in `App.xaml` — never duplicate styles per-window (dialog duplicates were consolidated into this dictionary)
- Theming: all custom styles live in `Themes/Controls.xaml`, merged in `App.xaml` — never duplicate styles per-window (dialog duplicates were consolidated into this dictionary). **Every control in the app carries a theme style (TASK 58, 2026-10-21):** an unstyled `ComboBox`/`CheckBox` falls back to the **light WPF system template** (grey border, system arrow, white popup list) and sticks out of the dark panels — the properties panel's drop-downs had been hand-painted with a `Background`/`Foreground` instead of a style (the chat Font drop-down was a copy of that), and every check box was default. The rule now: `ComboBox` → `YtComboBox`, `CheckBox` → `YtCheckBox`, the dense properties fields → `YtTextBoxCompact` (`BasedOn YtTextBox`: 11px, padding 4,2) — **no hand-painted `Background`/`BorderBrush` on a themed control**. `ytLive.Tests/PropertiesStyleTests.cs` is the guard: it fails if an unstyled drop-down, check box or properties field comes back
- Resolution tiers: 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier. Bottom bar: gear icon far left, stream stats (bitrate/FPS/dropped/duration) centered under preview, resolution dropdown far right. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height
- Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug)
+84
View File
@@ -0,0 +1,84 @@
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
namespace ytLive.Tests;
/// <summary>TASK 58 — the creator's style audit (2026-10-21): "in the Live scene, for the
/// layer Stream Alerts, the properties → display — why is the drop-down box not in the same
/// style as the others?" It was never given the theme's <c>YtComboBox</c> style — only a
/// hand-painted Background/Foreground — so WPF fell back to the light system template, and
/// the chat Font drop-down (copied from it) was the same. The sweep found the same smell in
/// every check box and every properties field.
///
/// <para>This test is the guard: every drop-down and check box in the app carries the theme's
/// style, and every field in the properties panel carries the compact theme style and paints
/// no colour of its own — the theme owns the look, so it cannot drift back.</para></summary>
public class PropertiesStyleTests
{
private static readonly XNamespace Xaml = "http://schemas.microsoft.com/winfx/2006/xaml";
[Fact]
public void Every_drop_down_check_box_and_properties_field_carries_the_theme_style()
{
// 1. The theme defines the styles the app relies on.
var theme = XDocument.Load(SourceAssert.RepoPath("Themes", "Controls.xaml"));
var keys = theme.Descendants()
.Where(e => e.Name.LocalName == "Style")
.Select(e => (string?)e.Attribute(Xaml + "Key"))
.Where(k => k != null)
.ToHashSet();
foreach (var required in new[] { "YtComboBox", "YtCheckBox", "YtTextBox", "YtTextBoxCompact" })
Assert.Contains(required, keys);
// 2. Every drop-down and check box in the app carries the theme style — an unstyled
// one falls back to the light WPF system template.
foreach (var file in AppXamlFiles())
{
var doc = XDocument.Load(file);
var where = Path.GetFileName(file);
foreach (var combo in doc.Descendants().Where(e => e.Name.LocalName == "ComboBox"))
AssertStyled(combo, "YtComboBox",
$"{where}: a ComboBox is not styled with YtComboBox — it renders with the light system template");
foreach (var check in doc.Descendants().Where(e => e.Name.LocalName == "CheckBox"))
AssertStyled(check, "YtCheckBox",
$"{where}: a CheckBox is not styled with YtCheckBox — it renders with the light system template");
}
// 3. Every field in the properties panel carries the compact theme style and paints
// no colours of its own.
var left = XDocument.Load(SourceAssert.RepoPath("Controls", "LeftPanel.xaml"));
var properties = left.Descendants().Single(e => e.Name.LocalName == "StackPanel"
&& (string?)e.Attribute(Xaml + "Name") == "ElementsPanel");
var fields = properties.Descendants().Where(e => e.Name.LocalName == "TextBox").ToList();
Assert.NotEmpty(fields);
foreach (var box in fields)
{
AssertStyled(box, "YtTextBoxCompact",
"a properties field is not styled with YtTextBoxCompact");
Assert.Null(box.Attribute("Background"));
Assert.Null(box.Attribute("BorderBrush"));
}
}
/// <summary>The app's XAML: the windows at the repo root plus the carved-up controls.</summary>
private static string[] AppXamlFiles()
{
var root = Path.GetDirectoryName(SourceAssert.RepoPath("ytLive.csproj"))!;
return Directory.GetFiles(root, "*.xaml", SearchOption.TopDirectoryOnly)
.Concat(Directory.GetFiles(Path.Combine(root, "Controls"), "*.xaml", SearchOption.TopDirectoryOnly))
.OrderBy(f => f)
.ToArray();
}
/// <summary>Asserts the element carries the named theme style.</summary>
private static void AssertStyled(XElement element, string key, string message)
{
var style = (string?)element.Attribute("Style");
Assert.True(style is not null && style.Contains(key, StringComparison.Ordinal), message);
}
}