Commit Graph
483 Commits
Author SHA1 Message Date
gramps 7d8021ca09 fix(dock): master strip on top, against the preview
During a live session attention sits on the preview, so the master sound bar belongs as close to it
as possible — master row first, the four bays below. Collapsed mode is unchanged: 36px is the
master strip on its own, so it stays exactly where the eye already is.
2026-10-30 12:45:12 -07:00
gramps 5552bf97a1 fix(dock): inside the work area, bottom edge aligned with the layers panel
The dock was a sibling row BELOW the whole work area, so it could not line up with the layers
panel and read as sitting under it. It now lives in the work-area grid (preview column, bottom
row), and the left panel spans content + dock rows — both columns end on the same line, which is
what the creator asked for.

No behaviour change; placement only.
2026-10-30 12:42:49 -07:00
gramps a7d1afc70b fix(dock): spans the preview column and loses 20px
- The dock now sits in the preview column (Grid.Column=1), so its left edge is the layers panel's
  right edge instead of starting under the brand column. It is a preview-column control, not a
  full-width footer.
- Expanded height 168 → 148 (112 bays + 36 master strip): the mockup's number read as wasted space
  beneath the 154px scene strip.

Guard: AudioDockLayoutTests pins 148 as the design number, so the constant cannot drift silently.
2026-10-30 12:39:36 -07:00
gramps 1a9e8b8d36 feat(audio dock): the shell — 168px, auto-collapsing, replacing the empty bottom bar
Unit 4 of the sound-bar refactor. `Controls/AudioDock` takes the bottom row that held the empty
`BottomBar` spacer (deleted): four bay containers (MIC / GAME / TRAX / LIP-SYNC) above a 36px
master strip, all token-styled.

Height comes from `Models/AudioDockLayout`, not from a setting: 168px normally, 36px below a 900px
window height. At the 1366×768 minimum a full dock leaves ~398px for the preview, so the collapse
is mandatory, not cosmetic. The dock listens to its window's SizeChanged and re-applies.

Nothing here pretends to be a meter yet — the bays and the master readouts fill in over the next
units, each with its own test.

Guard: AudioDockLayoutTests pins both sides of the threshold (768 collapsed, 1040 expanded), the
boundary itself, and the two heights as the design's numbers.
2026-10-30 12:37:17 -07:00
gramps 0f62e55474 feat(audio): the segment meter control (unit 3 of the dock)
`Controls/SegmentMeter` draws a 24-segment ladder in ONE OnRender pass — no per-segment elements
updating on the UI thread, which is the difference between a meter and a UI thread tax. Level and
peak are display fractions (what was MEASURED), never gain settings; vertical for the bay ladders,
horizontal for the master strip.

Bands come from `Helpers/MeterScale` (nominal/caution/peak, DESIGN.md thresholds) and live as
dependencies of the control, so the painting logic is pure and testable: the band a segment paints
is the level it measured. Brushes resolve from the theme tokens via SetResourceReference, so the
ladder cannot drift from DESIGN.md.

Peak-hold is the reason the header exists: a bar that follows the moment hides clipping, and
clipping is the one audio fault that cannot be fixed after the stream.

Guard: MeterScaleTests — band edges (inclusive at −12 and −3, as the standard states the ranges),
the −60…0 display scale, and peak-hold rising instantly then decaying at 0.35/s.
2026-10-30 12:36:05 -07:00
gramps 575caa7bc4 feat(audio): the meter knows which side a signal is on (unit 2 of the dock)
`AudioLevelMeter` measured one number for everything. It now also keeps smoothed per-channel
levels with the same attack/decay, plus `ImbalanceDb`. Mono samples report both sides equal (a mono
source is centred), so the two never disagree about whether audio exists.

The dock shows ONE ladder per source — two ladders per bay was visual noise for a rare failure —
but this creator is profoundly deaf on the right, so the one case that matters is "the sound is
panned away from me". That is now measurable and will drive a flag, not a second ladder.

Semantics that mattered: a silent side is compared at the −60 dBFS floor, so a fully one-sided
signal reads ≈54 dB rather than "nothing to compare". My first version returned 0 for exactly the
case the feature exists for; the test caught it.

Guard: StereoMeterTests — hard left, hard right (the inaudible case), balanced stereo, mono
centred, and silence-is-not-an-imbalance. 31 audio tests green.
2026-10-30 12:33:40 -07:00
gramps afe21682c2 feat(audio): the dock's dB scale, and the mono fold primitive
Unit 1 of the sound-bar refactor. The model stores linear gain; the dock shows decibels, because
"−6 dB" is readable and "0.50" is not. `Helpers/GainDb` owns the mapping:

- 0 dB = unity (the old 1.0), range −60 dB (true silence, exactly 0 gain) … +6 dB.
- Labels for the fader readout: "−∞", "-6.0", "+1.5".
- `MonoFold(L,R)` = (L+R)/2, the accessible downmix: hard-panned content becomes audible on both
  sides at half amplitude instead of absent from one. Creator context: profoundly deaf on the
  right, so single-sided audio is inaudible, not merely annoying.

