diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml
index 2416809..fe43b72 100644
--- a/Controls/LeftPanel.xaml
+++ b/Controls/LeftPanel.xaml
@@ -49,12 +49,19 @@
-
+
-
+
+
+
+
+
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
@@ -60,4 +84,4 @@
-
\ No newline at end of file
+
diff --git a/Controls/SceneThumbnailStrip.xaml.cs b/Controls/SceneThumbnailStrip.xaml.cs
index bf67dfc..092fffa 100644
--- a/Controls/SceneThumbnailStrip.xaml.cs
+++ b/Controls/SceneThumbnailStrip.xaml.cs
@@ -14,6 +14,16 @@ public partial class SceneThumbnailStrip : UserControl
{
InitializeComponent();
SizeChanged += (_, _) => Height = ActualWidth * DesignAspect;
+ Loaded += (_, _) =>
+ {
+ if (DataContext is MainViewModel vm)
+ foreach (var scene in vm.Scenes)
+ {
+ var staged = scene.IsStaged;
+ scene.IsStaged = !staged;
+ scene.IsStaged = staged;
+ }
+ };
}
private void Thumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
diff --git a/DESIGN.md b/DESIGN.md
index 60dc5c3..8c956dc 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -51,6 +51,13 @@ the first Stitch draft of the live screen.
Icons: Material Symbols Outlined.
+## Color roles (decided 2026-10-26)
+
+- **Red `#ff5451`:** YouTube references, on-air/GO LIVE, and record only.
+- **Attention orange `#ff8c3a`:** dropped packets, the live screen indicator, and blinking warning lights.
+- **Amber `#ffb95f`:** game audio and Super Chat only.
+- **`error` `#ef4444`:** unchanged for now; it sits close to the on-air red and may need revisiting.
+
## Open questions for the next pass
- These tokens have not been reconciled against the app's current WPF theme
diff --git a/Models/SceneElement.cs b/Models/SceneElement.cs
index 935eb01..e7443bc 100644
--- a/Models/SceneElement.cs
+++ b/Models/SceneElement.cs
@@ -84,6 +84,9 @@ public abstract class SceneElement : INotifyPropertyChanged
/// True only for AlertBox-type sources. Gates stream alerts properties panel.
public virtual bool IsAlertBox => false;
+ public virtual bool IsTextOverlay => false;
+ public virtual bool IsMediaSource => false;
+ public virtual bool IsLiveCapture => false;
public virtual bool IsBackground { get; set; }
private bool _useDefaultBackground = true;
diff --git a/Models/Source.cs b/Models/Source.cs
index 1709a1f..b178583 100644
--- a/Models/Source.cs
+++ b/Models/Source.cs
@@ -73,6 +73,7 @@ public class Source : SceneElement
Raise(nameof(IsLiveCapture));
Raise(nameof(DisplaySource));
Raise(nameof(IsWebSource));
+ Raise(nameof(IsTextOverlay));
}
}
}
@@ -85,6 +86,7 @@ public class Source : SceneElement
? ElementKind.Dynamic : ElementKind.Static;
public override bool IsWebSource => Type == SourceType.WebSource;
+ public override bool IsTextOverlay => Type == SourceType.TextOverlay;
/// The permanent scene background (live desktop/game capture). Never removable/reorderable.
private bool _isBackground;
@@ -110,7 +112,7 @@ public class Source : SceneElement
private string? _captureKey;
public string? CaptureKey { get => _captureKey; set => Set(ref _captureKey, value); }
- public bool IsLiveCapture => Type is SourceType.DisplayCapture or SourceType.WindowCapture;
+ public override bool IsLiveCapture => Type is SourceType.DisplayCapture or SourceType.WindowCapture;
// Image (asset stored in the layout database)
private string? _assetId;
@@ -190,7 +192,7 @@ public class Source : SceneElement
set => Set(ref _mediaPlaybackState, value);
}
- public bool IsMediaSource => Type == SourceType.MediaSource;
+ public override bool IsMediaSource => Type == SourceType.MediaSource;
// Alert video config (TASK 47): the shipped default clip lives in the Asset
// table (seeded like image assets); the creator's replacement is a FILE PATH
diff --git a/ViewModels/MainViewModel.Background.cs b/ViewModels/MainViewModel.Background.cs
index 0ce53d7..50bce24 100644
--- a/ViewModels/MainViewModel.Background.cs
+++ b/ViewModels/MainViewModel.Background.cs
@@ -155,14 +155,10 @@ public partial class MainViewModel
? OnAirImage
: SceneThumbnailImage(scene);
if (img == null)
- {
- var stagedLive = isStaged
- && SceneCatalog.Is(scene.Name, SceneCatalog.Live);
- img = stagedLive ? null : LoadBackgroundImage(scene);
- }
+ img = LoadBackgroundImage(scene);
if (img != null)
scene.Snapshot = img;
- else if (scene.Snapshot == null || (isStaged && OnAirImage == null))
+ else if (scene.Snapshot == null)
scene.Snapshot = CreatePlaceholderSnapshot(scene.Name);
}
}
@@ -443,6 +439,7 @@ public partial class MainViewModel
}
SeedOnAirImage();
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ OnPropertyChanged(nameof(LayerCount));
}
// ─── Scene strip tile artwork (creator ruling 2026-10-10) ───
diff --git a/ViewModels/MainViewModel.Scenes.cs b/ViewModels/MainViewModel.Scenes.cs
index 114ff1a..7256b3e 100644
--- a/ViewModels/MainViewModel.Scenes.cs
+++ b/ViewModels/MainViewModel.Scenes.cs
@@ -45,6 +45,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowPreviewPlaceholder));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ OnPropertyChanged(nameof(LayerCount));
OnPropertyChanged(nameof(CanChangeBackground));
OnPropertyChanged(nameof(CanAddWebcam));
OnPropertyChanged(nameof(CanAddYouTubeChat));
@@ -102,7 +103,10 @@ public partial class MainViewModel
if (scene.IsHidden || scene == StagedScene) return;
StagedScene = scene;
if (IsLive)
+ {
+ LiveScene = scene;
_transition.Start(SelectedTransitionType, TransitionDurationMs, null);
+ }
}
///
@@ -186,6 +190,7 @@ public partial class MainViewModel
}
public bool ShowEmptySceneHint => StagedScene != null && StagedScene.Elements.Count == 0;
+ public int LayerCount => StagedScene?.Elements.Count(e => !e.IsZoomLayer) ?? 0;
public bool ShowSourcesEmptyHint => StagedScene == null || StagedScene.Elements.Count == 0;
public bool IsCountdownAvailable => StagedScene != null
diff --git a/ViewModels/MainViewModel.Sources.cs b/ViewModels/MainViewModel.Sources.cs
index 761846a..a4f2ad5 100644
--- a/ViewModels/MainViewModel.Sources.cs
+++ b/ViewModels/MainViewModel.Sources.cs
@@ -85,6 +85,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
}
@@ -227,6 +228,7 @@ public partial class MainViewModel
SelectedElement = source;
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
}
@@ -260,6 +262,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
}
}
diff --git a/ViewModels/MainViewModel.Streaming.Operations.cs b/ViewModels/MainViewModel.Streaming.Operations.cs
index 3791f98..bf32176 100644
--- a/ViewModels/MainViewModel.Streaming.Operations.cs
+++ b/ViewModels/MainViewModel.Streaming.Operations.cs
@@ -65,6 +65,7 @@ public partial class MainViewModel : ViewModelBase
ResetHealth(StreamStatus.Offline);
_audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName);
IsRecording = true;
+ LiveScene = StagedScene;
await _framePump.StartAsync();
}
@@ -222,6 +223,7 @@ public partial class MainViewModel : ViewModelBase
// only once frames flow. The liveChatId (snippet.liveChatId) does not exist
// before that (2026-09-25 diagnosis), so chat resolution runs AFTER the pump
// starts. Chat is non-fatal — if it fails, the stream continues without chat.
+ LiveScene = StagedScene;
await _framePump.StartAsync(); // never throws; failures log + surface via Failed
string? liveChatId = null;
@@ -324,6 +326,7 @@ public partial class MainViewModel : ViewModelBase
StreamStatus = StreamStatus.Offline;
IsRecording = false;
+ LiveScene = null;
// TASK 41: a stopped test broadcast forgets its drawer session (idempotent)
// and drops the TEST face — a test never outlives its run.
TestSession.EndTestSilently();
diff --git a/ViewModels/MainViewModel.Webcam.cs b/ViewModels/MainViewModel.Webcam.cs
index d2d485c..ed6cb3a 100644
--- a/ViewModels/MainViewModel.Webcam.cs
+++ b/ViewModels/MainViewModel.Webcam.cs
@@ -375,6 +375,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
+ OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
// PreviewBitmapChanged fires only on the camera's first frame, so a config
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index af76267..8dcfa34 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -438,7 +438,7 @@ public partial class MainViewModel : ViewModelBase
{
_isLoading = false;
}
- StagedScene = Scenes.FirstOrDefault();
+ StagedScene = Scenes.FirstOrDefault(s => SceneCatalog.Is(s.Name, SceneCatalog.Starting)) ?? Scenes.FirstOrDefault();
UpdateActiveBackground();
UpdateActiveChatBoxPreviews();
UpdateActiveAlertBoxPreviews();