Compare commits

..

2 Commits

Author SHA1 Message Date
Cyril 2861aa143e fixup! ♻️(frontend) use react aria for side panel a11y 2026-02-23 14:55:38 +01:00
Cyril 4748b55089 ♻️(frontend) use react aria for side panel a11y
leverage FocusScope for tab containment, escape and focus restore
2026-02-20 16:08:15 +01:00
22 changed files with 158 additions and 156 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Download Crowdin files
uses: crowdin/github-action@v2
+5 -5
View File
@@ -23,7 +23,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -59,7 +59,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -96,7 +96,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -133,7 +133,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -172,7 +172,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
-
name: Docker meta
id: meta
+19 -19
View File
@@ -14,7 +14,7 @@ jobs:
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: show
@@ -41,7 +41,7 @@ jobs:
github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Check that the CHANGELOG has been modified in the current branch
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
@@ -67,15 +67,15 @@ jobs:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Restore the mail templates
uses: actions/cache@v5
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -95,7 +95,7 @@ jobs:
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
@@ -107,9 +107,9 @@ jobs:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"
@@ -129,9 +129,9 @@ jobs:
working-directory: src/agents
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"
@@ -149,9 +149,9 @@ jobs:
working-directory: src/summary
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"
@@ -216,7 +216,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Create writable /data
run: |
@@ -224,7 +224,7 @@ jobs:
sudo mkdir -p /data/static
- name: Restore the mail templates
uses: actions/cache@v5
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -258,7 +258,7 @@ jobs:
mc mb meet/meet-media-storage"
- name: Install Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"
@@ -281,7 +281,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install dependencies
run: cd src/frontend/ && npm ci
@@ -299,7 +299,7 @@ jobs:
working-directory: src/sdk/library
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
@@ -318,7 +318,7 @@ jobs:
working-directory: src/sdk/library
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
+1 -10
View File
@@ -8,18 +8,9 @@ and this project adheres to
## [Unreleased]
## [1.8.0] - 2026-02-20
### 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
- (frontend) Use React Aria FocusScope for side panel accessibility
## [1.7.0] - 2026-02-19
-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'),
-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.8.0"
version = "1.7.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.8.0"
version = "1.7.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.8.0",
"version": "1.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.8.0",
"version": "1.7.0",
"dependencies": {
"@fontsource-variable/material-symbols-outlined": "5.2.34",
"@fontsource/material-icons-outlined": "5.2.6",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.8.0",
"version": "1.7.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -7,7 +7,7 @@ import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { ReactNode, useEffect, useRef } from 'react'
import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects'
import { Admin } from './Admin'
@@ -15,9 +15,11 @@ import { Tools } from './Tools'
import { Info } from './Info'
import { HStack } from '@/styled-system/jsx'
const SIDE_PANEL_HEADING_ID = 'side-panel-heading'
const SIDE_PANEL_CLOSE_ID = 'side-panel-close'
type StyledSidePanelProps = {
title: string
ariaLabel: string
children: ReactNode
onClose: () => void
isClosed: boolean
@@ -29,7 +31,6 @@ type StyledSidePanelProps = {
const StyledSidePanel = ({
title,
ariaLabel,
children,
onClose,
isClosed,
@@ -38,7 +39,14 @@ const StyledSidePanel = ({
onBack,
backButtonLabel,
}: StyledSidePanelProps) => (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- role="dialog" makes this interactive
<aside
role="dialog"
aria-labelledby={!isClosed ? SIDE_PANEL_HEADING_ID : undefined}
aria-hidden={isClosed || undefined}
onKeyDown={(e) => {
if (e.key === 'Escape') onClose()
}}
className={css({
borderWidth: '1px',
borderStyle: 'solid',
@@ -63,57 +71,57 @@ const StyledSidePanel = ({
style={{
transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
}}
aria-hidden={isClosed}
aria-label={ariaLabel}
>
<HStack alignItems="center">
{isSubmenu && (
<Button
variant="secondaryText"
size="sm"
square
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={backButtonLabel}
onPress={onBack}
{isSubmenu && (
<Button
variant="secondaryText"
size="sm"
square
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={backButtonLabel}
onPress={onBack}
>
<RiArrowLeftLine size={20} aria-hidden="true" />
</Button>
)}
<Heading
id={SIDE_PANEL_HEADING_ID}
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
>
<RiArrowLeftLine size={20} aria-hidden="true" />
</Button>
)}
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
{title}
</Heading>
</HStack>
<Div
position="absolute"
top="5"
right="5"
style={{
paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
display: isClosed ? 'none' : undefined,
}}
>
{title}
</Heading>
</HStack>
<Div
position="absolute"
top="5"
right="5"
style={{
display: isClosed ? 'none' : undefined,
}}
>
<Button
invisible
variant="tertiaryText"
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
{children}
<Button
id={SIDE_PANEL_CLOSE_ID}
invisible
variant="tertiaryText"
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
{children}
</aside>
)
@@ -135,6 +143,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null}
</div>
)
export const SidePanel = () => {
const {
activePanelId,
@@ -150,14 +159,51 @@ export const SidePanel = () => {
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const triggerRef = useRef<HTMLElement | null>(null)
// The aside stays mounted (CSS slide + keepAlive), so we manually handle
// auto-focus on open and restore focus on close (via handleClose).
useEffect(() => {
if (!isSidePanelOpen) return
const active = document.activeElement as HTMLElement
// Menu items render as DIVs that unmount when the menu closes — resolve to the menu trigger
triggerRef.current =
active?.tagName === 'DIV'
? (document.querySelector<HTMLElement>('#room-options-trigger') ??
active)
: active
requestAnimationFrame(() => {
const closeBtn = document.getElementById(SIDE_PANEL_CLOSE_ID)
// Skip if a child panel already moved focus inside (e.g. Chat input)
if (closeBtn?.closest('aside')?.contains(document.activeElement)) return
closeBtn?.focus({ preventScroll: true })
})
}, [isSidePanelOpen])
const handleClose = () => {
const trigger = triggerRef.current
triggerRef.current = null
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
// Double RAF: first lets React re-render, second lets FocusScope release containment
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (trigger?.isConnected) {
trigger.focus({ preventScroll: true })
} else {
document
.querySelector<HTMLElement>('#room-options-trigger')
?.focus({ preventScroll: true })
}
})
})
}
return (
<StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)}
ariaLabel={t('ariaLabel')}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
onClose={handleClose}
closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`),
})}
@@ -4,7 +4,6 @@ import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { ReactNode } from 'react'
import { SubPanelId, useSidePanel } from '../hooks/useSidePanel'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
import {
useIsRecordingModeEnabled,
RecordingMode,
@@ -95,26 +94,10 @@ const ToolButton = ({
export const Tools = () => {
const { data } = useConfig()
const { openTranscript, openScreenRecording, activeSubPanelId, isToolsOpen } =
const { openTranscript, openScreenRecording, activeSubPanelId } =
useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
// Restore focus to the element that opened the Tools panel
// following the same pattern as Chat.
useRestoreFocus(isToolsOpen, {
// If the active element is a MenuItem (DIV) that will be unmounted when the menu closes,
// find the "more options" button ("Plus d'options") that opened the menu
resolveTrigger: (activeEl) => {
if (activeEl?.tagName === 'DIV') {
return document.querySelector<HTMLElement>('#room-options-trigger')
}
// For direct button clicks (e.g. "Plus d'outils"), use the active element as is
return activeEl
},
restoreFocusRaf: true,
preventScroll: true,
})
const isTranscriptEnabled = useIsRecordingModeEnabled(
RecordingMode.Transcript
)
@@ -80,10 +80,12 @@ export const ChatInput = ({
<TextArea
ref={inputRef}
onKeyDown={(e) => {
e.stopPropagation()
if (e.key !== 'Escape') e.stopPropagation()
submitOnEnter(e)
}}
onKeyUp={(e) => e.stopPropagation()}
onKeyUp={(e) => {
if (e.key !== 'Escape') e.stopPropagation()
}}
placeholder={t('textArea.placeholder')}
value={text}
onChange={(e) => {
@@ -15,7 +15,6 @@ import { ChatEntry } from '../components/chat/Entry'
import { useSidePanel } from '../hooks/useSidePanel'
import { LocalParticipant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { css } from '@/styled-system/css'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
export interface ChatProps
extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {}
@@ -36,18 +35,12 @@ export function Chat({ ...props }: ChatProps) {
const { isChatOpen } = useSidePanel()
const chatSnap = useSnapshot(chatStore)
// Keep track of the element that opened the chat so we can restore focus
// when the chat panel is closed.
useRestoreFocus(isChatOpen, {
// Avoid layout "jump" during the side panel slide-in animation.
// Focusing can trigger scroll into view; preventScroll keeps the animation smooth.
onOpened: () => {
requestAnimationFrame(() => {
inputRef.current?.focus({ preventScroll: true })
})
},
preventScroll: true,
})
React.useEffect(() => {
if (!isChatOpen) return
requestAnimationFrame(() => {
inputRef.current?.focus({ preventScroll: true })
})
}, [isChatOpen])
// Use useParticipants hook to trigger a re-render when the participant list changes.
const participants = useParticipants()
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.8.0",
"version": "1.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.8.0",
"version": "1.7.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.8.0",
"version": "1.7.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.8.0",
"version": "1.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.8.0",
"version": "1.7.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.8.0",
"version": "1.7.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.8.0"
version = "1.7.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",