Guard: GainDbTests pins unity, the −6 dB half, the exact-zero floor, the +6 dB ceiling, a
round-trip across the range, the labels, and the fold arithmetic (a right-only 0.5 arrives on the
left at 0.25).
2026-10-30 12:32:07 -07:00
gramps 17249229c8 feat(top bar): EDIT moves beside the gear; the gear wears the app's orange
- EDIT leaves the centre session cluster and sits immediately right of the gear, in the brand
  column — so mode lives with the controls that configure the app, not with the session buttons.
- The gear is a LlamaCasty control, so its icon is `attention` orange instead of muted grey.

Guard: UiShellTests asserts the gear fill and that EDIT appears before the centre (session)
cluster, so a later reformat cannot quietly put it back.
2026-10-30 11:18:36 -07:00
gramps 0f8647790d fix(top bar): a signed-out ON-AIR press starts sign-in instead of disabling the button
Reported: EDIT on + ON-AIR on, signed out → the button reads "Test Stream" and cannot be pressed.
Cause: the primary button bound straight to StartTestCommand, whose CanExecute needs a connection.
StartSession already routed signed-out ON-AIR to the sign-in flow; the test path bypassed it.

The route is now explicit and pure: RouteFor(kind, connected) → SignIn | Record | Live | Test. The
primary command calls SignInAsync for the SignIn route, BeginTestStream for Test, StartSession
otherwise — so the label, the action and the sign-in fallback cannot drift apart. Pinned in
SessionStartDecisionTests for both live and test, connected and not.
2026-10-30 11:17:11 -07:00
gramps fbeb04385b feat(top bar): EDIT decides live vs test on the primary button — the TEST button is gone
Creator ruling (2026-10-30):
- EDIT off + ON-AIR armed → the primary button reads "Go Live" and runs the livestream.
- EDIT on + ON-AIR armed → the same button reads "Test Stream" and runs the test broadcast.
- REC armed (or nothing armed) → "Start Recording", whatever EDIT says; there is no test
  recording, and an unarmed start records locally rather than doing nothing.

So the fifth button was redundant: `ShowTestButton` no longer feeds any control. The decision is a
pure function (`Models/SessionStartDecision`) so the whole matrix is testable without a window;
`PrimaryStartCommand` follows the same kind, so label and action cannot disagree.

Guard: SessionStartDecisionTests pins the four cases, including the no-dead-ends fallback that my
first version got wrong (unarmed returned Live instead of Record — the test caught it).

Note for the v1 gate: the "hide TEST at v1" item is now satisfied by construction — the test path
is a mode of the primary button, not a separate control.
2026-10-30 11:14:33 -07:00
gramps 9b08c3af4f fix(ui): no space inside the wordmark — adjacent Runs again
The comment I added between the two <Run> elements introduced whitespace, and whitespace between
inline Runs collapses to a rendered space: the wordmark read "LlamaCas ty". The comment moved
above the TextBlock, the Runs are contiguous, and UiShellTests asserts them adjacent so a future
edit cannot re-introduce the gap.
2026-10-30 11:09:16 -07:00
gramps 318871033f feat(ui): the capture context menu is always available (Ctrl+right-click); wordmark "ty" is YouTube red
1. The preview's right-click handler swallowed every right-click for the zoom box, so the
   element/backdrop context menu could not open at all. Sources and capture windows must be
   changeable AT ANY TIME — a creator swaps a window mid-stream without entering edit mode.
   Ctrl + right-click now bypasses the zoom path and lets WPF open the menu under the cursor;
   plain right-click keeps the zoom box. No menu markup changed.

2. The wordmark's "ty" is YouTube red (#ff0000), permanently. The token sweep had recoloured it
   to the interactive blue because it was a bare literal; DESIGN.md now records "never re-colour
   the wordmark" beside the token, and UiShellTests pins it.

Guard: UiShellTests gains the wordmark rule and the Ctrl+right-click chord.
2026-10-30 11:07:51 -07:00
gramps cb4f8ec758 fix(ui): REC / ON-AIR labels are white in every state
RecordPillToggle never set a base Foreground, and its label inherits the toggle's — so unchecked
inherited the Control default (black) on the dark bar, while checked was white on red. The style
now sets on-accent once; the checked trigger only changes the fill.

