Files
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

193 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DESIGN.md — LlamaCasty visual design standard
> **Status: the standard** (decided 2026-10-30). Token values here are normative;
> `Themes/Tokens.xaml` is the implementation, and no other file may hardcode a hex once the sweep
> lands. Two Google Stitch exports feed it (2026-10-26 edit screen, 2026-10-29 audio dock).
>
> **The exports' layouts and control drawings are exploratory, not spec.** The dock, the four
> bays, the page-nav sidebar and the skeuomorphic faders are *not* adopted — the product's design
> principle is a surface so obvious that a right-brained person can use it without a manual, and
> hardware cosplay is the opposite of that. Only the colour, type, spacing, radius and elevation
> system is adopted.
## Colour
### Surfaces (dark navy scale, darkest to lightest)
| Token | Hex | Use |
|---|---|---|
| `background` | `#0b1326` | App window background |
| `surface-container-lowest` | `#060e20` | Deepest recess (scene strip background, canvas housing) |
| `surface-container-low` | `#131b2e` | Panel background (sidebars, headers) |
| `surface-container` | `#171f33` | Card / grouped-section background |
| `surface-container-high` | `#222a3d` | Hover state, input fields |
| `surface-container-highest` | `#2d3449` | Selected/active row background |
| `surface-bright` | `#31394d` | Lightest surface, rare use (strong hover) |
| `outline` | `#334155` | Borders, dividers |
| `outline-variant` | `#1e293b` | Subtle dividers, inactive meter segments |
| `scrim-soft` | `#22FFFFFF` | Hover/press wash over a surface (13% white) |
| `scrim-strong` | `#26FFFFFF` | Stronger wash — selected icon, dimmed image (15% white) |
| `scrim-dark` | `#26000000` | Dim strip / shading over the preview (15% black) |
| `scrim-dark-strong` | `#00000080` | Heavy overlay — modal veil, poster frame (50% black) |
### Text
| Token | Hex | Use |
|---|---|---|
| `on-surface` | `#dae2fd` | Primary text |
| `on-surface-variant` | `#94a3b8` | Secondary / label text |
| `on-surface-disabled` | `#5b6478` | Disabled text and icons |
### Accent — one role each, never mixed
| Token | Hex | Role |
|---|---|---|
| `tertiary` (electric blue) | `#38bdf8` | **The** interactive accent: selection, focus, sliders, primary buttons, transform handles |
| `on-tertiary` | `#0b1326` | Dark ink, for large blue **surfaces** only |
| `on-accent` | `#ffffff` | Text/icon on any saturated accent fill — the blue EDIT button, the armed REC/ON-AIR pills. One rule: a coloured fill carries white ink |
| `primary` (red) | `#ff5451` | **On-air / recording only** — LIVE badge, program border, End/Go Live |
| `primary-container` | `#b91a24` | Red pressed/hover state |
| `secondary` (amber) | `#ffb95f` | Game audio channel, Super Chat highlight |
| `secondary-container` | `#ee9800` | Amber pressed/hover state |
| `attention` (orange) | `#ff8c3a` | Dropped frames, the live-screen indicator, blinking warnings |
| `positive` | `#22c55e` | **Status only** — connected, applied, healthy. Never an accent, never a button fill |
| `error` | `#ef4444` | Error text and icons |
| `error-container` | `#93000a` | Error surface (health banner, toast) — keeps error visually distinct from on-air red |
| `on-error` | `#ffdad6` | Text on an error surface |
| `info-container` | `#3a3b52` | Neutral notice surface (info toast) |
| `success-container` | `#1d5c38` | Success notice surface (success toast) |
| `warning-container` | `#b8860b` | Warning surface — health banner, warning toast |
| `test-container` | `#9c6f1c` | Test-stream chrome: the gold top bar and glow |
| `youtube` | `#ff0000` | **YouTube-branded surfaces only** — Stream Info / YPP rails, the YouTube account row, and the wordmark's "ty" (creator ruling: never re-colour it) | — the Stream Info and YPP rails, the YouTube account row. Not for state: live stays `primary` |
**The rule:** red means "live or recording" and nothing else. Never a Send button, a delete action,
a brand flourish, or a general accent — that was the defect in the first Stitch draft.
**`#e94560` is retired** as the app's general accent (it was in 15 files). Blue takes over every
interactive role; red keeps only live/record. The brand/wordmark *artwork* is unchanged — those
assets are the creator's art, not a theme value.
### Meter thresholds (both meters, all bays)
| Band | Range | Brush |
|---|---|---|
| Nominal | −∞ … −12 dB | `#38bdf8` |
| Caution | −12 … −3 dB | `#ffb95f` |
| Peak / clip | −3 … 0 dB+ | `#ff5451` |
Meters are **measured, never decorative** (see `docs/ai/output.md`): the ladder shows the live
level scaled by the current gain, not the gain setting.
## Typography
| Token | Family | Use |
|---|---|---|
| `sans` | Inter (400/500/600/700) | All UI text — labels, buttons, panel titles |
| `mono` | JetBrains Mono (500/600) | Numeric/telemetry values — bitrate, FPS, dB, coordinates, timers. Tabular figures stop digits shifting width as values change |
Icons: Material Symbols Outlined (bundled, not fetched).
### Scale
| Token | Size / line | Weight | Tracking | Use |
|---|---|---|---|---|
| `headline-lg` | 32 / 40 | 700 | −0.02em | Landing/empty-state titles only |
| `headline-md` | 24 / 32 | 600 | −0.015em | Dialog titles |
| `headline-sm` | 18 / 24 | 600 | −0.01em | Panel and section headers |
| `body-lg` | 15 / 22 | 400 | — | Default body, chat |
| `body-md` | 13 / 18 | 400 | — | Panel rows, form text |
| `body-sm` | 12 / 16 | 400 | — | Secondary text, hints |
| `label-lg` | 13 / 16 | 600 | 0.02em | `mono` — primary telemetry values |
| `label-md` | 11 / 14 | 500 | 0.04em | `mono` — labels, hotkey chips |
| `label-sm` | 10 / 12 | 500 | 0.05em | `mono` — dense telemetry only |
**Floors and implementation notes**
- **11px is the default label size; 10px is the absolute floor** and only for dense readouts that
sit beside a larger value. Nothing readable at 100% zoom may be smaller than 10px.
- **WPF has no letter-spacing.** Tracking values above are design intent, not a property; implement
as-is (no tracking) rather than faking it with padded strings.
- Mono numerics use `Typography.NumeralAlignment="Tabular"` so widths stay stable.
## Spacing
| Token | px | Use |
|---|---|---|
| `space-xs` | 4 | Inside a chip, icon-to-text |
| `space-sm` | 8 | Between related controls |
| `space-md` | 12 | Default gap inside a panel |
| `space-lg` | 16 | Panel padding, section separation |
| `space-xl` | 24 | Dialog padding, major separation |
| `gutter` | 12 | Between page columns |
| `margin` | 16 | Window edge to content |
| `margin-docked` | 8 | Edge to content when a rail/drawer is docked |
## Shape
| Token | px | Use |
|---|---|---|
| `radius-sm` | 4 | Chips, badges, keycap tags |
| `radius-md` | 8 | Buttons, inputs, cards, scene tiles |
| `radius-lg` | 16 | Preview viewport, drawers, dialogs |
| `radius-full` | pill | Status chips (LIVE, EDIT), toggles |
Recessed controls (fader wells, inputs) use the same outer radius as their housing so the seams
line up — do not mix a 4px well inside an 8px card.
## Elevation
| Level | Use | Implementation |
|---|---|---|
| −1 recessed | Fader wells, input fields, table headers | inset top shadow + `surface-container-lowest` |
| 0 panel | The default chassis | `surface-container*` + **1px `outline` border**, no shadow |
| 1 raised | Scene tiles, floating chips, action buttons | subtle drop shadow, 0 1px 2px |
| 2 floating | Drawers, popups, dialogs | larger spread + 1px top highlight |
**Budget rule:** `DropShadowEffect` is software-rendered in WPF. Shadows on small controls only —
**never** on the preview viewport, the scene strip, or either wing. Depth on large surfaces comes
from the surface scale and the 1px border, not from effects.
## Interaction
- Focus ring: 1px `#38bdf8` outside a 1px gap — visible against every surface level.
- Hover: step one surface level up; active: step two. No colour change on hover except for
destructive actions.
- Motion: 150ms `CubicEase(EaseOut)` for drawers/popovers; 90ms for hover/press. No springs —
WPF has no spring easing and hand-rolled overshoot reads as jank at 60Hz.
- Every action produces a visible outcome. No dead ends, no silent failures.
## Not in this standard
- The audio dock, its four bays, per-bay faders, 24-segment ladders, transport controls, lip-sync
slider and master telemetry strip — **exploratory**. The controls they re-house already exist in
the app; whether any of them gets a dock is a separate product decision.
- The page-nav sidebar (Broadcast Studio / Audio Mixer / Scene Matrix / Telemetry / Hotkeys) — the
app is one surface with an EDIT toggle and drawers, not a page router.
- CPU / GPU / NVENC readouts — no telemetry exists, and none of it changes a live decision.
- Acrylic/backdrop blur — WPF has no sub-region backdrop blur; panels are solid.
- **On-stream output** (chat box, social bar, alerts, brand flash) is styled by its own renderers,
not by this theme. Do not apply UI tokens to the broadcast picture.
## Rejected from the 2026-10-29 export
The export is a Material-3 dump from a warm seed; four tokens came out tinted by that seed and are
rejected in favour of the values above.
| Export value | Token | Why rejected |
|---|---|---|
| `#4cd7f6` | `tertiary` | Neon cyan beside the amber reads "toy"; `#38bdf8` is calmer and already used in-app |
| `#ab8986` | `outline` | Warm pink borders from the seed — wrong for a navy chassis |
| `#5b403e` | `outline-variant` | Same seed artifact |
| `#e4beba` | `on-surface-variant` | Pink secondary text; slate `#94a3b8` is legible and neutral |
## Implementation status
- **`Themes/Tokens.xaml` exists and is merged first in `App.xaml`** (2026-10-30). Guard:
`ytLive.Tests/DesignTokensTests.cs` — it parses this document and fails if a documented token has
no brush, if a value disagrees, or if the retired `#e94560` returns as a token.
- **Still to sweep:** `Themes/Controls.xaml` and the dozens of hardcoded hexes in XAML/C# still use
the legacy palette. Surfaces convert one at a time; the end state is that every colour, radius and
spacing value resolves from `Tokens.xaml` and a guard fails on any raw `#hex` outside it.
- **Fonts ship with the app** (`Assets/Fonts/*.ttf`, OFL-1.1) — no installation, no network, no
dependence on what the machine happens to have. Notice and license text travel with them (`THIRD-PARTY-NOTICES.txt` items 13-14).