diff --git a/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx b/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx
index 561c62c5..afbdbda5 100644
--- a/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx
+++ b/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from 'react'
+import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useDocumentPiP } from '../hooks/useDocumentPiP'
import { UNSAFE_PortalProvider } from '@react-aria/overlays'
@@ -113,7 +113,7 @@ export const DocumentPiPPortal = ({
height?: number
children: React.ReactNode
onClose?: () => void
-}) => {
+}): ReactNode => {
const { openPiP, closePiP, pipWindow, isSupported } = useDocumentPiP({
width,
height,
@@ -189,5 +189,5 @@ export const DocumentPiPPortal = ({
)
}, [children, container])
- return portal
+ return portal as unknown as ReactNode
}
diff --git a/src/frontend/src/features/pip/components/PipControlBar.tsx b/src/frontend/src/features/pip/components/PipControlBar.tsx
index cb15055b..f63d5ff3 100644
--- a/src/frontend/src/features/pip/components/PipControlBar.tsx
+++ b/src/frontend/src/features/pip/components/PipControlBar.tsx
@@ -3,12 +3,11 @@ import { AudioDevicesControl } from '@/features/rooms/livekit/components/control
import { VideoDeviceControl } from '@/features/rooms/livekit/components/controls/Device/VideoDeviceControl'
import { ScreenShareToggle } from '@/features/rooms/livekit/components/controls/ScreenShareToggle'
import { LeaveButton } from '@/features/rooms/livekit/components/controls/LeaveButton'
-import { ReactionsToggle } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import { SubtitlesToggle } from '@/features/rooms/livekit/components/controls/SubtitlesToggle'
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
import { OptionsButton } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
-import { PipLateralMenu } from './controls/PipLateralMenu'
+import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
/**
* Compact control bar for the Picture-in-Picture window.
@@ -31,9 +30,6 @@ export const PipControlBar = ({
-
-
-
)
@@ -61,13 +57,3 @@ const PipControlsCenter = styled('div', {
flex: '1 1 auto',
},
})
-
-const PipControlsRight = styled('div', {
- base: {
- display: 'flex',
- justifyContent: 'flex-end',
- alignItems: 'center',
- position: 'absolute',
- right: '1.35rem',
- },
-})
diff --git a/src/frontend/src/features/pip/components/PipView.tsx b/src/frontend/src/features/pip/components/PipView.tsx
index 684bdb7d..5e86a731 100644
--- a/src/frontend/src/features/pip/components/PipView.tsx
+++ b/src/frontend/src/features/pip/components/PipView.tsx
@@ -7,8 +7,9 @@ import {
import { useTracks } from '@livekit/components-react'
import { Track } from 'livekit-client'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
-import { GridLayout } from '@/features/rooms/livekit/components/layout/GridLayout'
+import { GridLayout } from '@/features/layout/components/GridLayout'
import { SidePanel } from '@/features/rooms/livekit/components/SidePanel'
+import { pipLayoutStore } from '../stores/pipLayoutStore'
import { PipControlBar } from './PipControlBar'
const pickTrackForPip = (
@@ -16,7 +17,7 @@ const pickTrackForPip = (
): TrackReferenceOrPlaceholder | undefined => {
// Prefer screen share when present; otherwise fallback to first available track.
const screenShareTrack = tracks
- .filter(isTrackReference)
+ .filter((track) => isTrackReference(track))
.find((track) => track.publication.source === Track.Source.ScreenShare)
if (screenShareTrack) return screenShareTrack
@@ -60,7 +61,7 @@ export const PipView = () => {
{/* Compact control bar for PiP; extend here when adding more actions. */}
{/* Side panel (effects, settings, etc.) opens within PiP window. */}
-
+
)
}
diff --git a/src/frontend/src/features/pip/components/RoomPiP.tsx b/src/frontend/src/features/pip/components/RoomPiP.tsx
index 9cbddfba..9d3387b6 100644
--- a/src/frontend/src/features/pip/components/RoomPiP.tsx
+++ b/src/frontend/src/features/pip/components/RoomPiP.tsx
@@ -1,3 +1,5 @@
+import type { ReactNode } from 'react'
+
import { DocumentPiPPortal } from './DocumentPiPPortal'
import { PipView } from './PipView'
import { useRoomPiP } from '../hooks/useRoomPiP'
@@ -5,13 +7,15 @@ import { useRoomPiP } from '../hooks/useRoomPiP'
/**
* Wrapper that mounts the PiP UI when room-level PiP state is enabled.
* Bridges Valtio-backed PiP state with DocumentPiPPortal and PipView rendering.
+ * PiP panel state is decoupled via explicit pipLayoutStore injection.
*/
-export const RoomPiP = () => {
+export const RoomPiP = (): ReactNode => {
const { isOpen, close } = useRoomPiP()
- return (
-
-
-
- )
+ const portal = DocumentPiPPortal({
+ isOpen,
+ onClose: close,
+ children: ,
+ })
+ return portal as ReactNode
}
diff --git a/src/frontend/src/features/pip/components/controls/PipLateralMenu.tsx b/src/frontend/src/features/pip/components/controls/PipLateralMenu.tsx
deleted file mode 100644
index c2b7e2f0..00000000
--- a/src/frontend/src/features/pip/components/controls/PipLateralMenu.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { css } from '@/styled-system/css'
-import { useState } from 'react'
-import { Dialog, DialogTrigger } from 'react-aria-components'
-import { Button } from '@/primitives'
-import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
-import { useTranslation } from 'react-i18next'
-import { StyledPopover } from '@/primitives/Popover'
-import { useOverlayBoundaryElement } from '@/primitives/useOverlayPortalContainer'
-import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
-import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
-import { ToolsToggle } from '@/features/rooms/livekit/components/controls/ToolsToggle'
-import { InfoToggle } from '@/features/rooms/livekit/components/controls/InfoToggle'
-import { AdminToggle } from '@/features/rooms/livekit/components/AdminToggle'
-
-const NavigationControls = ({ onPress }: { onPress?: () => void }) => (
- <>
-
-
-
-
-
- >
-)
-
-/**
- * PiP chevron menu that exposes Info/Chat/Participants/Tools/Admin.
- */
-export const PipLateralMenu = () => {
- const { t } = useTranslation('rooms')
- const [isOpen, setIsOpen] = useState(false)
- const boundaryElement = useOverlayBoundaryElement()
-
- const handlePress = () => setIsOpen(!isOpen)
- const handleClose = () => setIsOpen(false)
-
- return (
-
-
-
-
-
-
- )
-}
diff --git a/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx b/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx
index c320a7b8..93c350e1 100644
--- a/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx
+++ b/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react'
import { RiMoreFill } from '@remixicon/react'
import { Box, Button } from '@/primitives'
import { css } from '@/styled-system/css'
-import { OptionsMenuItems } from '@/features/rooms/livekit/components/controls/Options/OptionsMenuItems'
+import { PipOptionsMenuItems } from './PipOptionsMenuItems'
type PipOptionsMenuProps = {
wrapperRef: React.RefObject
@@ -77,7 +77,7 @@ export const PipOptionsMenu = ({
})}
>
-
+
)}
diff --git a/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx b/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx
new file mode 100644
index 00000000..e8ad1b74
--- /dev/null
+++ b/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx
@@ -0,0 +1,32 @@
+import { Menu as RACMenu, MenuSection } from 'react-aria-components'
+import { Separator } from '@/primitives/Separator'
+import { SettingsMenuItem } from '@/features/rooms/livekit/components/controls/Options/SettingsMenuItem'
+import { FeedbackMenuItem } from '@/features/rooms/livekit/components/controls/Options/FeedbackMenuItem'
+import { EffectsMenuItem } from '@/features/rooms/livekit/components/controls/Options/EffectsMenuItem'
+import { SupportMenuItem } from '@/features/rooms/livekit/components/controls/Options/SupportMenuItem'
+import { PictureInPictureMenuItem } from '@/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem'
+import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
+
+/**
+ * PiP options menu items: excludes transcript, screen recording, and full screen
+ * (those features are not relevant in the PiP window context).
+ */
+export const PipOptionsMenuItems = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/src/frontend/src/features/pip/stores/pipLayoutStore.ts b/src/frontend/src/features/pip/stores/pipLayoutStore.ts
new file mode 100644
index 00000000..e67253a6
--- /dev/null
+++ b/src/frontend/src/features/pip/stores/pipLayoutStore.ts
@@ -0,0 +1,17 @@
+import { proxy } from 'valtio'
+import type { PanelId, SubPanelId } from '@/features/rooms/livekit/types/panel'
+
+type PipLayoutState = {
+ activePanelId: PanelId | null
+ activeSubPanelId: SubPanelId | null
+}
+
+/**
+ * Separate layout store for the PiP window.
+ * Decouples PiP side panel state from the main view so opening Chat/Info/etc.
+ * in PiP does not affect the main window and vice versa.
+ */
+export const pipLayoutStore = proxy({
+ activePanelId: null,
+ activeSubPanelId: null,
+})
diff --git a/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx b/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx
index 900ca9f2..efd80688 100644
--- a/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx
+++ b/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx
@@ -1,4 +1,3 @@
-import { layoutStore } from '@/stores/layout'
import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
@@ -6,7 +5,7 @@ import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
-import { useSidePanel } from '../hooks/useSidePanel'
+import { type SidePanelStore, useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects'
@@ -144,7 +143,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null}
)
-export const SidePanel = () => {
+export const SidePanel = ({ store }: { store?: SidePanelStore }) => {
const {
activePanelId,
isParticipantsOpen,
@@ -156,7 +155,9 @@ export const SidePanel = () => {
isInfoOpen,
isSubPanelOpen,
activeSubPanelId,
- } = useSidePanel()
+ closePanel,
+ goBack,
+ } = useSidePanel(store)
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const title = t(`heading.${activeSubPanelId || activePanelId}`)
@@ -166,10 +167,7 @@ export const SidePanel = () => {
{
- layoutStore.activePanelId = null
- layoutStore.activeSubPanelId = null
- }}
+ onClose={closePanel}
closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`),
})}
@@ -177,7 +175,7 @@ export const SidePanel = () => {
isSubmenu={isSubPanelOpen}
isReactionToolbarOpen={isReactionToolbarOpen}
backButtonLabel={t('backToTools')}
- onBack={() => (layoutStore.activeSubPanelId = null)}
+ onBack={goBack}
>
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/EffectsMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/EffectsMenuItem.tsx
index 98a2e7d3..4ee37209 100644
--- a/src/frontend/src/features/rooms/livekit/components/controls/Options/EffectsMenuItem.tsx
+++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/EffectsMenuItem.tsx
@@ -2,11 +2,11 @@ import { RiImageCircleAiFill } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
-import { useSidePanel } from '../../../hooks/useSidePanel'
+import { type SidePanelStore, useSidePanel } from '../../../hooks/useSidePanel'
-export const EffectsMenuItem = () => {
+export const EffectsMenuItem = ({ store }: { store?: SidePanelStore }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
- const { toggleEffects } = useSidePanel()
+ const { toggleEffects } = useSidePanel(store)
return (