That is the consistency rule for session controls: white ink always, the fill carries the state
(neutral off, red live). EDIT follows the same rule. UiShellTests asserts the base foreground, so
the missing setter cannot come back silently.
2026-10-30 10:46:51 -07:00
gramps feef763035 fix(ui): EDIT label is white in both states
The armed state was fixed to on-accent last build, but the resting state kept on-surface-variant
(#94a3b8) — so with EDIT off, which is its normal state, the label was grey on the dark bar. The
label is white always now; only the fill changes (neutral → blue). The redundant foreground setter
is gone, and UiShellTests asserts the resting colour rather than the trigger.
2026-10-30 10:45:24 -07:00
gramps d25dbf58dc design: white ink on any accent fill (on-accent token)
The EDIT button's armed face used on-tertiary (#0b1326 navy) on blue — the creator read it as grey
on blue and could not read it. Same problem, latent, on the armed REC/ON-AIR pills, which used the
error ink.

One rule instead of two exceptions: a saturated accent fill carries WHITE ink. New token
on-accent #ffffff, used by the EDIT trigger and RecordPillToggle; on-tertiary stays for large blue
surfaces where dark ink is right. UiShellTests pins the EDIT foreground to it.
2026-10-30 10:34:19 -07:00
gramps 568586c541 fix(xaml): the app refused to launch — RecordPillToggle was defined before SegmentToggle
`BasedOn="{StaticResource SegmentToggle}"` resolves while the dictionary is being built, so a
forward reference fails at load with "Cannot find resource named 'SegmentToggle'". The style now
sits after SegmentToggle and SegmentLabel, where its base already exists.

Guard: ThemeResourceTests gains the ordering rule — within one dictionary, no StaticResource may
point at a key defined later in the same file. The existence check alone passed this, which is
exactly why the app died on launch while the tests were green.

Root cause of the unreliable test results this session: a stale `testhost` held ytLive.dll locked,
so builds silently kept the previous assembly (MSB3027 on copy). Killed it; the suite now rebuilds
and the two new facts actually run.
2026-10-30 10:29:22 -07:00
gramps 4841c27514 feat(ui): the creator's six corrections
1. Stream Info + YPP rails are YouTube surfaces → new `youtube` token #ff0000. It is documented
   as brand-only: live state stays `primary`, so the two reds never mean the same thing.
2. EDIT toggle: the checked trigger set the SAME colour as the resting face, so clicking it
   changed nothing visible. Resting is now the neutral surface with muted text; armed is blue with
   dark text, and the label follows the fill.
3. Active scene outline is orange again (`attention`) — it marks the scene you work on, not a
   selection; blue made it read as "selected". Live stays red.
4. Scene names wear the same pill as the hotkey chips (black, 10px radius, 8×2 padding), so both
   labels stay readable over any thumbnail.
5. REC / ON-AIR pills and the running stop button read red while armed/live — including test
   mode, where the top bar is gold. New `RecordPillToggle` style (its own template, because
   SegmentToggle hard-codes a transparent face); the stop button's grey `#333` literal is gone.
6. The properties list had `MaxHeight="420"` inside an Auto row, so it scrolled while the window
   had space left. The row is `*` now and the cap is gone.

Guard: UiShellTests pins all six to the markup and names the one that regresses.
2026-10-30 10:27:11 -07:00
gramps df0118ac52 docs(handoff): design conversion complete; spacing is a layout pass 2026-10-30 10:02:32 -07:00
gramps e89cc3682a test(socials): a real preview test — and the Cancel bug it immediately found
The two regressions that reached the creator both lived in preview XAML that no test loads: the
appearance controls painted nothing, then the bar vanished when the plate was off. Every existing
test was green through both.

This test drives the real drawer on a real MainWindow (hermetic temp DB, per the layout-pollution
rule) and reads the real preview elements:
- the bar is VISIBLE, whatever the plate switch says
- a drawer text colour reaches the glyph
- the plate carries its colour and ~50% alpha, and its 18,6 padding only when enabled
- the shadow is an actual DropShadowEffect on the glyph, and off by default
- Cancel restores the pre-edit look

It paid for itself on the first run: Cancel did NOT revert. The snapshot held the same config
object the live preview mutates, so "restore" restored the edited object. The snapshot is now the
six values, written back on cancel — entries untouched.

Also fixes the harness details that made the first attempts fail: everything runs on the app's STA
thread (window creation AND close), and the exception is rethrown with its stack so a failure is
diagnosable next time.
2026-10-30 10:02:06 -07:00
gramps 0822552830 design: the theme's 4px corners come from the radius token
Six control corners used a literal 4px while DESIGN.md defines radius-sm for exactly that role.
Component geometry stays literal on purpose (the pill toggle's 9px is half its 18px height — a
token there would be a lie about what the number means).
2026-10-30 09:59:12 -07:00
gramps ec01a7deb8 design: the last VM-side colours come from tokens (ThemeBrush)
The state colours the view-models hand to bindings — top bar, REC/ON-AIR dots, health banner,
meter bands, toast tints — were hex literals in C#. They now resolve from Themes/Tokens.xaml
through `Helpers/ThemeBrush`, so the values live in one place. Without an Application (unit tests)
the helper returns a neutral grey: visibly wrong rather than silently plausible.

Four tokens the code needed and the document lacked, added to DESIGN.md (so the doc-is-the-contract
guard demanded their brushes): info-container #3a3b52, success-container #1d5c38,
warning-container #b8860b, test-container #9c6f1c (the test-stream gold bar).

Also fixed by this: the top bar still painted the RETIRED brand red #e94560 when live — the
standard's #ff5451 now, and `OnAirSignTests` (which pinned the old value) says so. BarOn/BarOff
became properties rather than static fields, because a static field would resolve tokens before
the Application exists.

Verified: DesignTokensTests, GameMeterHonestyTests, NotificationAreaIntegrationTests,
OnAirSignTests, SessionTeardownTests — 38 tests green.
2026-10-30 09:58:52 -07:00
gramps 7adff685c0 design: the on-stream graphics join the standard (OutputPalette)
The broadcast picture is part of the app, so it stops carrying its own colours. Alert cards, the
ticker and the branding flash now paint from OutputPalette, which Mirrors DESIGN.md for the brand
tokens and owns the alert box's six-event accent language (gold/violet/pink — content, not
chrome; the standard has no purple and should not grow one, but they live in ONE file instead of
57 scattered literals).

