diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3d48b626..da9421a9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import './css/App.css'; import './css/index.css'; +import { CssBaseline, ThemeProvider } from '@mui/material'; import Room from './pages/MeetingRoom/index'; import GoodBye from './pages/GoodBye/index'; import WaitingRoom from './pages/WaitingRoom'; @@ -11,38 +12,42 @@ import { PublisherProvider } from './Context/PublisherProvider'; import RedirectToWaitingRoom from './components/RedirectToWaitingRoom'; import UnsupportedBrowserPage from './pages/UnsupportedBrowserPage'; import RoomContext from './Context/RoomContext'; +import customTheme from './utils/customTheme/customTheme'; const App = () => { return ( - - - }> - - - - } - /> - - - - - - - - } - /> - - } /> - } /> - } /> - - + + + + + }> + + + + } + /> + + + + + + + + } + /> + + } /> + } /> + } /> + + + ); }; diff --git a/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.spec.tsx b/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.spec.tsx index f35a0dfa..81925d8e 100644 --- a/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.spec.tsx +++ b/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.spec.tsx @@ -19,7 +19,13 @@ describe('SelectableOption', () => { it('renders with image when image is provided', () => { render( - {}} id="img-option" image="/test.jpg" /> + {}} + title="background" + id="img-option" + image="/test.jpg" + /> ); expect(screen.getByTestId('background-img-option')).toBeInTheDocument(); expect(screen.getByAltText('background')).toHaveAttribute('src', '/test.jpg'); diff --git a/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.tsx b/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.tsx index 67973420..938e9cf2 100644 --- a/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.tsx +++ b/frontend/src/components/BackgroundEffects/SelectableOption/SelectableOption.tsx @@ -1,6 +1,7 @@ import { ReactElement, ReactNode } from 'react'; import { Box, Paper, Tooltip } from '@mui/material'; import { DEFAULT_SELECTABLE_OPTION_WIDTH } from '../../../utils/constants'; +import { colors } from '../../../utils/customTheme/customTheme'; export type SelectableOptionProps = { isSelected: boolean; @@ -63,13 +64,13 @@ const SelectableOption = ({ height: size, overflow: 'hidden', borderRadius: '16px', - border: isSelected ? '2px solid #1976d2' : '', + border: isSelected ? `2px solid ${colors.primary}` : '', cursor: isDisabled ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.1s ease-in-out', - backgroundColor: isDisabled ? '#f5f5f5' : '#fff', + backgroundColor: isDisabled ? colors.backgroundDisabled : colors.background, opacity: isDisabled ? 0.5 : 1, }} {...otherProps} @@ -88,7 +89,7 @@ const SelectableOption = ({ background ) : ( diff --git a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx index 3f90ed4c..e862edc9 100644 --- a/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx +++ b/frontend/src/components/MeetingRoom/DeviceControlButton/DeviceControlButton.tsx @@ -13,6 +13,7 @@ import DeviceSettingsMenu from '../DeviceSettingsMenu'; import useBackgroundPublisherContext from '../../../hooks/useBackgroundPublisherContext'; import useConfigContext from '../../../hooks/useConfigContext'; import getControlButtonTooltip from '../../../utils/getControlButtonTooltip'; +import { colors } from '../../../utils/customTheme/customTheme'; export type DeviceControlButtonProps = { deviceType: 'audio' | 'video'; @@ -115,7 +116,7 @@ const DeviceControlButton = ({ data-testid={isAudio ? 'audio-dropdown-button' : 'video-dropdown-button'} > {open ? ( - + ) : ( )} diff --git a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx index ccdef9f4..9f3d822f 100644 --- a/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx +++ b/frontend/src/components/MeetingRoom/DeviceSettingsMenu/DeviceSettingsMenu.tsx @@ -14,6 +14,7 @@ import VideoDevices from '../VideoDevices'; import DropdownSeparator from '../DropdownSeparator'; import VideoDevicesOptions from '../VideoDevicesOptions'; import useConfigContext from '../../../hooks/useConfigContext'; +import { colors } from '../../../utils/customTheme/customTheme'; export type DeviceSettingsMenuProps = { deviceType: 'audio' | 'video'; @@ -55,7 +56,6 @@ const DeviceSettingsMenu = ({ const config = useConfigContext(); const isAudio = deviceType === 'audio'; const theme = useTheme(); - const customLightBlueColor = 'rgb(138, 180, 248)'; const { allowBackgroundEffects } = config.videoSettings; const shouldDisplayBackgroundEffects = hasMediaProcessorSupport() && allowBackgroundEffects; @@ -70,16 +70,16 @@ const DeviceSettingsMenu = ({ if (isAudio) { return ( <> - - - + + + ); } return ( <> - + {shouldDisplayBackgroundEffects && ( <> @@ -110,14 +110,14 @@ const DeviceSettingsMenu = ({ {renderSettingsMenu()} diff --git a/frontend/src/components/MeetingRoom/EmojiGrid/EmojiGridDesktop.tsx b/frontend/src/components/MeetingRoom/EmojiGrid/EmojiGridDesktop.tsx index 8ec03479..8db7b7a2 100644 --- a/frontend/src/components/MeetingRoom/EmojiGrid/EmojiGridDesktop.tsx +++ b/frontend/src/components/MeetingRoom/EmojiGrid/EmojiGridDesktop.tsx @@ -3,6 +3,7 @@ import { ReactElement, RefObject, useEffect, useState } from 'react'; import { PopperChildrenProps } from '@mui/base'; import SendEmojiButton from '../SendEmojiButton'; import emojiMap from '../../../utils/emojis'; +import { colors } from '../../../utils/customTheme/customTheme'; export type EmojiGridDesktopProps = { handleClickAway: (event: MouseEvent | TouchEvent) => void; @@ -57,8 +58,8 @@ const EmojiGridDesktop = ({ className="flex items-center justify-center" data-testid="emoji-grid" sx={{ - backgroundColor: 'rgb(32, 33, 36)', - color: '#fff', + backgroundColor: colors.darkGrey, + color: colors.onPrimary, padding: { xs: 1 }, borderRadius: 2, zIndex: 1, diff --git a/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx b/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx index 8d712bf9..8680642e 100644 --- a/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx +++ b/frontend/src/components/MeetingRoom/EmojiGridButton/EmojiGridButton.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import ToolbarButton from '../ToolbarButton'; import EmojiGrid from '../EmojiGrid/EmojiGrid'; import useConfigContext from '../../../hooks/useConfigContext'; +import { colors } from '../../../utils/customTheme/customTheme'; export type EmojiGridProps = { isEmojiGridOpen: boolean; @@ -46,7 +47,7 @@ const EmojiGridButton = ({ onClick={handleToggle} icon={ } ref={anchorRef} diff --git a/frontend/src/components/MeetingRoom/InputDevices/InputDevices.spec.tsx b/frontend/src/components/MeetingRoom/InputDevices/InputDevices.spec.tsx index 8780dcb9..905feaa1 100644 --- a/frontend/src/components/MeetingRoom/InputDevices/InputDevices.spec.tsx +++ b/frontend/src/components/MeetingRoom/InputDevices/InputDevices.spec.tsx @@ -74,7 +74,7 @@ describe('InputDevices Component', () => { }); it('renders all available audio input devices', () => { - render(); + render(); expect(screen.getByText('Microphone')).toBeInTheDocument(); @@ -85,7 +85,7 @@ describe('InputDevices Component', () => { }); it('changes audio input device on menu item click', () => { - render(); + render(); const micItem = screen.getByText('MacBook Pro Microphone (Built-in)'); fireEvent.click(micItem); @@ -97,7 +97,7 @@ describe('InputDevices Component', () => { }); it('does not call setAudioSource if selected device is not found', () => { - render(); + render(); const bogusItem = document.createElement('li'); bogusItem.textContent = 'Nonexistent Microphone'; @@ -110,7 +110,7 @@ describe('InputDevices Component', () => { publisherContext.publisher = null; mockUsePublisherContext.mockReturnValue(publisherContext); - render(); + render(); const micItem = screen.getByText('MacBook Pro Microphone (Built-in)'); fireEvent.click(micItem); @@ -120,7 +120,7 @@ describe('InputDevices Component', () => { }); it('shows check icon for selected device', () => { - render(); + render(); // The default audio device should be selected const checkIcon = screen.getByTestId('CheckIcon'); @@ -131,13 +131,13 @@ describe('InputDevices Component', () => { mockConfigContext.meetingRoomSettings.allowDeviceSelection = false; mockUseConfigContext.mockReturnValue(mockConfigContext); - render(); + render(); expect(screen.queryByText('Microphone')).not.toBeInTheDocument(); }); it('handles click event when audioDeviceId is found', () => { - render(); + render(); const micItem = screen.getByText('Soundcore Life A2 NC (Bluetooth)'); fireEvent.click(micItem); diff --git a/frontend/src/components/MeetingRoom/InputDevices/InputDevices.tsx b/frontend/src/components/MeetingRoom/InputDevices/InputDevices.tsx index a25a118c..8798c6de 100644 --- a/frontend/src/components/MeetingRoom/InputDevices/InputDevices.tsx +++ b/frontend/src/components/MeetingRoom/InputDevices/InputDevices.tsx @@ -9,10 +9,10 @@ import usePublisherContext from '../../../hooks/usePublisherContext'; import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage'; import useConfigContext from '../../../hooks/useConfigContext'; import cleanAndDedupeDeviceLabels from '../../../utils/cleanAndDedupeDeviceLabels'; +import { colors } from '../../../utils/customTheme/customTheme'; export type InputDevicesProps = { handleToggle: () => void; - customLightBlueColor: string; }; /** @@ -21,13 +21,9 @@ export type InputDevicesProps = { * Displays the audio input devices for a user. Handles switching audio input devices. * @param {InputDevicesProps} props - The props for the component. * @property {Function} handleToggle - The click handler to handle closing the menu. - * @property {string} customLightBlueColor - The custom color used for the toggled icon. * @returns {ReactElement | false} - The InputDevices component. */ -const InputDevices = ({ - handleToggle, - customLightBlueColor, -}: InputDevicesProps): ReactElement | false => { +const InputDevices = ({ handleToggle }: InputDevicesProps): ReactElement | false => { const { t } = useTranslation(); const { publisher } = usePublisherContext(); const { meetingRoomSettings } = useConfigContext(); @@ -83,14 +79,15 @@ const InputDevices = ({ backgroundColor: 'transparent', '&.Mui-selected': { backgroundColor: 'transparent', - color: customLightBlueColor, + color: colors.primaryLight, }, '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.12)', + backgroundColor: colors.primaryHover, }, }} > {isSelected ? ( - + ) : ( // Placeholder when CheckIcon is not displayed )} diff --git a/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.spec.tsx b/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.spec.tsx index 1d0f205e..679c5ddc 100644 --- a/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.spec.tsx +++ b/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.spec.tsx @@ -59,7 +59,7 @@ describe('OutputDevices Component', () => { }); it('renders all available audio output devices when supported', () => { - render(); + render(); expect(screen.getByText('Speakers')).toBeInTheDocument(); expect(screen.getByTestId('output-devices')).toBeInTheDocument(); @@ -73,7 +73,7 @@ describe('OutputDevices Component', () => { it('renders only default device when audio output is not supported', () => { (util.isGetActiveAudioOutputDeviceSupported as Mock).mockReturnValue(false); - render(); + render(); expect(screen.getByText('Speakers')).toBeInTheDocument(); expect(screen.getByText('System Default')).toBeInTheDocument(); @@ -83,7 +83,7 @@ describe('OutputDevices Component', () => { }); it('changes audio output device on menu item click when supported', async () => { - render(); + render(); const speakerItem = screen.getByText('Soundcore Life A2 NC (Bluetooth)'); fireEvent.click(speakerItem); @@ -97,7 +97,7 @@ describe('OutputDevices Component', () => { it('does not call setAudioOutputDevice when audio output is not supported', () => { (util.isGetActiveAudioOutputDeviceSupported as Mock).mockReturnValue(false); - render(); + render(); const defaultItem = screen.getByText('System Default'); fireEvent.click(defaultItem); @@ -107,7 +107,7 @@ describe('OutputDevices Component', () => { }); it('shows check icon for selected device', () => { - render(); + render(); // The device with deviceId 'default' should be selected const checkIcon = screen.getByTestId('CheckIcon'); @@ -117,7 +117,7 @@ describe('OutputDevices Component', () => { it('shows check icon for default device when only one device available', () => { (util.isGetActiveAudioOutputDeviceSupported as Mock).mockReturnValue(false); - render(); + render(); // When only default device is available, it should be selected const checkIcon = screen.getByTestId('CheckIcon'); @@ -128,7 +128,7 @@ describe('OutputDevices Component', () => { mockConfigContext.meetingRoomSettings.allowDeviceSelection = false; mockUseConfigContext.mockReturnValue(mockConfigContext); - render(); + render(); expect(screen.queryByTestId('output-device-title')).not.toBeInTheDocument(); expect(screen.queryByTestId('output-devices')).not.toBeInTheDocument(); diff --git a/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.tsx b/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.tsx index c32e1a75..a7d20435 100644 --- a/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.tsx +++ b/frontend/src/components/MeetingRoom/OutputDevices/OutputDevices.tsx @@ -10,10 +10,10 @@ import useAudioOutputContext from '../../../hooks/useAudioOutputContext'; import { isGetActiveAudioOutputDeviceSupported } from '../../../utils/util'; import useConfigContext from '../../../hooks/useConfigContext'; import cleanAndDedupeDeviceLabels from '../../../utils/cleanAndDedupeDeviceLabels'; +import { colors } from '../../../utils/customTheme/customTheme'; export type OutputDevicesProps = { handleToggle: () => void; - customLightBlueColor: string; }; /** @@ -22,13 +22,9 @@ export type OutputDevicesProps = { * Displays and switches audio output devices. * @param {OutputDevicesProps} props - The props for the component. * @property {() => void} handleToggle - Function to close the menu. - * @property {string} customLightBlueColor - The custom color used for the selected device. * @returns {ReactElement | false} - The OutputDevices component. */ -const OutputDevices = ({ - handleToggle, - customLightBlueColor, -}: OutputDevicesProps): ReactElement | false => { +const OutputDevices = ({ handleToggle }: OutputDevicesProps): ReactElement | false => { const { t } = useTranslation(); const { currentAudioOutputDevice, setAudioOutputDevice } = useAudioOutputContext(); const { meetingRoomSettings } = useConfigContext(); @@ -92,14 +88,15 @@ const OutputDevices = ({ backgroundColor: 'transparent', '&.Mui-selected': { backgroundColor: 'transparent', - color: customLightBlueColor, + color: colors.primaryLight, }, '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.12)', + backgroundColor: colors.primaryHover, }, }} > {isSelected ? ( - + ) : ( // Placeholder when CheckIcon is not displayed )} diff --git a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx index 462c419f..0fa37a77 100644 --- a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx +++ b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.spec.tsx @@ -61,14 +61,10 @@ describe('ReduceNoiseTestSpeakers', () => { vi.resetAllMocks(); }); - const defaultProps = { - customLightBlueColor: '#A338E6', - }; - it('renders the component with the correct elements', () => { mockHasMediaProcessorSupport.mockReturnValue(true); - render(); + render(); expect(screen.getByText('Advanced Noise Suppression')).toBeInTheDocument(); expect(screen.getByTestId('toggle-off-icon')).toBeInTheDocument(); @@ -78,7 +74,7 @@ describe('ReduceNoiseTestSpeakers', () => { it('does not render the component if media processor is not supported', () => { mockHasMediaProcessorSupport.mockReturnValue(false); - render(); + render(); expect(screen.queryByText('Advanced Noise Suppression')).not.toBeInTheDocument(); }); @@ -86,7 +82,7 @@ describe('ReduceNoiseTestSpeakers', () => { it('toggles the noise suppression state when clicked', async () => { mockHasMediaProcessorSupport.mockReturnValue(true); - render(); + render(); // Click the Advanced Noise Suppression button const toggleButton = screen.getByTestId('toggle-on-icon'); @@ -107,7 +103,7 @@ describe('ReduceNoiseTestSpeakers', () => { it('should update the UI when toggling the button', async () => { mockHasMediaProcessorSupport.mockReturnValue(true); - render(); + render(); const toggleButton = screen.getByText('Advanced Noise Suppression'); @@ -148,7 +144,7 @@ describe('ReduceNoiseTestSpeakers', () => { } as Partial as ConfigContextType; mockUseConfigContext.mockReturnValue(configContext); - render(); + render(); expect(screen.queryByText('Advanced Noise Suppression')).not.toBeInTheDocument(); }); diff --git a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx index 5af06990..44ee9b1c 100644 --- a/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx +++ b/frontend/src/components/MeetingRoom/ReduceNoiseTestSpeakers/ReduceNoiseTestSpeakers.tsx @@ -12,23 +12,16 @@ import DropdownSeparator from '../DropdownSeparator'; import SoundTest from '../../SoundTest'; import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage'; import useConfigContext from '../../../hooks/useConfigContext'; - -export type ReduceNoiseTestSpeakersProps = { - customLightBlueColor: string; -}; +import { colors } from '../../../utils/customTheme/customTheme'; /** * ReduceNoiseTestSpeakers Component * * This component displays options to enable advanced noise suppression * and to test the speakers. - * @param {ReduceNoiseTestSpeakersProps} props - the props for the component. - * @property {string} customLightBlueColor - the custom color used for the toggled icon. * @returns {ReactElement | false} Returns ReduceNoiseTestSpeakers component or false if the Vonage Media Processor is not supported. */ -const ReduceNoiseTestSpeakers = ({ - customLightBlueColor, -}: ReduceNoiseTestSpeakersProps): ReactElement | false => { +const ReduceNoiseTestSpeakers = (): ReactElement | false => { const { t } = useTranslation(); const { publisher, isPublishing } = usePublisherContext(); const config = useConfigContext(); @@ -70,7 +63,7 @@ const ReduceNoiseTestSpeakers = ({ sx={{ backgroundColor: 'transparent', '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.12)', + backgroundColor: colors.primaryHover, }, }} > @@ -90,7 +83,7 @@ const ReduceNoiseTestSpeakers = ({ diff --git a/frontend/src/components/MeetingRoom/SendEmojiButton/SendEmojiButton.tsx b/frontend/src/components/MeetingRoom/SendEmojiButton/SendEmojiButton.tsx index 09b39e40..a5c0218b 100644 --- a/frontend/src/components/MeetingRoom/SendEmojiButton/SendEmojiButton.tsx +++ b/frontend/src/components/MeetingRoom/SendEmojiButton/SendEmojiButton.tsx @@ -2,6 +2,7 @@ import { Button, Grid, GridSize } from '@mui/material'; import { ReactElement } from 'react'; import useSessionContext from '../../../hooks/useSessionContext'; import useIsSmallViewport from '../../../hooks/useIsSmallViewport'; +import { colors } from '../../../utils/customTheme/customTheme'; export type SendEmojiButtonProps = { emoji: string; @@ -27,7 +28,7 @@ const SendEmojiButton = ({ emoji }: SendEmojiButtonProps): ReactElement => { onClick={() => sendEmoji(emoji)} sx={{ '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.12)', + backgroundColor: colors.primaryHover, }, padding: '0.25rem', fontSize: '1.5rem', diff --git a/frontend/src/components/MeetingRoom/ToolbarOverflowButton/ToolbarOverflowButton.tsx b/frontend/src/components/MeetingRoom/ToolbarOverflowButton/ToolbarOverflowButton.tsx index 91f51d6d..ef8f73d9 100644 --- a/frontend/src/components/MeetingRoom/ToolbarOverflowButton/ToolbarOverflowButton.tsx +++ b/frontend/src/components/MeetingRoom/ToolbarOverflowButton/ToolbarOverflowButton.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import ToolbarButton from '../ToolbarButton'; import ToolbarOverflowMenu from '../ToolbarOverflowMenu'; import UnreadMessagesBadge from '../UnreadMessagesBadge'; +import { colors } from '../../../utils/customTheme/customTheme'; export type CaptionsState = { isUserCaptionsEnabled: boolean; @@ -65,7 +66,7 @@ const ToolbarOverflowButton = ({ onClick={handleButtonToggle} icon={ } sx={{ diff --git a/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.spec.tsx b/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.spec.tsx index 81f3559e..1abea539 100644 --- a/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.spec.tsx +++ b/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.spec.tsx @@ -83,7 +83,7 @@ describe('VideoDevices Component', () => { }); it('renders all available video devices', () => { - render(); + render(); expect(screen.getByText('Camera')).toBeInTheDocument(); expect(screen.getByText('FaceTime HD Camera')).toBeInTheDocument(); @@ -91,7 +91,7 @@ describe('VideoDevices Component', () => { }); it('changes video source on menu item click', () => { - render(); + render(); const camera2Item = screen.getByText('External Web Camera'); fireEvent.click(camera2Item); @@ -100,7 +100,7 @@ describe('VideoDevices Component', () => { }); it('does not call setVideoSource if selected device is not found', () => { - render(); + render(); const bogusItem = document.createElement('li'); bogusItem.textContent = 'Nonexistent Camera'; @@ -113,9 +113,7 @@ describe('VideoDevices Component', () => { mockConfigContext.meetingRoomSettings.allowDeviceSelection = false; mockUseConfigContext.mockReturnValue(mockConfigContext); - const { container } = render( - - ); + const { container } = render(); expect(container.firstChild).toBeNull(); }); diff --git a/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.tsx b/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.tsx index 0763cb08..283f7a17 100644 --- a/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.tsx +++ b/frontend/src/components/MeetingRoom/VideoDevices/VideoDevices.tsx @@ -9,10 +9,10 @@ import usePublisherContext from '../../../hooks/usePublisherContext'; import { setStorageItem, STORAGE_KEYS } from '../../../utils/storage'; import useConfigContext from '../../../hooks/useConfigContext'; import cleanAndDedupeDeviceLabels from '../../../utils/cleanAndDedupeDeviceLabels'; +import { colors } from '../../../utils/customTheme/customTheme'; export type VideoDevicesProps = { handleToggle: () => void; - customLightBlueColor: string; }; /** @@ -21,13 +21,9 @@ export type VideoDevicesProps = { * This component is responsible for rendering the list of video output devices (i.e. web cameras). * @param {VideoDevicesProps} props - the props for this component. * @property {() => void} handleToggle - the function that handles the toggle of video output device. - * @property {string} customLightBlueColor - the custom color used for the toggled icon. * @returns {ReactElement | false} - the video output devices component. */ -const VideoDevices = ({ - handleToggle, - customLightBlueColor, -}: VideoDevicesProps): ReactElement | false => { +const VideoDevices = ({ handleToggle }: VideoDevicesProps): ReactElement | false => { const { t } = useTranslation(); const { isPublishing, publisher } = usePublisherContext(); const { meetingRoomSettings } = useConfigContext(); @@ -93,14 +89,15 @@ const VideoDevices = ({ backgroundColor: 'transparent', '&.Mui-selected': { backgroundColor: 'transparent', - color: customLightBlueColor, + color: colors.primaryLight, }, '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.12)', + backgroundColor: colors.primaryHover, }, }} > {isSelected ? ( - + ) : ( // Placeholder when CheckIcon is not displayed )} diff --git a/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx b/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx index f7c34c87..6ed10af2 100644 --- a/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx +++ b/frontend/src/components/MeetingRoom/VideoDevicesOptions/VideoDevicesOptions.tsx @@ -2,6 +2,7 @@ import { Typography, MenuList, MenuItem } from '@mui/material'; import { ReactElement } from 'react'; import PortraitIcon from '@mui/icons-material/Portrait'; import { useTranslation } from 'react-i18next'; +import { colors } from '../../../utils/customTheme/customTheme'; export type VideoDevicesOptionsProps = { toggleBackgroundEffects: () => void; @@ -33,7 +34,7 @@ const VideoDevicesOptions = ({ sx={{ backgroundColor: 'transparent', '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.12)', + backgroundColor: colors.primaryHover, }, }} > diff --git a/frontend/src/components/MeetingRoom/VoiceIndicator/VoiceIndicator.tsx b/frontend/src/components/MeetingRoom/VoiceIndicator/VoiceIndicator.tsx index 1912de37..6f5d0456 100644 --- a/frontend/src/components/MeetingRoom/VoiceIndicator/VoiceIndicator.tsx +++ b/frontend/src/components/MeetingRoom/VoiceIndicator/VoiceIndicator.tsx @@ -1,5 +1,6 @@ import { Box, SxProps } from '@mui/material'; import { ReactElement } from 'react'; +import { colors } from '../../../utils/customTheme/customTheme'; export type VoiceIndicatorProps = { publisherAudioLevel: number; @@ -35,44 +36,47 @@ const VoiceIndicatorIcon = ({ size, }: VoiceIndicatorProps): ReactElement => { const barHeights = calculateBarHeights(publisherAudioLevel); + const isAnimating = publisherAudioLevel >= 5; return ( -
{barHeights.map((height, i) => ( -
-
-
+ ))} -
+ ); }; diff --git a/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx b/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx index 68ff986d..4e77e905 100644 --- a/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx +++ b/frontend/src/components/WaitingRoom/UserNameInput/UserNameInput.tsx @@ -1,7 +1,6 @@ import { TextField, Button, InputAdornment } from '@mui/material'; import React, { Dispatch, MouseEvent, ReactElement, SetStateAction, useState } from 'react'; import { PersonOutline } from '@mui/icons-material'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import useUserContext from '../../../hooks/useUserContext'; @@ -15,24 +14,6 @@ export type UserNameInputProps = { setUsername: Dispatch>; }; -declare module '@mui/material/styles' { - interface Palette { - blue: Palette['primary']; - } - - interface PaletteOptions { - blue?: PaletteOptions['primary']; - } -} - -const theme = createTheme({ - palette: { - blue: { - main: 'rgba(26,115,232,.9)', - }, - }, -}); - /** * UsernameInput Component * @@ -94,66 +75,62 @@ const UsernameInput = ({ username, setUsername }: UserNameInputProps): ReactElem }; return ( - -
-
-
{t('waitingRoom.title')}
-
-

{roomName}

-
-
- {t('waitingRoom.user.input.label')} -
-
- - - - ), - inputProps: { maxLength: 60 }, - }} - /> -
- + />
-
-
+ +
+ ); }; diff --git a/frontend/src/utils/customTheme/customTheme.spec.ts b/frontend/src/utils/customTheme/customTheme.spec.ts new file mode 100644 index 00000000..f2015ffe --- /dev/null +++ b/frontend/src/utils/customTheme/customTheme.spec.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import customTheme, { colors, fonts } from './customTheme'; + +describe('customTheme', () => { + describe('MUI theme object', () => { + it('should have correct palette colors', () => { + expect(customTheme.palette.primary.main).toBe(colors.primary); + expect(customTheme.palette.primary.contrastText).toBe(colors.onPrimary); + expect(customTheme.palette.background.default).toBe(colors.background); + expect(customTheme.palette.text.primary).toBe(colors.onBackground); + }); + + it('should have typography configured', () => { + expect(customTheme.typography.fontFamily).toBe(fonts.family); + }); + + it('should be in light mode', () => { + expect(customTheme.palette.mode).toBe('light'); + }); + + it('should have correct button primary styles', () => { + const primaryButton = customTheme.components?.MuiButton?.styleOverrides?.containedPrimary; + + if ( + typeof primaryButton === 'object' && + primaryButton !== null && + 'backgroundColor' in primaryButton && + 'color' in primaryButton && + 'boxShadow' in primaryButton + ) { + expect((primaryButton as { backgroundColor: string }).backgroundColor).toBe(colors.primary); + expect((primaryButton as { color: string }).color).toBe(colors.onPrimary); + } + }); + }); + + describe('theme structure validation', () => { + it('should have all required theme properties', () => { + expect(customTheme.palette).toBeDefined(); + expect(customTheme.components).toBeDefined(); + expect(customTheme.typography).toBeDefined(); + }); + + it('should have component overrides for major components', () => { + const { components } = customTheme; + expect(components?.MuiAppBar).toBeDefined(); + expect(components?.MuiPaper).toBeDefined(); + expect(components?.MuiTextField).toBeDefined(); + expect(components?.MuiTooltip).toBeDefined(); + }); + }); + + describe('color consistency', () => { + it('should use consistent primary color across components', () => { + const buttonPrimary = customTheme.components?.MuiButton?.styleOverrides?.containedPrimary; + const outlinedPrimary = customTheme.components?.MuiButton?.styleOverrides?.outlinedPrimary; + const textPrimary = customTheme.components?.MuiButton?.styleOverrides?.textPrimary; + + if ( + typeof buttonPrimary === 'object' && + buttonPrimary !== null && + 'backgroundColor' in buttonPrimary && + typeof outlinedPrimary === 'object' && + outlinedPrimary !== null && + 'color' in outlinedPrimary && + typeof textPrimary === 'object' && + textPrimary !== null && + 'color' in textPrimary + ) { + expect((buttonPrimary as { backgroundColor: string }).backgroundColor).toBe(colors.primary); + expect((outlinedPrimary as { color: string }).color).toBe(colors.primary); + expect((textPrimary as { color: string }).color).toBe(colors.primary); + } + }); + + it('should use consistent surface colors', () => { + const appBar = customTheme.components?.MuiAppBar?.styleOverrides?.root; + const paper = customTheme.components?.MuiPaper?.styleOverrides?.root; + + if ( + typeof appBar === 'object' && + appBar !== null && + typeof paper === 'object' && + paper !== null + ) { + if (typeof appBar === 'object' && appBar !== null && 'backgroundColor' in appBar) { + expect((appBar as { backgroundColor: string }).backgroundColor).toBe(colors.surface); + } + if (typeof paper === 'object' && paper !== null && 'backgroundColor' in paper) { + expect((paper as { backgroundColor: string }).backgroundColor).toBe(colors.background); + } + } + }); + }); +}); diff --git a/frontend/src/utils/customTheme/customTheme.ts b/frontend/src/utils/customTheme/customTheme.ts new file mode 100644 index 00000000..5ad20e38 --- /dev/null +++ b/frontend/src/utils/customTheme/customTheme.ts @@ -0,0 +1,155 @@ +import { createTheme } from '@mui/material'; + +// Material Design Color Palette +const colors = { + // Primary colors + primary: '#3E007E', + primaryLight: '#9575CD', + primaryHover: '#3E007E2F', + onPrimary: '#FFFFFF', + primaryContainer: '#6300C4', + onPrimaryContainer: '#FFFFFF', + surfaceTint: '#7F02F7', + + // Secondary colors + secondary: '#2F293B', + onSecondary: '#FFFFFF', + secondaryContainer: '#4C4659', + onSecondaryContainer: '#FFFFFF', + + // Tertiary colors (mapped to warning) + tertiary: '#2A005E', + onTertiary: '#FFFFFF', + tertiaryContainer: '#440291', + onTertiaryContainer: '#F7EDFF', + + // Error colors + error: '#600004', + onError: '#FFFFFF', + errorContainer: '#98000A', + onErrorContainer: '#FFFFFF', + + // Surface colors + background: '#FFFFFF', + backgroundDisabled: '#f5f5f5', + onBackground: '#1E1925', + surface: '#FCF8F8', + onSurface: '#000000', + surfaceVariant: '#E0E3E3', + onSurfaceVariant: '#000000', + surfaceContainer: '#E5E2E1', + surfaceContainerHigh: '#D7D4D3', + surfaceContainerHighest: '#C9C6C5', + + // Outline colors + outline: '#292D2D', + outlineVariant: '#464A4A', + + // Inverse colors + inverseSurface: '#313030', + inverseOnSurface: '#FFFFFF', + inversePrimary: '#D6BAFF', + + // Shadow and scrim + shadow: '#000000', + scrim: '#000000', + + // Neutral colors + darkGrey: '#202124', +} as const; + +// Typography +const fonts = { + family: ['system-ui', 'ui-sans-serif', 'Inter', 'Marker Felt', 'Trebuchet MS'].join(','), +} as const; + +const customTheme = createTheme({ + palette: { + mode: 'light', + primary: { + main: colors.primary, + contrastText: colors.onPrimary, + dark: colors.primaryContainer, + light: colors.surfaceTint, + }, + secondary: { + main: colors.secondary, + contrastText: colors.onSecondary, + dark: colors.secondaryContainer, + }, + warning: { + main: colors.tertiary, + contrastText: colors.onTertiary, + dark: colors.tertiaryContainer, + }, + error: { + main: colors.error, + contrastText: colors.onError, + dark: colors.errorContainer, + }, + background: { + default: colors.background, + paper: colors.surface, + }, + text: { + primary: colors.onBackground, + secondary: colors.onSurface, + }, + divider: colors.outline, + }, + components: { + // AppBar overrides + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: colors.surface, + color: colors.onSurface, + }, + }, + }, + // Paper overrides + MuiPaper: { + styleOverrides: { + root: { + backgroundColor: colors.background, + color: colors.onSurface, + }, + }, + }, + // TextField overrides + MuiTextField: { + styleOverrides: { + root: { + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: colors.outline, + }, + '&:hover fieldset': { + borderColor: colors.primary, + }, + '&.Mui-focused fieldset': { + borderColor: colors.primary, + }, + }, + }, + }, + }, + // Tooltip overrides + MuiTooltip: { + styleOverrides: { + tooltip: { + backgroundColor: colors.inverseSurface, + color: colors.inverseOnSurface, + }, + }, + }, + }, + typography: { + fontFamily: fonts.family, + }, +}); + +export default customTheme; + +// Export colors and utilities for use in other files +export { colors, fonts }; diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Electron-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Electron-linux.png index d3d07528..1462eb2f 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Electron-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Electron-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png index 6e437e6f..33da5ce5 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Microsoft-Edge-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Microsoft-Edge-linux.png index 6e437e6f..33da5ce5 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Microsoft-Edge-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Microsoft-Edge-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Mobile-Chrome-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Mobile-Chrome-linux.png index 17c5d08a..39b7c423 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Mobile-Chrome-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Mobile-Chrome-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Opera-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Opera-linux.png index 292f3f62..260cb02d 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Opera-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-Opera-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-firefox-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-firefox-linux.png index 0fc76de5..cf3e6e14 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-firefox-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Landing-page-UI-test-1-firefox-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Electron-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Electron-linux.png index 3d4a679a..09db261f 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Electron-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Electron-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png index a19dbfd6..4e9672f2 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Microsoft-Edge-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Microsoft-Edge-linux.png index a19dbfd6..4e9672f2 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Microsoft-Edge-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Microsoft-Edge-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Mobile-Chrome-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Mobile-Chrome-linux.png index 21c3c529..6f715f12 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Mobile-Chrome-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Mobile-Chrome-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Opera-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Opera-linux.png index 960becbe..13b1a686 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Opera-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-Opera-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-firefox-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-firefox-linux.png index bdbad4f5..2bf26347 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-firefox-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Unsupported-browser-page-UI-test-1-firefox-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Electron-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Electron-linux.png index 49e15e45..17d5648c 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Electron-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Electron-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png index be46c7e0..5f1d51b0 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Google-Chrome-Fake-Devices-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Microsoft-Edge-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Microsoft-Edge-linux.png index be46c7e0..5f1d51b0 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Microsoft-Edge-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Microsoft-Edge-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Mobile-Chrome-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Mobile-Chrome-linux.png index 5b5dc512..6ef1ace1 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Mobile-Chrome-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Mobile-Chrome-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Opera-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Opera-linux.png index 3bba8eff..a10dfeda 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Opera-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-Opera-linux.png differ diff --git a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-firefox-linux.png b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-firefox-linux.png index a0d43610..bf1137f4 100644 Binary files a/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-firefox-linux.png and b/integration-tests/tests/visualComparisons.spec.ts-snapshots/Waiting-page-UI-test-1-firefox-linux.png differ