Files
archy/.planning/phases/01-federation-mesh-hardening/01-15-SUMMARY.md
T

7.1 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed coverage duration completed status
01-federation-mesh-hardening 15 ui
vue
picture-in-picture
media
lifecycle
teleport
phase provides
01-federation-mesh-hardening MediaLightbox.vue's existing lightbox shell (Teleported backdrop, video element, pip button) and utils/pip's browser-support helper
usePipSession(): a singleton PiP session that owns a body-level custodial host, so an adopted video survives the unmount of whatever view rendered it
A lightbox→PiP handoff that reads as the video moving into the PiP window rather than the lightbox being dismissed
cloud-files
media-viewer
added patterns
Custodial host pattern: to keep a media element alive across a view teardown, move it into an off-screen div under document.body before the teardown runs. Both Teleport and KeepAlive move their subtree on deactivation, which the picture-in-picture spec treats as removal — so the element must be re-parented above the view tree, not merely referenced.
Order-of-operations as a documented invariant: adopt → animate → emit close. Adopting first is what makes the element survive the unmount that the emit triggers; the comment in onEnterPip says so explicitly so a future refactor cannot reorder it innocently.
created modified
neode-ui/src/composables/usePipSession.ts
neode-ui/src/components/__tests__/MediaLightboxPip.test.ts
neode-ui/src/components/cloud/MediaLightbox.vue
PiP entry is detected via the element's own `enterpictureinpicture` event rather than inferred from the toolbar button, so PiP entered by any route — the browser's native control, a keyboard shortcut — takes exactly the same handoff path.
The backdrop's close is driven by `transitionend` with a bounded 350ms fallback timer, covering browsers that skip the transition and the reduced-motion path where the duration is zero and the event never fires.
`release()` is idempotent and is called both by the session's own `leavepictureinpicture` listener (the primary path, since the lightbox has usually unmounted by then) and by the component's handler for the rare still-mounted case.
UIFIX-05
id description requirement verification human_judgment
D1 Entering PiP closes the lightbox once, having adopted the video first so it survives the unmount UIFIX-05
kind ref status
unit neode-ui/src/components/__tests__/MediaLightboxPip.test.ts#entering PiP emits close exactly once and adopts the video before doing so pass
false
id description requirement verification human_judgment
D2 The close reads as a handoff animation on the PiP path, and as an ordinary dismissal otherwise UIFIX-05
kind ref status
unit …#applies the handoff class on the PiP path; …#applies no handoff class on a button-driven close pass
false
id description requirement verification human_judgment
D3 Leaving PiP releases the session and tears down playback UIFIX-05
kind ref status
unit …#releases the session when picture-in-picture is left pass
false
id description requirement verification human_judgment
D4 The component's public contract (props/emits) is unchanged by the rework UIFIX-05
kind ref status
unit …#does not change props or emits declared by the component pass
false
85min 2026-08-01 complete

Phase 1 Plan 15: PiP Handoff That Survives the Lightbox (UIFIX-05) Summary

Made picture-in-picture a handoff rather than a dismissal: the video is re-parented to a body-level custodial host before the lightbox closes, so an active PiP session survives the unmount — and the close animates as the video moving out rather than the overlay disappearing.

Performance

  • Duration: ~85 min (across two sessions — see Deviations)
  • Completed: 2026-08-01
  • Tasks: 2/2
  • Files modified: 3 (1 new composable, 1 component, 1 new test file)

Accomplishments

  • usePipSession() owns a singleton off-screen host under document.body. adopt(video) moves the element there so it outlives the view that rendered it; release() pauses, detaches and tears down. A leavepictureinpicture listener attached at adopt time is the primary release path.
  • MediaLightbox listens for enterpictureinpicture/leavepictureinpicture on the video itself, so any route into PiP behaves identically.
  • The handoff ordering (adopt → animate → emit close) is enforced and documented in-place; a test asserts adoption happens before the single close emit.
  • The backdrop gains a lightbox-pip-handoff class on the PiP path only, with transitionend-driven close and a 350 ms fallback for browsers that skip the transition (including reduced-motion, where the duration is zero).

Task Commits

  1. Task 1: singleton PiP session with body-level custodial host3288a02d (feat)
  2. Task 2: lightbox handoff wiring, handoff animation, and test suite — committed with this SUMMARY

Files Created/Modified

  • neode-ui/src/composables/usePipSession.ts — the singleton session and custodial host (committed in 3288a02d).
  • neode-ui/src/components/cloud/MediaLightbox.vue — PiP event handlers, handoff class + CSS, transitionend/timer close path, and the pipSupportedisPipSupported() call-site update.
  • neode-ui/src/components/__tests__/MediaLightboxPip.test.ts — 5 tests, with jsdom stubs for the picture-in-picture APIs it does not implement.

Deviations from Plan

Process deviation: executor killed mid-verification by an SSH disconnect

Found during: post-implementation verification Issue: Task 1 had been committed (3288a02d); Task 2's component changes and test file were complete on disk but uncommitted when the orchestrating session and its agents died with the operator's SSH connection. Resolution: A follow-on session re-ran the suite (5/5 green), confirmed the full frontend suite was green, wrote this SUMMARY, and committed the remainder. Files modified: none beyond the original work

Known Stubs

None in product code. The test file stubs pictureInPictureEnabled, requestPictureInPicture and exitPictureInPicture because jsdom implements none of them; jsdom also logs "Not implemented: HTMLMediaElement.prototype.pause/load" to stderr during the release test — noise, not failure.

Threat Flags

None — no new endpoint or trust boundary. The one lifecycle risk (an orphaned element left attached to document.body after PiP ends) is closed by release() being idempotent and wired to both the session's own listener and the component handler.

Self-Check: PASSED

  • FOUND: neode-ui/src/composables/usePipSession.ts (created, commit 3288a02d)
  • FOUND: neode-ui/src/components/cloud/MediaLightbox.vue (modified)
  • FOUND: neode-ui/src/components/__tests__/MediaLightboxPip.test.ts (created, 5 tests)
  • CONFIRMED: npx vitest run full frontend suite green