It is a separate type because the renderers run on the frame-pump thread and must not touch
application resources. The duplication is made safe by OutputPaletteTests: it parses
Themes/Tokens.xaml and fails if any mirrored token drifts, and fails if a renderer reintroduces a
colour literal or "Segoe UI".

Ticker and flash glyphs now use the bundled Inter instead of Segoe UI. Neon red is the standard
live red — one red in the product, including the credit.

Fixed on the way: the palette's font fields were static pack-URI initialisers, which throw
TypeInitializationException in any host without a WPF Application (the test process caught it);
they are lazy now with a name fallback.
2026-10-30 09:30:56 -07:00
gramps c4642f1e99 design: every window inherits Inter (implicit Window style)
Dialogs are separate WPF windows, so the FontFamily set on MainWindow never reached them —
GoLive, Socials and the four pickers still rendered Segoe UI while the main window used Inter.

The theme now carries an implicit Window style (family + Display text mode + layout rounding).
Guard: DialogTypographyTests opens a real Window inside the real app and asserts it resolves the
bundled Inter family — which fails if the style or the font resource goes missing — and asserts
no dialog sets FontFamily itself, i.e. nobody opts out of the standard.
2026-10-30 09:18:50 -07:00
gramps 51d9dd09c7 fix(socials): the bar disappeared from the preview when the plate was off
My own regression from the previous commit: the plate Border WRAPPED the bar's content and its
Visibility was bound to BarBackgroundEnabled — so with the plate off (the default) the preview
collapsed the entire social bar. Nothing was wrong with the data; the element was there and
invisible.

Fix: the wrapper is always laid out. "Plate off" now means a TRANSPARENT background
(`SocialBarBackgroundBrush` returns Transparent when disabled) and zero padding, "on" means the
colour/opacity plate with 18,6 padding — which also matches what the renderer bakes for the
output, so preview and broadcast agree.

Guard (new fact in SocialBarAppearanceTests): the preview must never visibility-gate the bar on
BarBackgroundEnabled, the wrapper must bind SocialBarBackgroundBrush, and the plate's switching
must be Padding.
2026-10-30 09:06:53 -07:00
gramps c1212644b0 fix(socials): live preview of the bar appearance + the crash on dragging a slider
Two bugs from the creator's first look at the new controls.

1. CRASH — '{DependencyProperty.UnsetValue}' is not a valid value for property 'Fill'.
   The token conversion deleted `YtSliderKnobHoverBrush` while two slider triggers still
   referenced it. WPF does not fail that at build time: it substitutes UnsetValue and throws at
   the next layout, inside `Ellipse.OnRender` — so the first slider touched (the new opacity one)
   killed the app. The brush is back, as a glossier version of the rest state.
   Guard: `ThemeResourceTests` scans every app XAML and proves each {StaticResource} key is
   defined (theme/App resources or the same file). Proven by removing the key again — the test
   names it.

2. PREVIEW DID NOTHING — the preview bar is its own XAML rendering (not the rasterized strip the
   compositor overlays), and it was hard-wired white-on-transparent, so the drawer's controls had
   nothing to move. Now:
   - `SocialBarTextBrush` / `SocialBarBackgroundBrush` come from the same `SocialBarStyle` the
     renderer bakes, so preview and output cannot disagree.
   - the preview draws the plate (same colour + opacity) and the drop shadow.
   - the drawer applies changes to the live config as you edit (`ApplyAppearancePreview`), and
     Cancel restores the pre-edit snapshot — so the preview tracks the controls without
     persisting anything until Save.

46 tests green (theme, socials, design tokens, layout store, real-MainWindow meter test).
2026-10-30 08:57:09 -07:00
gramps c82636e1a9 feat(socials): bar appearance — text colour, background plate, opacity, text shadow
In the Socials drawer, under the show/hide switch: text colour, an optional background plate
with its own colour and opacity, and a text drop shadow. The bar had been hard-wired
white-on-transparent since it shipped.

- `SocialsConfig` gains five additive properties (BarTextColor, BarBackgroundEnabled,
  BarBackgroundColor, BarBackgroundOpacity, BarTextShadow).
- Persistence: five new Socials columns, added by the per-column idempotent migration pattern
  (a half-migrated DB heals instead of failing), written in Save and read back in Load with
  defaults for pre-existing rows.
