From 42ec7f8eef02d89191292ae7f386eb3d0559e1ab Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Fri, 8 May 2026 21:46:22 -0700 Subject: [PATCH] feat: add live camera cards --- .../components/CameraPreviewPanel.test.tsx | 13 ++ .../cameras/components/CameraPreviewPanel.tsx | 20 ++- ui/src/features/live/LiveCameraCard.tsx | 106 ++++++++++++++++ ui/src/features/live/LivePage.test.tsx | 118 ++++++++++++++++++ ui/src/features/live/LivePage.tsx | 47 +------ ui/src/styles/global.css | 5 + 6 files changed, 260 insertions(+), 49 deletions(-) create mode 100644 ui/src/features/live/LiveCameraCard.tsx create mode 100644 ui/src/features/live/LivePage.test.tsx diff --git a/ui/src/features/cameras/components/CameraPreviewPanel.test.tsx b/ui/src/features/cameras/components/CameraPreviewPanel.test.tsx index 502435c8..a5905916 100644 --- a/ui/src/features/cameras/components/CameraPreviewPanel.test.tsx +++ b/ui/src/features/cameras/components/CameraPreviewPanel.test.tsx @@ -252,6 +252,19 @@ describe('CameraPreviewPanel', () => { expect(screen.getByText('viewers 2')).toBeTruthy() }) + it('can hide push-to-talk for compact live camera cards', () => { + // Given: A ready preview rendered in a compact Live card context + mockReadyPreviewSession() + + // When: The preview panel opts out of talk controls + render() + + // Then: Preview controls remain while push-to-talk is not mounted + expect(screen.getByRole('button', { name: 'Attach preview' })).toBeTruthy() + expect(screen.queryByRole('button', { name: 'Hold to talk' })).toBeNull() + expect(usePushToTalkMock).not.toHaveBeenCalled() + }) + it('initializes hls.js playback when a playlist URL becomes ready', async () => { // Given: A ready preview session with a tokenized playlist URL mockReadyPreviewSession() diff --git a/ui/src/features/cameras/components/CameraPreviewPanel.tsx b/ui/src/features/cameras/components/CameraPreviewPanel.tsx index c8a99518..1814e9f8 100644 --- a/ui/src/features/cameras/components/CameraPreviewPanel.tsx +++ b/ui/src/features/cameras/components/CameraPreviewPanel.tsx @@ -28,6 +28,10 @@ type WebKitVideoElement = HTMLVideoElement & { interface CameraPreviewPanelProps { cameraName: string + title?: string + subtitle?: string + showTalkControl?: boolean + className?: string } function previewTone( @@ -103,7 +107,13 @@ async function exitActiveFullscreen(): Promise { } } -export function CameraPreviewPanel({ cameraName }: CameraPreviewPanelProps) { +export function CameraPreviewPanel({ + cameraName, + title = 'Live preview', + subtitle = 'On-demand HLS stream from the active runtime.', + showTalkControl = true, + className, +}: CameraPreviewPanelProps) { const { error, isPending, @@ -390,11 +400,11 @@ export function CameraPreviewPanel({ cameraName }: CameraPreviewPanelProps) { }, [error, playerError, playlistReady, playlistUrl, status?.enabled, warning]) return ( -
+
-

Live preview

-

On-demand HLS stream from the active runtime.

+

{title}

+

{subtitle}

{previewLabel(effectiveState)} @@ -438,7 +448,7 @@ export function CameraPreviewPanel({ cameraName }: CameraPreviewPanelProps) {

{statusMessage}

) : null} - + {showTalkControl ? : null}