Compare commits

..

5 Commits

Author SHA1 Message Date
lebaudantoine 5c85c176f1 very wip commit of a poc 2026-03-09 22:37:59 +01:00
Cyril d91f8bb6e1 Merge branch 'fix/homepage-knowmore-link' 2026-03-09 14:41:26 +01:00
lebaudantoine d612f9b26b 🩹(changelog) fix changelog organisation for v1.10 2026-03-09 14:32:30 +01:00
Cyril 042be17cfa ️(frontend) improve MoreLink a11y and UX on home page
Render MoreLink once in LeftColumn, make full phrase clickable and add icon.
2026-03-09 14:27:39 +01:00
Cyril d00f4fa695 ️(frontend) sync html lang attribute with i18n for screen readers
Set document.documentElement.lang in i18n init and on languageChanged,
2026-03-09 13:32:10 +01:00
17 changed files with 312 additions and 191 deletions
+5 -7
View File
@@ -11,16 +11,10 @@ and this project adheres to
### Changed
- ♿️(frontend) sync html lang attribute with i18n for screen readers #1111
- ♿️(frontend) improve MoreLink a11y and UX on home page #1112
## [1.10.0] - 2026-03-05
### Fixed
- 🩹(frontend) remove incorrect reference to ProConnect on the prejoin #1080
- ✨(frontend) add Ctrl+Shift+/ to open shortcuts settings #1050
- ♿(frontend) announce selected state to screen readers #1081
- 💄(frontend) truncate long names with ellipsis in reaction overlay #1099
### Changed
- 🔒️(backend) enhance API input validation to strengthen security #1053
@@ -34,6 +28,10 @@ and this project adheres to
- 💄(frontend) truncate pinned participant name with ellipsis on overflow #1056
- ♿(frontend) prevent focus ring clipping on invite dialog #1078
- ♿(frontend) dynamic tab title when connected to meeting #1060
- 🩹(frontend) remove incorrect reference to ProConnect on the prejoin #1080
- ✨(frontend) add Ctrl+Shift+/ to open shortcuts settings #1050
- ♿(frontend) announce selected state to screen readers #1081
- 💄(frontend) truncate long names with ellipsis in reaction overlay #1099
### Added
@@ -2,23 +2,25 @@ import { A, Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useConfig } from '@/api/useConfig'
const appTitle = import.meta.env.VITE_APP_TITLE ?? 'LaSuite Meet'
export const MoreLink = () => {
const { t } = useTranslation('home')
const { data } = useConfig()
if (!data?.manifest_link) return
if (!data?.manifest_link) return null
return (
<Text as={'p'} variant={'sm'} style={{ padding: '1rem 0' }}>
<Text as="p" variant="sm" style={{ padding: '1rem 0' }}>
<A
href={data?.manifest_link}
target="_blank"
rel="noopener noreferrer"
aria-label={t('moreLinkLabel')}
externalIcon
aria-label={t('moreLinkLabel', { appTitle })}
>
{t('moreLink')}
</A>{' '}
{t('moreAbout', { appTitle: `${import.meta.env.VITE_APP_TITLE}` })}
{t('moreLink')} {t('moreAbout', { appTitle })}
</A>
</Text>
)
}
+1 -14
View File
@@ -258,23 +258,10 @@ export const Home = () => {
</DialogTrigger>
</div>
<Separator />
<div
className={css({
display: { base: 'none', lg: 'inline' },
})}
>
<MoreLink />
</div>
<MoreLink />
</LeftColumn>
<RightColumn>
<IntroSlider />
<div
className={css({
display: { base: 'inline', lg: 'none' },
})}
>
<MoreLink />
</div>
</RightColumn>
</Columns>
<LaterMeetingDialog
@@ -10,20 +10,14 @@ import { decodeNotificationDataReceived } from './utils'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { WaitingParticipantNotification } from './components/WaitingParticipantNotification'
import {
Emoji,
Reaction,
} from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import {
ANIMATION_DURATION,
ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal'
import { Emoji } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import { ANIMATION_DURATION } from '@/features/rooms/livekit/components/ReactionPortal'
import { reactionsStore } from '@/stores/reaction.ts'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
const [reactions, setReactions] = useState<Reaction[]>([])
const instanceIdRef = useRef(0)
useEffect(() => {
@@ -51,16 +45,22 @@ export const MainNotificationToast = () => {
const handleEmoji = (emoji: string, participant: Participant) => {
if (!emoji || !Object.values(Emoji).includes(emoji as Emoji)) return
const id = instanceIdRef.current++
setReactions((prev) => [
...prev,
{
id,
emoji,
participant,
},
])
const newReaction = {
id: `distant-${id}`,
emoji: emoji,
isLocalParticipant: false,
participantName: participant.name || `${participant.identity}`,
}
reactionsStore.reactions.push(newReaction)
// Remove this reaction after animation
setTimeout(() => {
setReactions((prev) => prev.filter((instance) => instance.id !== id))
const index = reactionsStore.reactions.findIndex(
(r) => r.id === newReaction.id
)
if (index !== -1) reactionsStore.reactions.splice(index, 1)
}, ANIMATION_DURATION)
}
@@ -238,7 +238,6 @@ export const MainNotificationToast = () => {
<Div position="absolute" bottom={0} right={5} zIndex={1000}>
<ToastProvider />
<WaitingParticipantNotification />
<ReactionPortals reactions={reactions} />
</Div>
)
}
@@ -9,6 +9,7 @@ import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { accessibilityStore } from '@/stores/accessibility'
import { useSnapshot } from 'valtio'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { reactionsStore } from '@/stores/reaction.ts'
export const ANIMATION_DURATION = 3000
export const ANIMATION_DISTANCE = 300
@@ -118,12 +119,13 @@ export function FloatingReaction({
export function ReactionPortal({
emoji,
participant,
participantName,
isLocalParticipant,
}: {
emoji: string
participant: Participant
participantName: string
isLocalParticipant: boolean
}) {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const speed = useMemo(() => Math.random() * 1.5 + 0.5, [])
const scale = useMemo(() => Math.max(Math.random() + 0.5, 1), [])
return createPortal(
@@ -141,17 +143,18 @@ export function ReactionPortal({
emoji={emoji}
speed={speed}
scale={scale}
name={participant?.isLocal ? t('you') : participant.name}
isLocal={participant?.isLocal}
name={participantName}
isLocal={isLocalParticipant}
/>
</div>,
document.body
)
}
export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
export const ReactionPortals = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { announceReactions } = useSnapshot(accessibilityStore)
const { reactions } = useSnapshot(reactionsStore)
const [lastAnnouncedId, setLastAnnouncedId] = useState<number | null>(null)
const announce = useScreenReaderAnnounce()
@@ -181,7 +184,8 @@ export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
<ReactionPortal
key={instance.id}
emoji={instance.emoji}
participant={instance.participant}
participantName={instance.participantName}
isLocalParticipant={instance.isLocalParticipant}
/>
))}
</>
@@ -14,6 +14,11 @@ import { Admin } from './Admin'
import { Tools } from './Tools'
import { Info } from './Info'
import { HStack } from '@/styled-system/jsx'
import { useSnapshot } from 'valtio'
import {
WIP_ADDITIONAL_HEIGHT_REACTION,
WIP_MIN_HEIGHT,
} from '@/features/rooms/livekit/prefabs/VideoConference'
type StyledSidePanelProps = {
title: string
@@ -35,6 +40,7 @@ const StyledSidePanel = ({
isClosed,
closeButtonTooltip,
isSubmenu = false,
isReactionOpen,
onBack,
backButtonLabel,
}: StyledSidePanelProps) => (
@@ -56,12 +62,14 @@ const StyledSidePanel = ({
gap: 0,
right: 0,
top: 0,
bottom: '80px',
width: '360px',
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
})}
style={{
transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
bottom: isReactionOpen
? `${WIP_MIN_HEIGHT + WIP_ADDITIONAL_HEIGHT_REACTION}px`
: `${WIP_MIN_HEIGHT}px`,
}}
aria-hidden={isClosed}
aria-label={ariaLabel}
@@ -150,6 +158,9 @@ export const SidePanel = () => {
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const layoutSnap = useSnapshot(layoutStore)
const isReactionOpen = layoutSnap.showReaction
return (
<StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)}
@@ -165,6 +176,7 @@ export const SidePanel = () => {
isSubmenu={isSubPanelOpen}
backButtonLabel={t('backToTools')}
onBack={() => (layoutStore.activeSubPanelId = null)}
isReactionOpen={isReactionOpen}
>
<Panel isOpen={isParticipantsOpen}>
<ParticipantsList />
@@ -12,14 +12,11 @@ import {
} from '@/features/rooms/livekit/components/ReactionPortal'
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import {
Popover as RACPopover,
Dialog,
DialogTrigger,
} from 'react-aria-components'
import { FocusScope } from '@react-aria/focus'
import { Participant } from 'livekit-client'
import { FocusScope, useFocusManager } from '@react-aria/focus'
import useRateLimiter from '@/hooks/useRateLimiter'
import { layoutStore } from '@/stores/layout.ts'
import { useSnapshot } from 'valtio'
import { reactionsStore } from '@/stores/reaction.ts'
// eslint-disable-next-line react-refresh/only-export-components
export enum Emoji {
@@ -36,22 +33,117 @@ export enum Emoji {
export interface Reaction {
id: number
emoji: string
participant: Participant
isLocalParticipant: boolean
participantName: string
}
export const ReactionsToggle = () => {
const getFirstControlBarFocusable = () =>
document
.getElementById('desktop-control-bar')
?.querySelector<HTMLButtonElement>('button:not([disabled])') ?? null
const ReactionButton = ({ emoji, debouncedSendReaction, index }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const [reactions, setReactions] = useState<Reaction[]>([])
const focusManager = useFocusManager()
let onKeyDown = (e) => {
console.log(e)
switch (e.key) {
case 'ArrowRight':
focusManager?.focusNext({ wrap: true })
break
case 'ArrowLeft':
focusManager?.focusPrevious({ wrap: true })
break
case 'Escape':
layoutStore.showReaction = false
break
case 'Tab':
console.log(getFirstControlBarFocusable)
if (!e.shiftKey) getFirstControlBarFocusable()?.focus()
break
}
}
return (
<Button
onPress={() => debouncedSendReaction(emoji)}
onKeyDown={onKeyDown}
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
variant="primaryTextDark"
size="sm"
square
round
tabIndex={index != 0 && -1}
data-attr={`send-reaction-${emoji}`}
>
<img
src={`/assets/reactions/${emoji}.png`}
alt=""
className={css({
width: '28px',
height: '28px',
pointerEvents: 'none',
userSelect: 'none',
})}
/>
</Button>
)
}
const ForceFocusFirst = (props) => {
const focusManager = useFocusManager()
return (
<div
className={css({
display: 'flex',
gap: '0.2rem',
})}
onFocus={(e) => {
// Only trigger when focus enters from outside this div
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
const w = focusManager?.focusFirst()
}
}}
>
{props.children}
</div>
)
}
const Toolbar = (props) => {
return (
<div
role="toolbar"
className={css({
display: 'flex',
borderRadius: '20px',
padding: '0.15rem',
backgroundColor: 'primaryDark.100',
'&[data-entering]': {
animation: 'fade 200ms ease',
},
'&[data-exiting]': {
animation: 'fade 200ms ease-in reverse',
},
'@media (min-width: 610px)': {
marginRight: '59px',
},
})}
>
<FocusScope autoFocus>
<ForceFocusFirst>{props.children}</ForceFocusFirst>
</FocusScope>
</div>
)
}
export const ReactionToolbar = () => {
const instanceIdRef = useRef(0)
const room = useRoomContext()
const [isOpen, setIsOpen] = useState(false)
useRegisterKeyboardShortcut({
id: 'reaction',
handler: () => setIsOpen((prev) => !prev),
})
const sendReaction = async (emoji: string) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
@@ -64,17 +156,20 @@ export const ReactionsToggle = () => {
await room.localParticipant.publishData(data, { reliable: true })
const newReaction = {
id: instanceIdRef.current++,
id: `local-${instanceIdRef.current++}`,
emoji,
participant: room.localParticipant,
isLocalParticipant: room.localParticipant.isLocal,
participantName: room.localParticipant.name,
}
setReactions((prev) => [...prev, newReaction])
reactionsStore.reactions.push(newReaction)
// Remove this reaction after animation
setTimeout(() => {
setReactions((prev) =>
prev.filter((instance) => instance.id !== newReaction.id)
const index = reactionsStore.reactions.findIndex(
(r) => r.id === newReaction.id
)
if (index !== -1) reactionsStore.reactions.splice(index, 1)
}, ANIMATION_DURATION)
}
@@ -84,78 +179,49 @@ export const ReactionsToggle = () => {
windowMs: 1000,
})
return (
<div>
<Toolbar>
{Object.values(Emoji).map((emoji, index) => (
<ReactionButton
key={index}
index={index}
emoji={emoji}
debouncedSendReaction={debouncedSendReaction}
/>
))}
</Toolbar>
</div>
)
}
export const ReactionsToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const layoutSnap = useSnapshot(layoutStore)
useRegisterKeyboardShortcut({
id: 'reaction',
handler: () => (layoutStore.showReaction = !layoutSnap.showReaction),
})
return (
<>
<div className={css({ position: 'relative' })}>
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<ToggleButton
square
variant="primaryDark"
aria-label={t('button')}
tooltip={t('button')}
isSelected={isOpen}
onChange={setIsOpen}
>
<RiEmotionLine />
</ToggleButton>
<RACPopover
placement="top"
offset={8}
isNonModal
shouldCloseOnInteractOutside={() => false}
className={css({
borderRadius: '8px',
padding: '0.35rem',
backgroundColor: 'primaryDark.50',
'&[data-entering]': {
animation: 'fade 200ms ease',
},
'&[data-exiting]': {
animation: 'fade 200ms ease-in reverse',
},
})}
>
<Dialog className={css({ outline: 'none' })}>
{/* eslint-disable-next-line jsx-a11y/no-autofocus -- FocusScope autoFocus is programmatic focus for overlays, not the HTML autofocus attribute */}
<FocusScope contain autoFocus restoreFocus>
<div
role="toolbar"
aria-orientation="horizontal"
aria-label={t('button')}
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
{Object.values(Emoji).map((emoji, index) => (
<Button
key={index}
onPress={() => debouncedSendReaction(emoji)}
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
variant="primaryTextDark"
size="sm"
square
data-attr={`send-reaction-${emoji}`}
>
<img
src={`/assets/reactions/${emoji}.png`}
alt=""
className={css({
width: '28px',
height: '28px',
pointerEvents: 'none',
userSelect: 'none',
})}
/>
</Button>
))}
</div>
</FocusScope>
</Dialog>
</RACPopover>
</DialogTrigger>
<ToggleButton
square
variant="primaryDark"
aria-label={t('button')}
tooltip={t('button')}
isSelected={layoutSnap.showReaction}
onChange={() => {
layoutStore.showReaction = !layoutSnap.showReaction
}}
>
<RiEmotionLine />
</ToggleButton>
</div>
<ReactionPortals reactions={reactions} />
<ReactionPortals />
</>
)
}
@@ -16,9 +16,9 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
export function ControlBar({ onDeviceError }: ControlBarProps) {
const isMobile = useIsMobile()
if (isMobile) {
return <MobileControlBar onDeviceError={onDeviceError} />
}
// if (isMobile) {
// return <MobileControlBar onDeviceError={onDeviceError} />
// }
return <DesktopControlBar onDeviceError={onDeviceError} />
}
@@ -3,7 +3,10 @@ import { ControlBarAuxProps } from './ControlBar'
import { css } from '@/styled-system/css'
import { LeaveButton } from '../../components/controls/LeaveButton'
import { Track } from 'livekit-client'
import { ReactionsToggle } from '../../components/controls/ReactionsToggle'
import {
ReactionsToggle,
ReactionToolbar,
} from '../../components/controls/ReactionsToggle'
import { HandToggle } from '../../components/controls/HandToggle'
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { SubtitlesToggle } from '../../components/controls/SubtitlesToggle'
@@ -15,6 +18,8 @@ import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKey
import { useFullScreen } from '../../hooks/useFullScreen'
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
import { AudioDevicesControl } from '../../components/controls/Device/AudioDevicesControl'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout.ts'
export function DesktopControlBar({
onDeviceError,
@@ -24,6 +29,8 @@ export function DesktopControlBar({
const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({})
const layoutSnap = useSnapshot(layoutStore)
useRegisterKeyboardShortcut({
id: 'focus-toolbar',
handler: () => {
@@ -44,61 +51,76 @@ export function DesktopControlBar({
return (
<div
ref={desktopControlBarEl}
className={css({
width: '100vw',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
padding: '1.125rem',
bottom: 0,
left: 0,
right: 0,
})}
>
{layoutSnap.showReaction && <ReactionToolbar />}
<div
ref={desktopControlBarEl}
className={css({
width: '100vw',
display: 'flex',
justifyContent: 'flex-start',
flex: '1 1 33%',
alignItems: 'center',
gap: '0.5rem',
marginLeft: '0.5rem',
})}
/>
<div
className={css({
flex: '1 1 33%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
gap: '0.65rem',
padding: '1.125rem',
})}
style={{
paddingTop: layoutSnap.showReaction ? '0.65rem' : undefined,
}}
>
<AudioDevicesControl
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
<div
className={css({
display: 'flex',
justifyContent: 'flex-start',
flex: '1 1 33%',
alignItems: 'center',
gap: '0.5rem',
marginLeft: '0.5rem',
})}
/>
<VideoDeviceControl
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
/>
<ReactionsToggle />
{browserSupportsScreenSharing && (
<ScreenShareToggle
<div
id="desktop-control-bar"
className={css({
flex: '1 1 33%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
gap: '0.65rem',
})}
>
<AudioDevicesControl
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
onDeviceError?.({ source: Track.Source.Microphone, error })
}
/>
)}
<SubtitlesToggle />
<HandToggle />
<OptionsButton />
<LeaveButton />
<StartMediaButton />
<VideoDeviceControl
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
/>
<ReactionsToggle />
{browserSupportsScreenSharing && (
<ScreenShareToggle
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
/>
)}
<SubtitlesToggle />
<HandToggle />
<OptionsButton />
<LeaveButton />
<StartMediaButton />
</div>
<MoreOptions parentElement={desktopControlBarEl} />
</div>
<MoreOptions parentElement={desktopControlBarEl} />
</div>
)
}
@@ -6,7 +6,7 @@ import {
log,
} from '@livekit/components-core'
import { Participant, RoomEvent, Track } from 'livekit-client'
import React, { useCallback, useRef, useState, useEffect } from 'react'
import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react'
import {
ConnectionStateToast,
FocusLayoutContainer,
@@ -44,6 +44,11 @@ import { GridLayout } from '../components/layout/GridLayout'
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout.ts'
export const WIP_MIN_HEIGHT = 80
export const WIP_ADDITIONAL_HEIGHT_REACTION = 32
const LayoutWrapper = styled(
'div',
@@ -258,6 +263,15 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const [isShareErrorVisible, setIsShareErrorVisible] = useState(false)
const layoutSnap = useSnapshot(layoutStore)
const isReactionOpen = layoutSnap.showReaction
const height = useMemo(() => {
let h = WIP_MIN_HEIGHT
if (isReactionOpen) return h + WIP_ADDITIONAL_HEIGHT_REACTION
return h
}, [isReactionOpen])
return (
<div
className="lk-video-conference"
@@ -281,8 +295,8 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
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))`,
? `var(--lk-grid-gap) calc(358px + 3rem) calc(${height}px + var(--lk-grid-gap)) 16px`
: `var(--lk-grid-gap) var(--lk-grid-gap) calc(${height}px + var(--lk-grid-gap))`,
transition: 'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms',
maxHeight: '100%',
}}
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "Sie können einem Meeting beitreten, indem Sie den vollständigen Link in die Adressleiste Ihres Browsers einfügen.",
"joinMeetingTipHeading": "Wussten Sie schon?",
"loginToCreateMeeting": "Melden Sie sich an, um ein Meeting zu erstellen",
"moreLinkLabel": "Mehr erfahren neues Tab",
"moreLinkLabel": "Mehr erfahren über {{appTitle}} neues Tab",
"moreLink": "Mehr erfahren",
"moreAbout": "über {{appTitle}}",
"createMenu": {
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "You can join a meeting by pasting its full link in the browser's address bar.",
"joinMeetingTipHeading": "Did you know?",
"loginToCreateMeeting": "Login to create a meeting",
"moreLinkLabel": "Learn more - new tab",
"moreLinkLabel": "Learn more about {{appTitle}} - new tab",
"moreLink": "Learn more",
"moreAbout": "about {{appTitle}}",
"createMenu": {
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "Vous pouvez rejoindre une réunion en copiant directement son lien complet dans la barre d'adresse du navigateur.",
"joinMeetingTipHeading": "Astuce",
"loginToCreateMeeting": "Connectez-vous pour créer une réunion",
"moreLinkLabel": "En savoir plus - nouvelle fenêtre",
"moreLinkLabel": "En savoir plus sur {{appTitle}} - nouvelle fenêtre",
"moreLink": "En savoir plus",
"moreAbout": "sur {{appTitle}}",
"createMenu": {
+1 -1
View File
@@ -10,7 +10,7 @@
"joinMeetingTipContent": "U kunt deelnemen aan een vergadering door de volledige link in de adresbalk van de browser te plakken.",
"joinMeetingTipHeading": "Wist u dat?",
"loginToCreateMeeting": "Log in om een vergadering te maken",
"moreLinkLabel": "Meer informatie - nieuw tabblad",
"moreLinkLabel": "Meer informatie over {{appTitle}} - nieuw tabblad",
"moreLink": "Meer informatie",
"moreAbout": "over {{appTitle}}",
"createMenu": {
@@ -44,6 +44,11 @@ export const buttonRecipe = cva({
paddingY: 'var(--square-padding)',
},
},
round: {
true: {
borderRadius: '50%',
},
},
variant: {
primary: {
backgroundColor: 'primary.800',
+2
View File
@@ -10,6 +10,7 @@ type State = {
showSubtitles: boolean
activePanelId: PanelId | null
activeSubPanelId: SubPanelId | null
showReaction: boolean
}
export const layoutStore = proxy<State>({
@@ -18,4 +19,5 @@ export const layoutStore = proxy<State>({
showSubtitles: false,
activePanelId: null,
activeSubPanelId: null,
showReaction: false,
})
+10
View File
@@ -0,0 +1,10 @@
import { proxy } from 'valtio'
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle.tsx'
type State = {
reactions: Reaction[]
}
export const reactionsStore = proxy<State>({
reactions: [],
})