- `SocialBarRenderer.Render(entries, SocialBarStyle?)` — the look is a separate value type so the
  renderer stays a pure "entries + look" function and the preview can never read a half-applied
  config mid-edit. A malformed hex falls back to white/black instead of throwing inside a render
  tick.
- The plate hugs the entries (rounded 20px, padding) so it reads as a caption bar, not a band
  across the frame. Off by default, because transparent is what creators have today.
- The colour pickers reuse `BorderColorPalette` — one catalogue of the app's colours, per the
  TASK 60/63 rule.

Guard: SocialBarAppearanceTests paints the real strip and inspects pixels — default is white with
no plate, a red text colour reaches the glyphs, a 55% blue plate yields half-transparent blue
pixels, and the shadow adds dark pixels that were not there before.
2026-10-30 08:47:50 -07:00
gramps 9528128ca7 feat(settings): brand-logo thumbnail with a hover enlargement
The creator (2026-10-30): the brand-logo field in App Settings showed only a file NAME, so a
myopic user could not confirm which picture was set. Now a 28x28 thumbnail sits left of the
field showing the actual image, and hovering it opens an enlarged copy (up to 420x320) beside
the panel with the file name underneath. A click collapses it; moving the pointer away closes
it, unless the pointer walked onto the popup itself, where it stays so the creator can look
closely.

Mechanics:
- `MainViewModel.BrandLogoPreviewImage` decodes the stored asset ONCE and freezes it, so the
  thumbnail and the enlargement share one bitmap and no dispatcher hop is needed. A corrupt
  image returns null instead of taking the dialog down.
- The thumbnail is hidden when no image is set (NotNullToVis), so there is nothing to hover and
  no empty frame can appear.
- The popup is a separate window: WPF refuses to open one until its placement target is in a
  live visual tree, which is why the test hosts the control in a real window rather than
  asserting on a detached control.

Guard: BrandLogoPreviewTests — the wiring (image binding, the no-image gate, the four gesture
handlers, the readable enlargement size) and the control's open/close behaviour.
2026-10-30 08:39:42 -07:00
gramps 7a49b38cae docs(handoff): every XAML surface tokenised; VM state colours are the last unit 2026-10-30 08:30:45 -07:00
gramps c885bbd782 design: the code-set state colours now agree with the standard
Last inconsistency from the sweep: several VM-side state colours are still literals, and after
the palette moved they disagreed with everything around them — the top bar's LIVE state would
have painted the retired pink-red while the tile beside it used the standard red.

Values aligned (tokenisation of these sites is its own unit — they are strings bound to
Background/Foreground, so the fix is a VM->Brush refactor, not a find/replace):
- live / on-air / recorder dot: #e94560 -> #ff5451 (the standard red)
- error banner: #8f1f1f -> #93000a (error-container)
- audio meter bands follow the doc's stated thresholds: nominal cyan #38bdf8, caution amber
  #ffb95f, peak #ff5451 — they were green/yellow/red, which contradicted DESIGN.md's own table

GameMeterHonestyTests pinned the old band colours; its intent (the meter scales by volume) is
unchanged, so the two colour expectations now name the new bands.
2026-10-30 08:30:33 -07:00
gramps 8502c1d194 design: app settings + dialogs to tokens (surface 5 of the sweep)
OverlayHost (the App Settings surface) and every dialog — Go Live, Socials, Mic picker, Camera
picker, Hotkey config, Reuse Image, Rename Recording, Keep Broadcast — now resolve from tokens.
That closes the last place the creator sees a dialog: the settings overlay was still wearing the
old palette while the main window had moved on.

Every #e94560 in these files was an ERROR or validation affordance (hotkey conflicts, socials
validation, the remove tooltip, the go-live error line) — six sites all borrowing the retired
brand red to mean "something is wrong". They are `error` now, which is what the standard says
red-adjacent means; live/record keeps `primary`.

Guard list extended to all nine files.
2026-10-30 08:29:28 -07:00
gramps 8415bb5f37 design: preview pane to tokens (surface 4), and one palette list instead of two
110 hardcoded colours gone from the preview — the biggest surface in the app. Nicknames:
- the webcam selection ring is the interactive accent (was the retired brand red)
- the mute chips are `attention` (a muted mic is a caution, not a live state) with a white play
  glyph; the live badge is `primary`
- YPP progress bars/track, TRAX level zones, the error line, the dim strips and the poster veil
  all resolve from tokens (`positive`, `secondary-container`, `error-container`, `scrim-dark`,
  `scrim-dark-strong`)

Plus the duplication this sweep exposed: the webcam's right-click "Border Color" submenu carried
**seven hand-written colours**, a second source of truth beside the properties drop-down that
TASK 63 had already unified onto `Models/BorderColorPalette.cs` — and the two lists disagreed
(7 vs 14). The submenu is now built from the palette when it opens, so every offered colour is
the same colour everywhere, with the palette's name beside it. The click handler is unchanged;
generated rows inherit the menu's SceneElement DataContext.

