diff --git a/.agents/phases/todo/05_autohide_stage_controls/00_phase.md b/.agents/phases/todo/05_autohide_stage_controls/00_phase.md new file mode 100644 index 0000000..7de6b88 --- /dev/null +++ b/.agents/phases/todo/05_autohide_stage_controls/00_phase.md @@ -0,0 +1,43 @@ +# Phase 05 — The staged mirror's overlay fades away + +**Source:** `TODO.md` L3 — "the staged (fullscreen) tile's details should only show up briefly on mouse hover and then fade away when full screen" +**Story:** n/a +**Context:** `PLAN.md` §5 (E2E ladder — assertions on the viewer's side), `docs/kiosk-silverblue.md` (the mouseless-wall rule: "no pointer affordance may be the only way to see state"), `src/mirror/static/app.css` (staged-tile block, L594–612), `src/mirror/static/watch.js` (`stage()`/`unstage()`, the wall key router) + +## Objective + +A staged mirror is the video first. While a tile is on stage, its caption bar +(label; device · surface · dims · bitrate · fps — the `sub` line) and its +control cluster must be **hidden by default**, so a shared terminal's bottom +line is never covered. They appear **briefly** on mouse hover and on any wall +keypress (the kiosk has no mouse), and fade away again after an idle timeout. + +## Dependencies +- `04_reachable_share_cta` (complete) — the e2e conventions this phase's tests + reuse (the `device`-style two-browser fixture with the fake-media capture of + `tests/e2e/`), and the current `stage()`/`unstage()` behaviour this phase + modifies + +## Tasks +1. `01_hide_staged_overlay_at_rest.md` — staged `.cap` + `.actions` hidden at rest (CSS); the existing staged-caption test now asserts the new rest state +2. `02_reveal_on_activity.md` — reveal on pointer/keypress with a 3 s idle auto-hide (`watch.js` + the `[data-reveal]` CSS rules) +3. `03_e2e_autofade_lifecycle.md` — dedicated E2E: hidden at rest → mouse reveal → idle fade → key reveal → Exit usable while revealed → grid kiosk rules unchanged + +## Testing & Quality +- Unit/integration: the behaviour is frontend — per `PLAN.md` §5 it is asserted + in the browser (a real staged tile under the fake-media capture), never by + unit tests pretending to be WebRTC. The CSS-structure guards in + `tests/test_app.py::TestStylesheet` (`.actions` hide + non-hover reveal pair) + must keep passing. +- Coverage: **>90%** on new/modified code — the JS/CSS change is exercised end + to end by `tests/e2e/test_stage_autofade.py`, which runs in isolation in + this phase's final pass. +- No Python behaviour changes; the four gates of `PLAN.md` §5 stay green. + +## Completion Criteria +- [ ] with a staged tile idle, computed opacity of `.cap` and `.actions` is `0` — the shared video's bottom edge is unobstructed (the motivating case: a shared terminal's last line is visible) +- [ ] mouse movement over the staged tile reveals both (opacity `1`); after ~3 s of idle they fade back to `0` +- [ ] any wall keypress while staged reveals them, and the **Exit** button is visible and clickable while revealed (clicking it unstages) +- [ ] grid-tile behaviour is byte-for-byte unchanged (unselected `.actions` hidden; `data-selected`/focus reveal — the kiosk screenshot rule) +- [ ] test suite green (all four gates of `PLAN.md` §5), coverage >90% +- [ ] no behavior change in completed phases diff --git a/.agents/phases/todo/05_autohide_stage_controls/01_hide_staged_overlay_at_rest.md b/.agents/phases/todo/05_autohide_stage_controls/01_hide_staged_overlay_at_rest.md new file mode 100644 index 0000000..ce58ed3 --- /dev/null +++ b/.agents/phases/todo/05_autohide_stage_controls/01_hide_staged_overlay_at_rest.md @@ -0,0 +1,60 @@ +# Task 01 — Hide the staged overlay at rest + +**Phase:** `05_autohide_stage_controls` · **Source:** `TODO.md:3` — ""Primary Monitory", "Firefox 2160x1440", 2.2 Mbps, 20fps and the word "Exit" all block the screen share at the bottom. This makes sharing terminal windows annoying because it's difficult to see the bottom line. Those details should only show up briefly on mouse hover and then fade away when full screen." + +## Objective + +While a tile is staged (fullscreen), its caption bar and its control cluster +must be **hidden by default** so the shared video — a terminal's bottom line +included — is never covered. Grid-tile styling stays exactly as it is today. + +## Work + +1. `src/mirror/static/app.css` (staged block, L594–612) — change + `#watch-stage .tile .cap` from `opacity: 0.28` to `opacity: 0`; **delete** + the three staged reveal rules (`#watch-stage .tile:hover .cap`, + `:focus-within`, `[data-selected]`) and `#watch-stage .tile .actions + { opacity: 1 }`; **add** `#watch-stage .tile .actions { opacity: 0 }`, and + make both staged hide rules pointer-transparent — but a + `pointer-events: none` container does **not** shield children whose + default is `auto`, so the hidden Exit/mute buttons would stay live dead + zones over the video: scope it to the container *and its children*: + `#watch-stage .tile .cap, #watch-stage .tile .cap *` and `#watch-stage + .tile .actions, #watch-stage .tile .actions *` with `pointer-events: + none`. + The id-scoped selector must outrank the grid rules `.tile[data-selected] + .actions` and `.tile:focus-within .actions`, which also match the staged + tile (it keeps `data-selected` and holds focus) — an id in the selector is + what makes that true; verify by the computed-opacity assertion in step 3, + not by assumption. +2. `src/mirror/static/app.css` — replace the comment block `/* The staged + caption is quiet (0.28) so the video is what is being watched; a kiosk has + no mouse, so keyboard focus and the selection state are the way to read it + back. */` with the new contract: hidden at rest; task 02's `data-reveal` + (pointer activity or a keypress) is the only staged reveal, and the small + `::after` audio mark is unaffected (it is a separate rule and stays). +3. `tests/e2e/test_keyboard_wall.py::TestFocusVisibleControls::test_focus_moves_to_the_stage_and_back_with_g` + — replace the `# The staged caption is legible while the tile holds focus` + block (which waits for `.cap` opacity `1`) with the new rest-state + assertion: `wait_for_function` (timeout **10 s**) that the staged tile's + `.cap` **and** `.actions` computed opacity are `0`. 10 s stays green under + task 02 as well: an Enter-press reveal lasts 3 s + 200 ms fade, so the + settled `0` is always reachable inside the window. + Do **not** touch the grid guard in the same file + (`TestFocusVisibleControls::test_the_wall_stays_quiet_until_something_is_focused` — + grid `.actions` opacity `0` at rest / `data-selected` + focus reveal) — those + rules are intact. + +## Testing & Quality +- Unit/integration: `tests/test_app.py::TestStylesheet` guards must keep + passing — in particular `test_a_hidden_control_is_not_revealed_only_by_hover` + (the grid `.actions` hide + non-hover reveal pair is untouched) and + `test_no_rule_reintroduces_a_conflicting_display_for_hidden`. +- Coverage: **>90%** on this task's new/modified code — CSS is exercised by + the updated E2E assertion per project convention (no CSS unit harness). + +## Completion Criteria +- [ ] with a staged tile and no pointer activity, `getComputedStyle` opacity of `.cap` and `.actions` is `0` (asserted by the updated `test_focus_moves_to_the_stage_and_back_with_g`) +- [ ] grid tiles: unselected `.actions` still `0`; `data-selected`/focus reveal still `1` (existing E2E tests green) +- [ ] the `::after` audio mark (`data-audio="on"`/`"off"`) still renders on the staged tile (no rule touched it; spot-checked by the existing audio E2E tests) +- [ ] all four gates of `PLAN.md` §5 green diff --git a/.agents/phases/todo/05_autohide_stage_controls/02_reveal_on_activity.md b/.agents/phases/todo/05_autohide_stage_controls/02_reveal_on_activity.md new file mode 100644 index 0000000..9c3f7ad --- /dev/null +++ b/.agents/phases/todo/05_autohide_stage_controls/02_reveal_on_activity.md @@ -0,0 +1,76 @@ +# Task 02 — Reveal on activity, auto-hide when idle + +**Phase:** `05_autohide_stage_controls` · **Source:** `TODO.md:3` — "Those details should only show up briefly on mouse hover and then fade away when full screen." + +## Objective + +While staged, the caption and controls appear on mouse hover — and, for the +mouseless kiosk, on any wall keypress — then fade away again after an idle +timeout, so a parked pointer (or a one-time keypress) does not pin them up. + +## Work + +1. `src/mirror/static/watch.js` — add the idle mechanism beside + `stage()`/`unstage()`: `const STAGE_REVEAL_IDLE_MS = 3000;` (named + constant; a per-module `let stageRevealTimer = 0;`) and + `function revealStageControls()`: return when `state.staged` is null; set + `data-reveal` on the staged tile's element; clear and re-arm + `stageRevealTimer = setTimeout(...)` for `STAGE_REVEAL_IDLE_MS`; the + callback deletes the attribute **only if that tile is still staged** + (`state.staged === tile.id`) — the tile may have been unstaged or replaced + (n/p, 1-9, a click) before the timer fires. + - `ASSUMPTION: A1 — 3 s is the TODO's "briefly"; the fade itself is the + existing CSS opacity transition (~0.2 s on `.cap`, 0.15 s on `.actions`), + so no new animation is added (and `prefers-reduced-motion` already kills + transitions globally).` +2. `src/mirror/static/watch.js` — wire the triggers: + `ui.stage.addEventListener("pointermove", revealStageControls)` and + `ui.stage.addEventListener("pointerdown", revealStageControls)` + (`pointerdown` = a touch tap reveals, so a phone watcher can reach the + controls — `ASSUMPTION: A5`); and in `onWallKey`, after the bail-outs + (defaultPrevented / chords / text field) but before the `switch`, + `if (state.staged) revealStageControls();` — **any** wall keypress while + staged reveals, handled or not (a silent no-op key flashing the caption is + the debuggable outcome; the call never `preventDefault`s). + - `ASSUMPTION: A4 — keypress reveal is what keeps the kiosk rule intact + ("no pointer affordance may be the only way to see state", + docs/kiosk-silverblue.md + the wall key-router header): the staged tile + always holds focus (`stage()` calls `tile.el.focus()`), so the old + `:focus-within` reveal can no longer be used without defeating the TODO.` +3. `src/mirror/static/watch.js` — `unstage()`: clear `stageRevealTimer` and + delete `data-reveal` from the departing tile, so a pending reveal can never + leak back into the grid (grid tiles never read `data-reveal`). + - `ASSUMPTION: A2 — the hidden state is tied to the **staged** state (the + in-page `#watch-stage` covers the viewport, `position: fixed; inset: 0`), + not to `document.fullscreenElement`: the Silverblue kiosk forces the + browser *window* fullscreen (docs/kiosk-silverblue.md, + window-config.ini) and the element `requestFullscreen()` there is a + deliberate no-op — a fullscreen-element check would leave the kiosk's + caption permanently up.` + - `ASSUMPTION: A3 — the top-right `.actions` cluster (mute/expand) fades + with the caption: same occlusion class. The small "live" `.badge` (top- + left) and the `::after` audio mark stay always visible: status, not + detail, and both are tiny.` +4. `src/mirror/static/app.css` — add the reveal pair, mirroring task 01's + container+children scoping: `#watch-stage .tile[data-reveal] .cap, + #watch-stage .tile[data-reveal] .cap * { opacity: 1; pointer-events: + auto }` and the same for `.actions` — id-scoped, outranking task 01's + hide rules; `pointer-events: auto` is what makes the Exit/mute buttons + clickable again the moment the overlay is revealed (setting `opacity: 1` + on the children is harmless); the existing `transition` on both base + rules gives the fade in and out. + +## Testing & Quality +- Unit/integration: exercised by the E2E in task 03 (real staged tile; real + mouse and keyboard events through Playwright); the page must stay free of + `pageerror`/`console` errors — the e2e suites capture both from the first + line. +- Coverage: **>90%** on this task's new/modified code — the reveal lifecycle + is asserted in the browser per project convention. + +## Completion Criteria +- [ ] mouse move over the staged tile sets `data-reveal` (computed opacity `1`); after 3 s + transition with no pointer input, the attribute is gone and opacity is `0` again +- [ ] a wall keypress (`m`) while staged reveals the overlay; a parked tile hides it again +- [ ] unstaging (Exit click / `g` / Esc) clears the attribute and the pending timer; re-staging starts hidden +- [ ] replacing the staged tile (n/p / 1-9 / click) never leaves a stale `data-reveal` on the departing tile +- [ ] all four gates of `PLAN.md` §5 green diff --git a/.agents/phases/todo/05_autohide_stage_controls/03_e2e_autofade_lifecycle.md b/.agents/phases/todo/05_autohide_stage_controls/03_e2e_autofade_lifecycle.md new file mode 100644 index 0000000..1f2cde5 --- /dev/null +++ b/.agents/phases/todo/05_autohide_stage_controls/03_e2e_autofade_lifecycle.md @@ -0,0 +1,61 @@ +# Task 03 — E2E: the staged overlay's auto-hide lifecycle + +**Phase:** `05_autohide_stage_controls` · **Source:** `TODO.md:3` — ""Primary Monitory", "Firefox 2160x1440", 2.2 Mbps, 20fps and the word "Exit" all block the screen share at the bottom. This makes sharing terminal windows annoying because it's difficult to see the bottom line." + +## Objective + +A dedicated Playwright E2E suite that pins the whole lifecycle on a real +capture: hidden at rest, mouse reveal, idle fade, key reveal, Exit usable +while revealed, and the grid's kiosk reveal rules unchanged. Per `PLAN.md` +§5, everything is asserted on the **viewer's** side (the wall's DOM state), +never on the sharer's preview. + +## Work + +1. `tests/e2e/test_stage_autofade.py` — new file following the e2e + conventions of `tests/e2e/test_keyboard_wall.py`: a `device`-style fixture + (two browser contexts over the session `base_url`/`browser` fixtures, + fake-media capture, `pageerror` + `console` captured from the first line + of the test and asserted `[]` at the end); a helper that shares one mirror + and stages it (`Enter` → `body.staged`); and a probe that reads, for the + staged tile, the computed opacity of `.cap` and `.actions` plus the + presence of `data-reveal`. +2. `tests/e2e/test_stage_autofade.py` — the tests (each ends with + `assert errors == []`): + - **rest** — after staging and the settled fade, computed opacity of + `.cap` and `.actions` is `0` (the motivating case: the shared video's + bottom edge is unobstructed; no element other than the `video` itself + answers a `document.elementFromPoint` probe at the bottom-centre of the + stage). + - **mouse reveal** — `viewer.mouse.move(...)` inside `#watch-stage` → + `data-reveal` present, both opacities `1`. + - **idle fade** — after the last mouse move, wait out 3 s + transition → + `data-reveal` gone, both opacities `0`. + - **key reveal + Exit** — a wall keypress (`m`; tolerate and assert its + mute flip, as `TestWallCommands::test_m_flips_the_staged_videos_muted_flag` + does) reveals the overlay; while revealed the **Exit** button is visible + (`hidden` false) and a click on it unstages (`body` loses `staged`). + - **grid guard** — with nothing staged: an unselected tile's `.actions` + computed opacity is `0`; the `data-selected` tile's is `1` (the kiosk + screenshot rule survives the id-scoped overrides). +3. `tests/e2e/test_stage_autofade.py` — keep the module's docstring in the + style of its neighbours: name the bug it pins (the caption bar covered a + shared terminal's bottom line because the staged tile forced + `data-selected` → `.cap` opacity `1`), the probe that reproduces it + (computed opacity of `.cap` while staged and idle), and why each test + exists. + +## Testing & Quality +- Unit/integration: this **is** the test suite — real capture → encode → + SRTP → decode, then DOM assertions on the viewer per `PLAN.md` §5 (content + assertions over event assertions; the rest-state test's hit probe is the + strongest form: *nothing* but the video is at the bottom edge). +- Coverage: **>90%** on this task's new/modified code — the entire reveal + lifecycle is asserted here; the suite runs in isolation in the phase's + final pass (`uv run pytest tests/e2e/test_stage_autofade.py`). + +## Completion Criteria +- [ ] all five tests in `test_stage_autofade.py` pass, in isolation and inside `uv run pytest -m e2e` +- [ ] no regression in `test_keyboard_wall.py` (stage/unstage, focus hand-back, `m`, Exit — phases 01–04 behaviour intact) +- [ ] no new external asset, no new dependency (`PLAN.md` §8) +- [ ] all four gates of `PLAN.md` §5 green