Compare commits

..

24 Commits

Author SHA1 Message Date
Cyril c6d288574a ♻️(frontend) decouple pip state and simplify pip options
Prevent PiP actions from affecting main panel state and reduce PiP-only noise.
2026-02-13 11:21:44 +01:00
Cyril e0e6a205ac (perf) hide main layout when pip is open
avoid rendering main video tiles while pip is active
2026-02-11 09:20:56 +01:00
Cyril 9803e17a13 ♻️(refactor) move pip feature into dedicated module
group pip components and hooks under features/pip for isolation
2026-02-11 09:09:47 +01:00
Cyril 45cf8a425a (feat) move pip state to valtio store
replace context provider with shared store for pip ui state
2026-02-10 14:13:00 +01:00
Cyril 67dd60cc20 🐛(frontend) clamp pip tooltips on right edge
prevent pip tooltips from overflowing the right side of the window
2026-02-10 13:50:40 +01:00
Cyril 401480f41f fixup! 🌐(i18n) add PiP labels for each supported language 2026-02-10 13:50:39 +01:00
Cyril 68f62aebd9 fixup! (feat) connect PiP to room and options menu 2026-02-10 13:50:39 +01:00
Cyril cdba69c607 fixup! 🐛(fix) add react-aria overlays type shim 2026-02-10 13:50:39 +01:00
Cyril 84b7402404 (feat) connect PiP to room and options menu
wire room-level PiP state to the options menu
2026-02-10 13:50:38 +01:00
Cyril bc61134b28 💄(style) align pip layout with room styling
match pip background and spacing to
2026-02-10 13:50:38 +01:00
Cyril 986f945a20 💄(style) adjust tooltip positioning and fix ts error
raise tooltip offset and set arrowBoundaryOffset
2026-02-10 13:50:04 +01:00
Cyril 4fa8998eb2 🐛(fix) add react-aria overlays type shim
fix missing useUNSAFE_PortalContext export typing
2026-02-10 13:50:04 +01:00
Cyril e3ce5677a7 (feat) add pip lateral menu component
add pip chevron menu
2026-02-10 13:50:03 +01:00
Cyril 83265698fa 📝(docs) add pip component documentation
add jsdoc comments to pip components and hooks
2026-02-10 13:50:03 +01:00
Cyril f02135e902 🐛(fix) close pip menu on item action
close options menu when selecting transcription or
2026-02-10 13:50:03 +01:00
Cyril 3634f2b57d ♻️(refactor) extract pip control bar component
centralize pip controls in reusable component
2026-02-10 13:50:02 +01:00
Cyril 56591a3d4c ♻️(feat) extract pip options menu component
keep pip options layout reusable and isolated
2026-02-10 13:50:02 +01:00
Cyril 49afe8aa49 💄(fix) align overlays in pip
route menus/tooltips to the pip document
2026-02-10 13:50:02 +01:00
Cyril aa2f8ee4b2 (refactor) stabilize pip layout and controls
avoid layout jumps and prepare for more buttons
2026-02-10 13:50:01 +01:00
Cyril 0cf5d2cfe5 🐛(fix) rebuild pip root on reopen
prevent black screen after closing and reopening pip
2026-02-10 13:50:01 +01:00
Cyril c866e75265 🌐(i18n) add PiP labels for each supported language
add labels for PiP in each supported language
2026-02-10 13:50:01 +01:00
Cyril df42a543a2 (feat) connect PiP to room and options menu
connecting pip and room
2026-02-10 13:50:01 +01:00
Cyril 89cf09f3fd (feat) add pip portal and base layout
mount react content into pip window with styles
2026-02-10 13:50:00 +01:00
Cyril e2f06d82a0 (feat) add document pip hook
utility hook to cleanly open/close the pip window
2026-02-10 13:50:00 +01:00
70 changed files with 1263 additions and 936 deletions
+2 -24
View File
@@ -8,34 +8,12 @@ and this project adheres to
## [Unreleased]
### Changed
- 🔒️(agents) uninstall pip from the agents image
- 🔒️(summary) switch to Alpine base image
- 🔒️(backend) uninstall pip in the production image
### Fixed
- 🔒️(agents) upgrade OpenSSL to address CVE-2025-15467
- 📌(agents) pin protobuf to 6.33.5 to fix CVE-2026-0994
## [1.7.0] - 2026-02-19
### Added
- ✨(frontend) expose Windows app web link #976
- ✨(frontend) support additional shortcuts to broaden accessibility
### Changed
- ✨(frontend) add clickable settings general link in idle modal #974
## [1.6.0] - 2026-02-10
### Added
- ✨(backend) monitor throttling rate failure through sentry #964
- 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957
- ✨(feat) Introduce Picture-in-Picture (PiP) #890
### Changed
-3
View File
@@ -127,9 +127,6 @@ ARG MEET_STATIC_ROOT=/data/static
RUN mkdir -p /usr/local/etc/gunicorn
COPY docker/files/usr/local/etc/gunicorn/meet.py /usr/local/etc/gunicorn/meet.py
# Remove pip to reduce attack surface in production
RUN pip uninstall -y pip
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
+1 -5
View File
@@ -18,7 +18,6 @@ docker_build(
'localhost:5001/meet-backend:latest',
context='..',
dockerfile='../Dockerfile',
build_args={'DOCKER_USER': '1001:127'},
only=['./src/backend', './src/mail', './docker'],
target = 'backend-production',
live_update=[
@@ -34,7 +33,6 @@ clean_old_images('localhost:5001/meet-backend')
docker_build(
'localhost:5001/meet-frontend-dinum:latest',
context='..',
build_args={'DOCKER_USER': '1001:127'},
dockerfile='../docker/dinum-frontend/Dockerfile',
only=['./src/frontend', './docker', './.dockerignore'],
target = 'frontend-production',
@@ -59,7 +57,6 @@ clean_old_images('localhost:5001/meet-frontend-generic')
docker_build(
'localhost:5001/meet-summary:latest',
context='../src/summary',
build_args={'DOCKER_USER': '1001:127'},
dockerfile='../src/summary/Dockerfile',
only=['.'],
target = 'production',
@@ -72,9 +69,8 @@ clean_old_images('localhost:5001/meet-summary')
docker_build(
'localhost:5001/meet-agents:latest',
context='../src/agents',
build_args={'DOCKER_USER': '1001:127'},
dockerfile='../src/agents/Dockerfile',
only=['.'],
only=['.'],
target = 'production',
live_update=[
sync('../src/agents', '/app'),
@@ -1,9 +1,9 @@
upstream meet_backend {
server ${BACKEND_INTERNAL_HOST}:8000 fail_timeout=0;
server ${BACKEND_HOST}:8000 fail_timeout=0;
}
upstream meet_frontend {
server ${FRONTEND_INTERNAL_HOST}:8080 fail_timeout=0;
server ${FRONTEND_HOST}:8080 fail_timeout=0;
}
server {
+1 -2
View File
@@ -20,8 +20,8 @@ services:
user: ${DOCKER_USER:-1000}
restart: always
env_file:
- .env
- env.d/common
- env.d/backend
- env.d/postgresql
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
@@ -45,7 +45,6 @@ services:
- /docker-entrypoint.sh
command: ["nginx", "-g", "daemon off;"]
env_file:
- .env
- env.d/common
# Uncomment and set your values if using our nginx proxy example
# environment:
-5
View File
@@ -4,8 +4,6 @@ FROM python:3.13-slim AS base
RUN apt-get update && apt-get install -y \
libglib2.0-0 \
libgobject-2.0-0 \
"openssl=3.5.4-1~deb13u2" \
"libssl3t64=3.5.4-1~deb13u2" \
&& rm -rf /var/lib/apt/lists/*
FROM base AS builder
@@ -21,9 +19,6 @@ FROM base AS production
WORKDIR /app
# Remove pip to reduce attack surface in production
RUN pip uninstall -y pip
ARG DOCKER_USER
USER ${DOCKER_USER}
+2 -3
View File
@@ -1,15 +1,14 @@
[project]
name = "agents"
version = "1.7.0"
version = "1.5.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.3.10",
"livekit-plugins-deepgram==1.3.10",
"livekit-plugins-silero==1.3.10",
"livekit-plugins-kyutai-lasuite==0.0.6",
"python-dotenv==1.2.1",
"protobuf==6.33.5"
"python-dotenv==1.2.1"
]
[project.optional-dependencies]
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "1.7.0"
version = "1.5.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+18
View File
@@ -0,0 +1,18 @@
import type * as React from 'react';
declare module '@react-aria/overlays' {
export type PortalProviderContextValue = {
getContainer: () => HTMLElement | null;
};
export type PortalProviderProps = {
getContainer: () => HTMLElement | null;
children: React.ReactNode;
};
export function useUNSAFE_PortalContext(): PortalProviderContextValue;
export function UNSAFE_PortalProvider(
props: PortalProviderProps,
): JSX.Element;
}
-6
View File
@@ -5,12 +5,6 @@ server {
root /usr/share/nginx/html;
location = /.wellknown/windows-app-web-link {
default_type application/json;
alias /usr/share/nginx/html/.wellknown/windows-app-web-link;
add_header Content-Disposition "attachment; filename=windows-app-web-link";
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
+154 -515
View File
@@ -1,42 +1,42 @@
{
"name": "meet",
"version": "1.7.0",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.7.0",
"version": "1.5.0",
"dependencies": {
"@fontsource-variable/material-symbols-outlined": "5.2.34",
"@fontsource-variable/material-symbols-outlined": "5.2.30",
"@fontsource/material-icons-outlined": "5.2.6",
"@livekit/components-react": "2.9.13",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.6.1",
"@pandacss/preset-panda": "0.54.0",
"@react-aria/toast": "3.0.10",
"@react-types/overlays": "3.9.3",
"@react-aria/toast": "3.0.5",
"@react-types/overlays": "3.9.0",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.81.5",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.0.27",
"crisp-sdk-web": "1.0.25",
"derive-valtio": "0.2.0",
"hoofd": "1.7.3",
"humanize-duration": "3.33.2",
"i18next": "25.8.4",
"humanize-duration": "3.33.0",
"i18next": "25.3.1",
"i18next-browser-languagedetector": "8.2.0",
"i18next-parser": "9.3.0",
"i18next-resources-to-backend": "1.2.1",
"libphonenumber-js": "1.12.10",
"livekit-client": "2.15.7",
"posthog-js": "1.342.1",
"posthog-js": "1.256.2",
"react": "18.3.1",
"react-aria-components": "1.10.1",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"use-sound": "5.0.0",
"valtio": "2.3.0",
"wouter": "3.9.0"
"valtio": "2.1.5",
"wouter": "3.7.1"
},
"devDependencies": {
"@pandacss/dev": "0.54.0",
@@ -55,7 +55,7 @@
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.6",
"prettier": "3.8.1",
"prettier": "3.6.2",
"typescript": "5.8.3",
"vite": "7.0.8",
"vite-tsconfig-paths": "5.1.4"
@@ -363,9 +363,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1021,9 +1021,9 @@
"license": "MIT"
},
"node_modules/@fontsource-variable/material-symbols-outlined": {
"version": "5.2.34",
"resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-outlined/-/material-symbols-outlined-5.2.34.tgz",
"integrity": "sha512-FfJVoKdYlxvIHflVZlrevf6e1WmbJGeAdbcLmOmkftoP2Du0qrRD/SmNchIXijsKXF7j3z1m++e92Qi6/U+jRA==",
"version": "5.2.30",
"resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-outlined/-/material-symbols-outlined-5.2.30.tgz",
"integrity": "sha512-BjSx7nqvISJs2Pjd8sBH583AnD4k6dD4Em7AVISoLXzbX3PIFWAE2GPm13LlCys8u8idkyUd62L8yn6ts6DdbA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -1158,9 +1158,9 @@
"dev": true
},
"node_modules/@internationalized/date": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.11.0.tgz",
"integrity": "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz",
"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
@@ -1177,9 +1177,9 @@
}
},
"node_modules/@internationalized/number": {
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz",
"integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==",
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz",
"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
@@ -1354,252 +1354,6 @@
"node": ">= 8"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/api-logs": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.3.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/core": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
"integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/otlp-exporter-base": "0.208.0",
"@opentelemetry/otlp-transformer": "0.208.0",
"@opentelemetry/sdk-logs": "0.208.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-exporter-base": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/otlp-transformer": "0.208.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-transformer": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
"@opentelemetry/sdk-logs": "0.208.0",
"@opentelemetry/sdk-metrics": "2.2.0",
"@opentelemetry/sdk-trace-base": "2.2.0",
"protobufjs": "^7.3.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz",
"integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.5.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz",
"integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@pandacss/config": {
"version": "0.54.0",
"resolved": "https://registry.npmjs.org/@pandacss/config/-/config-0.54.0.tgz",
@@ -1992,85 +1746,6 @@
"resolved": "https://registry.npmjs.org/@pandacss/types/-/types-0.54.0.tgz",
"integrity": "sha512-5kspg2UOgFWrawbHeoleZvbZ6id/kIBLHoDeD1CnLbl9fEbZAZ3Avi5Hi1mIFe9E8PFzp9V+N9IfQL7pYhavfA=="
},
"node_modules/@posthog/core": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.20.1.tgz",
"integrity": "sha512-uoTmWkYCtLYFpiK37/JCq+BuCA/OZn1qQZn5cPv1EEKt3ni3Zgg48xWCnSEyGFl5KKSXlfCruiRTwnbAtCgrBA==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/@posthog/types": {
"version": "1.342.1",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.342.1.tgz",
"integrity": "sha512-bcyBdO88FWTkd5AVTa4Nu8T7RfY0WJrG7WMCXum/rcvNjYhS3DmOfKf8o/Bt56vA3J3yeU0vbgrmltYVoTAfaA==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@react-aria/autocomplete": {
"version": "3.0.0-beta.5",
"resolved": "https://registry.npmjs.org/@react-aria/autocomplete/-/autocomplete-3.0.0-beta.5.tgz",
@@ -2421,18 +2096,18 @@
}
},
"node_modules/@react-aria/i18n": {
"version": "3.12.15",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.15.tgz",
"integrity": "sha512-3CrAN7ORVHrckvTmbPq76jFZabqq+rScosGT5+ElircJ5rF5+JcdT99Hp5Xg6R10jk74e8G3xiqdYsUd+7iJMA==",
"version": "3.12.10",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.10.tgz",
"integrity": "sha512-1j00soQ2W0nTgzaaIsGFdMF/5aN60AEdCJPhmXGZiuWdWzMxObN9LQ9vdzYPTjTqyqMdSaSp9DZKs5I26Xovpw==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.11.0",
"@internationalized/date": "^3.8.2",
"@internationalized/message": "^3.1.8",
"@internationalized/number": "^3.6.5",
"@internationalized/number": "^3.6.3",
"@internationalized/string": "^3.2.7",
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.33.0",
"@react-types/shared": "^3.33.0",
"@react-aria/ssr": "^3.9.9",
"@react-aria/utils": "^3.29.1",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
@@ -2441,15 +2116,15 @@
}
},
"node_modules/@react-aria/interactions": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.0.tgz",
"integrity": "sha512-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA==",
"version": "3.25.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz",
"integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.33.0",
"@react-aria/ssr": "^3.9.9",
"@react-aria/utils": "^3.29.1",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.33.0",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
@@ -2473,15 +2148,15 @@
}
},
"node_modules/@react-aria/landmark": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.9.tgz",
"integrity": "sha512-YYyluDBCXupnMh91ccE5g27fczjYmzPebHqTkVYjH4B6k45pOoqsMmWBCMnOTl0qOCeioI+daT8W0MamAZzoSw==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.4.tgz",
"integrity": "sha512-1U5ce6cqg1qGbK4M4R6vwrhUrKXuUzReZwHaTrXxEY22IMxKDXIZL8G7pFpcKix2XKqjLZWf+g8ngGuNhtQ2QQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/utils": "^3.33.0",
"@react-types/shared": "^3.33.0",
"@react-aria/utils": "^3.29.1",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.6.0"
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
@@ -2782,9 +2457,9 @@
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz",
"integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
@@ -2904,18 +2579,18 @@
}
},
"node_modules/@react-aria/toast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.10.tgz",
"integrity": "sha512-irW5Cr4msbPo4A4ysjT70MDJbpGCe1h9SkFgdYXBPA4Xbi4jRT7TiEZeIS1I7Hsvp6shAK1Ld/m6NBS0b/gyzg==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.5.tgz",
"integrity": "sha512-uhwiZqPy6hqucBUL7z6uUZjAJ/ou3bNdTjZlXS+zbcm+T0dsjKDfzNkaebyZY7AX3cYkFCaRjc3N6omXwoAviw==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.15",
"@react-aria/interactions": "^3.27.0",
"@react-aria/landmark": "^3.0.9",
"@react-aria/utils": "^3.33.0",
"@react-stately/toast": "^3.1.3",
"@react-types/button": "^3.15.0",
"@react-types/shared": "^3.33.0",
"@react-aria/i18n": "^3.12.10",
"@react-aria/interactions": "^3.25.3",
"@react-aria/landmark": "^3.0.4",
"@react-aria/utils": "^3.29.1",
"@react-stately/toast": "^3.1.1",
"@react-types/button": "^3.12.2",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
@@ -2997,15 +2672,15 @@
}
},
"node_modules/@react-aria/utils": {
"version": "3.33.0",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.0.tgz",
"integrity": "sha512-yvz7CMH8d2VjwbSa5nGXqjU031tYhD8ddax95VzJsHSPyqHDEGfxul8RkhGV6oO7bVqZxVs6xY66NIgae+FHjw==",
"version": "3.29.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz",
"integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/ssr": "^3.9.9",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.11.0",
"@react-types/shared": "^3.33.0",
"@react-stately/utils": "^3.10.7",
"@react-types/shared": "^3.30.0",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
@@ -3437,13 +3112,13 @@
}
},
"node_modules/@react-stately/toast": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.3.tgz",
"integrity": "sha512-mT9QJKmD523lqFpOp0VWZ6QHZENFK7HrodnNJDVc7g616s5GNmemdlkITV43fSY3tHeThCVvPu+Uzh7RvQ9mpQ==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.1.tgz",
"integrity": "sha512-W4a6xcsFt/E+aHmR2eZK+/p7Y5rdyXSCQ5gKSnbck+S3lijEWAyV45Mv8v95CQqu0bQijj6sy2Js1szq10HVwg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.6.0"
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
@@ -3495,9 +3170,9 @@
}
},
"node_modules/@react-stately/utils": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz",
"integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==",
"version": "3.10.7",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz",
"integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
@@ -3549,12 +3224,12 @@
}
},
"node_modules/@react-types/button": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.15.0.tgz",
"integrity": "sha512-X/K2/Oeuq7Hi8nMIzx4/YlZuvWFiSOHZt27p4HmThCnNO/9IDFPmvPrpkYjWN5eN9Nuk+P5vZUb4A7QJgYpvGA==",
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.12.2.tgz",
"integrity": "sha512-QLoSCX8E7NFIdkVMa65TPieve0rKeltfcIxiMtrphjfNn+83L0IHMcbhjf4r4W19c/zqGbw3E53Hx8mNukoTUw==",
"license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.33.0"
"@react-types/shared": "^3.30.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
@@ -3724,12 +3399,12 @@
}
},
"node_modules/@react-types/overlays": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.3.tgz",
"integrity": "sha512-LzetThNNk8T26pQRbs1I7+isuFhdFYREy7wJCsZmbB0FnZgCukGTfOtThZWv+ry11veyVJiX68jfl4SV6ACTWA==",
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.0.tgz",
"integrity": "sha512-T2DqMcDN5p8vb4vu2igoLrAtuewaNImLS8jsK7th7OjwQZfIWJn5Y45jSxHtXJUddEg1LkUjXYPSXCMerMcULw==",
"license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.33.0"
"@react-types/shared": "^3.31.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
@@ -3785,9 +3460,9 @@
}
},
"node_modules/@react-types/shared": {
"version": "3.33.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.0.tgz",
"integrity": "sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw==",
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.31.0.tgz",
"integrity": "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
@@ -4348,6 +4023,7 @@
"version": "22.16.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.0.tgz",
"integrity": "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -4386,13 +4062,6 @@
"resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz",
"integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA=="
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
@@ -5420,16 +5089,15 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/crisp-sdk-web": {
"version": "1.0.27",
"resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.0.27.tgz",
"integrity": "sha512-aNWR3te65YiaVFu/iwdqOo3cyUBZHUheE4d6EtgQu/T18jh/9SpoYXjXF/OzUD3Cqy0pGryoqtuy5gxD8tqX9Q==",
"license": "MIT"
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.0.25.tgz",
"integrity": "sha512-CWTHFFeHRV0oqiXoPh/aIAKhFs6xcIM4NenGPnClAMCZUDQgQsF1OWmZWmnVNjJriXUmWRgDfeUxcxygS0dCRA=="
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -5708,15 +5376,6 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
@@ -6906,18 +6565,15 @@
}
},
"node_modules/humanize-duration": {
"version": "3.33.2",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.2.tgz",
"integrity": "sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ==",
"license": "Unlicense",
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
}
"version": "3.33.0",
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.0.tgz",
"integrity": "sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ==",
"license": "Unlicense"
},
"node_modules/i18next": {
"version": "25.8.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.4.tgz",
"integrity": "sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==",
"version": "25.3.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.1.tgz",
"integrity": "sha512-S4CPAx8LfMOnURnnJa8jFWvur+UX/LWcl6+61p9VV7SK2m0445JeBJ6tLD0D5SR0H29G4PYfWkEhivKG5p4RDg==",
"funding": [
{
"type": "individual",
@@ -6934,7 +6590,7 @@
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4"
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
@@ -7428,7 +7084,8 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/javascript-stringify": {
"version": "2.1.0",
@@ -7905,12 +7562,6 @@
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/look-it-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/look-it-up/-/look-it-up-2.1.0.tgz",
@@ -8088,7 +7739,7 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "github",
@@ -8392,6 +8043,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -8417,7 +8069,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"devOptional": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -8464,7 +8116,7 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -8609,31 +8261,33 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.342.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.342.1.tgz",
"integrity": "sha512-mMnQhWuKj4ejFicLtFzr52InmqploOyW1eInqXBkaVqE1DPhczBDmwsd9MSggY8kv0EXm8zgK+2tzBJUKcX5yg==",
"version": "1.256.2",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.256.2.tgz",
"integrity": "sha512-ypepnUHr33i5a1Uk39mozZXXTENRPC17HCG3WHKK6aRcpNwNs8uEqXaIKICGNM+qre+totKeTgl0WoaUFYmyoQ==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.208.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.20.1",
"@posthog/types": "1.342.1",
"core-js": "^3.38.1",
"dompurify": "^3.3.1",
"fflate": "^0.4.8",
"preact": "^10.28.2",
"query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^5.1.0"
"preact": "^10.19.3",
"web-vitals": "^4.2.4"
},
"peerDependencies": {
"@rrweb/types": "2.0.0-alpha.17",
"rrweb-snapshot": "2.0.0-alpha.17"
},
"peerDependenciesMeta": {
"@rrweb/types": {
"optional": true
},
"rrweb-snapshot": {
"optional": true
}
}
},
"node_modules/preact": {
"version": "10.28.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
"license": "MIT",
"version": "10.24.0",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.0.tgz",
"integrity": "sha512-aK8Cf+jkfyuZ0ZZRG9FbYqwmEiGQ4y/PUO4SuTWoyWL244nZZh7bd5h2APd4rSNDYTBNghg1L+5iJN3Skxtbsw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -8649,9 +8303,9 @@
}
},
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -8677,30 +8331,6 @@
"node": "10.* || >= 12.*"
}
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
@@ -8716,12 +8346,6 @@
"node": ">=6"
}
},
"node_modules/query-selector-shadow-dom": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
"integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -9123,6 +8747,17 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rrweb-snapshot": {
"version": "2.0.0-alpha.17",
"resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz",
"integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"postcss": "^8.4.38"
}
},
"node_modules/rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -9280,6 +8915,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -9291,6 +8927,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -9356,7 +8993,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9903,6 +9540,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
@@ -9976,9 +9614,9 @@
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -10005,9 +9643,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/valtio": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.3.0.tgz",
"integrity": "sha512-1MfKNcmOIdBSatiJsYgw420n6jnD+jeoI0V+RkOQbCB0ElLh6GKUfPr0hc9uq/KBGeghivDEarRsKFFdSQQnKw==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz",
"integrity": "sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==",
"license": "MIT",
"dependencies": {
"proxy-compare": "^3.0.1"
@@ -10293,9 +9931,9 @@
}
},
"node_modules/web-vitals": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
},
"node_modules/webrtc-adapter": {
@@ -10333,6 +9971,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -10398,9 +10037,9 @@
}
},
"node_modules/wouter": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.9.0.tgz",
"integrity": "sha512-sF/od/PIgqEQBQcrN7a2x3MX6MQE6nW0ygCfy9hQuUkuB28wEZuu/6M5GyqkrrEu9M6jxdkgE12yDFsQMKos4Q==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.7.1.tgz",
"integrity": "sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==",
"license": "Unlicense",
"dependencies": {
"mitt": "^3.0.1",
+11 -11
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.7.0",
"version": "1.5.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,35 +13,35 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@fontsource-variable/material-symbols-outlined": "5.2.34",
"@fontsource-variable/material-symbols-outlined": "5.2.30",
"@fontsource/material-icons-outlined": "5.2.6",
"@livekit/components-react": "2.9.13",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.6.1",
"@pandacss/preset-panda": "0.54.0",
"@react-aria/toast": "3.0.10",
"@react-types/overlays": "3.9.3",
"@react-aria/toast": "3.0.5",
"@react-types/overlays": "3.9.0",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.81.5",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.0.27",
"crisp-sdk-web": "1.0.25",
"derive-valtio": "0.2.0",
"hoofd": "1.7.3",
"humanize-duration": "3.33.2",
"i18next": "25.8.4",
"humanize-duration": "3.33.0",
"i18next": "25.3.1",
"i18next-browser-languagedetector": "8.2.0",
"i18next-parser": "9.3.0",
"i18next-resources-to-backend": "1.2.1",
"libphonenumber-js": "1.12.10",
"livekit-client": "2.15.7",
"posthog-js": "1.342.1",
"posthog-js": "1.256.2",
"react": "18.3.1",
"react-aria-components": "1.10.1",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"use-sound": "5.0.0",
"valtio": "2.3.0",
"wouter": "3.9.0"
"valtio": "2.1.5",
"wouter": "3.7.1"
},
"devDependencies": {
"@pandacss/dev": "0.54.0",
@@ -60,7 +60,7 @@
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.6",
"prettier": "3.8.1",
"prettier": "3.6.2",
"typescript": "5.8.3",
"vite": "7.0.8",
"vite-tsconfig-paths": "5.1.4"
@@ -1,6 +0,0 @@
[
{
"packageFamilyName" : "Visio_g3z6ba6vek6vg",
"paths" : [ "*" ]
}
]
@@ -0,0 +1,193 @@
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'
// Minimal base styles so the PiP window renders correctly on first paint.
const ensureBaseStyles = (target: Document) => {
if (target.getElementById('pip-base-styles')) return
const style = target.createElement('style')
style.id = 'pip-base-styles'
style.textContent = `
html, body { margin: 0; padding: 0; height: 100%; background: #0b0f19; }
body { overflow: hidden; }
* { box-sizing: border-box; }
`
target.head.appendChild(style)
}
// Clone existing styles to keep the PiP window visually consistent.
const copyStyles = (source: Document, target: Document) => {
if (target.getElementById('pip-style-clone')) return
const marker = target.createElement('meta')
marker.id = 'pip-style-clone'
target.head.appendChild(marker)
source.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => {
const cloned = node.cloneNode(true) as HTMLElement
target.head.appendChild(cloned)
})
}
const syncThemeAttribute = (source: Document, target: Document) => {
const theme = source.documentElement.getAttribute('data-lk-theme')
if (theme) {
target.documentElement.setAttribute('data-lk-theme', theme)
}
}
const cssVarNameCacheByElement = new WeakMap<HTMLElement, string[]>()
const cssVarNameCacheByUri = new Map<string, string[]>()
const syncCssVariables = (source: Document, target: Document) => {
const sourceView = source.defaultView
if (!sourceView) return
const getCachedVarNames = () => {
const docEl = source.documentElement
if (!docEl) return []
const cachedByElement = cssVarNameCacheByElement.get(docEl)
if (cachedByElement) return cachedByElement
const cachedByUri = source.baseURI
? cssVarNameCacheByUri.get(source.baseURI)
: undefined
if (cachedByUri) return cachedByUri
const varNames = new Set<string>()
const collectVarsFrom = (element: HTMLElement | null) => {
if (!element) return
const styles = sourceView.getComputedStyle(element)
for (let i = 0; i < styles.length; i += 1) {
const property = styles[i]
if (property.startsWith('--')) {
varNames.add(property)
}
}
}
collectVarsFrom(source.documentElement)
collectVarsFrom(source.body)
const result = Array.from(varNames)
cssVarNameCacheByElement.set(docEl, result)
if (source.baseURI) {
cssVarNameCacheByUri.set(source.baseURI, result)
}
return result
}
const varNames = getCachedVarNames()
if (!varNames.length) return
const rootStyles = sourceView.getComputedStyle(source.documentElement)
const bodyStyles = source.body
? sourceView.getComputedStyle(source.body)
: null
varNames.forEach((property) => {
const bodyValue = bodyStyles?.getPropertyValue(property)
const value = bodyValue || rootStyles.getPropertyValue(property)
if (value) {
target.documentElement.style.setProperty(property, value)
}
})
}
/**
* React Portal that renders children into a Document Picture-in-Picture window.
* Handles PiP window lifecycle, style injection, React root management, and uses UNSAFE_PortalProvider
* to ensure React Aria overlays render correctly within the PiP window.
* Creates a fresh React root on reopen to prevent black screen issues.
*/
export const DocumentPiPPortal = ({
isOpen,
width,
height,
children,
onClose,
}: {
isOpen: boolean
width?: number
height?: number
children: React.ReactNode
onClose?: () => void
}): ReactNode => {
const { openPiP, closePiP, pipWindow, isSupported } = useDocumentPiP({
width,
height,
})
const [container, setContainer] = useState<HTMLElement | null>(null)
const containerRef = useRef<HTMLElement | null>(null)
useEffect(() => {
if (!isOpen) {
closePiP()
setContainer(null)
containerRef.current = null
return
}
if (!isSupported) return
let cancelled = false
openPiP().then((win) => {
if (!win || cancelled) return
const doc = win.document
ensureBaseStyles(doc)
copyStyles(document, doc)
syncThemeAttribute(document, doc)
syncCssVariables(document, doc)
const existingContainer = containerRef.current
if (!existingContainer || existingContainer.ownerDocument !== doc) {
const nextContainer = doc.createElement('div')
nextContainer.id = 'pip-root'
nextContainer.style.width = '100%'
nextContainer.style.height = '100%'
nextContainer.style.display = 'flex'
nextContainer.style.alignItems = 'stretch'
nextContainer.style.justifyContent = 'center'
doc.body.appendChild(nextContainer)
containerRef.current = nextContainer
setContainer(nextContainer)
} else {
setContainer(existingContainer)
}
})
return () => {
cancelled = true
}
}, [closePiP, isOpen, isSupported, openPiP])
useEffect(() => {
if (!pipWindow) return
const handleClose = () => {
// Reset container so reopening PiP mounts a fresh root.
containerRef.current = null
setContainer(null)
onClose?.()
}
pipWindow.addEventListener('pagehide', handleClose)
pipWindow.addEventListener('beforeunload', handleClose)
return () => {
pipWindow.removeEventListener('pagehide', handleClose)
pipWindow.removeEventListener('beforeunload', handleClose)
}
}, [onClose, pipWindow])
const portal = useMemo(() => {
if (!container) return null
return createPortal(
// "UNSAFE" because it bypasses react-aria's default portal container.
// We need it to target the PiP document; otherwise overlays render in the main window.
<UNSAFE_PortalProvider getContainer={() => container}>
{children}
</UNSAFE_PortalProvider>,
container
)
}, [children, container])
return portal as unknown as ReactNode
}
@@ -0,0 +1,59 @@
import { styled } from '@/styled-system/jsx'
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 { 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'
/**
* Compact control bar for the Picture-in-Picture window.
* Centralizes all PiP controls (devices, reactions, screen share, options, etc.) in one reusable component.
*/
export const PipControlBar = ({
showScreenShare,
}: {
showScreenShare: boolean
}) => (
<PipControls>
<PipControlsCenter>
<AudioDevicesControl hideMenu />
<VideoDeviceControl hideMenu />
<ReactionsToggle />
{showScreenShare && <ScreenShareToggle />}
<SubtitlesToggle />
<HandToggle />
<OptionsButton />
<LeaveButton />
<StartMediaButton />
</PipControlsCenter>
</PipControls>
)
const PipControls = styled('div', {
base: {
flex: '0 0 auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
backgroundColor: 'primaryDark.50',
width: '100%',
position: 'relative',
},
})
const PipControlsCenter = styled('div', {
base: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
gap: '0.4rem',
flex: '1 1 auto',
},
})
@@ -0,0 +1,110 @@
import { styled } from '@/styled-system/jsx'
import { supportsScreenSharing } from '@livekit/components-core'
import {
isTrackReference,
TrackReferenceOrPlaceholder,
} from '@livekit/components-core'
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 { SidePanel } from '@/features/rooms/livekit/components/SidePanel'
import { pipLayoutStore } from '../stores/pipLayoutStore'
import { PipControlBar } from './PipControlBar'
const pickTrackForPip = (
tracks: TrackReferenceOrPlaceholder[]
): TrackReferenceOrPlaceholder | undefined => {
// Prefer screen share when present; otherwise fallback to first available track.
const screenShareTrack = tracks
.filter(isTrackReference)
.find((track) => track.publication.source === Track.Source.ScreenShare)
if (screenShareTrack) return screenShareTrack
return tracks[0]
}
/**
* Main view component for the Picture-in-Picture window.
* Handles track selection (prioritizes screen share), layout switching (grid for multiple participants),
* and renders the control bar and side panel within the PiP window.
*/
export const PipView = () => {
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
],
{ onlySubscribed: false }
)
const trackRef = pickTrackForPip(tracks)
const browserSupportsScreenSharing = supportsScreenSharing()
const hasMultipleTiles = tracks.length > 1
if (!trackRef && !hasMultipleTiles) return null
return (
<PipContainer>
{/* Keep stage height stable to avoid layout shifting on track changes. */}
<PipStage>
{hasMultipleTiles ? (
<PipGridWrapper>
<GridLayout tracks={tracks} style={{ height: '100%' }}>
<ParticipantTile disableMetadata />
</GridLayout>
</PipGridWrapper>
) : (
<ParticipantTile trackRef={trackRef} disableMetadata />
)}
</PipStage>
{/* Compact control bar for PiP; extend here when adding more actions. */}
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
{/* Side panel (effects, settings, etc.) opens within PiP window. */}
<SidePanel store={pipLayoutStore} />
</PipContainer>
)
}
const PipContainer = styled('div', {
base: {
width: '100%',
height: '100%',
display: 'grid',
gridTemplateRows: 'minmax(0, 1fr) auto',
backgroundColor: 'primaryDark.50',
'& .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%',
},
},
})
const PipStage = styled('div', {
base: {
position: 'relative',
minHeight: 0,
},
})
const PipGridWrapper = styled('div', {
base: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
},
})
@@ -0,0 +1,21 @@
import type { ReactNode } from 'react'
import { DocumentPiPPortal } from './DocumentPiPPortal'
import { PipView } from './PipView'
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 = (): ReactNode => {
const { isOpen, close } = useRoomPiP()
const portal = DocumentPiPPortal({
isOpen,
onClose: close,
children: <PipView />,
})
return portal as ReactNode
}
@@ -0,0 +1,86 @@
import React, { useEffect } from 'react'
import { RiMoreFill } from '@remixicon/react'
import { Box, Button } from '@/primitives'
import { css } from '@/styled-system/css'
import { PipOptionsMenuItems } from './PipOptionsMenuItems'
type PipOptionsMenuProps = {
wrapperRef: React.RefObject<HTMLDivElement>
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
label: string
}
/**
* PiP-specific options menu with absolute positioning for correct alignment in PiP window.
* Renders locally (unlike standard Menu) and closes automatically on item click or outside click.
*/
export const PipOptionsMenu = ({
wrapperRef,
isOpen,
setIsOpen,
label,
}: PipOptionsMenuProps) => {
// Close menu when a menu item action completes (e.g., transcription, effects, recording).
useEffect(() => {
if (!isOpen) return
const doc = wrapperRef.current?.ownerDocument ?? document
const handleMenuItemClick = (event: MouseEvent) => {
const target = event.target as HTMLElement | null
const wrapper = wrapperRef.current
if (!wrapper || !target) return
// Don't close if clicking the trigger button
if (wrapper.querySelector('button')?.contains(target)) return
// Close if clicking a menu item (action will have fired)
if (target.closest('[role="menuitem"]')) {
// Use requestAnimationFrame to ensure action completes first, without visible delay
requestAnimationFrame(() => {
setIsOpen(false)
})
}
}
doc.addEventListener('click', handleMenuItemClick, true)
return () => {
doc.removeEventListener('click', handleMenuItemClick, true)
}
}, [isOpen, setIsOpen, wrapperRef])
return (
<div
ref={wrapperRef}
className={css({
position: 'relative',
})}
>
<Button
id="room-options-trigger"
square
variant="primaryDark"
aria-label={label}
tooltip={label}
onPress={() => setIsOpen(!isOpen)}
>
<RiMoreFill />
</Button>
{isOpen && (
<div
className={css({
position: 'absolute',
left: '50%',
bottom: 'calc(100% + 0.85rem)',
transform: 'translateX(-50%)',
zIndex: 10,
})}
>
<Box size="sm" type="popover" variant="dark">
<PipOptionsMenuItems />
</Box>
</div>
)}
</div>
)
}
@@ -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 = () => (
<RACMenu
style={{
minWidth: '150px',
width: '300px',
}}
>
<MenuSection>
<PictureInPictureMenuItem />
<EffectsMenuItem store={pipLayoutStore} />
</MenuSection>
<Separator />
<MenuSection>
<SupportMenuItem />
<FeedbackMenuItem />
<SettingsMenuItem />
</MenuSection>
</RACMenu>
)
@@ -0,0 +1,96 @@
import { useCallback, useEffect, useRef, useState } from 'react'
type DocumentPictureInPicture = {
requestWindow: (options?: {
width?: number
height?: number
}) => Promise<Window>
}
type WindowWithDocumentPiP = Window & {
documentPictureInPicture?: DocumentPictureInPicture
}
export const useDocumentPiP = ({
width = 480,
height = 270,
}: {
width?: number
height?: number
} = {}) => {
const [pipWindow, setPipWindow] = useState<Window | null>(null)
const pipWindowRef = useRef<Window | null>(null)
const pendingPiPRef = useRef<Promise<Window | null> | null>(null)
const [isSupported] = useState(() => {
if (typeof window === 'undefined') return false
return 'documentPictureInPicture' in window
})
const openPiP = useCallback(async () => {
if (!isSupported) return null
const existingWindow = pipWindowRef.current
if (existingWindow && !existingWindow.closed) return existingWindow
if (pendingPiPRef.current) return pendingPiPRef.current
// Request a new PiP window from the browser API.
const pip = (window as WindowWithDocumentPiP).documentPictureInPicture
if (!pip) return null
const requestPromise = (async () => {
try {
const win = await pip.requestWindow({ width, height })
const currentWindow = pipWindowRef.current
if (currentWindow && !currentWindow.closed) return currentWindow
setPipWindow(win)
return win
} catch (error) {
// Avoid unhandled rejections if the user blocks or closes the request.
console.error('Failed to open Picture-in-Picture window', error)
return null
} finally {
pendingPiPRef.current = null
}
})()
pendingPiPRef.current = requestPromise
return requestPromise
}, [height, isSupported, width])
const closePiP = useCallback(() => {
if (!pipWindow) return
if (!pipWindow.closed) {
pipWindow.close()
}
setPipWindow(null)
}, [pipWindow])
useEffect(() => {
pipWindowRef.current = pipWindow
}, [pipWindow])
useEffect(() => {
if (!pipWindow) return
const handleClose = () => {
setPipWindow(null)
}
pipWindow.addEventListener('pagehide', handleClose)
pipWindow.addEventListener('beforeunload', handleClose)
return () => {
pipWindow.removeEventListener('pagehide', handleClose)
pipWindow.removeEventListener('beforeunload', handleClose)
}
}, [pipWindow])
return {
isSupported,
isOpen: !!pipWindow && !pipWindow.closed,
pipWindow,
openPiP,
closePiP,
}
}
@@ -0,0 +1,29 @@
import { useCallback } from 'react'
import { useSnapshot } from 'valtio'
import { roomPiPStore } from '@/stores/roomPiP'
export const useRoomPiP = () => {
const { isOpen } = useSnapshot(roomPiPStore)
const isSupported =
typeof window !== 'undefined' && 'documentPictureInPicture' in window
const open = useCallback(() => {
roomPiPStore.isOpen = true
}, [])
const close = useCallback(() => {
roomPiPStore.isOpen = false
}, [])
const toggle = useCallback(() => {
roomPiPStore.isOpen = !roomPiPStore.isOpen
}, [])
return {
isSupported,
isOpen,
open,
close,
toggle,
}
}
@@ -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<PipLayoutState>({
activePanelId: null,
activeSubPanelId: null,
})
@@ -1,4 +1,4 @@
import { A, Button, Dialog, H, P, ScreenReaderAnnouncer } from '@/primitives'
import { Button, Dialog, H, P, ScreenReaderAnnouncer } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css'
import { useSnapshot } from 'valtio'
@@ -9,8 +9,6 @@ import { navigateTo } from '@/navigation/navigateTo'
import humanizeDuration from 'humanize-duration'
import i18n from 'i18next'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { useSettingsDialog } from '@/features/settings/hook/useSettingsDialog'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
const IDLE_DISCONNECT_TIMEOUT_MS = 120000 // 2 minutes
const COUNTDOWN_ANNOUNCEMENT_SECONDS = [90, 60, 30]
@@ -20,7 +18,6 @@ export const IsIdleDisconnectModal = () => {
const connectionObserverSnap = useSnapshot(connectionObserverStore)
const [timeRemaining, setTimeRemaining] = useState(IDLE_DISCONNECT_TIMEOUT_MS)
const lastAnnouncementRef = useRef<number | null>(null)
const { openSettingsDialog } = useSettingsDialog()
const { t } = useTranslation('rooms', { keyPrefix: 'isIdleDisconnectModal' })
const announce = useScreenReaderAnnounce()
@@ -120,19 +117,7 @@ export const IsIdleDisconnectModal = () => {
}),
})}
</P>
<P>
{t('settingsPrefix')}{' '}
<A
color="primary"
onPress={() => {
connectionObserverStore.isIdleDisconnectModalOpen = false
openSettingsDialog(SettingsDialogExtendedKey.GENERAL)
}}
>
{t('settingsLink')}
</A>
{t('settingsSuffix')}
</P>
<P>{t('settings')}</P>
<HStack marginTop="2rem">
<Button
onPress={() => {
@@ -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'
@@ -135,7 +134,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null}
</div>
)
export const SidePanel = () => {
export const SidePanel = ({ store }: { store?: SidePanelStore }) => {
const {
activePanelId,
isParticipantsOpen,
@@ -147,24 +146,23 @@ export const SidePanel = () => {
isInfoOpen,
isSubPanelOpen,
activeSubPanelId,
} = useSidePanel()
closePanel,
goBack,
} = useSidePanel(store)
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
return (
<StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)}
ariaLabel={t('ariaLabel')}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
onClose={closePanel}
closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`),
})}
isClosed={!isSidePanelOpen}
isSubmenu={isSubPanelOpen}
backButtonLabel={t('backToTools')}
onBack={() => (layoutStore.activeSubPanelId = null)}
onBack={goBack}
>
<Panel isOpen={isParticipantsOpen}>
<ParticipantsList />
@@ -11,7 +11,9 @@ import {
ProcessorType,
} from '.'
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
export class UnifiedBackgroundTrackProcessor
implements BackgroundProcessorInterface
{
processor: ProcessorWrapper<BackgroundOptions>
opts: BackgroundOptions
processorType: ProcessorType
@@ -13,7 +13,8 @@ export interface ProcessorSerialized {
options: BackgroundOptions
}
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
export interface BackgroundProcessorInterface
extends TrackProcessor<Track.Kind> {
update(opts: BackgroundOptions): Promise<void>
options: BackgroundOptions
clone(): BackgroundProcessorInterface
@@ -6,7 +6,6 @@ import { ToggleButton } from '@/primitives'
import { chatStore } from '@/stores/chat'
import { useSidePanel } from '../../hooks/useSidePanel'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
export const ChatToggle = ({
onPress,
@@ -19,11 +18,6 @@ export const ChatToggle = ({
const { isChatOpen, toggleChat } = useSidePanel()
const tooltipLabel = isChatOpen ? 'open' : 'closed'
useRegisterKeyboardShortcut({
id: 'toggle-chat',
handler: toggleChat,
})
return (
<div
className={css({
@@ -18,7 +18,6 @@ import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
import { ToggleSource, CaptureOptionsBySource } from '@livekit/components-core'
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
type ToggleDeviceStyleProps = {
variant?: NonNullable<ButtonRecipeProps>['variant']
@@ -89,14 +88,12 @@ export const ToggleDevice = <T extends ToggleSource>({
const deviceShortcut = useDeviceShortcut(kind)
useRegisterKeyboardShortcut({
id: deviceShortcut?.id,
shortcut: deviceShortcut,
handler: async () => await toggle(),
isDisabled: cannotUseDevice,
})
const pushToTalkShortcut = getShortcutDescriptorById('push-to-talk')
useLongPress({
keyCode: kind === 'audioinput' ? pushToTalkShortcut?.code : undefined,
keyCode: kind === 'audioinput' ? 'KeyV' : undefined,
onKeyDown,
onKeyUp,
isDisabled: cannotUseDevice,
@@ -106,9 +103,7 @@ export const ToggleDevice = <T extends ToggleSource>({
const label = t(enabled ? 'disable' : 'enable', {
keyPrefix: `selectDevice.${kind}`,
})
return deviceShortcut?.shortcut
? appendShortcutLabel(label, deviceShortcut.shortcut)
: label
return deviceShortcut ? appendShortcutLabel(label, deviceShortcut) : label
}, [enabled, kind, deviceShortcut, t])
const Icon =
@@ -9,7 +9,6 @@ import {
closeLowerHandToasts,
showLowerHandToast,
} from '@/features/notifications/utils'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
const SPEAKING_DETECTION_DELAY = 3000
@@ -34,16 +33,6 @@ export const HandToggle = () => {
closeLowerHandToasts()
}, [isHandRaised])
const handleToggle = () => {
toggleRaisedHand()
resetToastState()
}
useRegisterKeyboardShortcut({
id: 'raise-hand',
handler: handleToggle,
})
useEffect(() => {
const shouldShowToast = isSpeaking && isHandRaised && !hasShownToast
@@ -79,7 +68,10 @@ export const HandToggle = () => {
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isHandRaised}
onPress={handleToggle}
onPress={() => {
toggleRaisedHand()
resetToastState()
}}
data-attr={`controls-hand-${tooltipLabel}`}
>
<RiHand />
@@ -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 (
<MenuItem
@@ -2,9 +2,27 @@ import { useTranslation } from 'react-i18next'
import { RiMoreFill } from '@remixicon/react'
import { Button, Menu } from '@/primitives'
import { OptionsMenuItems } from './OptionsMenuItems'
import { useOverlayPortalContainer } from '@/primitives/useOverlayPortalContainer'
import { useRef, useState } from 'react'
import { PipOptionsMenu } from '@/features/pip/components/controls/PipOptionsMenu'
export const OptionsButton = () => {
const { t } = useTranslation('rooms')
const portalContainer = useOverlayPortalContainer()
const isInPiP = portalContainer && portalContainer.ownerDocument !== document
const [isOpen, setIsOpen] = useState(false)
const wrapperRef = useRef<HTMLDivElement>(null)
if (isInPiP) {
return (
<PipOptionsMenu
wrapperRef={wrapperRef}
isOpen={isOpen}
setIsOpen={setIsOpen}
label={t('options.buttonLabel')}
/>
)
}
return (
<Menu variant="dark">
@@ -7,6 +7,7 @@ import { EffectsMenuItem } from './EffectsMenuItem'
import { SupportMenuItem } from './SupportMenuItem'
import { TranscriptMenuItem } from './TranscriptMenuItem'
import { ScreenRecordingMenuItem } from './ScreenRecordingMenuItem'
import { PictureInPictureMenuItem } from './PictureInPictureMenuItem'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = () => {
@@ -21,6 +22,7 @@ export const OptionsMenuItems = () => {
<TranscriptMenuItem />
<ScreenRecordingMenuItem />
<FullScreenMenuItem />
<PictureInPictureMenuItem />
<EffectsMenuItem />
</MenuSection>
<Separator />
@@ -0,0 +1,23 @@
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { RiPictureInPicture2Line } from '@remixicon/react'
import { menuRecipe } from '@/primitives/menuRecipe'
import { useRoomPiP } from '@/features/pip/hooks/useRoomPiP'
export const PictureInPictureMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { isSupported, isOpen, toggle } = useRoomPiP()
// Hide the entry when the browser doesn't support Document PiP.
if (!isSupported) return null
return (
<MenuItem
onAction={toggle}
className={menuRecipe({ icon: true, variant: 'dark' }).item}
>
<RiPictureInPicture2Line size={20} />
{isOpen ? t('pictureInPicture.exit') : t('pictureInPicture.enter')}
</MenuItem>
)
}
@@ -6,7 +6,6 @@ import { css } from '@/styled-system/css'
import { useParticipants } from '@livekit/components-react'
import { useSidePanel } from '../../../hooks/useSidePanel'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
export const ParticipantsToggle = ({
onPress,
@@ -28,11 +27,6 @@ export const ParticipantsToggle = ({
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
useRegisterKeyboardShortcut({
id: 'toggle-participants',
handler: toggleParticipants,
})
return (
<div
className={css({
@@ -11,7 +11,6 @@ import {
ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal'
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import { Toolbar as RACToolbar } from 'react-aria-components'
import { Participant } from 'livekit-client'
import useRateLimiter from '@/hooks/useRateLimiter'
@@ -42,11 +41,6 @@ export const ReactionsToggle = () => {
const [isVisible, setIsVisible] = useState(false)
useRegisterKeyboardShortcut({
id: 'reaction',
handler: () => setIsVisible((prev) => !prev),
})
const sendReaction = async (emoji: string) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
@@ -6,7 +6,8 @@ import {
import React from 'react'
/** @public */
export interface AllowMediaPlaybackProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
export interface AllowMediaPlaybackProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
label?: string
}
@@ -11,7 +11,8 @@ const ASPECT_RATIO = 16 / 10
const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1
/** @public */
export interface CarouselLayoutProps extends React.HTMLAttributes<HTMLMediaElement> {
export interface CarouselLayoutProps
extends React.HTMLAttributes<HTMLMediaElement> {
tracks: TrackReferenceOrPlaceholder[]
children: React.ReactNode
/** Place the tiles vertically or horizontally next to each other.
@@ -13,8 +13,7 @@ import { PaginationControl } from '../controls/PaginationControl'
/** @public */
export interface GridLayoutProps
extends
React.HTMLAttributes<HTMLDivElement>,
extends React.HTMLAttributes<HTMLDivElement>,
Pick<UseParticipantsOptions, 'updateOnlyOn'> {
children: React.ReactNode
tracks: TrackReferenceOrPlaceholder[]
@@ -12,6 +12,6 @@ export function useCanPublishTrack(trackSource: TrackSource): boolean {
return Boolean(
permissions?.canPublish &&
permissions?.canPublishSources?.includes(trackSource)
permissions?.canPublishSources?.includes(trackSource)
)
}
@@ -1,16 +1,19 @@
import { useMemo } from 'react'
import {
getShortcutDescriptorById,
ShortcutDescriptor,
} from '@/features/shortcuts/catalog'
import { Shortcut } from '@/features/shortcuts/types'
export const useDeviceShortcut = (kind: MediaDeviceKind) => {
return useMemo<ShortcutDescriptor | undefined>(() => {
return useMemo<Shortcut | undefined>(() => {
switch (kind) {
case 'audioinput':
return getShortcutDescriptorById('toggle-microphone')
return {
key: 'd',
ctrlKey: true,
}
case 'videoinput':
return getShortcutDescriptorById('toggle-camera')
return {
key: 'e',
ctrlKey: true,
}
default:
return undefined
}
@@ -1,74 +1,77 @@
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { PanelId, SubPanelId } from '../types/panel'
export enum PanelId {
PARTICIPANTS = 'participants',
EFFECTS = 'effects',
CHAT = 'chat',
TOOLS = 'tools',
ADMIN = 'admin',
INFO = 'info',
export { PanelId, SubPanelId }
export type SidePanelStore = {
activePanelId: PanelId | null
activeSubPanelId: SubPanelId | null
}
export enum SubPanelId {
TRANSCRIPT = 'transcript',
SCREEN_RECORDING = 'screenRecording',
}
export const useSidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
export const useSidePanel = (store: SidePanelStore = layoutStore) => {
const layoutSnap = useSnapshot(store)
const activePanelId = layoutSnap.activePanelId
const activeSubPanelId = layoutSnap.activeSubPanelId
const isParticipantsOpen = activePanelId == PanelId.PARTICIPANTS
const isEffectsOpen = activePanelId == PanelId.EFFECTS
const isChatOpen = activePanelId == PanelId.CHAT
const isToolsOpen = activePanelId == PanelId.TOOLS
const isAdminOpen = activePanelId == PanelId.ADMIN
const isInfoOpen = activePanelId == PanelId.INFO
const isTranscriptOpen = activeSubPanelId == SubPanelId.TRANSCRIPT
const isScreenRecordingOpen = activeSubPanelId == SubPanelId.SCREEN_RECORDING
const isParticipantsOpen = activePanelId === PanelId.PARTICIPANTS
const isEffectsOpen = activePanelId === PanelId.EFFECTS
const isChatOpen = activePanelId === PanelId.CHAT
const isToolsOpen = activePanelId === PanelId.TOOLS
const isAdminOpen = activePanelId === PanelId.ADMIN
const isInfoOpen = activePanelId === PanelId.INFO
const isTranscriptOpen = activeSubPanelId === SubPanelId.TRANSCRIPT
const isScreenRecordingOpen = activeSubPanelId === SubPanelId.SCREEN_RECORDING
const isSidePanelOpen = !!activePanelId
const isSubPanelOpen = !!activeSubPanelId
const toggleAdmin = () => {
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
store.activePanelId = isAdminOpen ? null : PanelId.ADMIN
if (layoutSnap.activeSubPanelId) store.activeSubPanelId = null
}
const toggleParticipants = () => {
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
store.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
if (layoutSnap.activeSubPanelId) store.activeSubPanelId = null
}
const toggleChat = () => {
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
store.activePanelId = isChatOpen ? null : PanelId.CHAT
if (layoutSnap.activeSubPanelId) store.activeSubPanelId = null
}
const toggleEffects = () => {
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
store.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
if (layoutSnap.activeSubPanelId) store.activeSubPanelId = null
}
const toggleTools = () => {
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
store.activePanelId = isToolsOpen ? null : PanelId.TOOLS
if (layoutSnap.activeSubPanelId) store.activeSubPanelId = null
}
const toggleInfo = () => {
layoutStore.activePanelId = isInfoOpen ? null : PanelId.INFO
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
store.activePanelId = isInfoOpen ? null : PanelId.INFO
if (layoutSnap.activeSubPanelId) store.activeSubPanelId = null
}
const openTranscript = () => {
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
layoutStore.activePanelId = PanelId.TOOLS
store.activeSubPanelId = SubPanelId.TRANSCRIPT
store.activePanelId = PanelId.TOOLS
}
const openScreenRecording = () => {
layoutStore.activeSubPanelId = SubPanelId.SCREEN_RECORDING
layoutStore.activePanelId = PanelId.TOOLS
store.activeSubPanelId = SubPanelId.SCREEN_RECORDING
store.activePanelId = PanelId.TOOLS
}
const closePanel = () => {
store.activePanelId = null
store.activeSubPanelId = null
}
const goBack = () => {
store.activeSubPanelId = null
}
return {
@@ -82,6 +85,8 @@ export const useSidePanel = () => {
toggleInfo,
openTranscript,
openScreenRecording,
closePanel,
goBack,
isSubPanelOpen,
isChatOpen,
isParticipantsOpen,
@@ -18,7 +18,8 @@ import { css } from '@/styled-system/css'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
export interface ChatProps
extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {}
extends React.HTMLAttributes<HTMLDivElement>,
ChatOptions {}
/**
* The Chat component adds a basis chat functionality to the LiveKit room. The messages are distributed to all participants
@@ -22,7 +22,7 @@ export function DesktopControlBar({
const desktopControlBarEl = useRef<HTMLDivElement>(null)
useRegisterKeyboardShortcut({
id: 'focus-toolbar',
shortcut: { key: 'F2' },
handler: () => {
const root = desktopControlBarEl.current
if (!root) return
@@ -38,9 +38,16 @@ import { Subtitles } from '@/features/subtitle/component/Subtitles'
import { CarouselLayout } from '../components/layout/CarouselLayout'
import { GridLayout } from '../components/layout/GridLayout'
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
import { RoomPiP } from '@/features/pip/components/RoomPiP'
import { useRoomPiP } from '@/features/pip/hooks/useRoomPiP'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
const SIDE_PANEL_WIDTH = '358px'
const CONTROL_BAR_HEIGHT = '80px'
const SIDE_PANEL_OFFSET = '16px'
const SIDE_PANEL_GAP = '3rem'
const LayoutWrapper = styled(
'div',
cva({
@@ -66,7 +73,8 @@ const LayoutWrapper = styled(
/**
* @public
*/
export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElement> {
export interface VideoConferenceProps
extends React.HTMLAttributes<HTMLDivElement> {
/** @alpha */
SettingsComponent?: React.ComponentType
}
@@ -242,8 +250,16 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const { isSidePanelOpen } = useSidePanel()
const { areSubtitlesOpen } = useSubtitles()
const { isOpen: isPiPOpen } = useRoomPiP()
const shouldRenderMainLayout = !isPiPOpen
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
const layoutInsetVars = {
'--lk-side-panel-width': SIDE_PANEL_WIDTH,
'--lk-controlbar-height': CONTROL_BAR_HEIGHT,
'--lk-side-panel-offset': SIDE_PANEL_OFFSET,
'--lk-side-panel-gap': SIDE_PANEL_GAP,
} as React.CSSProperties
return (
<div
@@ -258,75 +274,78 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
value={layoutContext}
// onPinChange={handleFocusStateChange}
>
<ScreenShareErrorModal
isOpen={isShareErrorVisible}
onClose={() => setIsShareErrorVisible(false)}
/>
<IsIdleDisconnectModal />
<div
// todo - extract these magic values into constant
style={{
position: 'absolute',
inset: isSidePanelOpen
? `var(--lk-grid-gap) calc(358px + 3rem) calc(80px + var(--lk-grid-gap)) 16px`
: `var(--lk-grid-gap) var(--lk-grid-gap) calc(80px + var(--lk-grid-gap))`,
transition: 'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms',
maxHeight: '100%',
}}
>
<LayoutWrapper areSubtitlesOpen={areSubtitlesOpen}>
<div
style={{
display: 'flex',
position: 'relative',
width: '100%',
}}
>
{!focusTrack ? (
<ScreenShareErrorModal
isOpen={isShareErrorVisible}
onClose={() => setIsShareErrorVisible(false)}
/>
<IsIdleDisconnectModal />
<div
style={{
position: 'absolute',
...layoutInsetVars,
inset: isSidePanelOpen
? `var(--lk-grid-gap) calc(var(--lk-side-panel-width) + var(--lk-side-panel-gap)) calc(var(--lk-controlbar-height) + var(--lk-grid-gap)) var(--lk-side-panel-offset)`
: `var(--lk-grid-gap) var(--lk-grid-gap) calc(var(--lk-controlbar-height) + var(--lk-grid-gap))`,
transition: 'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms',
maxHeight: '100%',
}}
>
{shouldRenderMainLayout && (
<LayoutWrapper areSubtitlesOpen={areSubtitlesOpen}>
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
style={{
display: 'flex',
position: 'relative',
width: '100%',
}}
>
<GridLayout tracks={tracks} style={{ padding: 0 }}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div
className="lk-focus-layout-wrapper"
style={{ height: 'auto' }}
>
<FocusLayoutContainer style={{ padding: 0 }}>
<CarouselLayout
tracks={carouselTracks}
style={{
minWidth: '200px',
}}
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<ParticipantTile />
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
<GridLayout tracks={tracks} style={{ padding: 0 }}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div
className="lk-focus-layout-wrapper"
style={{ height: 'auto' }}
>
<FocusLayoutContainer style={{ padding: 0 }}>
<CarouselLayout
tracks={carouselTracks}
style={{
minWidth: '200px',
}}
>
<ParticipantTile />
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
</div>
)}
</div>
</LayoutWrapper>
<Subtitles />
<MainNotificationToast />
</div>
<ControlBar
onDeviceError={(e) => {
console.error(e)
if (
e.source == Track.Source.ScreenShare &&
e.error.toString() ==
'NotAllowedError: Permission denied by system'
) {
setIsShareErrorVisible(true)
}
}}
/>
<SidePanel />
</LayoutWrapper>
)}
<Subtitles />
<MainNotificationToast />
</div>
<ControlBar
onDeviceError={(e) => {
console.error(e)
if (
e.source == Track.Source.ScreenShare &&
e.error.toString() ==
'NotAllowedError: Permission denied by system'
) {
setIsShareErrorVisible(true)
}
}}
/>
<SidePanel />
<RoomPiP />
</LayoutContextProvider>
)}
<RoomAudioRenderer />
@@ -9,7 +9,8 @@ import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet
// and suspend/resume it as needed to manage audio state
let audioContext: AudioContext
export interface AudioProcessorInterface extends TrackProcessor<Track.Kind.Audio> {
export interface AudioProcessorInterface
extends TrackProcessor<Track.Kind.Audio> {
name: string
}
@@ -0,0 +1,17 @@
/**
* Panel identifiers for the side panel (Info, Chat, Participants, etc.).
* Extracted to avoid circular dependencies between layout store and useSidePanel.
*/
export enum PanelId {
PARTICIPANTS = 'participants',
EFFECTS = 'effects',
CHAT = 'chat',
TOOLS = 'tools',
ADMIN = 'admin',
INFO = 'info',
}
export enum SubPanelId {
TRANSCRIPT = 'transcript',
SCREEN_RECORDING = 'screenRecording',
}
@@ -1,83 +0,0 @@
import { Shortcut } from './types'
// Central list of current keyboard shortcuts.
// Keep a single source of truth for display and, later, customization
export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
export type ShortcutId =
| 'focus-toolbar'
| 'toggle-microphone'
| 'toggle-camera'
| 'push-to-talk'
| 'toggle-chat'
| 'toggle-participants'
| 'raise-hand'
| 'recording'
| 'reaction'
| 'fullscreen'
export const getShortcutDescriptorById = (id: ShortcutId) =>
shortcutCatalog.find((item) => item.id === id)
export type ShortcutDescriptor = {
id: ShortcutId
category: ShortcutCategory
shortcut?: Shortcut
kind?: 'press' | 'longPress'
code?: string // used when kind === 'longPress' (KeyboardEvent.code)
description?: string
}
export const shortcutCatalog: ShortcutDescriptor[] = [
{
id: 'focus-toolbar',
category: 'navigation',
shortcut: { key: 'F2' },
},
{
id: 'toggle-microphone',
category: 'media',
shortcut: { key: 'd', ctrlKey: true },
},
{
id: 'toggle-camera',
category: 'media',
shortcut: { key: 'e', ctrlKey: true },
},
{
id: 'push-to-talk',
category: 'media',
kind: 'longPress',
code: 'KeyV',
},
{
id: 'reaction',
category: 'interaction',
shortcut: { key: 'E', ctrlKey: true, shiftKey: true },
},
{
id: 'fullscreen',
category: 'interaction',
shortcut: { key: 'F', ctrlKey: true, shiftKey: true },
},
{
id: 'recording',
category: 'interaction',
shortcut: { key: 'L', ctrlKey: true, shiftKey: true },
},
{
id: 'raise-hand',
category: 'interaction',
shortcut: { key: 'H', ctrlKey: true, shiftKey: true },
},
{
id: 'toggle-chat',
category: 'interaction',
shortcut: { key: 'M', ctrlKey: true, shiftKey: true },
},
{
id: 'toggle-participants',
category: 'interaction',
shortcut: { key: 'P', ctrlKey: true, shiftKey: true },
},
]
@@ -1,6 +1,4 @@
export type Shortcut = {
key: string
ctrlKey?: boolean
shiftKey?: boolean
altKey?: boolean
}
@@ -11,13 +11,11 @@ export const useKeyboardShortcuts = () => {
// This approach handles basic shortcuts but isn't comprehensive.
// Issues might occur. First draft.
const onKeyDown = async (e: KeyboardEvent) => {
const { key, metaKey, ctrlKey, shiftKey, altKey } = e
const { key, metaKey, ctrlKey } = e
if (!key) return
const shortcutKey = formatShortcutKey({
key,
ctrlKey: ctrlKey || (isMacintosh() && metaKey),
shiftKey,
altKey,
})
const shortcut = shortcutsSnap.shortcuts.get(shortcutKey)
if (!shortcut) return
@@ -1,28 +1,26 @@
import { useEffect } from 'react'
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
import { formatShortcutKey } from '@/features/shortcuts/utils'
import { ShortcutId, getShortcutDescriptorById } from './catalog'
import { Shortcut } from '@/features/shortcuts/types'
export type useRegisterKeyboardShortcutProps = {
id?: ShortcutId
shortcut?: Shortcut
handler: () => Promise<void | boolean | undefined> | void
isDisabled?: boolean
}
export const useRegisterKeyboardShortcut = ({
id,
shortcut,
handler,
isDisabled = false,
}: useRegisterKeyboardShortcutProps) => {
useEffect(() => {
if (!id) return
const descriptor = getShortcutDescriptorById(id)
if (!descriptor?.shortcut) return
const formattedKey = formatShortcutKey(descriptor.shortcut)
if (!shortcut) return
const formattedKey = formatShortcutKey(shortcut)
if (isDisabled) {
keyboardShortcutsStore.shortcuts.delete(formattedKey)
} else {
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
}
}, [handler, id, isDisabled])
}, [handler, shortcut, isDisabled])
}
+4 -16
View File
@@ -4,27 +4,15 @@ import { Shortcut } from '@/features/shortcuts/types'
export const CTRL = 'ctrl'
export const formatShortcutKey = (shortcut: Shortcut) => {
const parts = []
if (shortcut.ctrlKey) parts.push(CTRL)
if (shortcut.altKey) parts.push('alt')
if (shortcut.shiftKey) parts.push('shift')
parts.push(shortcut.key.toUpperCase())
return parts.join('+')
if (shortcut.ctrlKey) return `${CTRL}+${shortcut.key.toUpperCase()}`
return shortcut.key.toUpperCase()
}
export const appendShortcutLabel = (label: string, shortcut: Shortcut) => {
if (!shortcut.key) return
const parts: string[] = []
let formattedKeyLabel = shortcut.key.toLowerCase()
if (shortcut.ctrlKey) {
parts.push(isMacintosh() ? '⌘' : 'Ctrl')
formattedKeyLabel = `${isMacintosh() ? '⌘' : 'Ctrl'}+${formattedKeyLabel}`
}
if (shortcut.altKey) {
parts.push(isMacintosh() ? '⌥' : 'Alt')
}
if (shortcut.shiftKey) {
parts.push('Shift')
}
parts.push(shortcut.key.toLowerCase())
const formattedKeyLabel = parts.join('+')
return `${label} (${formattedKeyLabel})`
}
@@ -20,7 +20,8 @@ export interface TranscriptionSegment {
lastReceivedTime: number
}
export interface TranscriptionSegmentWithParticipant extends TranscriptionSegment {
export interface TranscriptionSegmentWithParticipant
extends TranscriptionSegment {
participant: Participant
}
+5 -3
View File
@@ -154,9 +154,7 @@
"isIdleDisconnectModal": {
"title": "Bist du noch da?",
"body": "Du bist der einzige Teilnehmer. Dieses Gespräch endet in {{duration}}. Möchtest du das Gespräch fortsetzen?",
"settingsPrefix": "Um diese Nachricht nicht mehr zu sehen, gehe zu",
"settingsLink": "Einstellungen > Allgemein",
"settingsSuffix": ".",
"settings": "Um diese Nachricht nicht mehr zu sehen, gehe zu Einstellungen > Allgemein.",
"stayButton": "Gespräch fortsetzen",
"leaveButton": "Jetzt verlassen",
"countdownAnnouncement": "Das Gespräch endet in {{duration}}."
@@ -236,6 +234,10 @@
"username": "Ihren Namen aktualisieren",
"effects": "Effekte anwenden",
"switchCamera": "Kamera wechseln",
"pictureInPicture": {
"enter": "Bild-im-Bild",
"exit": "Bild-im-Bild schließen"
},
"fullscreen": {
"enter": "Vollbild",
"exit": "Vollbildmodus verlassen"
+5 -3
View File
@@ -154,9 +154,7 @@
"isIdleDisconnectModal": {
"title": "Are you still there?",
"body": "You are the only participant. This call will end in {{duration}}. Would you like to continue the call?",
"settingsPrefix": "To stop seeing this message, go to",
"settingsLink": "Settings > General",
"settingsSuffix": ".",
"settings": "To stop seeing this message, go to Settings > General.",
"stayButton": "Continue the call",
"leaveButton": "Leave now",
"countdownAnnouncement": "Call ends in {{duration}}."
@@ -236,6 +234,10 @@
"username": "Update Your Name",
"effects": "Backgrounds and Effects",
"switchCamera": "Switch camera",
"pictureInPicture": {
"enter": "Picture-in-picture",
"exit": "Close picture-in-picture"
},
"fullscreen": {
"enter": "Fullscreen",
"exit": "Exit fullscreen mode"
+5 -3
View File
@@ -154,9 +154,7 @@
"isIdleDisconnectModal": {
"title": "Êtes-vous toujours là ?",
"body": "Vous êtes le seul participant. Cet appel va donc se terminer dans {{duration}}. Voulez-vous poursuivre l'appel ?",
"settingsPrefix": "Pour ne plus voir ce message, allez dans",
"settingsLink": "Paramètres > Général",
"settingsSuffix": ".",
"settings": "Pour ne plus voir ce message, accèder à Paramètres > Général.",
"stayButton": "Poursuivre l'appel",
"leaveButton": "Quitter maintenant",
"countdownAnnouncement": "L'appel se termine dans {{duration}}."
@@ -236,6 +234,10 @@
"username": "Choisir votre nom",
"effects": "Arrière-plans et effets",
"switchCamera": "Changer de caméra",
"pictureInPicture": {
"enter": "Image dans l'image",
"exit": "Fermer l'image dans l'image"
},
"fullscreen": {
"enter": "Plein écran",
"exit": "Quitter le mode plein écran"
+5 -3
View File
@@ -154,9 +154,7 @@
"isIdleDisconnectModal": {
"title": "Ben je er nog?",
"body": "Je bent de enige deelnemer. Dit gesprek wordt over {{duration}} beëindigd. Wil je het gesprek voortzetten?",
"settingsPrefix": "Om dit bericht niet meer te zien, ga naar",
"settingsLink": "Instellingen > Algemeen",
"settingsSuffix": ".",
"settings": "Om dit bericht niet meer te zien, ga naar Instellingen > Algemeen.",
"stayButton": "Gesprek voortzetten",
"leaveButton": "Nu verlaten",
"countdownAnnouncement": "Het gesprek eindigt over {{duration}}."
@@ -236,6 +234,10 @@
"username": "Verander uw naam",
"effects": "Pas effecten toe",
"switchCamera": "Selecteer camera",
"pictureInPicture": {
"enter": "Beeld-in-beeld",
"exit": "Beeld-in-beeld sluiten"
},
"fullscreen": {
"enter": "Volledig scherm",
"exit": "Stop volledig scherm stand"
+28 -2
View File
@@ -1,10 +1,16 @@
import { ReactNode } from 'react'
import { ReactNode, useMemo } from 'react'
import { MenuTrigger } from 'react-aria-components'
import { StyledPopover } from './Popover'
import { Box } from './Box'
import {
useOverlayBoundaryElement,
useOverlayPortalContainer,
} from './useOverlayPortalContainer'
/**
* a Menu is a tuple of a trigger component (most usually a Button) that toggles menu items in a tooltip around the trigger
*
* Uses UNSAFE_PortalProvider context automatically for portal container (no need for UNSTABLE_portalContainer).
*/
export const Menu = ({
children,
@@ -16,10 +22,30 @@ export const Menu = ({
placement?: 'bottom' | 'top' | 'left' | 'right'
}) => {
const [trigger, menu] = children
const boundaryElement = useOverlayBoundaryElement()
const portalContainer = useOverlayPortalContainer()
// Detect if we're in PiP: portal container is in a different document than the main window
const isInPiP = useMemo(
() =>
portalContainer &&
portalContainer.ownerDocument &&
portalContainer.ownerDocument !== document,
[portalContainer]
)
// Default placement: 'bottom' in PiP, 'top' elsewhere (to match existing behavior)
const defaultPlacement = isInPiP ? 'bottom' : 'top'
const shouldFlip = isInPiP ? false : undefined
return (
<MenuTrigger>
{trigger}
<StyledPopover placement={placement}>
<StyledPopover
placement={placement ?? defaultPlacement}
shouldFlip={shouldFlip}
boundaryElement={boundaryElement}
>
<Box size="sm" type="popover" variant={variant}>
{menu}
</Box>
+5 -1
View File
@@ -8,6 +8,7 @@ import {
} from 'react-aria-components'
import { styled } from '@/styled-system/jsx'
import { Box } from './Box'
import { useOverlayBoundaryElement } from './useOverlayPortalContainer'
export const StyledPopover = styled(RACPopover, {
base: {
@@ -65,6 +66,8 @@ const StyledOverlayArrow = styled(OverlayArrow, {
*
* Note: to show a list of actionable items, like a dropdown menu, prefer using a <Menu> or <Select>.
* This is here when needing to show unrestricted content in a box.
*
* Uses UNSAFE_PortalProvider context automatically for portal container (no need for UNSTABLE_portalContainer).
*/
export const Popover = ({
children,
@@ -82,10 +85,11 @@ export const Popover = ({
withArrow?: boolean
} & Omit<DialogProps, 'children'>) => {
const [trigger, popoverContent] = children
const boundaryElement = useOverlayBoundaryElement()
return (
<DialogTrigger>
{trigger}
<StyledPopover>
<StyledPopover boundaryElement={boundaryElement}>
{withArrow && (
<StyledOverlayArrow variant={variant}>
<svg width={12} height={12} viewBox="0 0 12 12">
+32 -8
View File
@@ -6,12 +6,17 @@ import {
type TooltipProps,
} from 'react-aria-components'
import { styled } from '@/styled-system/jsx'
import { useOverlayPortalContainer } from './useOverlayPortalContainer'
import { VisualOnlyTooltip } from './VisualOnlyTooltip'
export type TooltipWrapperProps = {
tooltip?: string
tooltipType?: 'instant' | 'delayed'
}
const INSTANT_TOOLTIP_DELAY_MS = 150
const DELAYED_TOOLTIP_DELAY_MS = 1000
/**
* Wrap a component you want to apply a tooltip on (for example a Button)
*
@@ -24,11 +29,25 @@ export const TooltipWrapper = ({
}: {
children: ReactNode
} & TooltipWrapperProps) => {
const portalContainer = useOverlayPortalContainer()
const isExternalDocument =
portalContainer && portalContainer.ownerDocument !== document
return tooltip ? (
<TooltipTrigger delay={tooltipType === 'instant' ? 150 : 1000}>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
isExternalDocument ? (
<VisualOnlyTooltip tooltip={tooltip}>{children}</VisualOnlyTooltip>
) : (
<TooltipTrigger
delay={
tooltipType === 'instant'
? INSTANT_TOOLTIP_DELAY_MS
: DELAYED_TOOLTIP_DELAY_MS
}
>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
)
) : (
children
)
@@ -39,6 +58,8 @@ export const TooltipWrapper = ({
*
* Style taken from example at https://react-spectrum.adobe.com/react-aria/Tooltip.html
*/
const DEFAULT_TOOLTIP_GAP_PX = 8
const StyledTooltip = styled(RACTooltip, {
base: {
boxShadow: '0 8px 20px rgba(0 0 0 / 0.1)',
@@ -53,11 +74,11 @@ const StyledTooltip = styled(RACTooltip, {
fontSize: 14,
transform: 'translate3d(0, 0, 0)',
'&[data-placement=top]': {
marginBottom: '8px',
marginBottom: `${DEFAULT_TOOLTIP_GAP_PX}px`,
'--origin': 'translateY(4px)',
},
'&[data-placement=bottom]': {
marginTop: '8px',
marginTop: `${DEFAULT_TOOLTIP_GAP_PX}px`,
'--origin': 'translateY(-4px)',
},
'&[data-placement=right]': {
@@ -107,10 +128,13 @@ const TooltipArrow = () => {
const Tooltip = ({
children,
arrowBoundaryOffset,
...props
}: Omit<TooltipProps, 'children'> & { children: ReactNode }) => {
}: {
children: ReactNode
} & Partial<Omit<TooltipProps, 'children'>>) => {
return (
<StyledTooltip {...props}>
<StyledTooltip arrowBoundaryOffset={arrowBoundaryOffset ?? 0} {...props}>
<TooltipArrow />
{children}
</StyledTooltip>
@@ -2,11 +2,14 @@ import {
type ReactElement,
cloneElement,
isValidElement,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import { css } from '@/styled-system/css'
import { useUNSAFE_PortalContext } from '@react-aria/overlays'
export type VisualOnlyTooltipProps = {
children: ReactElement
@@ -32,11 +35,17 @@ export const VisualOnlyTooltip = ({
tooltipPosition = 'top',
}: VisualOnlyTooltipProps) => {
const [isVisible, setIsVisible] = useState(false)
const { getContainer } = useUNSAFE_PortalContext()
const wrapperRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState<{
top: number
left: number
} | null>(null)
const [computedStyle, setComputedStyle] = useState<{
left: number
arrowLeft: number
} | null>(null)
const isBottom = tooltipPosition === 'bottom'
@@ -53,9 +62,28 @@ export const VisualOnlyTooltip = ({
const hideTooltip = () => {
setIsVisible(false)
setPosition(null)
setComputedStyle(null)
}
const tooltipData = isVisible && position ? { isVisible, position } : null
useLayoutEffect(() => {
if (!tooltipRef.current || !isVisible || !position) return
const tooltipWidth = tooltipRef.current.getBoundingClientRect().width
const doc = tooltipRef.current.ownerDocument
const viewportWidth = doc.defaultView?.innerWidth ?? window.innerWidth
const padding = 8
const desiredLeft = position.left - tooltipWidth / 2
const maxLeft = viewportWidth - padding - tooltipWidth
if (desiredLeft <= maxLeft) {
setComputedStyle(null)
return
}
setComputedStyle({ left: maxLeft, arrowLeft: position.left - maxLeft })
}, [isVisible, position])
const portalContainer = useMemo(() => {
if (getContainer) return getContainer()
return wrapperRef.current?.ownerDocument?.body ?? document.body
}, [getContainer])
const wrappedChild = isValidElement(children)
? cloneElement(children, {
...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
@@ -73,11 +101,14 @@ export const VisualOnlyTooltip = ({
>
{wrappedChild}
</div>
{tooltipData &&
{isVisible &&
position &&
portalContainer &&
createPortal(
<div
aria-hidden="true"
role="presentation"
ref={tooltipRef}
className={css({
position: 'fixed',
padding: '2px 8px',
@@ -87,12 +118,12 @@ export const VisualOnlyTooltip = ({
fontSize: 14,
whiteSpace: 'nowrap',
pointerEvents: 'none',
zIndex: 9999,
zIndex: 100001,
boxShadow: '0 8px 20px rgba(0 0 0 / 0.1)',
'&::after': {
content: '""',
position: 'absolute',
left: '50%',
left: 'var(--tooltip-arrow-left, 50%)',
transform: 'translateX(-50%)',
border: '4px solid transparent',
...(isBottom
@@ -107,16 +138,27 @@ export const VisualOnlyTooltip = ({
},
})}
style={{
top: `${tooltipData.position.top}px`,
left: `${tooltipData.position.left}px`,
transform: isBottom
? 'translate(-50%, 0)'
: 'translate(-50%, -100%)',
top: `${position.top}px`,
left: computedStyle
? `${computedStyle.left}px`
: `${position.left}px`,
transform: computedStyle
? isBottom
? 'translateY(0)'
: 'translateY(-100%)'
: isBottom
? 'translate(-50%, 0)'
: 'translate(-50%, -100%)',
...(computedStyle
? {
'--tooltip-arrow-left': `${computedStyle.arrowLeft}px`,
}
: null),
}}
>
{tooltip}
</div>,
document.body
portalContainer
)}
</>
)
@@ -0,0 +1,21 @@
import { useMemo } from 'react'
import { useUNSAFE_PortalContext } from '@react-aria/overlays'
/**
* Hook to retrieve the portal container for overlays (menus, tooltips, popovers).
* Returns the container from UNSAFE_PortalProvider context (pip-root in PiP, undefined in main window).
*/
export const useOverlayPortalContainer = () => {
const { getContainer } = useUNSAFE_PortalContext()
return useMemo(() => getContainer?.() ?? undefined, [getContainer])
}
/**
* Hook to retrieve the boundary element for overlay positioning.
* Returns the portal container in PiP (for PiP-relative positioning), undefined in main window.
*/
export const useOverlayBoundaryElement = () => {
const portalContainer = useOverlayPortalContainer()
return portalContainer
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { proxy } from 'valtio'
import {
PanelId,
SubPanelId,
} from '@/features/rooms/livekit/hooks/useSidePanel'
} from '@/features/rooms/livekit/types/panel'
type State = {
showHeader: boolean
+9
View File
@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
type State = {
isOpen: boolean
}
export const roomPiPStore = proxy<State>({
isOpen: false,
})
@@ -75,10 +75,6 @@ backend:
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
ROOM_SUBTITLE_ENABLED: True
EXTERNAL_API_ENABLED: True
APPLICATION_JWT_AUDIENCE: https://meet.127.0.0.1.nip.io/external-api/v1.0/
APPLICATION_JWT_SECRET_KEY: devKeyApplication
APPLICATION_BASE_URL: https://meet.127.0.0.1.nip.io
migrate:
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.7.0",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.7.0",
"version": "1.5.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.7.0",
"version": "1.5.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.7.0",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.7.0",
"version": "1.5.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.7.0",
"version": "1.5.0",
"author": "",
"license": "ISC",
"description": "",
+7 -7
View File
@@ -1,9 +1,12 @@
FROM python:3.13-alpine3.23 AS base
FROM python:3.13-slim AS base
# Install ffmpeg for audio/video processing (format conversion, extraction, compression)
# See summary/core/file_service.py for usage.
RUN apk add --no-cache "ffmpeg=8.0.1-r1"
RUN apt-get update && \
apt-get install -y --no-install-recommends ffmpeg=7:7.1.3-* && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
FROM base AS builder
@@ -11,13 +14,13 @@ WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir .
RUN pip3 install --no-cache-dir .
FROM base AS development
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -e ".[dev]" || pip install --no-cache-dir -e .
RUN pip3 install --no-cache-dir -e ".[dev]" || pip3 install --no-cache-dir -e .
CMD ["uvicorn", "summary.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
@@ -25,9 +28,6 @@ FROM base AS production
WORKDIR /app
# Remove pip to reduce attack surface in production
RUN pip uninstall -y pip
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.7.0"
version = "1.5.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",