Guard list extended: Controls/PreviewPane.xaml.
2026-10-30 08:28:21 -07:00
gramps 66b85d3fcc docs(handoff): preview pane next, with the colour-picker trap recorded 2026-10-30 08:03:44 -07:00
gramps 3edb0d10be design: three tokens the sweep needed — positive, scrim-dark, scrim-dark-strong
The palette had no green for status and no black scrim, so two surfaces were about to either
invent literals or misuse an accent. Creator's call (2026-10-30): give green a token rather
than painting "connected" blue, which already means five other things.

- `positive` #22c55e — STATUS ONLY (connected, applied, healthy). Never an accent, never a
  button fill; the doc says so and the guard parses the doc.
- `scrim-dark` #26000000 (15% black) and `scrim-dark-strong` #00000080 (50%) — the preview's dim
  strips and heavy overlays. 18 sites justify a token, and they are structural, not decoration.
- LeftPanel's ✓ confirm now wears `positive` instead of the interactive accent, matching the
  status rule the new token states.

DesignTokensTests still green: the doc is the contract, so the three new rows demanded three
new brushes and got them.
2026-10-30 08:03:18 -07:00
gramps 2dbc35b285 design: left panel to tokens (surface 3 of the sweep) — 101 colours gone
The layers/properties panel is the surface the creator looks at most, and it carried the
largest hardcoded palette in the app: 50 label greys, 15 icon blues, and a scatter of
one-off hexes. All of it resolves from tokens now.

Three sites needed a decision rather than a mapping, because the same hex meant different things:
- the + add icon and the chat author name were the old brand red → interactive accent (blue)
- the webcam lock alert is an ERROR, not decoration → error surface + error border
- the ✕ revert affordance was red, which the standard reserves for live/record → muted. ✓ stays
  a positive confirm, now in the interactive accent rather than a green that exists nowhere else
  in the palette. If green checks are missed, a `positive` token is the way back, not a literal.
- Super Chat gold was #eab308; it is the standard's amber now (game audio + Super Chat only).

Guard list extended: Controls/LeftPanel.xaml can no longer regress to a raw colour.
2026-10-30 08:00:43 -07:00
gramps 8a2cb92d8c docs(handoff): shell converted, left panel is next 2026-10-30 07:56:13 -07:00
gramps 0275e30103 design: shell to tokens — top bar, window, scene strip (surface 2 of the sweep)
- Telemetry readouts (bitrate, FPS, dropped, live duration) now wear JetBrains Mono, so the
  tabular digits stop shifting width as values change — the first place the bundled mono face
  is actually visible.
- Scene tiles: staged = blue (selection), live/program = red. Both were orange, which meant the
  same colour marked two different states and neither was the live colour.
- EDIT toggle: "on" is the interactive accent, not orange. Orange now means warning only.
- Wordmark's two-tone accent, avatar chrome, readout panel, tile borders and the program badge
  all resolve from tokens.

Guard list extended: MainWindow.xaml, Controls/TopBar.xaml, Controls/SceneThumbnailStrip.xaml —
these three can no longer regress to a hardcoded colour.
2026-10-30 07:56:02 -07:00
gramps ffdf3b6fc9 design: wire the sans family into the window; fonts ship as explicit Resource items
The bundled faces were packaged but unused — no style referenced FontFamily.Sans, so the app
still rendered Segoe UI. MainWindow now sets FontFamily from the token, which cascades to every
child (dialogs follow in the sweep).

Font packaging: six explicit <Resource> items instead of the Assets\Fonts\*.ttf glob, so the
guard can name exactly what must ship.

Investigation worth recording: a runtime assertion that the families resolve from
pack://application:,,,/ would have needed an Application instance inside the test host, and
without one WPF resolves the pack authority against the test host — every probe failed,
including the app icon that demonstrably works. The resources ARE embedded (the lowercase
`assets/fonts/inter-regular.ttf` entry is present in ytLive.g.resources); the assertion was
removed rather than faked, and the real confirmation is a launch, which is a visual check.

Guard remains structural: files are real binaries, each is a Resource in the csproj, the theme
uses pack URIs, notices + both OFL texts are present, and no third family sneaks in.
2026-10-30 07:53:37 -07:00
gramps 1defb71568 docs(handoff): design standard landed, token sweep in flight 2026-10-30 07:47:14 -07:00
gramps 4ef84f2a97 design: convert Themes/Controls.xaml to tokens (surface 1 of the sweep)
The theme file that every control inherits is now token-driven: 73 hardcoded colours became
Brush.* references, and the two role changes the standard requires landed here first.

- Red leaves the chrome. The seven #e94560 sites (base button, focus borders, checked box and
  segment backgrounds, the selected-layer accent strip) were all interactive/selection roles,
  so they are blue now. Red stays for live/record only.
- The slider was green: its fill and gloss knob are blue now, with the track on
  SurfaceContainerLowest. The old green fill predated the standard.
- The pill toggle was ON = green, OFF = red — a settings control using the live colour. It is
  now OFF = neutral surface, ON = blue.
