diff --git a/src/frontend/src/features/pip/components/PictureInPictureConference.tsx b/src/frontend/src/features/pip/components/PictureInPictureConference.tsx
new file mode 100644
index 00000000..7a528176
--- /dev/null
+++ b/src/frontend/src/features/pip/components/PictureInPictureConference.tsx
@@ -0,0 +1,8 @@
+import { PictureInPicturePortal } from '@/features/pip/components/PictureInPicturePortal'
+import { PipView } from '@/features/pip/components/PipView'
+
+export const PictureInPictureConference = () => (
+
+
+
+)
diff --git a/src/frontend/src/features/pip/components/PipControlBar.tsx b/src/frontend/src/features/pip/components/PipControlBar.tsx
new file mode 100644
index 00000000..4bf8a973
--- /dev/null
+++ b/src/frontend/src/features/pip/components/PipControlBar.tsx
@@ -0,0 +1,64 @@
+import { styled } from '@/styled-system/jsx'
+import { useTranslation } from 'react-i18next'
+import { AudioDevicesControl } from '@/features/rooms/livekit/components/controls/Device/AudioDevicesControl'
+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 { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
+import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
+import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
+
+export const PipControlBar = ({
+ showScreenShare,
+}: {
+ showScreenShare: boolean
+}) => {
+ const { t } = useTranslation('rooms', {
+ keyPrefix: 'pictureInPicture',
+ })
+
+ return (
+
+
+
+
+
+ {showScreenShare && }
+
+
+
+
+
+ )
+}
+
+const PipControls = styled('div', {
+ base: {
+ flex: '0 0 auto',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '0.5rem',
+ padding: '1.125rem',
+ width: '100%',
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ },
+})
+
+const PipControlsCenter = styled('div', {
+ base: {
+ display: 'flex',
+ flexWrap: 'nowrap',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '0.4rem',
+ flex: '1 1 auto',
+ },
+})
diff --git a/src/frontend/src/features/pip/components/PipFloatingReactions.tsx b/src/frontend/src/features/pip/components/PipFloatingReactions.tsx
new file mode 100644
index 00000000..e612d6b0
--- /dev/null
+++ b/src/frontend/src/features/pip/components/PipFloatingReactions.tsx
@@ -0,0 +1,46 @@
+import { useSnapshot } from 'valtio'
+import { reactionsStore } from '@/stores/reactions'
+import { useMemo, useRef } from 'react'
+import { FloatingReaction } from '@/features/reactions/components/ReactionPortals'
+import { Reaction } from '@/features/reactions/types'
+import { css } from '@/styled-system/css'
+
+export const PipFloatingReactions = () => {
+ const { reactions } = useSnapshot(reactionsStore)
+
+ return (
+ <>
+ {reactions.map((reaction) => (
+
+ ))}
+ >
+ )
+}
+
+const PipFloatingReaction = ({ reaction }: { reaction: Reaction }) => {
+ const containerRef = useRef(null)
+ const speed = useMemo(() => Math.random() * 1.5 + 0.5, [])
+ const scale = useMemo(() => Math.max(Math.random() + 0.5, 1), [])
+ return (
+
+
+
+ )
+}
diff --git a/src/frontend/src/features/pip/components/PipView.tsx b/src/frontend/src/features/pip/components/PipView.tsx
new file mode 100644
index 00000000..da463acc
--- /dev/null
+++ b/src/frontend/src/features/pip/components/PipView.tsx
@@ -0,0 +1,42 @@
+import { PipControlBar } from './PipControlBar'
+import { PipFloatingReactions } from './PipFloatingReactions'
+import { ReactionsToolbar } from '@/features/reactions/components/toolbar/ReactionsToolbar'
+import { styled } from '@/styled-system/jsx'
+
+const Container = styled('div', {
+ base: {
+ position: 'relative',
+ width: '100%',
+ height: '100%',
+ display: 'grid',
+ gridTemplateRows: 'minmax(0, 1fr) auto auto',
+ backgroundColor: 'primaryDark.50',
+ // Disable LiveKit's own border-radius on tiles so our containers
+ // (GridCell, Thumbnail, StageFrame) own the clipping exclusively.
+ '--lk-border-radius': '4px',
+ '& .lk-participant-tile': {
+ height: '100%',
+ },
+ '& .lk-participant-media': {
+ height: '100%',
+ },
+ '& .lk-participant-media-video': {
+ height: '100%',
+ objectFit: 'cover',
+ },
+ '& .lk-grid-layout': {
+ height: '100%',
+ width: '100%',
+ },
+ },
+})
+
+export const PipView = () => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx
index fe7d9090..81a7cc15 100644
--- a/src/frontend/src/features/rooms/components/Conference.tsx
+++ b/src/frontend/src/features/rooms/components/Conference.tsx
@@ -32,8 +32,7 @@ import { isFireFox } from '@/utils/livekit'
import { useIsMobile } from '@/utils/useIsMobile'
import { navigateTo } from '@/navigation/navigateTo'
import { connectionObserverStore } from '@/stores/connectionObserver'
-import { PictureInPicturePortal } from '@/features/pip/components/PictureInPicturePortal'
-import { Spinner } from '@/primitives/Spinner'
+import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
export const Conference = ({
roomId,
@@ -293,9 +292,7 @@ export const Conference = ({
{...mediaDeviceError}
onClose={() => setMediaDeviceError({ error: null, kind: null })}
/>
-
-
-
+
diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json
index 4389e5ef..07eadd20 100644
--- a/src/frontend/src/locales/de/rooms.json
+++ b/src/frontend/src/locales/de/rooms.json
@@ -236,7 +236,8 @@
"title": "Ihr Videoanruf befindet sich in einem anderen Fenster.",
"description": "Im Bild-im-Bild-Modus bleiben Sie mit dem Anruf verbunden, während Sie andere Aufgaben erledigen.",
"bringBack": "Anruf hierher zurückholen"
- }
+ },
+ "controlBar": "Besprechungssteuerung"
},
"options": {
"buttonLabel": "Weitere Optionen",
diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json
index c7486c9d..1c724203 100644
--- a/src/frontend/src/locales/en/rooms.json
+++ b/src/frontend/src/locales/en/rooms.json
@@ -236,7 +236,8 @@
"title": "Your video call is in another window.",
"description": "Picture-in-Picture mode allows you to stay connected to the call while performing other tasks.",
"bringBack": "Bring the call back here"
- }
+ },
+ "controlBar": "Meeting controls"
},
"options": {
"buttonLabel": "More Options",
diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json
index f3864224..0f03f877 100644
--- a/src/frontend/src/locales/fr/rooms.json
+++ b/src/frontend/src/locales/fr/rooms.json
@@ -236,7 +236,8 @@
"title": "Votre appel vidéo est dans une autre fenêtre.",
"description": "Le mode image dans l'image vous permet de rester connecté à l'appel tout en effectuant d'autres tâches.",
"bringBack": "Ramener l'appel ici"
- }
+ },
+ "controlBar": "Commandes de la réunion"
},
"options": {
"buttonLabel": "Plus d'options",
diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json
index 19d113b7..b6734794 100644
--- a/src/frontend/src/locales/nl/rooms.json
+++ b/src/frontend/src/locales/nl/rooms.json
@@ -236,7 +236,8 @@
"title": "Uw videogesprek bevindt zich in een ander venster.",
"description": "Met de beeld-in-beeld-modus kunt u verbonden blijven met het gesprek terwijl u andere taken uitvoert.",
"bringBack": "Gesprek hier terughalen"
- }
+ },
+ "controlBar": "Vergaderbesturing"
},
"options": {
"buttonLabel": "Meer opties",