Compare commits
11 commits
f12cbe7517
...
46b1ca5874
| Author | SHA1 | Date | |
|---|---|---|---|
| 46b1ca5874 | |||
| 2f9f7092ed | |||
| 2909d8b1d7 | |||
| c150bce28e | |||
| 8e29c1dc1e | |||
| 48ca16bc5e | |||
| 2e6d2a1e5e | |||
| db341f9446 | |||
| 9e176d8f10 | |||
| cb1402ec8d | |||
| 94b0a71edc |
31 changed files with 3788 additions and 14 deletions
325
DESIGN.md
Normal file
325
DESIGN.md
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
# DESIGN.md — TeamsISO design system
|
||||
|
||||
Target framework: **WinUI 3 (Windows App SDK)**. Tokens are framework-agnostic;
|
||||
the WinUI XAML implementation lives in `src/TeamsISO.App/Themes/` (post-port).
|
||||
|
||||
## Color
|
||||
|
||||
### Strategy
|
||||
|
||||
**Restrained — committed accent + neutral surface.** The surface is the work;
|
||||
the cyan accent is reserved for live state, focus, and the few moments that
|
||||
actually need attention. Coral is reserved for destructive and error.
|
||||
Everything else is neutral.
|
||||
|
||||
This means: no rainbow status pills, no per-feature accent colors, no
|
||||
Slack-style chroma everywhere. If something is cyan, the operator's eye
|
||||
should know why.
|
||||
|
||||
### Scene sentence
|
||||
|
||||
**Dark (default):** A solo broadcast operator at 1:50am, ambient room lights
|
||||
at 5%, leaning into a 24-inch monitor, twenty minutes before a live
|
||||
international interview.
|
||||
|
||||
**Light:** A morning recording session in a glass-walled conference room with
|
||||
the sun coming through the blinds, monitor brightness at 80%. Or a daytime
|
||||
producer monitoring a remote interview from a hotel desk during a working
|
||||
session before lunch.
|
||||
|
||||
The default is dark — that's the dominant operator scene. Light mode exists
|
||||
because not every show happens at 1:50am.
|
||||
|
||||
### Dark palette
|
||||
|
||||
Every neutral is tinted toward cyan (h ≈ 200, chroma 0.005–0.008) so the
|
||||
dark surface reads as deliberate dark, not as chromatically dead.
|
||||
|
||||
| Token | Role | Hex | OKLCH (approx) |
|
||||
|---|---|---|---|
|
||||
| `bg.canvas` | Window canvas | `#0A0A0A` | `oklch(0.12 0.005 200)` |
|
||||
| `bg.rail` | Left rail | `#080808` | `oklch(0.10 0.005 200)` |
|
||||
| `bg.surface` | Card / row | `#141416` | `oklch(0.18 0.006 200)` |
|
||||
| `bg.elevated` | Popovers, menus | `#1C1C1F` | `oklch(0.22 0.007 200)` |
|
||||
| `bg.hover` | Hover fill | `#26272B` | `oklch(0.28 0.008 200)` |
|
||||
| `bg.active` | Pressed fill | `#33343A` | `oklch(0.34 0.010 200)` |
|
||||
| `border.subtle` | Hairlines | `#26272B` | `oklch(0.28 0.008 200)` |
|
||||
| `border.strong` | Hover / focus | `#3A3B40` | `oklch(0.36 0.010 200)` |
|
||||
| `fg.primary` | Body text | `#F4F4F6` | `oklch(0.96 0.004 200)` |
|
||||
| `fg.secondary` | Subdued text | `#A3A4AA` | `oklch(0.70 0.006 200)` |
|
||||
| `fg.tertiary` | Captions | `#6B6C72` | `oklch(0.50 0.006 200)` |
|
||||
| `fg.disabled` | Disabled | `#404145` | `oklch(0.32 0.006 200)` |
|
||||
|
||||
### Light palette
|
||||
|
||||
Mirrored token names; cyan-tinted off-white so the surface still reads as
|
||||
Wild Dragon, not as generic white.
|
||||
|
||||
| Token | Role | Hex | OKLCH (approx) |
|
||||
|---|---|---|---|
|
||||
| `bg.canvas` | Window canvas | `#FAFAFB` | `oklch(0.98 0.003 200)` |
|
||||
| `bg.rail` | Left rail | `#F0F1F3` | `oklch(0.95 0.004 200)` |
|
||||
| `bg.surface` | Card / row | `#FFFFFF` | `oklch(1.00 0.000 200)` |
|
||||
| `bg.elevated` | Popovers, menus | `#FFFFFF` | `oklch(1.00 0.000 200)` (+ shadow) |
|
||||
| `bg.hover` | Hover fill | `#ECEEF1` | `oklch(0.93 0.005 200)` |
|
||||
| `bg.active` | Pressed fill | `#E0E3E7` | `oklch(0.89 0.006 200)` |
|
||||
| `border.subtle` | Hairlines | `#E5E7EB` | `oklch(0.91 0.004 200)` |
|
||||
| `border.strong` | Hover / focus | `#D1D5DA` | `oklch(0.85 0.006 200)` |
|
||||
| `fg.primary` | Body text | `#0A0A0A` | `oklch(0.12 0.005 200)` |
|
||||
| `fg.secondary` | Subdued text | `#4A4B50` | `oklch(0.36 0.006 200)` |
|
||||
| `fg.tertiary` | Captions | `#71747A` | `oklch(0.53 0.006 200)` |
|
||||
| `fg.disabled` | Disabled | `#B3B6BC` | `oklch(0.76 0.005 200)` |
|
||||
|
||||
### Accents — context-aware
|
||||
|
||||
Some accents work in both modes; others need a darker variant for AA contrast
|
||||
when used as text on the light canvas. The token table splits them:
|
||||
|
||||
| Token | Dark | Light | Reserved for |
|
||||
|---|---|---|---|
|
||||
| `accent.cyan.surface` | `#97EDF0` | `#97EDF0` | Primary button fill, badge fill (text on top is near-black in both modes — works) |
|
||||
| `accent.cyan.text` | `#97EDF0` | `#0E7C82` | Cyan-as-text (links, "live" labels, active state) |
|
||||
| `accent.cyan.hover` | `#B5F2F4` | `#0890A0` | Cyan hover |
|
||||
| `accent.cyan.muted` | `#1B3537` | `#E6F8F9` | Cyan tint background, active speaker row fill |
|
||||
| `accent.coral` | `#FB819C` | `#D43E5C` | Destructive, error, alert (as both border + text) |
|
||||
| `accent.coral.bg` | `#3A1922` | `#FDECF0` | Coral tint background |
|
||||
| `status.live` | `#4ADE80` | `#15803D` | Recording active, REC dot, "live" pill |
|
||||
| `status.live.bg` | `#13261A` | `#DCFCE7` | Live pill background |
|
||||
| `status.warn` | `#FBBF24` | `#B45309` | Low disk, NDI degraded |
|
||||
|
||||
**Discipline.** Cyan is the only color that competes with body text for
|
||||
attention. It earns its place — wasted cyan is the design failing.
|
||||
`accent.cyan.surface` (#97EDF0) reads identically in both modes because
|
||||
its text is always near-black. `accent.cyan.text` exists specifically so
|
||||
captions and inline labels stay readable on a light canvas.
|
||||
|
||||
## Theming
|
||||
|
||||
### The toggle
|
||||
|
||||
A single icon button (sun ↔ moon) lives in the title bar, positioned to the
|
||||
left of the window controls. One click swaps the theme. State persists via
|
||||
`UIPreferences.Theme` (`Dark | Light | System`). Default is `System` which
|
||||
follows the Windows app-mode preference.
|
||||
|
||||
The toggle is also surfaced inside the settings drawer under an "Appearance"
|
||||
group as a tri-state pill (System / Dark / Light), so power users find it in
|
||||
the obvious place too.
|
||||
|
||||
### Implementation (WinUI 3)
|
||||
|
||||
WinUI 3's `ThemeDictionary` pattern handles automatic swapping based on
|
||||
`FrameworkElement.RequestedTheme`. Tokens live as `Color` resources keyed by
|
||||
theme, then `SolidColorBrush` references them via `{ThemeResource}`:
|
||||
|
||||
```xml
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<Color x:Key="BgCanvasColor">#0A0A0A</Color>
|
||||
<SolidColorBrush x:Key="BgCanvas" Color="{ThemeResource BgCanvasColor}"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<Color x:Key="BgCanvasColor">#FAFAFB</Color>
|
||||
<SolidColorBrush x:Key="BgCanvas" Color="{ThemeResource BgCanvasColor}"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
At runtime, the root `Page.RequestedTheme = ElementTheme.Dark | Light`
|
||||
switch propagates down the visual tree instantly — no app restart, no
|
||||
flicker. The custom title bar (drawn via `AppWindow.TitleBar.ExtendsContent`)
|
||||
gets a manual color update on the same code path since its system buttons
|
||||
aren't part of the XAML tree.
|
||||
|
||||
### System mode
|
||||
|
||||
When `UIPreferences.Theme == System`, the app reads
|
||||
`Application.Current.RequestedTheme` at startup and re-reads when the OS
|
||||
theme changes (via `UISettings.ColorValuesChanged`). This is the default —
|
||||
operators who don't care get whatever their Windows session is set to.
|
||||
|
||||
## Typography
|
||||
|
||||
### Scale (1.25 step ratio enforced)
|
||||
|
||||
| Token | Family | Size | Weight | Line-height | Letter-spacing |
|
||||
|---|---|---|---|---|---|
|
||||
| `text.display` | Inter | 22 | 600 | 1.2 | -0.01em |
|
||||
| `text.title` | Inter | 18 | 600 | 1.25 | -0.005em |
|
||||
| `text.heading` | Inter | 14 | 600 | 1.3 | 0 |
|
||||
| `text.body` | Inter | 13 | 400 | 1.45 | 0 |
|
||||
| `text.subtle` | Inter | 13 | 400 | 1.45 | 0 |
|
||||
| `text.caption` | Inter | 11 | 500 | 1.3 | 0.04em (smallcaps) |
|
||||
| `text.mono` | JetBrains Mono | 12 | 400 | 1.4 | 0 |
|
||||
|
||||
Body text caps at 65–75ch where it wraps. Inline status text doesn't wrap —
|
||||
it truncates with ellipsis.
|
||||
|
||||
### Fonts in WinUI 3
|
||||
|
||||
WPF's `pack://application:,,,/Assets/Fonts/#Inter` resource URI doesn't carry
|
||||
over. WinUI 3 uses `ms-appx:///Assets/Fonts/Inter.ttf#Inter`. Migration is
|
||||
mechanical but required.
|
||||
|
||||
## Spacing (8px grid)
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `space.xs` | 4 | Icon-to-text, tiny gaps |
|
||||
| `space.s` | 8 | Row internal padding, pill padding |
|
||||
| `space.m` | 12 | Card internal padding |
|
||||
| `space.l` | 16 | Card padding, between cards |
|
||||
| `space.xl` | 24 | Section gap |
|
||||
| `space.xxl` | 32 | Page edge padding |
|
||||
| `space.xxxl` | 48 | Hero section / large blocks |
|
||||
|
||||
**Rhythm rule.** No two adjacent regions share the same padding value. The
|
||||
participant table breathes at `space.xl`; in-row controls compress to
|
||||
`space.s`. Same padding everywhere is monotony.
|
||||
|
||||
## Radii
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `radius.s` | 6 | Pills, inline tags, menu items |
|
||||
| `radius.m` | 8 | Buttons, text inputs, dropdowns |
|
||||
| `radius.l` | 12 | Cards, drawers, modals |
|
||||
| `radius.pill` | 999 | Status pills, ISO toggle |
|
||||
|
||||
## Elevation
|
||||
|
||||
Elevation through **tone**, not through shadow. The dark surface makes
|
||||
realistic drop-shadows look bolted-on. A `bg.elevated` tone difference does
|
||||
the same job with less visual noise.
|
||||
|
||||
| Layer | Background | Border |
|
||||
|---|---|---|
|
||||
| Canvas | `bg.canvas` | none |
|
||||
| Card | `bg.surface` | `border.subtle` |
|
||||
| Drawer / Popover | `bg.elevated` | `border.strong` |
|
||||
| Modal | `bg.elevated` | `border.strong` + 50% canvas scrim |
|
||||
|
||||
## Icons
|
||||
|
||||
**Single icon system, one stroke width, one optical size.** The previous GUI
|
||||
inlined ~12 bespoke `<Path Data="...">` icons with stroke widths varying
|
||||
between 1.2 and 1.6. The redesign uses **WinUI 3's bundled Segoe Fluent Icons
|
||||
font** as the baseline, with a custom subset added only where a broadcast
|
||||
concept isn't covered (e.g. NDI signal lock, ISO routing state).
|
||||
|
||||
Sizes: 16 (inline), 20 (button), 24 (rail / hero).
|
||||
Stroke: inherited from font; no hand-stroked paths.
|
||||
|
||||
## Motion
|
||||
|
||||
- Ease-out exponential (`cubic-bezier(0.16, 1, 0.3, 1)`) for entry.
|
||||
- Ease-in-out for state changes that aren't entries.
|
||||
- Durations: 120ms for affordance feedback, 200ms for panel transitions,
|
||||
280ms hero (rarely used).
|
||||
- No bounce. No elastic. No spring overshoots.
|
||||
- **Never animate** layout properties. Animate `Translation` and `Opacity`
|
||||
(WinUI 3's composition layer handles these GPU-cheaply).
|
||||
|
||||
## Component decisions
|
||||
|
||||
### Buttons — finally have a real hierarchy
|
||||
|
||||
The previous design used `Wd.Button.Ghost` for everything. The redesign has
|
||||
**three commitments**:
|
||||
|
||||
| Variant | Use | Look |
|
||||
|---|---|---|
|
||||
| `Primary` | Single per surface, the brand action ("Apply", "Start session") | Cyan fill, near-black text |
|
||||
| `Secondary` | Common operator actions ("Refresh", "Presets") | Transparent fill, `border.strong`, hover cyan border |
|
||||
| `Tertiary` | Inline, low-frequency ("Dismiss", "Show advanced") | Text-only, no border, cyan on hover |
|
||||
| `Destructive` | Stop, leave, delete | Coral border, coral text, no fill |
|
||||
|
||||
**One Primary per surface.** If a screen has two primaries, the design is
|
||||
unranked.
|
||||
|
||||
### ISO toggle — keep, refine
|
||||
|
||||
The status-coded pill (LIVE cyan / ERROR coral / NO SIGNAL amber) is good.
|
||||
Two evolutions:
|
||||
|
||||
1. The hover treatment thickens to a 2px cyan border — preserve.
|
||||
2. Add a half-height ascender showing instantaneous audio level above the
|
||||
pill. The operator sees who's talking without needing the active-speaker
|
||||
row highlight to fire on next tick.
|
||||
|
||||
### Tables (Participants)
|
||||
|
||||
This is the product. The table gets:
|
||||
|
||||
- Row height 56 (current) → 64 to give the audio meter + signal indicator
|
||||
room to breathe.
|
||||
- The "active speaker" cyan left-border treatment stays. It's good.
|
||||
- One participant action per row at rest (the ISO toggle). Other actions
|
||||
(open preview, custom name, presets) live in a right-click context menu
|
||||
(already exists) and in a row hover-revealed kebab — *not* visible at rest.
|
||||
- Column count: avatar+name · NDI signal+codec · audio meter · output name ·
|
||||
ISO toggle. Five columns. The current six-plus + custom-name editing
|
||||
inline pushes density too far.
|
||||
|
||||
### Status — one place, not three
|
||||
|
||||
Recording / disk / session / control-surface state currently lives in:
|
||||
1. Rail bottom dot (engine status)
|
||||
2. Header right pill (status text)
|
||||
3. Footer columns (six monospace fields)
|
||||
|
||||
The redesign consolidates to **two places only**:
|
||||
|
||||
- **Header right** — session timer, REC indicator + count, disk-free.
|
||||
These are at-a-glance.
|
||||
- **Status overlay (popover from rail bottom dot)** — control surface URLs,
|
||||
log path, version, control-surface tokens. These are on-demand.
|
||||
|
||||
The footer goes away entirely. It was theatre, not information.
|
||||
|
||||
### Settings — drawer, not permanent panel
|
||||
|
||||
The 380px right settings panel is the single biggest spatial misallocation.
|
||||
Settings are rarely changed mid-show. The redesign moves them to a **right-side
|
||||
drawer** that slides in over the participants area, dismissable with `Esc`.
|
||||
The participants table reclaims full width when the drawer is closed.
|
||||
|
||||
Trigger: rail "settings" icon. Same affordance as today, different surface.
|
||||
|
||||
### Onboarding
|
||||
|
||||
First-launch only. Three panes max, each one panes deep — no carousel.
|
||||
Operator-tone copy ("Pick your NDI groups" not "Welcome to TeamsISO!").
|
||||
Skippable from the first frame.
|
||||
|
||||
### Empty states
|
||||
|
||||
The participants table empty state currently is implicit (rows just don't
|
||||
appear). The redesign adds **one** empty state with a single instructive
|
||||
sentence ("No NDI sources yet — open Teams and start a meeting") and a
|
||||
single secondary button ("Refresh"). No illustration. No mascot.
|
||||
|
||||
## Anti-patterns specific to this app (audited against absolute bans)
|
||||
|
||||
The current XAML has none of the impeccable absolute bans (no gradient text,
|
||||
no side-stripe borders, no glassmorphism). It does have:
|
||||
|
||||
- **Identical card grids** — the in-call control bar's seven identical ghost
|
||||
buttons. Redesign: collapse to a single dense bar with primary controls
|
||||
surfaced and secondary controls in an overflow menu.
|
||||
- **Status duplication** — fix as above.
|
||||
- **Bespoke SVG icons** — fix as above.
|
||||
|
||||
## Migration boundary
|
||||
|
||||
The view-model surface in `src/TeamsISO.App/ViewModels/` is the contract.
|
||||
The redesign rewrites everything in `Views/` (WinUI 3) but leaves view-model
|
||||
properties and commands untouched. Any place where the redesign needs a new
|
||||
piece of view-model state, the contract widens via additive properties —
|
||||
existing bindings keep working until the new view stops needing the old shape.
|
||||
|
||||
This means: the engine, the OSC bridge, the control surface, the preset
|
||||
store, the recording pipeline — none of those move. The redesign is
|
||||
a frontend-only operation.
|
||||
174
PRODUCT.md
Normal file
174
PRODUCT.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# PRODUCT.md — TeamsISO
|
||||
|
||||
## Register
|
||||
|
||||
**Product.** This is a tool, not a destination. The design serves the operator
|
||||
running a live broadcast. The UI is judged by how invisible it gets once the
|
||||
show is rolling.
|
||||
|
||||
## Product purpose
|
||||
|
||||
TeamsISO is a per-participant NDI ISO controller for Microsoft Teams. It sits
|
||||
between Teams' raw NDI broadcast output and a live-production switcher (vMix,
|
||||
OBS, Resolve, Ross, hardware capture), and does three things:
|
||||
|
||||
1. **Routes** each guest as a clean, individually-addressable, normalized NDI
|
||||
source (consistent framerate, resolution, aspect, audio routing — regardless
|
||||
of what each participant's webcam is doing).
|
||||
2. **Records** every active ISO to disk simultaneously — raw BGRA + manifest
|
||||
+ ffmpeg convert script — so post-production gets a per-guest archive
|
||||
ready to cut from.
|
||||
3. **Orchestrates Teams itself** — launch/hide Teams windows, drive in-call
|
||||
controls (mute, camera, share, leave, raise hand, quick-join) via
|
||||
UIAutomation, so the operator never has to alt-tab away from the routing
|
||||
table while the show is live.
|
||||
|
||||
External control surface (REST + WebSocket + OSC on localhost) lets a
|
||||
Companion / Stream Deck / TouchOSC controller drive routing remotely.
|
||||
|
||||
## Users — the primary persona
|
||||
|
||||
**Solo operator.** One person, one Windows laptop or desk machine, often
|
||||
running the show alone from a hotel room, conference green room, or home
|
||||
studio. Picture them at 1:50am, twenty minutes before a live international
|
||||
broadcast, ambient room lights down, the Teams call already started, four
|
||||
guests joining staggered over the next ten minutes. They need to:
|
||||
|
||||
- See which participants are present, online, and producing NDI signal.
|
||||
- Toggle each one's ISO on as they join.
|
||||
- Confirm at a glance that recording is live, the disk has room, and the
|
||||
control surface is reachable.
|
||||
- Drop a marker if the host says something quotable.
|
||||
- Mute themselves without alt-tabbing.
|
||||
|
||||
If the UI demands more than a glance for any of those, the show suffers.
|
||||
|
||||
### Secondary personas (informed, not designed-for)
|
||||
|
||||
- **TD at a broadcast desk** — multi-monitor, may use the OSC bridge to a
|
||||
hardware control surface. Can tolerate a denser layout because their eyes
|
||||
aren't the only thing on the surface.
|
||||
- **Producer monitoring** — glances occasionally, mostly hands-off. Will see
|
||||
this app over someone's shoulder; first read matters.
|
||||
- **IT/AV admin** — installs it once, tunes config, walks away. Needs settings
|
||||
to be findable, not present-at-all-times.
|
||||
|
||||
The design optimizes for the solo operator. Everyone else is downstream.
|
||||
|
||||
## Brand
|
||||
|
||||
**Wild Dragon LLC.** Reference: wilddragon.net.
|
||||
|
||||
Palette anchors:
|
||||
- Canvas: near-black (`#0A0A0A`)
|
||||
- Primary accent: cyan (`#97EDF0`)
|
||||
- Secondary blue: (`#9AE0FD`)
|
||||
- Coral (error / destructive): (`#FB819C`)
|
||||
- Earth (warning): (`#423825`)
|
||||
|
||||
Typography:
|
||||
- Sans: **Inter** (variable, bundled as a resource — not assumed installed).
|
||||
- Mono: **JetBrains Mono** (also bundled).
|
||||
|
||||
The brand carries the surface but doesn't shout. Wild Dragon's authority is
|
||||
in the restraint, not the saturation.
|
||||
|
||||
## Voice and tone
|
||||
|
||||
**Operator-first, terse, broadcaster-native.** The UI talks like a confident
|
||||
peer, not a Slack bot.
|
||||
|
||||
- "Stop all" not "Are you sure you want to stop all ISOs?"
|
||||
- "Disk low — 8.3 GB" not "Heads up! Your disk space is running low."
|
||||
- "Joined call · 4 guests" not "You have successfully joined a Teams meeting!"
|
||||
- Numbers carry their unit, no sentence wraps them.
|
||||
- Never apologetic. Never bubbly. Never "Let's get started!"
|
||||
|
||||
When something goes wrong, name it: "NDI receiver dropped — restarting" beats
|
||||
"Something went wrong, please try again."
|
||||
|
||||
## Strategic principles
|
||||
|
||||
These are the design's load-bearing commitments. Any choice that contradicts
|
||||
one of these is wrong, even if it would otherwise be pretty.
|
||||
|
||||
### 1. One operator, one screen, one show.
|
||||
|
||||
The design is for someone running a live broadcast alone. Their attention
|
||||
budget for chrome is roughly zero. Anything that's not the participants table
|
||||
should fade until it's needed.
|
||||
|
||||
### 2. The participants table IS the product.
|
||||
|
||||
Everything else is support staff. Routing toggles, ISO state, and per-guest
|
||||
signal health get the real estate, the contrast, and the typographic hierarchy.
|
||||
|
||||
### 3. Progressive disclosure, not progressive density.
|
||||
|
||||
The current GUI's failure mode is "every feature gets its own visible button."
|
||||
The redesign's failure mode would be the opposite — burying important things
|
||||
in menus. The discipline: surface the half-dozen actions an operator needs
|
||||
mid-show; hide setup, presets, control-surface config, and exotic options
|
||||
behind purposeful entry points.
|
||||
|
||||
### 4. At-a-glance status is sacred.
|
||||
|
||||
Recording state, disk health, control-surface reachability, session timer,
|
||||
NDI signal-per-participant — these are the operator's situational awareness.
|
||||
They must be readable in peripheral vision, in one place, without scanning.
|
||||
|
||||
### 5. Confident neutrality over decorative warmth.
|
||||
|
||||
This is a broadcast tool. It looks like one. No empty-state mascots, no
|
||||
illustrated onboarding cards, no celebratory toasts. Restraint is the brand.
|
||||
|
||||
## Anti-references — what this is NOT
|
||||
|
||||
The "vibe-coded GUI" failure mode is the enemy. The redesign should never
|
||||
read as AI-generated. Concretely, this means none of:
|
||||
|
||||
- **Generic SaaS dashboard.** No "hero metric + supporting stats + gradient
|
||||
accent" cards. No "icon + heading + body text" card grids.
|
||||
- **Cards-in-a-grid template.** Same-sized cards repeated endlessly is the
|
||||
defining LLM-design tell. If a layout would benefit from cards-in-a-grid,
|
||||
it benefits more from a table.
|
||||
- **Card-with-icon-and-text rows.** The in-call control bar's current
|
||||
"icon + label" buttons (Mute / Camera / Share / Marker / Notes / Leave)
|
||||
read AI-generated. The redesign uses iconography differently.
|
||||
- **Zoom pastel.** Soft purples, friendly mint greens, rounded everything,
|
||||
Inter-at-low-weight.
|
||||
- **Skeuomorphic broadcast hardware.** No woodgrain, no chrome bezels, no
|
||||
fake LCD readouts, no metallic gradients. Wild Dragon's confidence is in
|
||||
flat surfaces with real typography.
|
||||
- **Tour-everything onboarding.** No "Let's get started!" wizards with cute
|
||||
copy. The OnboardingWindow exists for first-launch config, not pageantry.
|
||||
- **Modal-as-first-thought.** Settings, presets, help all currently live in
|
||||
modals; some should be drawers or inline-progressive. Modal is a last resort.
|
||||
|
||||
## Technical constraints (informing design)
|
||||
|
||||
- Windows-only (Teams' NDI is Windows-only anyway).
|
||||
- **WinUI 3 / Windows App SDK** is the new frontend target.
|
||||
- Engine layer (.NET 8) is preserved verbatim — view-model surface is the
|
||||
swap boundary.
|
||||
- Bundled fonts via WinUI 3's `FontFamily` packaging (the WPF
|
||||
`pack://...#Inter` resource URI doesn't translate; needs migration).
|
||||
- MSIX-signed installer is on the v1.0 path; the new shell needs to package
|
||||
cleanly through that pipeline.
|
||||
- The external control surface (REST/WebSocket on `:9755`, OSC on `:9000`)
|
||||
must not regress — its HTML control panel at `/ui` is a separate design
|
||||
surface but shares brand tokens.
|
||||
|
||||
## What "done" looks like
|
||||
|
||||
The redesign is finished when:
|
||||
|
||||
1. A first-time operator can launch TeamsISO, join a Teams meeting, and
|
||||
route their first ISO without reading documentation.
|
||||
2. A returning operator at 1:50am can find the four things they need
|
||||
(participant signal · ISO toggle · recording state · disk free) in under
|
||||
half a second of glance.
|
||||
3. Nothing on the surface reads as AI-generated. Show this to a working
|
||||
broadcast engineer and they say "someone who knows the job built this."
|
||||
4. The design system is documented in DESIGN.md tightly enough that a future
|
||||
contributor can add a new view that looks like it belongs.
|
||||
|
|
@ -2,13 +2,14 @@
|
|||
"solution": {
|
||||
"path": "TeamsISO.sln",
|
||||
"projects": [
|
||||
"src/TeamsISO.Engine/TeamsISO.Engine.csproj",
|
||||
"src/TeamsISO.Engine.NdiInterop/TeamsISO.Engine.NdiInterop.csproj",
|
||||
"src/TeamsISO.Console/TeamsISO.Console.csproj",
|
||||
"src/TeamsISO.App/TeamsISO.App.csproj",
|
||||
"src/tests/TeamsISO.Engine.Tests/TeamsISO.Engine.Tests.csproj",
|
||||
"src/tests/TeamsISO.Engine.IntegrationTests/TeamsISO.Engine.IntegrationTests.csproj",
|
||||
"src/tests/TeamsISO.App.Tests/TeamsISO.App.Tests.csproj"
|
||||
"src\\TeamsISO.Engine\\TeamsISO.Engine.csproj",
|
||||
"src\\TeamsISO.Engine.NdiInterop\\TeamsISO.Engine.NdiInterop.csproj",
|
||||
"src\\TeamsISO.Console\\TeamsISO.Console.csproj",
|
||||
"src\\TeamsISO.App\\TeamsISO.App.csproj",
|
||||
"src\\TeamsISO.App.WinUI\\TeamsISO.App.WinUI.csproj",
|
||||
"src\\tests\\TeamsISO.Engine.Tests\\TeamsISO.Engine.Tests.csproj",
|
||||
"src\\tests\\TeamsISO.Engine.IntegrationTests\\TeamsISO.Engine.IntegrationTests.csproj",
|
||||
"src\\tests\\TeamsISO.App.Tests\\TeamsISO.App.Tests.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
TeamsISO.sln
21
TeamsISO.sln
|
|
@ -5,21 +5,23 @@ VisualStudioVersion = 17.0.31903.59
|
|||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{46E05E34-8A87-4986-87D3-FE0DE4E05F44}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine", "src/TeamsISO.Engine/TeamsISO.Engine.csproj", "{F0D24EAE-9225-4DC4-B3D2-6966077287A0}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine", "src\TeamsISO.Engine\TeamsISO.Engine.csproj", "{F0D24EAE-9225-4DC4-B3D2-6966077287A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.NdiInterop", "src/TeamsISO.Engine.NdiInterop/TeamsISO.Engine.NdiInterop.csproj", "{E737E54B-73DE-4F74-909C-1F0F5CF82AC6}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.NdiInterop", "src\TeamsISO.Engine.NdiInterop\TeamsISO.Engine.NdiInterop.csproj", "{E737E54B-73DE-4F74-909C-1F0F5CF82AC6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DBDF4A1D-4215-42D5-B456-2CE7159DF848}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.Tests", "src/tests/TeamsISO.Engine.Tests/TeamsISO.Engine.Tests.csproj", "{F8DBD7AB-E160-4B75-88FC-BAECDD4D44E8}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.Tests", "src\tests\TeamsISO.Engine.Tests\TeamsISO.Engine.Tests.csproj", "{F8DBD7AB-E160-4B75-88FC-BAECDD4D44E8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App", "src/TeamsISO.App/TeamsISO.App.csproj", "{80DCE039-3BBC-4D3F-B44B-51F324591C29}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App", "src\TeamsISO.App\TeamsISO.App.csproj", "{80DCE039-3BBC-4D3F-B44B-51F324591C29}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.IntegrationTests", "src/tests/TeamsISO.Engine.IntegrationTests/TeamsISO.Engine.IntegrationTests.csproj", "{A85E331D-026E-4BDE-B89C-0CC4C95001CE}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.IntegrationTests", "src\tests\TeamsISO.Engine.IntegrationTests\TeamsISO.Engine.IntegrationTests.csproj", "{A85E331D-026E-4BDE-B89C-0CC4C95001CE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Console", "src/TeamsISO.Console/TeamsISO.Console.csproj", "{C3254998-9428-4264-A8FB-EAC9E1F9F432}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Console", "src\TeamsISO.Console\TeamsISO.Console.csproj", "{C3254998-9428-4264-A8FB-EAC9E1F9F432}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App.Tests", "src/tests/TeamsISO.App.Tests/TeamsISO.App.Tests.csproj", "{B5A6F1E7-3D2C-4F89-9A55-7E1B2A4C8D6F}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App.Tests", "src\tests\TeamsISO.App.Tests\TeamsISO.App.Tests.csproj", "{B5A6F1E7-3D2C-4F89-9A55-7E1B2A4C8D6F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App.WinUI", "src\TeamsISO.App.WinUI\TeamsISO.App.WinUI.csproj", "{14928B5A-E45C-4265-A5D7-D13B5ED18F84}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -58,6 +60,10 @@ Global
|
|||
{B5A6F1E7-3D2C-4F89-9A55-7E1B2A4C8D6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5A6F1E7-3D2C-4F89-9A55-7E1B2A4C8D6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B5A6F1E7-3D2C-4F89-9A55-7E1B2A4C8D6F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{14928B5A-E45C-4265-A5D7-D13B5ED18F84}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{14928B5A-E45C-4265-A5D7-D13B5ED18F84}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{14928B5A-E45C-4265-A5D7-D13B5ED18F84}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{14928B5A-E45C-4265-A5D7-D13B5ED18F84}.Release|Any CPU.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{F0D24EAE-9225-4DC4-B3D2-6966077287A0} = {46E05E34-8A87-4986-87D3-FE0DE4E05F44}
|
||||
|
|
@ -68,5 +74,6 @@ Global
|
|||
{A85E331D-026E-4BDE-B89C-0CC4C95001CE} = {DBDF4A1D-4215-42D5-B456-2CE7159DF848}
|
||||
{C3254998-9428-4264-A8FB-EAC9E1F9F432} = {46E05E34-8A87-4986-87D3-FE0DE4E05F44}
|
||||
{B5A6F1E7-3D2C-4F89-9A55-7E1B2A4C8D6F} = {DBDF4A1D-4215-42D5-B456-2CE7159DF848}
|
||||
{14928B5A-E45C-4265-A5D7-D13B5ED18F84} = {46E05E34-8A87-4986-87D3-FE0DE4E05F44}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
791
docs/preview/redesigned-mainwindow.html
Normal file
791
docs/preview/redesigned-mainwindow.html
Normal file
|
|
@ -0,0 +1,791 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>TeamsISO — redesigned MainWindow preview</title>
|
||||
<style>
|
||||
:root {
|
||||
/* Dark palette — mirrors src/TeamsISO.App.WinUI/Themes/Tokens.xaml */
|
||||
--bg-canvas: #0a0a0a;
|
||||
--bg-rail: #080808;
|
||||
--bg-surface: #141416;
|
||||
--bg-elevated: #1c1c1f;
|
||||
--bg-hover: #26272b;
|
||||
--bg-active: #33343a;
|
||||
--border-subtle: #26272b;
|
||||
--border-strong: #3a3b40;
|
||||
--fg-primary: #f4f4f6;
|
||||
--fg-secondary: #a3a4aa;
|
||||
--fg-tertiary: #6b6c72;
|
||||
--fg-disabled: #404145;
|
||||
--fg-on-accent: #0a0a0a;
|
||||
--accent-cyan-surface: #97edf0;
|
||||
--accent-cyan-text: #97edf0;
|
||||
--accent-cyan-hover: #b5f2f4;
|
||||
--accent-cyan-muted: #1b3537;
|
||||
--accent-coral: #fb819c;
|
||||
--accent-coral-bg: #3a1922;
|
||||
--status-live: #4ade80;
|
||||
--status-live-bg: #13261a;
|
||||
--status-warn: #fbbf24;
|
||||
--status-warn-bg: #3a2e12;
|
||||
--shadow-drawer: rgba(0,0,0,0.55);
|
||||
}
|
||||
html[data-theme="light"] {
|
||||
--bg-canvas: #fafafb;
|
||||
--bg-rail: #f0f1f3;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-elevated: #ffffff;
|
||||
--bg-hover: #eceef1;
|
||||
--bg-active: #e0e3e7;
|
||||
--border-subtle: #e5e7eb;
|
||||
--border-strong: #d1d5da;
|
||||
--fg-primary: #0a0a0a;
|
||||
--fg-secondary: #4a4b50;
|
||||
--fg-tertiary: #71747a;
|
||||
--fg-disabled: #b3b6bc;
|
||||
--fg-on-accent: #0a0a0a;
|
||||
--accent-cyan-surface: #97edf0;
|
||||
--accent-cyan-text: #0e7c82;
|
||||
--accent-cyan-hover: #0890a0;
|
||||
--accent-cyan-muted: #e6f8f9;
|
||||
--accent-coral: #d43e5c;
|
||||
--accent-coral-bg: #fdecf0;
|
||||
--status-live: #15803d;
|
||||
--status-live-bg: #dcfce7;
|
||||
--status-warn: #b45309;
|
||||
--status-warn-bg: #fef3c7;
|
||||
--shadow-drawer: rgba(0,0,0,0.15);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
background: #1a1a1c;
|
||||
color: var(--fg-primary);
|
||||
font-family: 'Inter', -apple-system, system-ui, 'Segoe UI Variable Display', 'Segoe UI', sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
html[data-theme="light"] body { background: #e8e9eb; }
|
||||
.preview-shell {
|
||||
max-width: 1304px;
|
||||
margin: 24px auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.preview-banner {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 16px; margin-bottom: 16px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 8px;
|
||||
color: var(--fg-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.preview-banner strong { color: var(--fg-primary); font-weight: 600; }
|
||||
.preview-banner-actions { display: flex; gap: 8px; }
|
||||
.preview-banner-actions button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--fg-primary);
|
||||
padding: 6px 14px;
|
||||
font-size: 12px; font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.preview-banner-actions button:hover { border-color: var(--accent-cyan-text); }
|
||||
.preview-banner-actions .primary {
|
||||
background: var(--accent-cyan-surface);
|
||||
border-color: var(--accent-cyan-surface);
|
||||
color: var(--fg-on-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.window {
|
||||
width: 1280px; height: 780px;
|
||||
background: var(--bg-canvas);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr;
|
||||
overflow: hidden;
|
||||
color: var(--fg-primary);
|
||||
box-shadow: 0 16px 60px var(--shadow-drawer);
|
||||
position: relative;
|
||||
}
|
||||
.rail {
|
||||
background: var(--bg-rail);
|
||||
border-right: 1px solid var(--border-subtle);
|
||||
display: flex; flex-direction: column;
|
||||
padding: 12px 0 12px 0;
|
||||
}
|
||||
.rail-top { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||
.rail-btn {
|
||||
width: 48px; height: 48px;
|
||||
margin: 4px 8px;
|
||||
border-radius: 8px;
|
||||
border: 0; background: transparent;
|
||||
color: var(--fg-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease-out, color 120ms ease-out;
|
||||
}
|
||||
.rail-btn:hover { background: var(--bg-hover); color: var(--accent-cyan-text); }
|
||||
.rail-brand {
|
||||
width: 48px; height: 56px;
|
||||
margin: 0 8px 8px;
|
||||
}
|
||||
.rail-brand .mark {
|
||||
width: 40px; height: 40px;
|
||||
background: var(--accent-cyan-muted);
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--accent-cyan-text);
|
||||
font-size: 22px; font-weight: 700;
|
||||
}
|
||||
.rail-divider {
|
||||
height: 1px; background: var(--border-subtle);
|
||||
margin: 4px 14px 12px;
|
||||
}
|
||||
.rail-btn.active {
|
||||
background: var(--accent-cyan-muted);
|
||||
color: var(--accent-cyan-text);
|
||||
}
|
||||
.rail-status-puck {
|
||||
width: 48px; height: 48px;
|
||||
margin: 12px 8px;
|
||||
border-radius: 24px;
|
||||
background: var(--status-live-bg);
|
||||
border: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rail-status-puck .dot {
|
||||
width: 10px; height: 10px;
|
||||
background: var(--status-live);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon {
|
||||
width: 20px; height: 20px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 1.6;
|
||||
stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-rows: 44px auto 1fr auto 32px;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.titlebar {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto auto auto;
|
||||
align-items: center;
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
.titlebar-app {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.titlebar-app .name {
|
||||
font-size: 14px; font-weight: 600;
|
||||
}
|
||||
.titlebar-app .version {
|
||||
font-family: 'JetBrains Mono', 'Cascadia Mono', Consolas, monospace;
|
||||
font-size: 11px; color: var(--fg-tertiary);
|
||||
}
|
||||
.titlebar-pills {
|
||||
display: flex; gap: 8px;
|
||||
padding: 0 12px 0 0;
|
||||
}
|
||||
.pill {
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
padding: 0 12px;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.pill .dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--fg-tertiary);
|
||||
}
|
||||
.pill.live { background: var(--status-live-bg); border-color: transparent; color: var(--status-live); }
|
||||
.pill.live .dot { background: var(--status-live); }
|
||||
.pill.rec { background: var(--accent-coral-bg); border-color: transparent; color: var(--accent-coral); }
|
||||
.pill.rec .dot { background: var(--accent-coral); }
|
||||
.titlebar-tool {
|
||||
width: 46px; height: 32px;
|
||||
border: 0; background: transparent;
|
||||
color: var(--fg-primary);
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.titlebar-tool:hover { background: var(--bg-hover); }
|
||||
.titlebar-tool.close:hover { background: #c42b1c; color: white; }
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
padding: 18px 32px 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
.section-title {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
}
|
||||
.display-title {
|
||||
font-size: 22px; font-weight: 600; letter-spacing: -0.01em;
|
||||
color: var(--fg-primary);
|
||||
}
|
||||
.count-badge {
|
||||
height: 22px; padding: 0 12px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 999px;
|
||||
display: inline-flex; align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.section-actions {
|
||||
display: flex; gap: 8px; align-items: center;
|
||||
}
|
||||
.input {
|
||||
width: 200px; height: 34px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--fg-primary);
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
font-family: inherit; font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.input:focus { border-color: var(--accent-cyan-text); }
|
||||
.input::placeholder { color: var(--fg-tertiary); }
|
||||
.btn {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: transparent;
|
||||
color: var(--fg-primary);
|
||||
font-family: inherit; font-size: 12px; font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: border-color 120ms ease-out, background 120ms ease-out;
|
||||
}
|
||||
.btn:hover { border-color: var(--accent-cyan-text); background: var(--bg-hover); }
|
||||
.btn.primary {
|
||||
background: var(--accent-cyan-surface);
|
||||
border-color: var(--accent-cyan-surface);
|
||||
color: var(--fg-on-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn.primary:hover {
|
||||
background: var(--accent-cyan-hover);
|
||||
border-color: var(--accent-cyan-hover);
|
||||
}
|
||||
.btn.destructive {
|
||||
color: var(--accent-coral);
|
||||
border-color: var(--accent-coral);
|
||||
}
|
||||
|
||||
.table {
|
||||
padding: 0 32px;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
.table-head {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 2fr 1fr 1.2fr 1.5fr auto;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
color: var(--fg-tertiary);
|
||||
font-size: 11px; font-weight: 500;
|
||||
letter-spacing: 0.08em; text-transform: uppercase;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.table-head > * { padding: 0 4px; }
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 2fr 1fr 1.2fr 1.5fr auto;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding-right: 12px;
|
||||
position: relative;
|
||||
transition: background 120ms ease-out;
|
||||
}
|
||||
.row:hover { background: var(--bg-hover); }
|
||||
.row.active-speaker {
|
||||
background: var(--accent-cyan-muted);
|
||||
}
|
||||
.row .left-accent {
|
||||
position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
||||
background: var(--accent-cyan-text);
|
||||
display: none;
|
||||
}
|
||||
.row.active-speaker .left-accent { display: block; }
|
||||
|
||||
.row-avatar {
|
||||
width: 56px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.avatar {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-active);
|
||||
color: var(--fg-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; font-weight: 600;
|
||||
}
|
||||
.row.active-speaker .avatar {
|
||||
background: var(--accent-cyan-muted);
|
||||
color: var(--accent-cyan-text);
|
||||
}
|
||||
.row-name { line-height: 1.3; }
|
||||
.row-name .name {
|
||||
font-size: 14px; font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.row-name .codec {
|
||||
font-size: 11px; color: var(--fg-secondary);
|
||||
}
|
||||
.row-signal {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
||||
}
|
||||
.row-signal .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
}
|
||||
.row-signal.locked .dot { background: var(--status-live); }
|
||||
.row-signal.degraded { color: var(--status-warn); }
|
||||
.row-signal.degraded .dot { background: var(--status-warn); }
|
||||
.meter { display: flex; align-items: center; gap: 2px; height: 24px; }
|
||||
.meter span {
|
||||
width: 4px; border-radius: 2px;
|
||||
background: var(--bg-active);
|
||||
}
|
||||
.meter.active span { background: var(--fg-secondary); }
|
||||
.row.active-speaker .meter.active span { background: var(--accent-cyan-text); }
|
||||
.row-output {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 13px;
|
||||
color: var(--fg-primary);
|
||||
}
|
||||
.iso-pill {
|
||||
width: 80px;
|
||||
padding: 6px 0;
|
||||
border-radius: 999px;
|
||||
text-align: center;
|
||||
font-size: 11px; font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.iso-pill.live {
|
||||
background: var(--status-live-bg);
|
||||
color: var(--status-live);
|
||||
border: 1px solid var(--status-live);
|
||||
}
|
||||
.iso-pill.off {
|
||||
background: var(--bg-surface);
|
||||
color: var(--fg-secondary);
|
||||
border: 1px solid var(--border-strong);
|
||||
}
|
||||
|
||||
.in-call {
|
||||
padding: 12px 32px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
background: var(--bg-canvas);
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.in-call .label {
|
||||
font-size: 11px; font-weight: 500; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; color: var(--fg-tertiary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
padding: 0 32px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
background: var(--bg-canvas);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; color: var(--fg-tertiary);
|
||||
}
|
||||
.status-bar .left {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.status-bar .left .dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--accent-cyan-text);
|
||||
}
|
||||
|
||||
/* Settings drawer */
|
||||
.drawer {
|
||||
position: absolute;
|
||||
top: 44px; right: 0; bottom: 0;
|
||||
width: 400px;
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
transform: translateX(100%);
|
||||
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
display: flex; flex-direction: column;
|
||||
z-index: 5;
|
||||
}
|
||||
.drawer.open { transform: translateX(0); }
|
||||
.drawer-head {
|
||||
height: 56px;
|
||||
padding: 0 12px 0 20px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.drawer-head .title {
|
||||
font-size: 18px; font-weight: 600;
|
||||
}
|
||||
.drawer-tabs {
|
||||
display: flex; gap: 6px;
|
||||
padding: 12px 20px 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
.drawer-tab {
|
||||
padding: 8px 12px;
|
||||
border: 0; background: transparent;
|
||||
color: var(--fg-tertiary);
|
||||
font-family: inherit; font-size: 12px; font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.drawer-tab.active {
|
||||
color: var(--fg-primary);
|
||||
border-bottom-color: var(--accent-cyan-text);
|
||||
}
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.drawer-body h3 {
|
||||
font-size: 14px; font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--fg-primary);
|
||||
}
|
||||
.drawer-body p {
|
||||
font-size: 12px;
|
||||
color: var(--fg-secondary);
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.theme-picker { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
.theme-pick-btn {
|
||||
flex: 1;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: transparent;
|
||||
color: var(--fg-primary);
|
||||
font-family: inherit; font-size: 13px; font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.theme-pick-btn.active {
|
||||
border-color: var(--accent-cyan-text);
|
||||
background: var(--accent-cyan-muted);
|
||||
}
|
||||
.accent-swatches { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.swatch {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
.swatch .chip {
|
||||
width: 80px; height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.swatch .label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px; color: var(--fg-tertiary);
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
}
|
||||
.drawer-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 13px;
|
||||
}
|
||||
.drawer-row .v {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.drawer-foot {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
display: flex; justify-content: flex-end; gap: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-shell">
|
||||
<div class="preview-banner">
|
||||
<div>
|
||||
<strong>TeamsISO redesign — interactive preview</strong>
|
||||
The same XAML that's in <code>src/TeamsISO.App.WinUI/Views/MainWindow.xaml</code>, rendered as HTML so you can see and toggle it before the WinUI 3 .exe activation issue is resolved.
|
||||
</div>
|
||||
<div class="preview-banner-actions">
|
||||
<button id="open-drawer">Open settings</button>
|
||||
<button id="toggle-theme" class="primary">Toggle dark / light</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="window">
|
||||
<!-- RAIL -->
|
||||
<div class="rail">
|
||||
<div class="rail-top">
|
||||
<button class="rail-btn rail-brand" title="About TeamsISO">
|
||||
<div class="mark">W</div>
|
||||
</button>
|
||||
<div class="rail-divider"></div>
|
||||
<button class="rail-btn active" title="Participants">
|
||||
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="9" r="3.2"/><path d="M5 19c0-3.5 3.1-6 7-6s7 2.5 7 6"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" title="Launch / surface Teams">
|
||||
<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="7" width="13" height="10" rx="2"/><path d="M16 11l5-3v8l-5-3z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" title="Hide / show Teams windows">
|
||||
<svg class="icon" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-settings" title="Settings">
|
||||
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3h.1a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v.1a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rail-status-puck" title="Engine status">
|
||||
<div class="dot"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="content">
|
||||
<!-- Title bar -->
|
||||
<div class="titlebar">
|
||||
<div class="titlebar-app">
|
||||
<span class="name">TeamsISO</span>
|
||||
<span class="version">v1.0.0-alpha</span>
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="titlebar-pills">
|
||||
<div class="pill live"><div class="dot"></div>live · 00:14:32</div>
|
||||
<div class="pill rec"><div class="dot"></div>rec 3 · 00:11:08</div>
|
||||
<div class="pill">482 GB free</div>
|
||||
</div>
|
||||
<button class="titlebar-tool" id="titlebar-theme" title="Theme">
|
||||
<svg class="icon" viewBox="0 0 24 24" id="theme-icon-mark"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
</button>
|
||||
<button class="titlebar-tool" title="Minimize">
|
||||
<svg class="icon" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
<button class="titlebar-tool" title="Maximize">
|
||||
<svg class="icon" viewBox="0 0 24 24"><rect x="5" y="5" width="14" height="14"/></svg>
|
||||
</button>
|
||||
<button class="titlebar-tool close" title="Close">
|
||||
<svg class="icon" viewBox="0 0 24 24"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Section header -->
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<span class="display-title">Participants</span>
|
||||
<span class="count-badge">4</span>
|
||||
</div>
|
||||
<div></div>
|
||||
<div class="section-actions">
|
||||
<input class="input" placeholder="Filter participants"/>
|
||||
<button class="btn">Refresh</button>
|
||||
<button class="btn">Presets</button>
|
||||
<button class="btn primary">Enable all online</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table">
|
||||
<div class="table-head">
|
||||
<div></div>
|
||||
<div>Participant</div>
|
||||
<div>Signal</div>
|
||||
<div>Audio</div>
|
||||
<div>Output name</div>
|
||||
<div>ISO</div>
|
||||
</div>
|
||||
|
||||
<div class="row active-speaker">
|
||||
<div class="left-accent"></div>
|
||||
<div class="row-avatar"><div class="avatar">MA</div></div>
|
||||
<div class="row-name"><div class="name">Maya Rodriguez</div><div class="codec">MS Teams · 1920×1080 · 30fps</div></div>
|
||||
<div class="row-signal locked"><div class="dot"></div>locked</div>
|
||||
<div>
|
||||
<div class="meter active">
|
||||
<span style="height:24px"></span>
|
||||
<span style="height:20px"></span>
|
||||
<span style="height:28px"></span>
|
||||
<span style="height:18px"></span>
|
||||
<span style="height:12px"></span>
|
||||
<span style="height:22px"></span>
|
||||
<span style="height:8px"></span>
|
||||
<span style="height:14px"></span>
|
||||
<span style="height:6px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-output">TEAMSISO_maya</div>
|
||||
<div><div class="iso-pill live">LIVE</div></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-avatar"><div class="avatar">DC</div></div>
|
||||
<div class="row-name"><div class="name">Daniel Chen</div><div class="codec">MS Teams · 1280×720 · 30fps</div></div>
|
||||
<div class="row-signal locked"><div class="dot"></div>locked</div>
|
||||
<div>
|
||||
<div class="meter active">
|
||||
<span style="height:10px"></span>
|
||||
<span style="height:14px"></span>
|
||||
<span style="height:8px"></span>
|
||||
<span style="height:12px"></span>
|
||||
<span style="height:6px"></span>
|
||||
<span style="height:9px"></span>
|
||||
<span style="height:4px"></span>
|
||||
<span style="height:3px"></span>
|
||||
<span style="height:2px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-output">TEAMSISO_daniel</div>
|
||||
<div><div class="iso-pill live">LIVE</div></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-avatar"><div class="avatar">AK</div></div>
|
||||
<div class="row-name"><div class="name">Aïcha Koné</div><div class="codec">MS Teams · 1920×1080 · 30fps</div></div>
|
||||
<div class="row-signal degraded"><div class="dot"></div>degraded</div>
|
||||
<div>
|
||||
<div class="meter">
|
||||
<span style="height:3px"></span>
|
||||
<span style="height:4px"></span>
|
||||
<span style="height:3px"></span>
|
||||
<span style="height:2px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-output" style="color:var(--fg-secondary)">TEAMSISO_aicha</div>
|
||||
<div><div class="iso-pill off">OFF</div></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-avatar"><div class="avatar">SP</div></div>
|
||||
<div class="row-name"><div class="name">Sam Park</div><div class="codec">MS Teams · 1920×1080 · 30fps</div></div>
|
||||
<div class="row-signal locked"><div class="dot"></div>locked</div>
|
||||
<div>
|
||||
<div class="meter active">
|
||||
<span style="height:8px"></span>
|
||||
<span style="height:12px"></span>
|
||||
<span style="height:16px"></span>
|
||||
<span style="height:7px"></span>
|
||||
<span style="height:5px"></span>
|
||||
<span style="height:3px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-output">TEAMSISO_sam</div>
|
||||
<div><div class="iso-pill live">LIVE</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In-call control -->
|
||||
<div class="in-call">
|
||||
<span class="label">In-call</span>
|
||||
<button class="btn destructive">⊘ Muted</button>
|
||||
<button class="btn">⌗ Camera</button>
|
||||
<button class="btn">⇪ Share</button>
|
||||
<button class="btn">▷ Marker</button>
|
||||
<button class="btn destructive">Leave</button>
|
||||
<button class="btn" style="width:36px;padding:0;">⋯</button>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div class="status-bar">
|
||||
<div class="left">
|
||||
<div class="dot"></div>
|
||||
<span>control surface · 127.0.0.1:9755</span>
|
||||
</div>
|
||||
<div>F1 help · Ctrl+M marker · Ctrl+Shift+S panic · Ctrl+K command palette</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings drawer -->
|
||||
<div class="drawer" id="drawer">
|
||||
<div class="drawer-head">
|
||||
<div class="title">Settings</div>
|
||||
<button class="titlebar-tool" id="drawer-close" title="Close (Esc)">
|
||||
<svg class="icon" viewBox="0 0 24 24"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="drawer-tabs">
|
||||
<button class="drawer-tab active">Appearance</button>
|
||||
<button class="drawer-tab">Routing</button>
|
||||
<button class="drawer-tab">Display</button>
|
||||
<button class="drawer-tab">Control</button>
|
||||
<button class="drawer-tab">Advanced</button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<h3>Appearance</h3>
|
||||
<p>Dark is the default for the 1:50am operator scene; light is for daytime production. System follows the Windows app-mode preference.</p>
|
||||
<div class="theme-picker">
|
||||
<button class="theme-pick-btn" data-theme="dark">Dark</button>
|
||||
<button class="theme-pick-btn active" data-theme="dark">System</button>
|
||||
<button class="theme-pick-btn" data-theme="light">Light</button>
|
||||
</div>
|
||||
<h3>Accent peek</h3>
|
||||
<p>These accents work in both themes. Cyan stays bright as a surface fill (text on top is near-black regardless). For inline text on light, the palette substitutes a darker cyan automatically.</p>
|
||||
<div class="accent-swatches">
|
||||
<div class="swatch"><div class="chip" style="background:var(--accent-cyan-surface)"></div><div class="label">Cyan</div></div>
|
||||
<div class="swatch"><div class="chip" style="background:var(--accent-coral)"></div><div class="label">Coral</div></div>
|
||||
<div class="swatch"><div class="chip" style="background:var(--status-live)"></div><div class="label">Live</div></div>
|
||||
<div class="swatch"><div class="chip" style="background:var(--status-warn)"></div><div class="label">Warn</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-foot">
|
||||
<button class="btn">Reset to defaults</button>
|
||||
<button class="btn primary">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const html = document.documentElement;
|
||||
const themeIcon = document.getElementById('theme-icon-mark');
|
||||
const sunPath = 'M12 1v2 M12 21v2 M4.2 4.2l1.4 1.4 M18.4 18.4l1.4 1.4 M1 12h2 M21 12h2 M4.2 19.8l1.4-1.4 M18.4 5.6l1.4-1.4 M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8';
|
||||
const moonPath = 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z';
|
||||
|
||||
function applyTheme(t) {
|
||||
html.dataset.theme = t;
|
||||
themeIcon.setAttribute('d', t === 'light' ? sunPath : moonPath);
|
||||
themeIcon.parentElement.innerHTML = `<svg class="icon" viewBox="0 0 24 24" id="theme-icon-mark"><path d="${t === 'light' ? sunPath : moonPath}"/></svg>`;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
applyTheme(html.dataset.theme === 'light' ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
document.getElementById('toggle-theme').addEventListener('click', toggle);
|
||||
document.getElementById('titlebar-theme').addEventListener('click', toggle);
|
||||
|
||||
const drawer = document.getElementById('drawer');
|
||||
document.getElementById('rail-settings').addEventListener('click', () => drawer.classList.add('open'));
|
||||
document.getElementById('open-drawer').addEventListener('click', () => drawer.classList.add('open'));
|
||||
document.getElementById('drawer-close').addEventListener('click', () => drawer.classList.remove('open'));
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') drawer.classList.remove('open'); });
|
||||
|
||||
applyTheme('dark');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
199
docs/superpowers/plans/2026-05-12-winui3-migration.md
Normal file
199
docs/superpowers/plans/2026-05-12-winui3-migration.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# WinUI 3 migration plan
|
||||
|
||||
**Started:** 2026-05-12 (overnight)
|
||||
**Status:** in flight — scaffold + redesigned MainWindow + theme system landed,
|
||||
runtime activation blocked, view-model wiring not yet started.
|
||||
|
||||
The full plan for replatforming TeamsISO from WPF / .NET 8 to WinUI 3 /
|
||||
Windows App SDK 1.6 LTS. The redesigned UI per the approved shape brief
|
||||
(PRODUCT.md, DESIGN.md, the 2026-05-12 chat transcript) lands as the new
|
||||
TeamsISO.App.WinUI project alongside the existing WPF host, so the WPF
|
||||
host keeps building and shipping until the WinUI 3 build is feature-
|
||||
complete and tested against a real Teams meeting.
|
||||
|
||||
## Why two projects instead of in-place rewrite
|
||||
|
||||
The WPF and WinUI 3 XAML dialects look similar but diverge in enough
|
||||
places (resource URIs, DataGrid availability, WindowChrome vs AppWindow,
|
||||
DispatcherTimer vs DispatcherQueueTimer, pack:// vs ms-appx:///, ThemeResource
|
||||
vs DynamicResource semantics) that an in-place rewrite would break the
|
||||
working WPF host for hours-to-days. Coexisting both projects means:
|
||||
|
||||
1. `dotnet build TeamsISO.Windows.slnf` keeps producing a working WPF .exe
|
||||
throughout the migration.
|
||||
2. Each WinUI 3 view can be migrated and verified independently.
|
||||
3. The engine layer (TeamsISO.Engine, TeamsISO.Engine.NdiInterop) and the
|
||||
view-models (TeamsISO.App/ViewModels/) are **shared** via ProjectReference.
|
||||
This is the key bet: the view-model surface is portable to WinUI 3 with
|
||||
zero changes because they're plain CLR types implementing
|
||||
INotifyPropertyChanged.
|
||||
4. When the WinUI 3 build reaches feature parity + passes a real-show test,
|
||||
we retire `src/TeamsISO.App` and the WinUI 3 project becomes the only
|
||||
shipping host.
|
||||
|
||||
## Architectural decisions (locked)
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Framework | Windows App SDK 1.6 LTS | Latest LTS, Win10 1809+ compat |
|
||||
| Packaging | Unpackaged (`WindowsPackageType=None`) | Keeps existing MSI installer path |
|
||||
| Target framework | `net8.0-windows10.0.19041.0` | WindowsAppSDK 1.6 minimum |
|
||||
| Platform floor | Win10 17763 (1809) | Working broadcast hardware |
|
||||
| RuntimeIdentifier | `win-x64` (pinned) | Flattens native DLLs to output dir |
|
||||
| Theme strategy | `ThemeDictionary` (Default = Dark, Light) | Built-in {ThemeResource} swap |
|
||||
| DataGrid | `CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2` | Only maintained free option |
|
||||
| View-model | Reuse from TeamsISO.App via ProjectReference | Zero porting cost |
|
||||
| Window chrome | `AppWindow.TitleBar.ExtendsContentIntoTitleBar` | Modern WinUI 3 API |
|
||||
| Tray icon | WinForms `NotifyIcon` (same as WPF host) | No WinUI 3 equivalent |
|
||||
| Custom Main | Yes (`DISABLE_XAML_GENERATED_MAIN`) | Explicit Bootstrap.TryInitialize |
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1 — Scaffold (done)
|
||||
|
||||
- [x] `src/TeamsISO.App.WinUI/` project created with WindowsAppSDK 1.6
|
||||
- [x] `Themes/Tokens.xaml` with Dark + Light ThemeDictionaries
|
||||
- [x] `Themes/Controls.xaml` with Button hierarchy + typographic ramp
|
||||
- [x] `App.xaml` + `App.xaml.cs` minimal startup
|
||||
- [x] `Program.cs` custom Main with Bootstrap.TryInitialize
|
||||
- [x] Assets copied (Inter.ttf, JetBrainsMono.ttf, dragon-mark.png, icon)
|
||||
- [x] Solution updated (.sln + .slnf paths backslash-normalized)
|
||||
- [x] `dotnet build TeamsISO.Windows.slnf -c Debug` is clean
|
||||
|
||||
### Phase 2 — MainWindow shell (done)
|
||||
|
||||
- [x] 64px left rail with brand mark + nav buttons + status puck
|
||||
- [x] 44px custom title bar with absorbed live pills + theme toggle
|
||||
- [x] Section header (Participants count + filter + actions + primary)
|
||||
- [x] Participants list (ItemsRepeater + DataTemplate, mock data)
|
||||
- [x] Conditional in-call control bar
|
||||
- [x] Slim status bar at bottom
|
||||
- [x] Theme toggle wires Window.Content.RequestedTheme + title-bar colors
|
||||
|
||||
### Phase 3 — Runtime activation (blocked, next priority)
|
||||
|
||||
The compiled .exe shows "TeamsISO.exe - This application could not be
|
||||
started" before Main() runs. COREHOST_TRACE confirms .NET host loads
|
||||
CoreCLR successfully; the failure is downstream in the WinUI / WindowsAppSDK
|
||||
activation path. Suspected causes (in priority order):
|
||||
|
||||
1. **Missing manifest**: WinUI 3 unpackaged needs a specific COM activation
|
||||
manifest. Our custom `app.manifest` was deferred because it didn't merge
|
||||
cleanly with the framework-emitted one. Reintroduce with proper
|
||||
`uap:VisualElements`.
|
||||
2. **Microsoft.WindowsDesktop.App framework reference**: runtimeconfig.json
|
||||
includes `Microsoft.WindowsDesktop.App 8.0.0`, which WinUI 3 doesn't
|
||||
want. The .NET SDK adds it implicitly from the `-windows` target
|
||||
framework moniker. Try `<EnableMsixTooling>true</EnableMsixTooling>`
|
||||
+ remove from frameworks list.
|
||||
3. **WindowsAppRuntime version mismatch**: the installed runtime is
|
||||
`Microsoft.WindowsAppRuntime.1.6 (6000.519.329.0)`. Bootstrap.TryInitialize
|
||||
should accept any 1.6.x, but verify with the actual HResult returned
|
||||
(need a way to capture it without losing the early-failure window).
|
||||
4. **Visual C++ Redistributable**: native dependencies might require a
|
||||
newer VC redist than what's installed. Check WindowsAppSDK 1.6's
|
||||
redist requirements.
|
||||
|
||||
**Next session's first action**: enable the legacy bootstrap-trace
|
||||
environment variables (`WINDOWSAPPRUNTIME_BOOTSTRAP_VERBOSE=1`) or attach
|
||||
a debugger to TeamsISO.exe immediately at launch (the failure happens
|
||||
before WinMain so a debugger has to be attached very early) and capture
|
||||
the actual error.
|
||||
|
||||
### Phase 4 — View-model wiring
|
||||
|
||||
Once runtime activation succeeds, hook the WinUI host into the existing
|
||||
view-model layer:
|
||||
|
||||
- [ ] `MainViewModel` instantiated by `App.OnLaunched` (mirror WPF
|
||||
App.xaml.cs:OnStartup)
|
||||
- [ ] Constructor wires the `IsoController` + `NdiInteropPInvoke`
|
||||
- [ ] `DispatcherQueue` substitutes for WPF's `Dispatcher` — view-model's
|
||||
`Dispatcher.InvokeAsync` calls need adapting to
|
||||
`DispatcherQueue.TryEnqueue`
|
||||
- [ ] `INotifyPropertyChanged` works as-is
|
||||
- [ ] `ICommand` works as-is
|
||||
- [ ] `ObservableCollection` works as-is
|
||||
- [ ] Bindings in MainWindow.xaml updated from {Binding ...} to {x:Bind ...}
|
||||
where possible (compile-time-checked, slightly faster)
|
||||
|
||||
### Phase 5 — DataGrid migration
|
||||
|
||||
Replace the placeholder `ItemsRepeater` with
|
||||
`CommunityToolkit.WinUI.UI.Controls.DataGrid`:
|
||||
|
||||
- [ ] Column definitions: avatar+name+codec, signal+lock, audio meter,
|
||||
output-name, ISO toggle
|
||||
- [ ] Row template with active-speaker cyan-left-border trigger
|
||||
- [ ] Selection mode = single
|
||||
- [ ] Right-click context menu (open preview, custom name, restart ISO)
|
||||
- [ ] Sort: JoinOrder / Alphabetical / OnlineFirst / LoudestFirst (matches
|
||||
`UIPreferences.SortMode`)
|
||||
|
||||
### Phase 6 — Secondary windows
|
||||
|
||||
- [ ] Settings drawer (`SettingsDrawer.xaml`) — slide-in from right,
|
||||
preserves the 5 tabs from the WPF settings panel
|
||||
- [ ] Help dialog (`HelpDialog.xaml`) — `ContentDialog`, keyboard shortcut
|
||||
cheat sheet
|
||||
- [ ] About dialog (`AboutDialog.xaml`) — version, logs path, update check
|
||||
- [ ] Onboarding (`OnboardingWindow.xaml`) — first-launch only, three panes
|
||||
- [ ] Notes viewer (`NotesViewer.xaml`) — markdown editor over %LOCALAPPDATA%
|
||||
- [ ] Preview window (`PreviewWindow.xaml`) — floating per-participant
|
||||
preview at 20Hz
|
||||
- [ ] Presets dialog (`PresetsDialog.xaml`) — `ContentDialog` with the
|
||||
save/load/duplicate/export/import row
|
||||
|
||||
### Phase 7 — Hardening
|
||||
|
||||
- [ ] Single-instance mutex + bring-to-front (port from WPF `App.xaml.cs`)
|
||||
- [ ] Crash diagnostics (3 unhandled-exception channels → Serilog file
|
||||
sink → crash dialog with log path)
|
||||
- [ ] REST control surface + OSC bridge wiring (both services are
|
||||
framework-agnostic; just instantiate in `App.OnLaunched`)
|
||||
- [ ] Tray icon (port `TrayIconHost.cs` — WinForms.NotifyIcon works on
|
||||
WinUI 3 with `UseWindowsForms=true`)
|
||||
- [ ] Update banner + background check (port `UpdateChecker.cs`)
|
||||
- [ ] Disk space watcher
|
||||
- [ ] CLI args (`--apply-preset NAME`)
|
||||
- [ ] Keyboard shortcuts (F1, Ctrl+M, Ctrl+Shift+S, Ctrl+R, NumPad 1-9 +
|
||||
digits 1-9)
|
||||
- [ ] `UIPreferences.Theme` field added, persistence on theme toggle
|
||||
|
||||
### Phase 8 — Tests + verification
|
||||
|
||||
- [ ] Build the WinUI 3 project in `TeamsISO.App.Tests` (currently targets
|
||||
`net8.0-windows`, may need to adjust for the new target framework)
|
||||
- [ ] Add WinUI 3 specific tests where applicable
|
||||
- [ ] End-to-end test: launch against the live Teams meeting on the dev
|
||||
machine, confirm participants discover + ISO toggle works
|
||||
- [ ] Build artifacts: MSI signing path through the existing
|
||||
`.forgejo/workflows/release.yml`
|
||||
|
||||
### Phase 9 — Retire WPF host
|
||||
|
||||
- [ ] `dotnet sln remove src/TeamsISO.App/TeamsISO.App.csproj`
|
||||
- [ ] Delete `src/TeamsISO.App/` directory
|
||||
- [ ] Update README.md and CHANGELOG.md
|
||||
- [ ] Tag v1.0.0 (the original v1.0 cut moves to v0.9; v1.0 = first WinUI
|
||||
3 release)
|
||||
|
||||
## Risk register
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Activation failure not resolvable | Pivot to WinUI 3 packaged (MSIX) mode; the existing MSI workflow has to change but it's not the end of the world |
|
||||
| `Dispatcher` → `DispatcherQueue` semantics differ | Wrap with a small `IDispatcher` interface in the engine layer; both hosts provide an impl |
|
||||
| Custom WPF-style WindowChrome can't fully reproduce in AppWindow API | Accept a slightly different drag-region shape; the title-bar buttons API gives us close-button colors and click handling |
|
||||
| WebView2 + WindowsAppSDK version conflicts | Pin WebView2 explicitly in the .csproj |
|
||||
| CommunityToolkit DataGrid 7.x maintenance ending | Plan a fallback to `WinUI.TableView` 1.4.x as a contingency |
|
||||
| Performance regression on the participants table (thumbnails at 20Hz × N rows) | Profile early; if needed, use `Win2D` for the audio meter and signal indicator |
|
||||
|
||||
## What I'm NOT doing
|
||||
|
||||
- Replacing the engine layer
|
||||
- Touching the NDI native interop
|
||||
- Changing the control surface protocol (REST/WebSocket/OSC)
|
||||
- Migrating tests right now (Phase 8)
|
||||
- Adding new product features (anything not in the redesign brief stays
|
||||
for a follow-on release)
|
||||
147
docs/superpowers/work-log-2026-05-12.md
Normal file
147
docs/superpowers/work-log-2026-05-12.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Work log — overnight session 2026-05-12 → 2026-05-13
|
||||
|
||||
The redesign brief was approved with one edit (add dark + light theming), the
|
||||
WinUI 3 replatform was green-lit explicitly, and you said don't stop until
|
||||
told to. This log is what happened.
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Read me first when you wake up:**
|
||||
|
||||
1. **Pull. The forgejo credentials expired so I couldn't push.** Authenticate
|
||||
and `git push origin main` to land six commits.
|
||||
2. **The WPF host (the running build) is fine.** I didn't touch it. Your
|
||||
May 2026 batch still works exactly as it did.
|
||||
3. **The new WinUI 3 project builds clean** (`dotnet build TeamsISO.Windows.slnf
|
||||
-c Debug` → 0 warnings, 0 errors). The redesigned MainWindow is in place
|
||||
with the new IA, the dark/light theme system, the theme toggle, the
|
||||
live-pill title bar — everything from the shape brief.
|
||||
4. **The .exe doesn't launch.** It shows "TeamsISO.exe - This application
|
||||
could not be started" before Main() runs. Diagnostics captured; the
|
||||
.NET host loads CoreCLR fine, so the failure is in the WinUI 3 /
|
||||
WindowsAppSDK activation path. Three credible suspects documented in
|
||||
`docs/superpowers/plans/2026-05-12-winui3-migration.md` Phase 3.
|
||||
5. **You can see the redesign visually** via the SVG mockup we approved in
|
||||
chat. Tomorrow's first session: fix activation, then the .exe shows the
|
||||
real thing.
|
||||
|
||||
## Commits landed (local only — push needed)
|
||||
|
||||
In chronological order on `main`:
|
||||
|
||||
| SHA | Subject |
|
||||
|---|---|
|
||||
| `94b0a71` | docs: PRODUCT.md + DESIGN.md (ground-up GUI redesign brief) |
|
||||
| `cb1402e` | feat(winui3): scaffold TeamsISO.App.WinUI alongside the WPF host |
|
||||
| `9e176d8` | feat(winui3): redesigned MainWindow + custom title bar + theme toggle |
|
||||
| `db341f9` | build(winui3): pin RID + flatten native DLLs into output dir |
|
||||
|
||||
Plus whatever lands during the rest of the session — see `git log
|
||||
--oneline f12cbe7..HEAD` for the full set.
|
||||
|
||||
## What you'll find in the tree
|
||||
|
||||
```
|
||||
Teams ISO/
|
||||
├─ PRODUCT.md ← new, baseline product brief
|
||||
├─ DESIGN.md ← new, token-level design system
|
||||
├─ docs/superpowers/
|
||||
│ ├─ plans/2026-05-12-winui3-migration.md ← new, full migration plan
|
||||
│ └─ work-log-2026-05-12.md ← this file
|
||||
├─ src/
|
||||
│ ├─ TeamsISO.App/ ← unchanged, the WPF host
|
||||
│ └─ TeamsISO.App.WinUI/ ← new, the WinUI 3 host
|
||||
│ ├─ TeamsISO.App.WinUI.csproj
|
||||
│ ├─ Program.cs ← custom Main with Bootstrap
|
||||
│ ├─ App.xaml + App.xaml.cs
|
||||
│ ├─ Assets/ ← Inter, JetBrainsMono, dragon-mark
|
||||
│ ├─ Themes/
|
||||
│ │ ├─ Tokens.xaml ← ThemeDictionary (Dark + Light)
|
||||
│ │ └─ Controls.xaml ← Button hierarchy + type ramp
|
||||
│ ├─ Models/MockParticipant.cs ← interim until VM wires
|
||||
│ └─ Views/
|
||||
│ └─ MainWindow.xaml + .xaml.cs ← redesigned per shape brief
|
||||
├─ TeamsISO.sln ← updated
|
||||
└─ TeamsISO.Windows.slnf ← updated, backslash-normalized
|
||||
```
|
||||
|
||||
## What works right now
|
||||
|
||||
* WinUI 3 build: clean
|
||||
* WPF build: still clean (I built it to confirm nothing regressed)
|
||||
* Theme tokens: Dark + Light palettes both correct, mapped to {ThemeResource}
|
||||
* MainWindow layout: matches the approved SVG mockup pixel-by-pixel intent
|
||||
* Theme toggle: code-behind flips RequestedTheme + title-bar button colors
|
||||
* Mock data: 4 sample participants render in the list, one as active speaker
|
||||
|
||||
## What's blocked
|
||||
|
||||
**Activation failure on the unpackaged .exe.** Diagnostic summary:
|
||||
|
||||
* `dotnet --info` shows .NET 8.0.301 SDK + 8.0.6/8.0.8/8.0.18 runtimes for
|
||||
both NETCore.App and WindowsDesktop.App
|
||||
* `Get-AppxPackage Microsoft.WindowsAppRuntime.*` confirms
|
||||
Microsoft.WindowsAppRuntime.1.6 (6000.519.329.0) is installed
|
||||
* `dotnet build -c Debug` produces TeamsISO.exe in
|
||||
`src/TeamsISO.App.WinUI/bin/Debug/net8.0-windows10.0.19041.0/win-x64/`
|
||||
* The .exe is x64 (PE machine 0x8664 confirmed)
|
||||
* Native runtime files (Microsoft.WindowsAppRuntime.Bootstrap.dll,
|
||||
WebView2Loader.dll) are flattened to the output dir alongside the .exe
|
||||
* Launching the .exe results in a Windows error dialog
|
||||
"TeamsISO.exe - This application could not be started" with no exit code
|
||||
* `COREHOST_TRACE=1` confirms the .NET host loads CoreCLR successfully
|
||||
and is about to launch the managed host — the failure is downstream
|
||||
* `dotnet TeamsISO.dll` produces the same error
|
||||
* `dotnet publish -r win-x64 --self-contained` produces the same error
|
||||
|
||||
The error happens **before my Program.Main runs**, which means
|
||||
`Bootstrap.TryInitialize(0x00010006)` isn't the culprit. The failure is
|
||||
in the CLR-to-WinUI handoff. The migration plan lists three credible
|
||||
suspects in priority order (manifest, runtimeconfig.json
|
||||
Microsoft.WindowsDesktop.App entry, VC++ redist).
|
||||
|
||||
## What I did NOT do
|
||||
|
||||
* Touch the WPF host. Your running build is intact. The May 2026 batch
|
||||
ships as-is.
|
||||
* Touch Teams orchestration. The live meeting that was running was off
|
||||
limits — no UIA, no mute toggling, no share-tray opening from my code.
|
||||
* Push to forgejo. The credential prompt would need you. Run
|
||||
`git push origin main` when you're up.
|
||||
* Run the WPF .exe to take screenshots. With your meeting live I didn't
|
||||
want to bring TeamsISO up and risk the single-instance / NDI runtime
|
||||
interactions.
|
||||
* Add light theme to the WPF host. I considered it as a stepping-stone
|
||||
but you green-lit WinUI 3 and I didn't want to spend the night porting
|
||||
in two directions.
|
||||
* Migrate view-models or wire the engine into the WinUI host. Phase 4 of
|
||||
the migration plan starts there once Phase 3 (activation) unblocks.
|
||||
|
||||
## Suggested first session tomorrow
|
||||
|
||||
1. `git push origin main` (after authenticating)
|
||||
2. Open the WinUI project in Visual Studio if you have it installed; the
|
||||
F5 launch path will show the actual activation error in a way the
|
||||
command-line launch doesn't.
|
||||
3. If no VS, attach windbg / dnSpy to the .exe at launch and capture the
|
||||
actual exception. The COREHOST trace I dumped to
|
||||
`$env:TEMP/teamsiso-corehost.log` may still be there for context.
|
||||
4. Once activation works, mock data renders → you'll see the new design.
|
||||
5. Decide between continuing in-place (port view-models next) or
|
||||
integrating an HTML control panel preview to show stakeholders before
|
||||
the WinUI 3 build is feature-complete.
|
||||
|
||||
## Honest assessment
|
||||
|
||||
The redesign work is solid; the design system is real, the XAML matches
|
||||
the shape brief faithfully, and the theme infrastructure is correct. The
|
||||
activation issue is annoying but isolated — it's a build/runtime
|
||||
configuration problem, not a design or architecture problem. Five
|
||||
minutes with the actual error message in hand and it's likely a one-line
|
||||
csproj fix.
|
||||
|
||||
The biggest risk to the v1.0 timeline isn't tonight's work; it's the
|
||||
WinUI 3 view-model wiring (Phase 4) and the secondary windows (Phase 6).
|
||||
Those need real-meeting testing time once the build runs.
|
||||
|
||||
— end of log
|
||||
21
src/TeamsISO.App.WinUI/App.xaml
Normal file
21
src/TeamsISO.App.WinUI/App.xaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application
|
||||
x:Class="TeamsISO.App.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!--
|
||||
Tokens.xaml owns the color/typography/spacing primitives in
|
||||
a ThemeDictionary so {ThemeResource} on the consumer side
|
||||
auto-swaps when RequestedTheme flips. Controls.xaml owns
|
||||
the actual Style targets (Button, TextBlock, etc.) and
|
||||
references the tokens via {ThemeResource}.
|
||||
-->
|
||||
<ResourceDictionary Source="ms-appx:///Themes/Tokens.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Themes/Controls.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
38
src/TeamsISO.App.WinUI/App.xaml.cs
Normal file
38
src/TeamsISO.App.WinUI/App.xaml.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.UI.Xaml;
|
||||
using TeamsISO.App.WinUI.Views;
|
||||
|
||||
namespace TeamsISO.App.WinUI;
|
||||
|
||||
/// <summary>
|
||||
/// WinUI 3 application entry. The full startup pipeline from the WPF host
|
||||
/// (NDI runtime preflight, IsoController wiring, single-instance mutex, REST
|
||||
/// + OSC bridge, tray icon, crash diagnostics, auto-update banner, onboarding)
|
||||
/// will migrate over in subsequent commits — this initial scaffold just brings
|
||||
/// up MainWindow so the redesigned shell can be developed and previewed.
|
||||
///
|
||||
/// The engine layer (TeamsISO.Engine) is unchanged; the WinUI 3 host's
|
||||
/// responsibility is binding its view-models to the same controller surface
|
||||
/// the WPF host already uses.
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _mainWindow;
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
_mainWindow = new MainWindow();
|
||||
_mainWindow.Activate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the active main window so settings can swap RequestedTheme on
|
||||
/// the root element without having to thread the Window reference through
|
||||
/// every consumer.
|
||||
/// </summary>
|
||||
internal Window? MainWindow => _mainWindow;
|
||||
}
|
||||
BIN
src/TeamsISO.App.WinUI/Assets/Fonts/Inter.ttf
Normal file
BIN
src/TeamsISO.App.WinUI/Assets/Fonts/Inter.ttf
Normal file
Binary file not shown.
BIN
src/TeamsISO.App.WinUI/Assets/Fonts/JetBrainsMono.ttf
Normal file
BIN
src/TeamsISO.App.WinUI/Assets/Fonts/JetBrainsMono.ttf
Normal file
Binary file not shown.
BIN
src/TeamsISO.App.WinUI/Assets/dragon-mark.png
Normal file
BIN
src/TeamsISO.App.WinUI/Assets/dragon-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
src/TeamsISO.App.WinUI/Assets/teamsiso.ico
Normal file
BIN
src/TeamsISO.App.WinUI/Assets/teamsiso.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
src/TeamsISO.App.WinUI/Assets/wild-dragon-wordmark.png
Normal file
BIN
src/TeamsISO.App.WinUI/Assets/wild-dragon-wordmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
56
src/TeamsISO.App.WinUI/Models/MockParticipant.cs
Normal file
56
src/TeamsISO.App.WinUI/Models/MockParticipant.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Stand-in for the real ParticipantViewModel until the view-model bindings
|
||||
/// migrate over from the WPF host. Lets the redesigned MainWindow render with
|
||||
/// representative data so the visual design can be validated independently
|
||||
/// of the engine layer. Removed in the view-model wiring commit.
|
||||
/// </summary>
|
||||
public sealed class MockParticipant
|
||||
{
|
||||
public string DisplayName { get; init; } = "";
|
||||
public string Initials { get; init; } = "";
|
||||
public string SourceCodec { get; init; } = "MS Teams · 1920x1080 · 30fps";
|
||||
public string SignalState { get; init; } = "locked"; // locked | degraded | offline
|
||||
public string OutputName { get; init; } = "";
|
||||
public string IsoState { get; init; } = "OFF"; // LIVE | OFF | ERROR
|
||||
public double AudioLevel { get; init; } = 0.0; // 0..1
|
||||
public bool IsActiveSpeaker { get; init; } = false;
|
||||
|
||||
public static List<MockParticipant> Sample()
|
||||
{
|
||||
return new List<MockParticipant>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DisplayName = "Maya Rodriguez", Initials = "MA",
|
||||
SourceCodec = "MS Teams · 1920x1080 · 30fps",
|
||||
SignalState = "locked", OutputName = "TEAMSISO_maya",
|
||||
IsoState = "LIVE", AudioLevel = 0.82, IsActiveSpeaker = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
DisplayName = "Daniel Chen", Initials = "DC",
|
||||
SourceCodec = "MS Teams · 1280x720 · 30fps",
|
||||
SignalState = "locked", OutputName = "TEAMSISO_daniel",
|
||||
IsoState = "LIVE", AudioLevel = 0.35,
|
||||
},
|
||||
new()
|
||||
{
|
||||
DisplayName = "Aicha Kone", Initials = "AK",
|
||||
SourceCodec = "MS Teams · 1920x1080 · 30fps",
|
||||
SignalState = "degraded", OutputName = "TEAMSISO_aicha",
|
||||
IsoState = "OFF", AudioLevel = 0.12,
|
||||
},
|
||||
new()
|
||||
{
|
||||
DisplayName = "Sam Park", Initials = "SP",
|
||||
SourceCodec = "MS Teams · 1920x1080 · 30fps",
|
||||
SignalState = "locked", OutputName = "TEAMSISO_sam",
|
||||
IsoState = "LIVE", AudioLevel = 0.48,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
63
src/TeamsISO.App.WinUI/Program.cs
Normal file
63
src/TeamsISO.App.WinUI/Program.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.ApplicationModel.DynamicDependency;
|
||||
|
||||
namespace TeamsISO.App.WinUI;
|
||||
|
||||
/// <summary>
|
||||
/// Custom Main for the unpackaged WinUI 3 host.
|
||||
///
|
||||
/// The default XAML-compiler-generated Main (disabled here via the
|
||||
/// DISABLE_XAML_GENERATED_MAIN compile constant) calls Application.Start
|
||||
/// directly. That's fine for MSIX-packaged WinUI 3 apps where the OS
|
||||
/// activates the package and the runtime is found via the package
|
||||
/// dependency, but for an unpackaged .exe the Windows App Runtime has to
|
||||
/// be bootstrapped explicitly before any WinUI 3 type is touched.
|
||||
///
|
||||
/// Bootstrap.TryInitialize(0x00010006) targets WindowsAppSDK 1.6 (the LTS
|
||||
/// branch we ship against). The major nibble 0x0001 is the runtime major;
|
||||
/// the minor 0x0006 is the runtime minor. If the user's machine has a
|
||||
/// compatible 1.6.x framework installed (the broadcast-tool installer
|
||||
/// will eventually ensure that as a prereq), Bootstrap connects and the
|
||||
/// rest of the WinUI 3 surface comes alive. If not, the call returns a
|
||||
/// negative HResult that we surface via Environment.Exit so the .exe
|
||||
/// dies cleanly rather than throwing an opaque "this application could
|
||||
/// not be started" dialog.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
/// <summary>WindowsAppSDK 1.6 major/minor packed as 0x00010006.</summary>
|
||||
private const uint WindowsAppSdkMajorMinor = 0x00010006;
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
if (!Bootstrap.TryInitialize(WindowsAppSdkMajorMinor, out int hr))
|
||||
{
|
||||
// The runtime isn't installed (or this build's bootstrap can't
|
||||
// find a compatible package version). Surface a Win32 error code
|
||||
// so postmortem inspection of the launch failure has more than
|
||||
// "application could not be started" to go on.
|
||||
return hr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
Application.Start(p =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(
|
||||
DispatcherQueue.GetForCurrentThread());
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Bootstrap.Shutdown();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
108
src/TeamsISO.App.WinUI/Services/ThemeManager.cs
Normal file
108
src/TeamsISO.App.WinUI/Services/ThemeManager.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the active theme for the WinUI 3 host. Three preferences:
|
||||
/// <c>System</c> follows the Windows app-mode setting (default for new
|
||||
/// users); <c>Dark</c> and <c>Light</c> pin one regardless of the OS choice.
|
||||
/// The persistence path will land alongside the existing UIPreferences in
|
||||
/// the next commit — for now state lives in-process.
|
||||
///
|
||||
/// All public mutations push <see cref="Themed"/> to subscribers so the
|
||||
/// host (MainWindow) can update the AppWindow title-bar button colors
|
||||
/// (system buttons aren't part of the visual tree and need a separate
|
||||
/// poke when ElementTheme changes).
|
||||
/// </summary>
|
||||
public sealed class ThemeManager
|
||||
{
|
||||
public static ThemeManager Current { get; } = new();
|
||||
|
||||
private ThemeManager()
|
||||
{
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += OnSystemColorsChanged;
|
||||
}
|
||||
|
||||
private readonly UISettings _uiSettings;
|
||||
private string _preference = "System";
|
||||
|
||||
public string Preference => _preference;
|
||||
|
||||
public event EventHandler<ElementTheme>? Themed;
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the preference to an absolute <see cref="ElementTheme"/>
|
||||
/// suitable for <see cref="FrameworkElement.RequestedTheme"/>.
|
||||
/// <c>System</c> resolves to the OS app-mode.
|
||||
/// </summary>
|
||||
public ElementTheme ResolveTheme() => _preference switch
|
||||
{
|
||||
"Dark" => ElementTheme.Dark,
|
||||
"Light" => ElementTheme.Light,
|
||||
_ => IsSystemDark() ? ElementTheme.Dark : ElementTheme.Light,
|
||||
};
|
||||
|
||||
public bool PreferenceMatches(string value) => string.Equals(_preference, value, StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Cycle dark ↔ light from the title-bar toggle. If the current
|
||||
/// preference is <c>System</c>, the cycle pins to the opposite of the
|
||||
/// currently-resolved theme so the click has a visible effect.
|
||||
/// </summary>
|
||||
public ElementTheme Toggle()
|
||||
{
|
||||
var current = ResolveTheme();
|
||||
Set(current == ElementTheme.Dark ? "Light" : "Dark");
|
||||
return ResolveTheme();
|
||||
}
|
||||
|
||||
/// <summary>Set the preference and broadcast the resolved theme.</summary>
|
||||
public void Set(string preference)
|
||||
{
|
||||
if (preference != "System" && preference != "Dark" && preference != "Light")
|
||||
{
|
||||
throw new ArgumentException("Preference must be System, Dark, or Light.", nameof(preference));
|
||||
}
|
||||
|
||||
_preference = preference;
|
||||
Themed?.Invoke(this, ResolveTheme());
|
||||
}
|
||||
|
||||
private bool IsSystemDark()
|
||||
{
|
||||
// UISettings.GetColorValue(UIColorType.Background) returns
|
||||
// black-ish in dark mode, white-ish in light mode — the most
|
||||
// reliable cross-version check for app mode on desktop WinUI 3.
|
||||
var bg = _uiSettings.GetColorValue(UIColorType.Background);
|
||||
return ((5 * bg.G) + (2 * bg.R) + bg.B) < 8 * 128;
|
||||
}
|
||||
|
||||
private void OnSystemColorsChanged(UISettings sender, object args)
|
||||
{
|
||||
// Only re-broadcast if the operator hasn't pinned a preference —
|
||||
// otherwise the explicit choice wins regardless of what the OS does.
|
||||
if (_preference == "System")
|
||||
{
|
||||
Themed?.Invoke(this, ResolveTheme());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the AppWindow title-bar foreground for the given resolved
|
||||
/// theme so the system min/max/close buttons stay readable.
|
||||
/// </summary>
|
||||
public static Color TitleBarForegroundFor(ElementTheme theme) =>
|
||||
theme == ElementTheme.Dark
|
||||
? Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6)
|
||||
: Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
|
||||
|
||||
public static Color TitleBarHoverBgFor(ElementTheme theme) =>
|
||||
theme == ElementTheme.Dark
|
||||
? Color.FromArgb(0xFF, 0x33, 0x34, 0x3A)
|
||||
: Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
|
||||
}
|
||||
142
src/TeamsISO.App.WinUI/TeamsISO.App.WinUI.csproj
Normal file
142
src/TeamsISO.App.WinUI/TeamsISO.App.WinUI.csproj
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
TeamsISO WinUI 3 host. Coexists with the WPF project (src/TeamsISO.App)
|
||||
during the redesign migration. Shares the engine (TeamsISO.Engine) and
|
||||
the NDI interop assembly via ProjectReference. Once the WinUI 3 build is
|
||||
feature-complete and tested against a real Teams meeting, the WPF
|
||||
project is retired and this becomes the only shipping host.
|
||||
|
||||
Target framework choice: net8.0-windows10.0.19041.0 is the minimum the
|
||||
Windows App SDK supports cleanly. Going higher (e.g. 22621) would lock
|
||||
out Win10 1809+ operators, which is undesirable for a broadcast tool
|
||||
that still has to run on hardware in working broadcast suites.
|
||||
|
||||
Packaging mode: WindowsPackageType=None for "unpackaged" — the .exe
|
||||
drops directly into Program Files via the existing MSI rather than
|
||||
going through MSIX. The Windows App Runtime install becomes a prereq
|
||||
of the MSI (or bootstrapped at startup), which matches how operators
|
||||
install NDI Runtime today.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<RootNamespace>TeamsISO.App.WinUI</RootNamespace>
|
||||
<AssemblyName>TeamsISO</AssemblyName>
|
||||
<!--
|
||||
Default app.manifest deferred: WinUI 3 emits its own manifest with the
|
||||
DPI awareness + supportedOS GUIDs that match what we want. Our custom
|
||||
manifest (kept in tree at app.manifest) describes the same intent but
|
||||
doesn't currently merge cleanly with the framework-emitted manifest;
|
||||
see docs/superpowers/plans/2026-05-12-winui3-migration.md for the
|
||||
follow-up to reintroduce it via uap:VisualElements.
|
||||
-->
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<!--
|
||||
Pinning the Windows SDK projection package: WindowsAppSDK 1.6 requires
|
||||
Microsoft.Windows.SDK.NET.Ref >= 10.0.19041.38, but the .NET 8.0.301
|
||||
SDK installed here ships an older Ref. Setting this explicitly avoids
|
||||
having to upgrade the .NET SDK on the build host.
|
||||
-->
|
||||
<WindowsSdkPackageVersion>10.0.19041.38</WindowsSdkPackageVersion>
|
||||
<!--
|
||||
Disable the XAML compiler's auto-generated Program.Main so we can write
|
||||
one that bootstraps the Windows App Runtime explicitly. The default
|
||||
generated Main calls Application.Start directly, with no Bootstrap
|
||||
initialization step — that's fine for packaged MSIX apps but blocks
|
||||
unpackaged launch on a machine where the runtime is installed only as
|
||||
a framework package. Program.cs in this project takes ownership of
|
||||
Main and calls Bootstrap.TryInitialize(0x00010006) before Start.
|
||||
-->
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
<!--
|
||||
RuntimeIdentifier locks the build to win-x64 so runtime DLLs from
|
||||
runtimes/win-x64/native (Microsoft.WindowsAppRuntime.Bootstrap.dll,
|
||||
WebView2Loader.dll) flatten into the output dir alongside the .exe.
|
||||
Without this, those DLLs sit in runtimes/win-x64/native/ and the
|
||||
loader doesn't find them at activation time.
|
||||
-->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<!--
|
||||
Suppress the auto-injected UndockedRegFreeWinRT ModuleInitializer.
|
||||
The bundled initializer P/Invokes Microsoft.WindowsAppRuntime.dll
|
||||
during module load, BEFORE our Program.Main has a chance to call
|
||||
Bootstrap.TryInitialize. Since the runtime DLL lives in the framework
|
||||
MSIX package (not the output dir) on a framework-dependent install,
|
||||
the P/Invoke fails to locate it and the .exe dies with the generic
|
||||
"this application could not be started" dialog — diagnosed by
|
||||
following the Microsoft.WindowsAppSDK.UndockedRegFreeWinRT.CS.targets
|
||||
chain and reading the auto-init source. Our Program.cs handles the
|
||||
bootstrap explicitly in the right order.
|
||||
-->
|
||||
<WindowsAppSdkUndockedRegFreeWinRTInitialize>false</WindowsAppSdkUndockedRegFreeWinRTInitialize>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationIcon>Assets\teamsiso.ico</ApplicationIcon>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
WindowsAppSDK 1.6 is the current LTS branch (Win10 1809-compatible at
|
||||
a 10.0.17763 floor, which matches our SupportedOSPlatformVersion).
|
||||
DataGrid lives in the older 7.x Community Toolkit because the 8.x line
|
||||
dropped it; 7.1.2 still works on WinUI 3 / WindowsAppSDK 1.6 and is the
|
||||
only currently-maintained free DataGrid for this stack.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250602001" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TeamsISO.Engine\TeamsISO.Engine.csproj" />
|
||||
<ProjectReference Include="..\TeamsISO.Engine.NdiInterop\TeamsISO.Engine.NdiInterop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\teamsiso.ico" />
|
||||
<Content Include="Assets\dragon-mark.png" />
|
||||
<Content Include="Assets\wild-dragon-wordmark.png" />
|
||||
<Content Include="Assets\Fonts\Inter.ttf" />
|
||||
<Content Include="Assets\Fonts\JetBrainsMono.ttf" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Post-build runtimeconfig patch. .NET 8 SDK adds Microsoft.WindowsDesktop.App
|
||||
as an implicit framework reference for any -windows target framework moniker.
|
||||
WinUI 3 doesn't use that framework; including it in runtimeconfig.json
|
||||
forces the .NET host to resolve and load WindowsDesktop.App at startup,
|
||||
which contributes to the WindowsAppSDK activation chain on unpackaged
|
||||
apps. This target rewrites the generated runtimeconfig.json to drop the
|
||||
WindowsDesktop.App entry so the host loads only NETCore.App.
|
||||
|
||||
The target runs after the framework-dependent build copies the
|
||||
runtimeconfig.json to the output dir, before the bin/Debug/.../win-x64/
|
||||
directory is final. It's a string-level rewrite — sufficient because the
|
||||
JSON shape is stable across SDK versions and the WindowsDesktop.App
|
||||
entry has a deterministic indent + sibling structure.
|
||||
-->
|
||||
<Target Name="StripWindowsDesktopAppFromRuntimeConfig" AfterTargets="GenerateBuildRuntimeConfigurationFiles">
|
||||
<PropertyGroup>
|
||||
<_RuntimeConfigPath>$(OutDir)$(AssemblyName).runtimeconfig.json</_RuntimeConfigPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="Exists('$(_RuntimeConfigPath)')">
|
||||
<_RuntimeConfigContent>$([System.IO.File]::ReadAllText('$(_RuntimeConfigPath)'))</_RuntimeConfigContent>
|
||||
<_PatchedContent>$([System.Text.RegularExpressions.Regex]::Replace($(_RuntimeConfigContent), ',\s*\{\s*"name":\s*"Microsoft\.WindowsDesktop\.App"[^\}]*\}', ''))</_PatchedContent>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile Condition="Exists('$(_RuntimeConfigPath)') and '$(_RuntimeConfigContent)' != '$(_PatchedContent)'"
|
||||
File="$(_RuntimeConfigPath)"
|
||||
Lines="$(_PatchedContent)"
|
||||
Overwrite="true"/>
|
||||
<Message Condition="Exists('$(_RuntimeConfigPath)') and '$(_RuntimeConfigContent)' != '$(_PatchedContent)'"
|
||||
Text="Stripped Microsoft.WindowsDesktop.App from $(_RuntimeConfigPath)"
|
||||
Importance="high"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
170
src/TeamsISO.App.WinUI/Themes/Controls.xaml
Normal file
170
src/TeamsISO.App.WinUI/Themes/Controls.xaml
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!--
|
||||
Control styles. Where WPF needed full ControlTemplate overrides to
|
||||
achieve the Wild Dragon look, WinUI 3's built-in VisualStateManager
|
||||
handles hover / pressed / focus states cleanly, so most styles here
|
||||
just set surface properties (background, foreground, padding, corner
|
||||
radius) and the framework handles state.
|
||||
|
||||
The few exceptions — rail icon button, ISO toggle pill — re-template
|
||||
because their interaction model (Teams-style hover wash; status-coded
|
||||
pill that preserves background on hover) doesn't fit the default
|
||||
Button template.
|
||||
-->
|
||||
|
||||
<!-- ════════════ TextBlock (typographic ramp) ════════════ -->
|
||||
|
||||
<Style x:Key="TextDisplay" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextDisplaySize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgPrimary}"/>
|
||||
<Setter Property="LineHeight" Value="26"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TextTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextTitleSize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgPrimary}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TextHeading" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextHeadingSize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgPrimary}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TextBody" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextBodySize}"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgPrimary}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TextSubtle" TargetType="TextBlock" BasedOn="{StaticResource TextBody}">
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgSecondary}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TextCaption" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextCaptionSize}"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgTertiary}"/>
|
||||
<Setter Property="CharacterSpacing" Value="80"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TextMono" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontMono}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextMonoSize}"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgSecondary}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ════════════ Button hierarchy ════════════
|
||||
Primary — single per surface, the brand action. Cyan fill, near-black text.
|
||||
Secondary — common operator actions. Transparent + bordered.
|
||||
Tertiary — inline dismissals, low-frequency. Text-only.
|
||||
Destructive — Stop / Leave / Delete. Coral border + text.
|
||||
-->
|
||||
|
||||
<Style x:Key="ButtonPrimary" TargetType="Button">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextBodySize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Background" Value="{ThemeResource AccentCyanSurface}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource AccentCyanSurface}"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgOnAccent}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="16,8"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource RadiusM}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ButtonSecondary" TargetType="Button">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextBodySize}"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource BorderStrong}"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgPrimary}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="14,7"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource RadiusM}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ButtonTertiary" TargetType="Button">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource FontSans}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource TextBodySize}"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgSecondary}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource RadiusM}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ButtonDestructive" TargetType="Button" BasedOn="{StaticResource ButtonSecondary}">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource AccentCoral}"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource AccentCoral}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Title-bar caption buttons. 46x32 to match Windows 11 standard. The
|
||||
close button gets a coral-red hover treatment via VSM override
|
||||
(handled per-button inline since WinUI 3's default Close-button
|
||||
red is the wrong red for our palette). -->
|
||||
<Style x:Key="ButtonCaption" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgPrimary}"/>
|
||||
<Setter Property="Width" Value="46"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<!-- Rail icon button. Vertical-square 48x48 with rounded fill on hover.
|
||||
Used in the left rail; the icon (FontIcon) sits inside as Content. -->
|
||||
<Style x:Key="ButtonRailIcon" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource FgSecondary}"/>
|
||||
<Setter Property="Width" Value="48"/>
|
||||
<Setter Property="Height" Value="48"/>
|
||||
<Setter Property="Margin" Value="0,4"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource RadiusM}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<!-- ════════════ Card / Pill / Status containers ════════════ -->
|
||||
|
||||
<Style x:Key="CardBorder" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource BgSurface}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource BorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource RadiusL}"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PillBorder" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource BgElevated}"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource RadiusPill}"/>
|
||||
<Setter Property="Padding" Value="10,4"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
194
src/TeamsISO.App.WinUI/Themes/Tokens.xaml
Normal file
194
src/TeamsISO.App.WinUI/Themes/Tokens.xaml
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!--
|
||||
TeamsISO design tokens — Wild Dragon brand × redesigned IA.
|
||||
|
||||
Color tokens live inside ResourceDictionary.ThemeDictionaries so
|
||||
{ThemeResource} consumers swap automatically when RequestedTheme
|
||||
flips (no app restart, no flicker). Brushes are paired per theme
|
||||
because Color resolution is theme-scoped.
|
||||
|
||||
Spacing, radii, and typography tokens are theme-agnostic and live
|
||||
at the top level.
|
||||
|
||||
Token naming mirrors DESIGN.md. The dark palette is the canonical
|
||||
reference; the light palette inverts the value scale while
|
||||
preserving brand recognition (cyan-tinted off-whites, not pure
|
||||
white surfaces).
|
||||
-->
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- ════════════ DARK ════════════ -->
|
||||
<ResourceDictionary x:Key="Default">
|
||||
|
||||
<!-- Surfaces -->
|
||||
<Color x:Key="BgCanvasColor">#FF0A0A0A</Color>
|
||||
<Color x:Key="BgRailColor">#FF080808</Color>
|
||||
<Color x:Key="BgSurfaceColor">#FF141416</Color>
|
||||
<Color x:Key="BgElevatedColor">#FF1C1C1F</Color>
|
||||
<Color x:Key="BgHoverColor">#FF26272B</Color>
|
||||
<Color x:Key="BgActiveColor">#FF33343A</Color>
|
||||
|
||||
<!-- Borders -->
|
||||
<Color x:Key="BorderSubtleColor">#FF26272B</Color>
|
||||
<Color x:Key="BorderStrongColor">#FF3A3B40</Color>
|
||||
|
||||
<!-- Text -->
|
||||
<Color x:Key="FgPrimaryColor">#FFF4F4F6</Color>
|
||||
<Color x:Key="FgSecondaryColor">#FFA3A4AA</Color>
|
||||
<Color x:Key="FgTertiaryColor">#FF6B6C72</Color>
|
||||
<Color x:Key="FgDisabledColor">#FF404145</Color>
|
||||
<Color x:Key="FgOnAccentColor">#FF0A0A0A</Color>
|
||||
|
||||
<!-- Accents (context-aware) -->
|
||||
<Color x:Key="AccentCyanSurfaceColor">#FF97EDF0</Color>
|
||||
<Color x:Key="AccentCyanTextColor">#FF97EDF0</Color>
|
||||
<Color x:Key="AccentCyanHoverColor">#FFB5F2F4</Color>
|
||||
<Color x:Key="AccentCyanMutedColor">#FF1B3537</Color>
|
||||
|
||||
<Color x:Key="AccentCoralColor">#FFFB819C</Color>
|
||||
<Color x:Key="AccentCoralBgColor">#FF3A1922</Color>
|
||||
|
||||
<Color x:Key="StatusLiveColor">#FF4ADE80</Color>
|
||||
<Color x:Key="StatusLiveBgColor">#FF13261A</Color>
|
||||
<Color x:Key="StatusWarnColor">#FFFBBF24</Color>
|
||||
<Color x:Key="StatusWarnBgColor">#FF3A2E12</Color>
|
||||
|
||||
<!-- Brushes — one per Color, used by consumers via {ThemeResource} -->
|
||||
<SolidColorBrush x:Key="BgCanvas" Color="{ThemeResource BgCanvasColor}"/>
|
||||
<SolidColorBrush x:Key="BgRail" Color="{ThemeResource BgRailColor}"/>
|
||||
<SolidColorBrush x:Key="BgSurface" Color="{ThemeResource BgSurfaceColor}"/>
|
||||
<SolidColorBrush x:Key="BgElevated" Color="{ThemeResource BgElevatedColor}"/>
|
||||
<SolidColorBrush x:Key="BgHover" Color="{ThemeResource BgHoverColor}"/>
|
||||
<SolidColorBrush x:Key="BgActive" Color="{ThemeResource BgActiveColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="BorderSubtle" Color="{ThemeResource BorderSubtleColor}"/>
|
||||
<SolidColorBrush x:Key="BorderStrong" Color="{ThemeResource BorderStrongColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="FgPrimary" Color="{ThemeResource FgPrimaryColor}"/>
|
||||
<SolidColorBrush x:Key="FgSecondary" Color="{ThemeResource FgSecondaryColor}"/>
|
||||
<SolidColorBrush x:Key="FgTertiary" Color="{ThemeResource FgTertiaryColor}"/>
|
||||
<SolidColorBrush x:Key="FgDisabled" Color="{ThemeResource FgDisabledColor}"/>
|
||||
<SolidColorBrush x:Key="FgOnAccent" Color="{ThemeResource FgOnAccentColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="AccentCyanSurface" Color="{ThemeResource AccentCyanSurfaceColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCyanText" Color="{ThemeResource AccentCyanTextColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCyanHover" Color="{ThemeResource AccentCyanHoverColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCyanMuted" Color="{ThemeResource AccentCyanMutedColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="AccentCoral" Color="{ThemeResource AccentCoralColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCoralBg" Color="{ThemeResource AccentCoralBgColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="StatusLive" Color="{ThemeResource StatusLiveColor}"/>
|
||||
<SolidColorBrush x:Key="StatusLiveBg" Color="{ThemeResource StatusLiveBgColor}"/>
|
||||
<SolidColorBrush x:Key="StatusWarn" Color="{ThemeResource StatusWarnColor}"/>
|
||||
<SolidColorBrush x:Key="StatusWarnBg" Color="{ThemeResource StatusWarnBgColor}"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
<!-- ════════════ LIGHT ════════════ -->
|
||||
<ResourceDictionary x:Key="Light">
|
||||
|
||||
<!-- Surfaces — cyan-tinted off-whites; not pure white -->
|
||||
<Color x:Key="BgCanvasColor">#FFFAFAFB</Color>
|
||||
<Color x:Key="BgRailColor">#FFF0F1F3</Color>
|
||||
<Color x:Key="BgSurfaceColor">#FFFFFFFF</Color>
|
||||
<Color x:Key="BgElevatedColor">#FFFFFFFF</Color>
|
||||
<Color x:Key="BgHoverColor">#FFECEEF1</Color>
|
||||
<Color x:Key="BgActiveColor">#FFE0E3E7</Color>
|
||||
|
||||
<!-- Borders -->
|
||||
<Color x:Key="BorderSubtleColor">#FFE5E7EB</Color>
|
||||
<Color x:Key="BorderStrongColor">#FFD1D5DA</Color>
|
||||
|
||||
<!-- Text -->
|
||||
<Color x:Key="FgPrimaryColor">#FF0A0A0A</Color>
|
||||
<Color x:Key="FgSecondaryColor">#FF4A4B50</Color>
|
||||
<Color x:Key="FgTertiaryColor">#FF71747A</Color>
|
||||
<Color x:Key="FgDisabledColor">#FFB3B6BC</Color>
|
||||
<Color x:Key="FgOnAccentColor">#FF0A0A0A</Color>
|
||||
|
||||
<!-- Accents — surface fill stays bright cyan; text variant darkens for AA -->
|
||||
<Color x:Key="AccentCyanSurfaceColor">#FF97EDF0</Color>
|
||||
<Color x:Key="AccentCyanTextColor">#FF0E7C82</Color>
|
||||
<Color x:Key="AccentCyanHoverColor">#FF0890A0</Color>
|
||||
<Color x:Key="AccentCyanMutedColor">#FFE6F8F9</Color>
|
||||
|
||||
<Color x:Key="AccentCoralColor">#FFD43E5C</Color>
|
||||
<Color x:Key="AccentCoralBgColor">#FFFDECF0</Color>
|
||||
|
||||
<Color x:Key="StatusLiveColor">#FF15803D</Color>
|
||||
<Color x:Key="StatusLiveBgColor">#FFDCFCE7</Color>
|
||||
<Color x:Key="StatusWarnColor">#FFB45309</Color>
|
||||
<Color x:Key="StatusWarnBgColor">#FFFEF3C7</Color>
|
||||
|
||||
<SolidColorBrush x:Key="BgCanvas" Color="{ThemeResource BgCanvasColor}"/>
|
||||
<SolidColorBrush x:Key="BgRail" Color="{ThemeResource BgRailColor}"/>
|
||||
<SolidColorBrush x:Key="BgSurface" Color="{ThemeResource BgSurfaceColor}"/>
|
||||
<SolidColorBrush x:Key="BgElevated" Color="{ThemeResource BgElevatedColor}"/>
|
||||
<SolidColorBrush x:Key="BgHover" Color="{ThemeResource BgHoverColor}"/>
|
||||
<SolidColorBrush x:Key="BgActive" Color="{ThemeResource BgActiveColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="BorderSubtle" Color="{ThemeResource BorderSubtleColor}"/>
|
||||
<SolidColorBrush x:Key="BorderStrong" Color="{ThemeResource BorderStrongColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="FgPrimary" Color="{ThemeResource FgPrimaryColor}"/>
|
||||
<SolidColorBrush x:Key="FgSecondary" Color="{ThemeResource FgSecondaryColor}"/>
|
||||
<SolidColorBrush x:Key="FgTertiary" Color="{ThemeResource FgTertiaryColor}"/>
|
||||
<SolidColorBrush x:Key="FgDisabled" Color="{ThemeResource FgDisabledColor}"/>
|
||||
<SolidColorBrush x:Key="FgOnAccent" Color="{ThemeResource FgOnAccentColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="AccentCyanSurface" Color="{ThemeResource AccentCyanSurfaceColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCyanText" Color="{ThemeResource AccentCyanTextColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCyanHover" Color="{ThemeResource AccentCyanHoverColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCyanMuted" Color="{ThemeResource AccentCyanMutedColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="AccentCoral" Color="{ThemeResource AccentCoralColor}"/>
|
||||
<SolidColorBrush x:Key="AccentCoralBg" Color="{ThemeResource AccentCoralBgColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="StatusLive" Color="{ThemeResource StatusLiveColor}"/>
|
||||
<SolidColorBrush x:Key="StatusLiveBg" Color="{ThemeResource StatusLiveBgColor}"/>
|
||||
<SolidColorBrush x:Key="StatusWarn" Color="{ThemeResource StatusWarnColor}"/>
|
||||
<SolidColorBrush x:Key="StatusWarnBg" Color="{ThemeResource StatusWarnBgColor}"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- ════════════ SPACING (8px grid) — theme-agnostic ════════════ -->
|
||||
<x:Double x:Key="SpaceXS">4</x:Double>
|
||||
<x:Double x:Key="SpaceS">8</x:Double>
|
||||
<x:Double x:Key="SpaceM">12</x:Double>
|
||||
<x:Double x:Key="SpaceL">16</x:Double>
|
||||
<x:Double x:Key="SpaceXL">24</x:Double>
|
||||
<x:Double x:Key="SpaceXXL">32</x:Double>
|
||||
<x:Double x:Key="SpaceXXXL">48</x:Double>
|
||||
|
||||
<!-- ════════════ RADII ════════════ -->
|
||||
<CornerRadius x:Key="RadiusS">6</CornerRadius>
|
||||
<CornerRadius x:Key="RadiusM">8</CornerRadius>
|
||||
<CornerRadius x:Key="RadiusL">12</CornerRadius>
|
||||
<CornerRadius x:Key="RadiusPill">999</CornerRadius>
|
||||
|
||||
<!-- ════════════ TYPOGRAPHY ════════════ -->
|
||||
<!--
|
||||
WinUI 3 font URIs use ms-appx, not WPF's pack://. The "#Inter" suffix
|
||||
is the font's family name as declared in the .ttf's name table —
|
||||
without it, the font loads as a generic font and falls back to the
|
||||
system default.
|
||||
-->
|
||||
<FontFamily x:Key="FontSans">ms-appx:///Assets/Fonts/Inter.ttf#Inter</FontFamily>
|
||||
<FontFamily x:Key="FontMono">ms-appx:///Assets/Fonts/JetBrainsMono.ttf#JetBrains Mono</FontFamily>
|
||||
|
||||
<x:Double x:Key="TextDisplaySize">22</x:Double>
|
||||
<x:Double x:Key="TextTitleSize">18</x:Double>
|
||||
<x:Double x:Key="TextHeadingSize">14</x:Double>
|
||||
<x:Double x:Key="TextBodySize">13</x:Double>
|
||||
<x:Double x:Key="TextCaptionSize">11</x:Double>
|
||||
<x:Double x:Key="TextMonoSize">12</x:Double>
|
||||
|
||||
</ResourceDictionary>
|
||||
75
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml
Normal file
75
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ContentDialog
|
||||
x:Class="TeamsISO.App.WinUI.Views.AboutDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="About TeamsISO"
|
||||
PrimaryButtonText="Close"
|
||||
DefaultButton="Primary"
|
||||
Background="{ThemeResource BgElevated}"
|
||||
BorderBrush="{ThemeResource BorderStrong}">
|
||||
|
||||
<StackPanel Spacing="14" MinWidth="380">
|
||||
<StackPanel Orientation="Horizontal" Spacing="14">
|
||||
<Border Width="56" Height="56"
|
||||
CornerRadius="{ThemeResource RadiusM}"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="W"
|
||||
FontFamily="{ThemeResource FontSans}"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="TeamsISO" Style="{StaticResource TextTitle}"/>
|
||||
<TextBlock Text="Per-participant NDI ISO controller for Microsoft Teams"
|
||||
Style="{StaticResource TextSubtle}"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="280"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border Height="1" Background="{ThemeResource BorderSubtle}"/>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Version" Style="{StaticResource TextSubtle}"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="1.0.0-alpha" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Host" Style="{StaticResource TextSubtle}" Margin="0,6,0,0"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="WinUI 3 (WindowsAppSDK 1.6)" Style="{StaticResource TextMono}" Margin="0,6,0,0"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Engine" Style="{StaticResource TextSubtle}" Margin="0,6,0,0"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text=".NET 8 + NDI 5" Style="{StaticResource TextMono}" Margin="0,6,0,0"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Brand" Style="{StaticResource TextSubtle}" Margin="0,6,0,0"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="Wild Dragon · wilddragon.net" Style="{StaticResource TextMono}" Margin="0,6,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="{ThemeResource BorderSubtle}"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Open logs folder"
|
||||
ToolTipService.ToolTip="%LOCALAPPDATA%\TeamsISO\Logs"/>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Open recordings"
|
||||
ToolTipService.ToolTip="%USERPROFILE%\Videos\TeamsISO"/>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Check for updates"
|
||||
ToolTipService.ToolTip="Query forge.wilddragon.net for a newer release"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
TextWrapping="Wrap"
|
||||
Text="Proprietary © Wild Dragon LLC 2026."/>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
11
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs
Normal file
11
src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Views;
|
||||
|
||||
public sealed partial class AboutDialog : ContentDialog
|
||||
{
|
||||
public AboutDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
110
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml
Normal file
110
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ContentDialog
|
||||
x:Class="TeamsISO.App.WinUI.Views.HelpDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Keyboard shortcuts"
|
||||
PrimaryButtonText="Close"
|
||||
DefaultButton="Primary"
|
||||
Background="{ThemeResource BgElevated}"
|
||||
BorderBrush="{ThemeResource BorderStrong}">
|
||||
|
||||
<ScrollViewer MaxHeight="540" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="6" MinWidth="420">
|
||||
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
Text="GLOBAL"
|
||||
Margin="0,4,0,4"/>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="F1" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Open this help dialog" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Ctrl + K" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Open the command palette" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Ctrl + Shift + S" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Stop every running ISO (panic)" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Ctrl + R" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Refresh NDI discovery" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Ctrl + M" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Drop a recording marker" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
Text="PARTICIPANTS"
|
||||
Margin="0,16,0,4"/>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="NumPad 1-9 / 1-9" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Toggle ISO for the Nth visible participant" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Right-click row" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Open preview, rename output, restart pipeline" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
Text="LOOK"
|
||||
Margin="0,16,0,4"/>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Theme toggle" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Title-bar sun/moon icon swaps dark / light" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
<Grid Padding="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Active speaker" Style="{StaticResource TextMono}"/>
|
||||
<TextBlock Grid.Column="1" Text="Row highlights with a cyan left border" Style="{StaticResource TextBody}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Style="{StaticResource TextCaption}"
|
||||
Text="CONTROL SURFACE"
|
||||
Margin="0,16,0,4"/>
|
||||
<TextBlock Style="{StaticResource TextBody}"
|
||||
TextWrapping="Wrap">
|
||||
External control via REST + WebSocket on 127.0.0.1:9755 and OSC on UDP 127.0.0.1:9000.
|
||||
Self-contained HTML panel at /ui. Use Bitfocus Companion or TouchOSC. See
|
||||
docs/CONTROL-SURFACE.md for the command vocabulary.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
11
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs
Normal file
11
src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Views;
|
||||
|
||||
public sealed partial class HelpDialog : ContentDialog
|
||||
{
|
||||
public HelpDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
519
src/TeamsISO.App.WinUI/Views/MainWindow.xaml
Normal file
519
src/TeamsISO.App.WinUI/Views/MainWindow.xaml
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="TeamsISO.App.WinUI.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:models="using:TeamsISO.App.WinUI.Models"
|
||||
xmlns:views="using:TeamsISO.App.WinUI.Views">
|
||||
|
||||
<!--
|
||||
TeamsISO MainWindow — redesigned IA per the approved shape brief.
|
||||
|
||||
Structure:
|
||||
[64 rail] [content]
|
||||
[44 title bar — drag region]
|
||||
[section header]
|
||||
[participants list — hero]
|
||||
[in-call control — conditional]
|
||||
[32 status bar]
|
||||
|
||||
The rail's bottom puck opens the engine-status popover that absorbs
|
||||
what used to live in the WPF footer (logs path, version, control
|
||||
surface URL details). The title bar absorbs the live state pills
|
||||
(session timer · REC · disk free) so the operator's at-a-glance
|
||||
read stays in peripheral vision regardless of scroll position.
|
||||
|
||||
Settings is a right-side drawer (opened from the rail settings icon)
|
||||
rather than a permanent 380px panel — the participants list claims
|
||||
the full content width when settings aren't being actively edited.
|
||||
-->
|
||||
|
||||
<Grid Background="{ThemeResource BgCanvas}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.Resources>
|
||||
<!-- Drawer slide-in: 220ms ease-out-quart, translates 400px → 0 -->
|
||||
<Storyboard x:Key="DrawerSlideIn">
|
||||
<DoubleAnimation Storyboard.TargetName="DrawerTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
To="0"
|
||||
Duration="0:0:0.22">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuarticEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="DrawerSlideOut">
|
||||
<DoubleAnimation Storyboard.TargetName="DrawerTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
To="400"
|
||||
Duration="0:0:0.18">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuarticEase EasingMode="EaseIn"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<!-- ═══════════════════════ LEFT RAIL ═══════════════════════ -->
|
||||
<Border Grid.Column="0"
|
||||
Background="{ThemeResource BgRail}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="2" Padding="0,12,0,0">
|
||||
<!-- Wild Dragon brand mark -->
|
||||
<Button x:Name="BrandButton"
|
||||
Style="{StaticResource ButtonRailIcon}"
|
||||
Width="48" Height="56"
|
||||
Margin="8,0,8,8"
|
||||
ToolTipService.ToolTip="About TeamsISO">
|
||||
<Border Width="40" Height="40"
|
||||
CornerRadius="{ThemeResource RadiusM}"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="W"
|
||||
FontFamily="{ThemeResource FontSans}"
|
||||
FontSize="22"
|
||||
FontWeight="Bold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Button>
|
||||
|
||||
<Border Height="1"
|
||||
Background="{ThemeResource BorderSubtle}"
|
||||
Margin="14,4,14,12"/>
|
||||
|
||||
<!-- Participants / Home (active) -->
|
||||
<Button Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Participants">
|
||||
<Grid>
|
||||
<Border Width="48" Height="48"
|
||||
CornerRadius="{ThemeResource RadiusM}"
|
||||
Background="{ThemeResource AccentCyanMuted}"/>
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!-- Launch / surface Teams -->
|
||||
<Button Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Launch Microsoft Teams (or surface its window)">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Hide / show Teams windows -->
|
||||
<Button Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
ToolTipService.ToolTip="Hide / show Microsoft Teams windows">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Button>
|
||||
|
||||
<!-- Settings drawer trigger -->
|
||||
<Button x:Name="SettingsButton"
|
||||
Style="{StaticResource ButtonRailIcon}"
|
||||
Margin="8,0"
|
||||
Click="OnSettingsClick"
|
||||
ToolTipService.ToolTip="Settings">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Engine status puck — opens the status popover -->
|
||||
<Button x:Name="StatusPuckButton"
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource ButtonRailIcon}"
|
||||
Width="48" Height="48"
|
||||
Margin="8,12"
|
||||
CornerRadius="24"
|
||||
Background="{ThemeResource StatusLiveBg}"
|
||||
ToolTipService.ToolTip="Engine status">
|
||||
<Ellipse Width="10" Height="10"
|
||||
Fill="{ThemeResource StatusLive}"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ═══════════════════════ CONTENT ═══════════════════════ -->
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="32"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── Title bar ─── -->
|
||||
<!--
|
||||
AppTitleBar is the drag region. Window.SetTitleBar(AppTitleBar)
|
||||
in code-behind makes this element the operating-system-defined
|
||||
drag area. The system Min/Max/Close buttons render to the right
|
||||
of this element automatically (their colors come from
|
||||
AppWindow.TitleBar.ButtonForegroundColor etc.); we draw
|
||||
everything else.
|
||||
-->
|
||||
<Grid x:Name="AppTitleBar"
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource BgCanvas}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Padding="24,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="TeamsISO"
|
||||
Style="{StaticResource TextHeading}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="VersionLabel"
|
||||
Text="v1.0.0-alpha"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgTertiary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Live pills (session timer / REC count / disk) live in the
|
||||
title bar so peripheral-vision status reads from the same
|
||||
place whether the operator is scrolled, settings-drawer
|
||||
open, or in-call. Conditionally shown via code-behind. -->
|
||||
<StackPanel x:Name="LivePillsPanel"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center"
|
||||
Padding="0,0,12,0">
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource StatusLiveBg}"
|
||||
Padding="10,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Width="7" Height="7"
|
||||
Fill="{ThemeResource StatusLive}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="SessionTimerText"
|
||||
Text="live · 00:14:32"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource StatusLive}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource AccentCoralBg}"
|
||||
Padding="10,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Width="7" Height="7"
|
||||
Fill="{ThemeResource AccentCoral}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="RecPillText"
|
||||
Text="rec 3 · 00:11:08"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource AccentCoral}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource BgSurface}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
Padding="10,4">
|
||||
<TextBlock x:Name="DiskFreeText"
|
||||
Text="482 GB free"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgSecondary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Theme toggle — single-click cycle between Dark and Light.
|
||||
Persisted to UIPreferences.Theme on click. -->
|
||||
<Button x:Name="ThemeToggleButton"
|
||||
Grid.Column="3"
|
||||
Style="{StaticResource ButtonCaption}"
|
||||
Click="OnThemeToggleClick"
|
||||
ToolTipService.ToolTip="Toggle theme (dark / light)">
|
||||
<FontIcon x:Name="ThemeToggleIcon"
|
||||
Glyph=""
|
||||
FontSize="14"/>
|
||||
</Button>
|
||||
|
||||
<!-- The Min / Max / Close buttons that follow in Grid.Column 4,5
|
||||
are NOT drawn here — the WindowsAppSDK title-bar API draws
|
||||
them itself, overlaid on the drag region we've defined.
|
||||
The reserved columns 4 and 5 are just visual placeholders
|
||||
in this layout to remind future readers where they land. -->
|
||||
<Border Grid.Column="4" Width="138" Background="Transparent"/>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── Section header ─── -->
|
||||
<Grid Grid.Row="1" Padding="32,18,32,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="Participants"
|
||||
Style="{StaticResource TextDisplay}"/>
|
||||
<Border CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource BgSurface}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="1"
|
||||
Padding="10,3"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="ParticipantCountText"
|
||||
Text="4"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<TextBox x:Name="FilterInput"
|
||||
PlaceholderText="Filter"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Refresh"
|
||||
ToolTipService.ToolTip="Refresh NDI discovery"/>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
Content="Presets"
|
||||
ToolTipService.ToolTip="Save or load operator presets"/>
|
||||
<Button Style="{StaticResource ButtonPrimary}"
|
||||
Content="Enable all online"
|
||||
HorizontalAlignment="Right"
|
||||
ToolTipService.ToolTip="Enable ISOs for every online participant"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── Participants list (hero) ─── -->
|
||||
<ScrollViewer Grid.Row="2"
|
||||
Padding="32,0,32,0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsRepeater x:Name="ParticipantsRepeater">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Orientation="Vertical" Spacing="0"/>
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:MockParticipant">
|
||||
<Grid Height="64"
|
||||
Padding="0,0,12,0"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="1.2*"/>
|
||||
<ColumnDefinition Width="1.5*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0"
|
||||
Width="3" Height="64"
|
||||
Background="{ThemeResource AccentCyanText}"
|
||||
Visibility="{Binding IsActiveSpeaker}"/>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Padding="14,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border Width="36" Height="36"
|
||||
CornerRadius="18"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="{Binding Initials}"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding SourceCodec}"
|
||||
Style="{StaticResource TextCaption}"
|
||||
Foreground="{ThemeResource FgSecondary}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse Width="8" Height="8"
|
||||
Fill="{ThemeResource StatusLive}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding SignalState}"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Column="3" VerticalAlignment="Center" Height="22">
|
||||
<ProgressBar Maximum="1.0"
|
||||
Value="{Binding AudioLevel}"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Column="4"
|
||||
Text="{Binding OutputName}"
|
||||
Style="{StaticResource TextMono}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<Border Grid.Column="5"
|
||||
CornerRadius="{ThemeResource RadiusPill}"
|
||||
Background="{ThemeResource StatusLiveBg}"
|
||||
BorderBrush="{ThemeResource StatusLive}"
|
||||
BorderThickness="1"
|
||||
Padding="14,6"
|
||||
MinWidth="80"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding IsoState}"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource StatusLive}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─── In-call control (conditional) ─── -->
|
||||
<Border Grid.Row="3"
|
||||
Padding="32,12,32,12"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Background="{ThemeResource BgCanvas}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="IN-CALL"
|
||||
Style="{StaticResource TextCaption}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Style="{StaticResource ButtonDestructive}"
|
||||
ToolTipService.ToolTip="Toggle microphone mute">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Muted"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
ToolTipService.ToolTip="Toggle camera">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Camera"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
ToolTipService.ToolTip="Open Teams share tray">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Share"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonSecondary}"
|
||||
ToolTipService.ToolTip="Drop a timestamped marker into every active recording">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<FontIcon Glyph="" FontSize="14"/>
|
||||
<TextBlock Text="Marker"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource ButtonDestructive}"
|
||||
ToolTipService.ToolTip="Leave the Teams call">
|
||||
<TextBlock Text="Leave"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ─── Settings drawer (slides over rows 1-3) ─── -->
|
||||
<views:SettingsDrawer x:Name="SettingsDrawerHost"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="4"
|
||||
HorizontalAlignment="Right"
|
||||
Width="400"
|
||||
IsHitTestVisible="False">
|
||||
<views:SettingsDrawer.RenderTransform>
|
||||
<TranslateTransform x:Name="DrawerTransform" X="400"/>
|
||||
</views:SettingsDrawer.RenderTransform>
|
||||
</views:SettingsDrawer>
|
||||
|
||||
<!-- ─── Status bar ─── -->
|
||||
<Grid Grid.Row="4"
|
||||
Padding="32,8,32,8"
|
||||
Background="{ThemeResource BgCanvas}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse Width="6" Height="6"
|
||||
Fill="{ThemeResource AccentCyanText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="control surface · 127.0.0.1:9755"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgSecondary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="F1 help · Ctrl+M marker · Ctrl+Shift+S panic stop · Ctrl+K command palette"
|
||||
Style="{StaticResource TextMono}"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource FgTertiary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
130
src/TeamsISO.App.WinUI/Views/MainWindow.xaml.cs
Normal file
130
src/TeamsISO.App.WinUI/Views/MainWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using TeamsISO.App.WinUI.Models;
|
||||
using TeamsISO.App.WinUI.Services;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Views;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Title = "TeamsISO";
|
||||
|
||||
// ── Custom title bar wiring ───────────────────────────────────────
|
||||
// ExtendsContentIntoTitleBar=true tells WindowsAppSDK to draw the
|
||||
// window chrome over our content instead of reserving a Windows-default
|
||||
// caption strip. SetTitleBar marks AppTitleBar as the drag region —
|
||||
// clicks on it route to the system drag handler, everything else stays
|
||||
// hit-testable as a normal XAML element. The system min/max/close
|
||||
// buttons render on top of the right edge regardless; we just provide
|
||||
// their colors so they match our palette.
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(AppTitleBar);
|
||||
|
||||
AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
|
||||
AppWindow.TitleBar.ButtonHoverForegroundColor = Colors.White;
|
||||
|
||||
// ── Initial size & position ───────────────────────────────────────
|
||||
// 1280x780 matches the WPF host's default — fits comfortably on a
|
||||
// 14-inch laptop while giving the participants table 600+ pixels
|
||||
// of vertical breathing room.
|
||||
AppWindow.Resize(new SizeInt32(1280, 780));
|
||||
|
||||
// ── Mock data wiring (interim) ────────────────────────────────────
|
||||
// Until ParticipantViewModel binds in the engine wiring commit, the
|
||||
// table is populated from a static sample list so the visual design
|
||||
// can be validated end-to-end against representative data.
|
||||
ParticipantsRepeater.ItemsSource = MockParticipant.Sample();
|
||||
|
||||
// ── Theme system ──────────────────────────────────────────────────
|
||||
// Subscribe to ThemeManager so picker changes from anywhere
|
||||
// (settings drawer, title-bar toggle, system color change) reach
|
||||
// the title-bar buttons and the visual tree consistently. Apply
|
||||
// once at construction so the initial state matches the preference
|
||||
// before the first frame.
|
||||
ThemeManager.Current.Themed += (_, theme) => ApplyResolvedTheme(theme);
|
||||
ApplyResolvedTheme(ThemeManager.Current.ResolveTheme());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cycle the active theme between Dark and Light from the title-bar
|
||||
/// toggle. The actual swap lives in <see cref="ThemeManager"/>; this
|
||||
/// handler just calls Toggle() and lets the subscription propagate.
|
||||
/// </summary>
|
||||
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeManager.Current.Toggle();
|
||||
}
|
||||
|
||||
private bool _drawerOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the settings drawer with a slide animation. The drawer is
|
||||
/// pre-translated 400px off the right edge in XAML; the storyboard
|
||||
/// animates X back to 0 to slide it in. Hit-testing is gated so the
|
||||
/// off-screen drawer doesn't intercept clicks on the participants list.
|
||||
/// </summary>
|
||||
private void OnSettingsClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_drawerOpen)
|
||||
{
|
||||
CloseDrawer();
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenDrawer()
|
||||
{
|
||||
if (_drawerOpen) return;
|
||||
_drawerOpen = true;
|
||||
SettingsDrawerHost.IsHitTestVisible = true;
|
||||
var sb = (Microsoft.UI.Xaml.Media.Animation.Storyboard)((Microsoft.UI.Xaml.Controls.Grid)Content)
|
||||
.Resources["DrawerSlideIn"];
|
||||
sb.Begin();
|
||||
SettingsDrawerHost.CloseRequested -= OnDrawerCloseRequested;
|
||||
SettingsDrawerHost.CloseRequested += OnDrawerCloseRequested;
|
||||
}
|
||||
|
||||
private void CloseDrawer()
|
||||
{
|
||||
if (!_drawerOpen) return;
|
||||
_drawerOpen = false;
|
||||
var sb = (Microsoft.UI.Xaml.Media.Animation.Storyboard)((Microsoft.UI.Xaml.Controls.Grid)Content)
|
||||
.Resources["DrawerSlideOut"];
|
||||
sb.Completed += (_, _) => SettingsDrawerHost.IsHitTestVisible = false;
|
||||
sb.Begin();
|
||||
}
|
||||
|
||||
private void OnDrawerCloseRequested(object? sender, System.EventArgs e) => CloseDrawer();
|
||||
|
||||
/// <summary>
|
||||
/// Push a resolved theme to the visual tree and to the AppWindow
|
||||
/// title-bar buttons. Called on every <see cref="ThemeManager.Themed"/>
|
||||
/// event and once at construction.
|
||||
/// </summary>
|
||||
private void ApplyResolvedTheme(ElementTheme theme)
|
||||
{
|
||||
if (Content is FrameworkElement root)
|
||||
{
|
||||
root.RequestedTheme = theme;
|
||||
}
|
||||
|
||||
AppWindow.TitleBar.ButtonForegroundColor = ThemeManager.TitleBarForegroundFor(theme);
|
||||
AppWindow.TitleBar.ButtonHoverBackgroundColor = ThemeManager.TitleBarHoverBgFor(theme);
|
||||
AppWindow.TitleBar.ButtonPressedBackgroundColor = ThemeManager.TitleBarHoverBgFor(theme);
|
||||
|
||||
// Glyph cue: sun () means current is Light, click moves to Dark;
|
||||
// moon () means current is Dark, click moves to Light.
|
||||
ThemeToggleIcon.Glyph = theme == ElementTheme.Light ? "" : "";
|
||||
}
|
||||
}
|
||||
104
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml
Normal file
104
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ContentDialog
|
||||
x:Class="TeamsISO.App.WinUI.Views.OnboardingDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Welcome to TeamsISO"
|
||||
PrimaryButtonText="Get started"
|
||||
SecondaryButtonText="Skip"
|
||||
DefaultButton="Primary"
|
||||
Background="{ThemeResource BgElevated}"
|
||||
BorderBrush="{ThemeResource BorderStrong}">
|
||||
|
||||
<!--
|
||||
First-launch only. Three sections, one pane deep — no carousel,
|
||||
no celebration. Operator-tone copy ("Pick your NDI groups" not
|
||||
"Welcome to TeamsISO!"). Skippable from the first frame.
|
||||
|
||||
Suppressed after dismissal via UIPreferences (Phase 7).
|
||||
-->
|
||||
|
||||
<StackPanel Spacing="20" MinWidth="500" MaxWidth="540">
|
||||
<TextBlock Style="{StaticResource TextSubtle}" TextWrapping="Wrap">
|
||||
TeamsISO sits between Microsoft Teams' NDI broadcast and your live-production switcher.
|
||||
One-time setup gets you to the participants table.
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Border Width="28" Height="28"
|
||||
CornerRadius="14"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="1"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="Install the NDI Runtime"
|
||||
Style="{StaticResource TextHeading}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Style="{StaticResource TextSubtle}"
|
||||
Margin="38,0,0,0"
|
||||
TextWrapping="Wrap">
|
||||
From https://ndi.video/tools/. TeamsISO won't start without it — the engine relies on
|
||||
NDI 5 for discovery and routing. If the runtime is missing, you'll see a launch error;
|
||||
install it then relaunch.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Border Width="28" Height="28"
|
||||
CornerRadius="14"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="2"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="Enable Teams NDI broadcast"
|
||||
Style="{StaticResource TextHeading}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Style="{StaticResource TextSubtle}"
|
||||
Margin="38,0,0,0"
|
||||
TextWrapping="Wrap">
|
||||
Teams admin must enable NDI broadcast for your tenant. In Teams, Settings → Devices →
|
||||
"Allow NDI usage." Per-participant streams will appear as TeamsISO discovers them.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Border Width="28" Height="28"
|
||||
CornerRadius="14"
|
||||
Background="{ThemeResource AccentCyanMuted}">
|
||||
<TextBlock Text="3"
|
||||
Style="{StaticResource TextBody}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentCyanText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="Pick your transcoder topology"
|
||||
Style="{StaticResource TextHeading}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Style="{StaticResource TextSubtle}"
|
||||
Margin="38,0,0,0"
|
||||
TextWrapping="Wrap">
|
||||
Defaults to 1920×1080 at 30 fps with letterbox aspect. Adjust under Settings → Routing.
|
||||
Recording outputs land in %USERPROFILE%\Videos\TeamsISO\<date>\.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<CheckBox x:Name="DontShowAgain"
|
||||
Content="Don't show this again"
|
||||
IsChecked="True"/>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
18
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs
Normal file
18
src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Views;
|
||||
|
||||
public sealed partial class OnboardingDialog : ContentDialog
|
||||
{
|
||||
public OnboardingDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the user wants the dialog suppressed on subsequent launches.
|
||||
/// Caller persists this to UIPreferences alongside the existing
|
||||
/// "shown welcome" flag.
|
||||
/// </summary>
|
||||
public bool SuppressFutureLaunches => DontShowAgain.IsChecked == true;
|
||||
}
|
||||
99
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml
Normal file
99
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<UserControl
|
||||
x:Class="TeamsISO.App.WinUI.Views.SettingsDrawer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!--
|
||||
Settings drawer — slides in from the right over the participants
|
||||
table when the operator clicks the rail's settings icon. Esc dismiss.
|
||||
Hosted inline (not as a separate Window) so the drawer feels like
|
||||
part of the main surface rather than a satellite. Width fixed at
|
||||
400px to give every setting a 320px input field after padding.
|
||||
|
||||
The five tabs mirror the WPF host's settings groups so the operator
|
||||
finds the same toggles in the same places. The Appearance tab is
|
||||
new — tri-state Theme picker (System / Dark / Light) plus a peek at
|
||||
the accent palette so the operator can verify Wild Dragon brand is
|
||||
respected on a light desk.
|
||||
-->
|
||||
|
||||
<Grid Background="{ThemeResource BgSurface}"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="1,0,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="56"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Grid Grid.Row="0"
|
||||
Padding="20,0,12,0"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="Settings"
|
||||
Style="{StaticResource TextTitle}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button x:Name="CloseButton"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource ButtonCaption}"
|
||||
Click="OnCloseClick"
|
||||
ToolTipService.ToolTip="Close (Esc)">
|
||||
<FontIcon Glyph="" FontSize="12"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Tabs + body -->
|
||||
<NavigationView Grid.Row="1"
|
||||
x:Name="SettingsNav"
|
||||
PaneDisplayMode="Top"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsSettingsVisible="False"
|
||||
OpenPaneLength="0"
|
||||
SelectionChanged="OnTabSelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<NavigationViewItem Tag="Appearance" Content="Appearance" IsSelected="True"/>
|
||||
<NavigationViewItem Tag="Routing" Content="Routing"/>
|
||||
<NavigationViewItem Tag="Display" Content="Display"/>
|
||||
<NavigationViewItem Tag="Control" Content="Control"/>
|
||||
<NavigationViewItem Tag="Advanced" Content="Advanced"/>
|
||||
</NavigationView.MenuItems>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
Padding="20">
|
||||
<StackPanel x:Name="TabContent" Spacing="16"/>
|
||||
</ScrollViewer>
|
||||
</NavigationView>
|
||||
|
||||
<!-- Footer: Apply / Reset -->
|
||||
<Grid Grid.Row="2"
|
||||
Padding="16,12"
|
||||
BorderBrush="{ThemeResource BorderSubtle}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
x:Name="DirtyHint"
|
||||
Text="Changes apply on close."
|
||||
Style="{StaticResource TextCaption}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource ButtonTertiary}"
|
||||
Content="Reset to defaults"
|
||||
Margin="0,0,8,0"
|
||||
Click="OnResetClick"/>
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource ButtonPrimary}"
|
||||
Content="Apply"
|
||||
Click="OnApplyClick"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
229
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs
Normal file
229
src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using TeamsISO.App.WinUI.Services;
|
||||
|
||||
namespace TeamsISO.App.WinUI.Views;
|
||||
|
||||
public sealed partial class SettingsDrawer : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when the operator clicks the close button or hits Esc. The host
|
||||
/// (MainWindow) handles the actual collapse animation since drawer
|
||||
/// hosting lives at that level — the drawer just signals intent.
|
||||
/// </summary>
|
||||
public event EventHandler? CloseRequested;
|
||||
|
||||
public SettingsDrawer()
|
||||
{
|
||||
InitializeComponent();
|
||||
BuildAppearanceTab();
|
||||
}
|
||||
|
||||
private void OnCloseClick(object sender, RoutedEventArgs e) => CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private void OnApplyClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Tabs already persist on change; Apply is here for affordance
|
||||
// consistency with the WPF host. Could surface a toast in the future.
|
||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void OnResetClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Defaults restoration plumbing follows in the view-model wiring
|
||||
// commit. For now, just clear the dirty hint.
|
||||
DirtyHint.Text = "Reset queued — apply or close to commit.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tab-switch handler builds the body for the selected tab. We assemble
|
||||
/// content imperatively rather than via separate XAML pages because the
|
||||
/// drawer's content is data-driven (most rows are simple label + control
|
||||
/// + tooltip triples) and pulling that into five XAML files would
|
||||
/// triplicate the row layout. The body recomposes on every selection
|
||||
/// change, which is cheap given the small surface.
|
||||
/// </summary>
|
||||
private void OnTabSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
TabContent.Children.Clear();
|
||||
if (args.SelectedItemContainer is not NavigationViewItem item) return;
|
||||
|
||||
switch (item.Tag as string)
|
||||
{
|
||||
case "Appearance":
|
||||
BuildAppearanceTab();
|
||||
break;
|
||||
case "Routing":
|
||||
TabContent.Children.Add(SettingHeader("Routing"));
|
||||
TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to."));
|
||||
TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant."));
|
||||
TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed."));
|
||||
TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source."));
|
||||
TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit."));
|
||||
break;
|
||||
case "Display":
|
||||
TabContent.Children.Add(SettingHeader("Display & participants"));
|
||||
TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table."));
|
||||
TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline."));
|
||||
TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table."));
|
||||
TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized."));
|
||||
TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens."));
|
||||
TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch."));
|
||||
TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call."));
|
||||
break;
|
||||
case "Control":
|
||||
TabContent.Children.Add(SettingHeader("External control surface"));
|
||||
TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck."));
|
||||
TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces."));
|
||||
TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth)."));
|
||||
TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md."));
|
||||
break;
|
||||
case "Advanced":
|
||||
TabContent.Children.Add(SettingHeader("Advanced"));
|
||||
TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window."));
|
||||
TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write."));
|
||||
TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory."));
|
||||
TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appearance tab — theme picker, accent palette peek. The theme picker
|
||||
/// is the only setting that affects the UI immediately (others apply on
|
||||
/// the engine layer once wired). Selection writes to UIPreferences and
|
||||
/// flips Window.Content.RequestedTheme synchronously.
|
||||
/// </summary>
|
||||
private void BuildAppearanceTab()
|
||||
{
|
||||
TabContent.Children.Add(SettingHeader("Appearance"));
|
||||
|
||||
var themeLabel = new TextBlock
|
||||
{
|
||||
Style = (Style)Application.Current.Resources["TextBody"],
|
||||
Text = "Theme",
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
TabContent.Children.Add(themeLabel);
|
||||
|
||||
var themeRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
|
||||
foreach (var (label, value) in new[]
|
||||
{
|
||||
("System", "System"),
|
||||
("Dark", "Dark"),
|
||||
("Light", "Light"),
|
||||
})
|
||||
{
|
||||
var btn = new RadioButton
|
||||
{
|
||||
Content = label,
|
||||
Tag = value,
|
||||
GroupName = "Theme",
|
||||
IsChecked = ThemeManager.Current.PreferenceMatches(value),
|
||||
};
|
||||
btn.Checked += (s, _) =>
|
||||
{
|
||||
if (s is RadioButton rb && rb.Tag is string v)
|
||||
{
|
||||
ThemeManager.Current.Set(v);
|
||||
}
|
||||
};
|
||||
themeRow.Children.Add(btn);
|
||||
}
|
||||
TabContent.Children.Add(themeRow);
|
||||
|
||||
TabContent.Children.Add(SettingNote(
|
||||
"Dark is the default for the 1:50am operator scene; light is for daytime " +
|
||||
"production. System follows the Windows app-mode preference."));
|
||||
|
||||
TabContent.Children.Add(SettingDivider());
|
||||
TabContent.Children.Add(SettingHeader("Accent peek"));
|
||||
|
||||
var accentRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 12 };
|
||||
accentRow.Children.Add(AccentSwatch("Cyan", "AccentCyanSurface"));
|
||||
accentRow.Children.Add(AccentSwatch("Coral", "AccentCoral"));
|
||||
accentRow.Children.Add(AccentSwatch("Live", "StatusLive"));
|
||||
accentRow.Children.Add(AccentSwatch("Warn", "StatusWarn"));
|
||||
TabContent.Children.Add(accentRow);
|
||||
|
||||
TabContent.Children.Add(SettingNote(
|
||||
"These accents work in both themes. Cyan stays bright as a surface fill " +
|
||||
"(text on top is near-black regardless of theme). For inline text use, the " +
|
||||
"light palette substitutes a darker cyan automatically."));
|
||||
}
|
||||
|
||||
// ──────────────────── helpers ──────────────────────────────────────────
|
||||
|
||||
private static TextBlock SettingHeader(string text) => new()
|
||||
{
|
||||
Style = (Style)Application.Current.Resources["TextHeading"],
|
||||
Text = text,
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
};
|
||||
|
||||
private static Grid SettingRow(string label, string value, string? tooltip = null)
|
||||
{
|
||||
var g = new Grid { Margin = new Thickness(0, 0, 0, 6) };
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(180) });
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
var l = new TextBlock
|
||||
{
|
||||
Style = (Style)Application.Current.Resources["TextBody"],
|
||||
Text = label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var v = new TextBlock
|
||||
{
|
||||
Style = (Style)Application.Current.Resources["TextSubtle"],
|
||||
Text = value,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (tooltip is not null)
|
||||
{
|
||||
ToolTipService.SetToolTip(l, tooltip);
|
||||
}
|
||||
Grid.SetColumn(l, 0);
|
||||
Grid.SetColumn(v, 1);
|
||||
g.Children.Add(l);
|
||||
g.Children.Add(v);
|
||||
return g;
|
||||
}
|
||||
|
||||
private static TextBlock SettingNote(string text) => new()
|
||||
{
|
||||
Style = (Style)Application.Current.Resources["TextCaption"],
|
||||
Text = text,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 4, 0, 12),
|
||||
};
|
||||
|
||||
private static Border SettingDivider() => new()
|
||||
{
|
||||
Height = 1,
|
||||
Background = (SolidColorBrush)Application.Current.Resources["BorderSubtle"],
|
||||
Margin = new Thickness(0, 12, 0, 12),
|
||||
};
|
||||
|
||||
private static Border AccentSwatch(string label, string brushKey)
|
||||
{
|
||||
var inner = new StackPanel { Orientation = Orientation.Vertical, Spacing = 4 };
|
||||
var swatch = new Border
|
||||
{
|
||||
Width = 80,
|
||||
Height = 32,
|
||||
CornerRadius = new Microsoft.UI.Xaml.CornerRadius(6),
|
||||
Background = (SolidColorBrush)Application.Current.Resources[brushKey],
|
||||
};
|
||||
var caption = new TextBlock
|
||||
{
|
||||
Style = (Style)Application.Current.Resources["TextCaption"],
|
||||
Text = label,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
inner.Children.Add(swatch);
|
||||
inner.Children.Add(caption);
|
||||
return new Border { Child = inner };
|
||||
}
|
||||
}
|
||||
32
src/TeamsISO.App.WinUI/app.manifest
Normal file
32
src/TeamsISO.App.WinUI/app.manifest
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="TeamsISO.App.WinUI"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- TeamsISO is a normal-trust desktop app; no UAC elevation needed.
|
||||
Network listens (control surface :9755 and OSC :9000) bind to
|
||||
127.0.0.1 only, which doesn't require admin. -->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Win10 1809 (17763) is the floor — same as TargetPlatformMinVersion. -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
<!-- Crisp text on high-DPI broadcast monitors. -->
|
||||
<gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
Loading…
Reference in a new issue