- Added scrim-soft/scrim-strong tokens (#22FFFFFF/#26FFFFFF) for the existing white washes, and
  Color.* resources for the two gradients (a GradientStop needs a Color, not a Brush).

Guard: DesignTokensTests gains Converted_surfaces_carry_no_raw_colour — a declared list of
converted files, so each sweep commit is guarded as it lands instead of waiting for one global
grep at the end. Pure white/black highlights are the only allowed literals.

No layout, spacing or behaviour change: this unit is colour only.
2026-10-30 07:47:00 -07:00
gramps 84c335a929 design: bundle Inter + JetBrains Mono (OFL-1.1) as the standard typefaces
The DESIGN.md type standard needs the fonts actually present — the machine's installed faces
are not a design system. Four Inter weights (400/500/600/700) and two JetBrains Mono weights
(500/600) ship as WPF resources from Assets/Fonts; the token families resolve them with the
`/Assets/Fonts/#Family` pack form, so nothing depends on an install or a network fetch.

Licensing (recorded in docs/ai/product.md): OFL-1.1 is a free licence but is NOT GPL-compatible
in the linking sense. These are separate .ttf files loaded through WPF's font stack — an
aggregate, not linked code — so they coexist with the GPL-3.0 app. Obligations carried:
copyright line in THIRD-PARTY-NOTICES.txt (new items 13-14), the OFL text beside the files
(Assets/Fonts/OFL-*.txt), no modification, no reserved-name reuse.

Guard: BundledFontsTests — files exist and are real binaries, the csproj ships them, the theme
uses pack URIs (a bare family name would silently fall back), the notice and both OFL texts are
present, and no third font family sneaks in.

Pending visual confirmation: the first launch should render panel text in Inter and telemetry
in JetBrains Mono.
2026-10-30 07:45:19 -07:00
gramps f2bf577f34 design: Themes/Tokens.xaml — one place a raw value may live, guarded by DESIGN.md
Tokens.xaml carries every colour, type size, spacing step, radius and motion duration from
DESIGN.md; App.xaml merges it BEFORE Controls.xaml so styles can resolve tokens.

Guard (DesignTokensTests, 4 facts): the test PARSES DESIGN.md — the colour tables under
"## Colour" are the contract — and fails if a documented token has no brush, if the hex
disagrees, if the meter bands drift, if Tokens ever merges after Controls, or if the retired
brand red #e94560 comes back as a token. Adding a colour to the standard now requires
implementing it, and changing the theme without the document is caught.

Inter/JetBrains Mono are declared as token families with system fallbacks; the bundled font
assets are the next unit.

No surface consumes tokens yet — this unit is the foundation only.
2026-10-30 07:43:17 -07:00
gramps 565b8dbd6f docs(design): DESIGN.md becomes the visual standard (tokens merged, controls rejected)
Merges the 2026-10-29 Stitch export into the existing 2026-10-26 token sheet and promotes the
file from "redesign exploration" to the normative standard:

- type scale (32→10px), spacing, radii and elevation levels — the three scales the old
  DESIGN.md listed as missing
- meter thresholds: cyan ≤ -12 dB, amber -12..-3, red > -3
- interaction rules: focus ring, hover step, 150ms/90ms easing, no springs

Four export tokens rejected as Material-3 warm-seed artifacts (recorded with reasons):
tertiary #4cd7f6 (kept #38bdf8), outline #ab8986, outline-variant #5b403e,
on-surface-variant #e4beba (kept slate #94a3b8).

Product rulings recorded: #e94560 retired as the app's general accent (blue takes every
interactive role, red is live/record only, wordmark artwork untouched); error gets its own
surface (#93000a) so it cannot be mistaken for on-air red.

The export's dock, four bays, page-nav sidebar, CPU/GPU/NVENC readouts and acrylic blur are
explicitly marked exploratory, not spec — the design principle is a simpler surface, not
hardware cosplay. On-stream output styling stays outside this theme.

Falsified facts fixed in the same commit: product.md brand section now points at DESIGN.md for UI
tokens (MARCOM.md stays the marketing palette), and ui-surface.md no longer calls DESIGN.md an
unreconciled exploration.

Docs-only unit: scope-check run, no build.
2026-10-30 07:41:43 -07:00
gramps 5a102a4a37 docs: decisions log, queue trim, HANDOFF rewrite — the map stops being a treadmill
TASKS.md 467 → 66 lines: one line per live item, detail stays in TASKS/
task-*.md. The old catalogue (63 rows of paragraph status prose plus the
historical narration) is preserved verbatim as TASKS/archive.md — a
2026-10-29 snapshot, not the live queue. The "out of product" list and every
retired decision moved to docs/ai/decisions.md with cause of death.

HANDOFF.md 440 → 40 lines, and it is now OVERWRITTEN, not appended: branch,
what's in flight, next action, live landmines, pending creator checks. History
belongs to git log; the previous handoff is reachable at ee3c592:HANDOFF.md.

docs/ai/decisions.md (new) collects what no longer governs: the paid tier and
its pricing, Polar, Store IAP, the Store route and why it died, the deleted
Velopack updater, signing economics, the retired monetization ideas, and the
permanent out-of-product list.

Facts that lived in the old ai.md "Current limitations" section were re-homed
rather than dropped with the router rewrite: OAuth/DPAPI/session behaviour →
docs/ai/youtube-api.md; paths, persistence and the Google project number →
docs/ai/environment.md.

verify.sh caps now cover all four always-read files (AGENTS 210, ai 100,
HANDOFF 40, TASKS 120).

Docs-only unit: scope-check run, no build.
2026-10-29 11:10:51 -07:00
gramps c9ac348976 docs(ai): ai.md becomes a router; invariants move to AGENTS.md; budgets gated
ai.md 228 → 64 lines. It now carries only: what the product is, the build
commands, the architecture table, a "where the truth lives" table, and a
pointer to HANDOFF. No narrative, no status, no duplicated invariants.

AGENTS.md (195 lines) is now the single always-read file:
- the 20 invariants that break output, wipe user data, or ship a legal/trust
  breach — one line each, pointing at the doc that explains it (they replace
  110 lines of invariant stubs that duplicated docs/ai/*.md verbatim)
- Response style + No-Fluff Mode moved in from ai.md (rules belong in the rules file)
- onboarding is now: HANDOFF → ai.md router → the ONE docs/ai file → dir index;
  TASKS.md is read when the question is "what's next", not to write code
- memory rides with the change but is no longer a unit of its own; HANDOFF is
  ≤40 lines and overwritten, never appended

Anti-rebloat gate: scripts/verify.sh gains step 1/4, a doc-budget check
(AGENTS ≤210, ai ≤100; HANDOFF/TASKS caps land with the queue trim). A commit
that pads a capped file instead of moving detail fails the gate.

schema.md and docs/index.md corrected for the new roles and reading order.
Docs-only unit: scope-check run, no build.
2026-10-29 11:08:07 -07:00
gramps a311c8feb1 docs(ai): partition ai.md into five subsystem files (verbatim moves)
ai.md 1,308 → 228 lines. No prose rewritten: every block was moved as-is so
nothing could be lost in translation, then replaced by a pointer.

New files:
- docs/ai/environment.md — LAN/SSH + Gitea push recipe, Windows dotnet
  build/run, test-suite map, hermetic real-MainWindow test rule
- docs/ai/product.md — brand/naming, AI transparency, GPL licensing
  prohibitions, free-forever monetization, auth/account assumptions,
  packaging & distribution
- docs/ai/youtube-api.md — go-live ordering, the broadcast-binding fix, the
  private-only lock, health parsing, end-stream pre-flight, Test mode,
  chat insert shape
- docs/ai/overlays.md — social bar, native alert box (animations, clip,
  ticker), chat box conventions
- docs/ai/ui-surface.md — control surface, scene strip, preview rail,
  hotkeys, broadcast drawer, toasts, transitions, brand logo, launch
  geometry, theme/properties conventions

ai.md keeps the response/derivation rules, the architecture table, the four
existing invariant stubs, and one pointer per moved subsystem.
docs/ai/index.md routes every file with a "read when" column.

References falsified by the move were fixed in the same commit: AGENTS.md
build note → environment.md → Run, and the "Related" footers in
frame-pipeline.md / output.md / capture.md now point at the owning files.

Docs-only unit: scope-check run, no build (no source touched).
2026-10-29 11:05:53 -07:00
gramps ee3c59210d Settings: group preferences and open from gear
Group App Settings by purpose and let right-click on the gear open settings directly while preserving the left-click menu.

WPF event reference: https://learn.microsoft.com/en-us/dotnet/api/system.windows.uielement.previewmouserightbuttondown?view=windowsdesktop-10.0
2026-10-28 17:57:02 -07:00
gramps 769c616706 Preview: promote selected layer for hit testing
Temporarily raise the selected preview item and prioritize it in the custom hit test without changing scene order. Refresh integration-test expectations for Brand logo layers and explicit thumbnail assets.

WPF references: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.panel.zindex?view=windowsdesktop-10.0 https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/hit-testing-in-the-visual-layer
2026-10-28 13:09:15 -07:00
gramps 7da287244b Transitions in edit mode and on output: program transitions run while recording or streaming; the preview animates the outgoing picture (fade/move fade, wipe shrinks) in every mode
Test: PreviewTransitionTests (with the transition, wipe, and brand logo tests).
2026-10-27 10:20:53 -07:00
gramps 841c905c06 Brand logo: one locked image layer per scene above the background, image from App Settings
- Source.IsBrandLogo persisted (additive migration); layer healed on load, removal blocked
- Image stored as an asset; settings keep the asset id and file name
- App Settings: Brand Logo file field with Browse
- Default placement top-left; per-scene eye uses the existing element visibility
Test: BrandLogoTests; layout/background tests pass (34). Not pushed.
2026-10-27 09:25:49 -07:00
gramps b86b6bb601 Transitions: Wipe (left-to-right reveal), added to the App Settings picker
Test: WipeTransitionTests. Not pushed.
2026-10-27 09:12:59 -07:00