+
+
+
+
+ class="video-element"
+ >
+
+
+
+
+
-
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss
index d4621ad6..f504195f 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss
@@ -1,159 +1,350 @@
:host {
- .container {
- height: 100%;
- background-color: var(--ov-background-color);
+ display: block;
+ width: 100%;
+ height: 100%;
+
+ .prejoin-container {
+ min-height: 100vh;
+ background: var(--ov-background-color);
display: flex;
- justify-content: center;
align-items: center;
+ justify-content: center;
+ padding: 20px;
+ box-sizing: border-box;
+ position: relative;
}
- #loading-container {
+ // Loading State
+ .loading-overlay {
position: absolute;
- top: 40%;
+ top: 0;
left: 0;
right: 0;
- text-align: center;
- color: var(--ov-text-primary-color);
- .mat-mdc-progress-spinner {
- margin: auto;
- }
- }
-
- #prejoin-card {
+ bottom: 0;
display: flex;
- flex-direction: column;
align-items: center;
justify-content: center;
- margin: auto;
- border-radius: var(--ov-surface-radius);
- width: 90%;
- max-width: 370px;
- // max-height: 650px;
- height: min-content;
- padding: 55px 30px;
- background-color: var(--ov-surface-color);
- box-shadow: 6px 4px 20px rgba(0, 0, 0, 0.3);
- position: relative;
- }
+ background-color: var(--ov-background-color, #f5f5f5);
+ z-index: 1000;
- ::ng-deep .lang-btn {
- position: absolute;
- top: 10px;
- right: 10px;
- height: 25px !important;
- font-size: 14px !important;
- }
-
- ::ng-deep .lang-btn mat-icon {
- color: var(--ov-text-surface-color) !important;
- }
-
- .video-container {
- margin: auto;
- height: 35vh;
- width: 100%;
- max-width: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
-
- #video-poster {
- height: 100%;
- width: 100%;
- position: relative;
- border-radius: var(--ov-surface-radius);
- overflow: hidden;
- }
-
- .media-controls-container {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- width: 100%;
- margin-top: 15px;
- height: auto;
- }
-
- .participant-name-container {
- display: block !important;
- width: 100%;
- margin: 10px 0;
- }
-
- .video-controls-container,
- .audio-controls-container {
- width: calc(50% - 10px);
- margin: 5px 0;
- }
-
- .join-btn-container {
- width: 100%;
- margin-top: 15px;
- }
-
- #join-button {
- background-color: var(--ov-primary-action-color);
- color: var(--ov-secondary-action-color);
- font-weight: bold;
- border-radius: var(--ov-surface-radius);
- width: 100%;
- height: 50px;
- transition: background-color 0.3s;
- }
-
- // #join-button:hover {
- // background-color: lighten(var(--ov-primary-action-color), 10%);
- // }
-
- .error {
- font-size: 12px;
- font-weight: bold;
- font-style: italic;
- color: var(--ov-error-color);
- margin-top: 5px;
- }
-
- @media (max-width: 768px) {
- #prejoin-card {
- padding: 10px;
- }
-
- .video-container {
- height: 40vh;
- }
-
- .media-controls-container {
+ .loading-content {
+ display: flex;
flex-direction: column;
align-items: center;
- height: auto;
- }
+ gap: 16px;
- .video-controls-container,
- .audio-controls-container {
+ .loading-text {
+ color: var(--ov-text-primary-color, #333);
+ font-size: 16px;
+ font-weight: 500;
+ }
+
+ .mat-mdc-progress-spinner {
+ --mdc-circular-progress-active-indicator-color: var(--ov-primary-action-color, #4285f4);
+ }
+ }
+ }
+
+ // Main Content
+ .prejoin-main {
+ width: 100%;
+ max-width: 520px;
+ background: var(--ov-surface-color, #ffffff);
+ border-radius: var(--ov-surface-radius);
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.08),
+ 0 2px 8px rgba(0, 0, 0, 0.04);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+ .prejoin-header {
+ padding: 16px 20px 0;
+ display: flex;
+ justify-content: flex-end;
+
+ ::ng-deep .language-selector {
+ .mat-mdc-button {
+ padding: 8px 12px;
+ border-radius: var(--ov-surface-radius);
+ background-color: transparent;
+ border: 1px solid var(--ov-border-color, #e0e0e0);
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: var(--ov-hover-color, #f5f5f5);
+ }
+ }
+
+ mat-icon {
+ color: var(--ov-text-secondary-color, #666) !important;
+ font-size: 18px;
+ }
+ }
+ }
+
+ // Video Preview Section
+ .video-preview-section {
+ padding: 24px 24px 20px;
+
+ .video-preview-container {
+ position: relative;
width: 100%;
+ aspect-ratio: 16/9;
+ border-radius: var(--ov-surface-radius);
+ overflow: hidden;
+ background: #000;
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
+
+ .video-frame {
+ width: 100%;
+ height: 100%;
+ position: relative;
+
+ ::ng-deep .video-element {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+
+ video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+ }
+
+ .video-overlay {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ // background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%);
+ padding: 16px;
+ z-index: 9999;
+
+ .device-controls {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+ }
+ }
}
}
- @media (max-width: 800px) and (orientation: landscape) {
- .media-controls-container {
- flex-direction: row;
- justify-content: space-between;
+ // Configuration Section
+ .configuration-section {
+ padding: 0 24px 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+
+ .input-section {
+ ::ng-deep .name-input {
+ .mat-mdc-form-field {
+ width: 100%;
+
+ .mat-mdc-text-field-wrapper {
+ border-radius: var(--ov-surface-radius);
+ background-color: var(--ov-input-background, #f8f9fa);
+ border: 1px solid var(--ov-border-color, #e0e0e0);
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: var(--ov-primary-action-color, #4285f4);
+ }
+
+ &.mdc-text-field--focused {
+ border-color: var(--ov-primary-action-color, #4285f4);
+ box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
+ }
+ }
+
+ .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ }
+
+ input {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--ov-text-primary-color, #333);
+ padding: 16px;
+
+ &::placeholder {
+ color: var(--ov-text-secondary-color, #666);
+ font-weight: 400;
+ }
+ }
+ }
+ }
}
- .video-controls-container,
- .audio-controls-container {
- width: 48%;
+ .error-message {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 16px;
+ background-color: rgba(244, 67, 54, 0.08);
+ border: 1px solid rgba(244, 67, 54, 0.2);
+ border-radius: var(--ov-surface-radius);
+ color: var(--ov-error-color, #d32f2f);
+
+ .error-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+
+ .error-text {
+ font-size: 14px;
+ font-weight: 500;
+ }
+ }
+
+ .join-section {
+ .join-button {
+ width: 100%;
+ height: 56px;
+ background: var(--ov-primary-action-color, #4285f4);
+ color: white;
+ border-radius: var(--ov-surface-radius);
+ font-size: 16px;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ box-shadow: 0 2px 8px rgba(66, 133, 244, 0.2);
+
+ &:hover:not([disabled]) {
+ background: var(--ov-primary-action-hover, #3367d6);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 16px rgba(66, 133, 244, 0.3);
+ }
+
+ &:active:not([disabled]) {
+ transform: translateY(0);
+ }
+
+ &[disabled] {
+ background: var(--ov-disabled-color, #ccc);
+ color: var(--ov-disabled-text-color, #999);
+ cursor: not-allowed;
+ box-shadow: none;
+ }
+
+ .join-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ }
+ }
}
}
- @media (max-height: 630px) {
- .video-container {
- height: 30vh;
+ // Responsive Design
+ @media (max-width: 640px) {
+ .prejoin-container {
+ padding: 16px;
+ min-height: 100vh;
}
- .media-controls-container {
- height: auto;
+ .prejoin-main {
+ max-width: 100%;
+ border-radius: var(--ov-surface-radius);
}
+
+ .video-preview-section {
+ padding: 16px 20px 12px;
+
+ .video-preview-container {
+ aspect-ratio: 4/3;
+ }
+ }
+
+ .configuration-section {
+ padding: 0 20px 20px;
+ gap: 16px;
+ }
+
+ .prejoin-header {
+ padding: 12px 16px 0;
+ }
+ }
+
+ @media (max-width: 480px) {
+ .prejoin-container {
+ padding: 12px;
+ }
+
+ .video-preview-section {
+ padding: 12px 16px 8px;
+ }
+
+ .configuration-section {
+ padding: 0 16px 16px;
+ }
+
+ .video-overlay .device-controls {
+ gap: 8px;
+
+ ::ng-deep .device-selector .mat-mdc-icon-button {
+ width: 44px;
+ height: 44px;
+
+ mat-icon {
+ font-size: 18px;
+ }
+ }
+ }
+ }
+
+ @media (max-height: 640px) {
+ .prejoin-container {
+ align-items: flex-start;
+ padding-top: 20px;
+ }
+
+ .video-preview-section .video-preview-container {
+ aspect-ratio: 16/9;
+ }
+ }
+
+ // Dark theme support
+ @media (prefers-color-scheme: dark) {
+ .prejoin-container {
+ background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
+ }
+
+ .prejoin-main {
+ background: #2d2d2d;
+ box-shadow:
+ 0 8px 32px rgba(0, 0, 0, 0.3),
+ 0 2px 8px rgba(0, 0, 0, 0.2);
+ }
+
+ .configuration-section .input-section ::ng-deep .name-input .participant-name-input-container .input-wrapper {
+ background-color: #3a3a3a;
+ border-color: #555;
+ }
+ }
+
+ // Animation keyframes
+ @keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+
+ .prejoin-main {
+ animation: fadeIn 0.3s ease-out;
}
}
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts
index 99eb6803..e48dd8bd 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts
@@ -19,6 +19,8 @@ import { TranslateService } from '../../services/translate/translate.service';
import { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model';
+import { VirtualBackgroundService } from '../../services/virtual-background/virtual-background.service';
+import { BackgroundEffect } from '../../models/background-effect.model';
/**
* @internal
@@ -56,6 +58,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
showLogo: boolean = true;
showParticipantName: boolean = true;
+ // Future feature preparation
+ backgroundEffectEnabled: boolean = false;
+ availableBackgroundEffects: BackgroundEffect[] = [];
+ selectedBackgroundEffect: BackgroundEffect | undefined;
+
videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined;
private tracks: LocalTrack[];
@@ -74,9 +81,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService,
private translateService: TranslateService,
+ private virtualBackgroundService: VirtualBackgroundService,
private changeDetector: ChangeDetectorRef
) {
this.log = this.loggerSrv.get('PreJoinComponent');
+ this.availableBackgroundEffects = this.virtualBackgroundService.getBackgrounds();
}
async ngOnInit() {
@@ -105,14 +114,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
private async initializeDevices() {
- try {
- this.tracks = await this.openviduService.createLocalTracks();
- this.openviduService.setLocalTracks(this.tracks);
- this.videoTrack = this.tracks.find((track) => track.kind === 'video');
- this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
- } catch (error) {
- this.log.e('Error creating local tracks:', error);
- }
+ await this.initializeDevicesWithRetry();
}
onDeviceSelectorClicked() {
@@ -122,24 +124,27 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
join() {
- if (this.showParticipantName && !this.participantName) {
+ if (this.showParticipantName && !this.participantName?.trim()) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return;
}
+ // Clear any previous errors
+ this._error = undefined;
+
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
// Assign participant name to the observable if it is defined
- if (this.participantName) {
- this.libService.updateGeneralConfig({ participantName: this.participantName });
+ if (this.participantName?.trim()) {
+ this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
// Wait for the next tick to ensure the participant name propagates
// through the observable before emitting onReadyToJoin
this.libService.participantName$
.pipe(
takeUntil(this.destroy$),
- filter((name) => name === this.participantName),
+ filter((name) => name === this.participantName?.trim()),
tap(() => this.onReadyToJoin.emit())
)
.subscribe();
@@ -150,7 +155,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
onParticipantNameChanged(name: string) {
- if (name) this.participantName = name;
+ this.participantName = name?.trim() || '';
+ // Clear error when user starts typing
+ if (this._error && this.participantName) {
+ this._error = undefined;
+ }
}
onEnterPressed() {
@@ -210,4 +219,48 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
this.onAudioEnabledChanged.emit(enabled);
}
+
+ /**
+ * Future method for background effects
+ * @param effect - The background effect to apply
+ */
+ onBackgroundEffectChanged(effect: string) {
+ // TODO: Implement background effect logic
+ // this.selectedBackgroundEffect = effect;
+ // this.log.d('Background effect changed to:', effect);
+ // this.virtualBackgroundService.applyBackground(this.virtualBackgroundService.getBackgrounds()[0]);
+ }
+
+ /**
+ * Enhanced error handling with better UX
+ */
+ private handleError(error: any) {
+ this.log.e('PreJoin component error:', error);
+ this._error = error.message || 'An unexpected error occurred';
+ this.changeDetector.markForCheck();
+ }
+
+ /**
+ * Improved device initialization with error handling
+ */
+ private async initializeDevicesWithRetry(maxRetries: number = 3): Promise
{
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ this.tracks = await this.openviduService.createLocalTracks();
+ this.openviduService.setLocalTracks(this.tracks);
+ this.videoTrack = this.tracks.find((track) => track.kind === 'video');
+ this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
+ return; // Success, exit retry loop
+ } catch (error) {
+ this.log.w(`Device initialization attempt ${attempt} failed:`, error);
+
+ if (attempt === maxRetries) {
+ this.handleError(error);
+ } else {
+ // Wait before retrying
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
+ }
+ }
+ }
+ }
}
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html
index ea1b47c2..89a812f4 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.html
@@ -1,55 +1,81 @@
-
-
-
0">
-
-
-
- {{ 'PANEL.SETTINGS.DISABLED_AUDIO' | translate }}
- {{ microphoneSelected.label }}
-
-
+
+ @if (compact) {
+
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss
index cc20fb5f..cc04e7b1 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.scss
@@ -1,103 +1,29 @@
-$ov-selection-color-btn: #afafaf;
-$ov-selection-color: #cccccc;
+@use '../device-selector-shared' as shared;
:host {
- .device-container-element {
- border-radius: var(--ov-surface-radius);
- border: 1px solid $ov-selection-color-btn;
- }
- .device-container-element.mute-btn {
- border: 1px solid var(--ov-error-color);
- }
- #audio-devices-form {
- width: 100%;
- height: 50px;
- }
+ display: flex;
+ align-items: center;
- #audio-devices-not-found {
- font-size: 13px;
- }
+ .audio-device-selector {
+ @include shared.device-selector-base();
- #microphone-button {
- color:#000000
- }
-
- ::ng-deep .mat-mdc-text-field-wrapper,
- ::ng-deep .mat-mdc-form-field-flex,
- ::ng-deep .mat-mdc-select-trigger {
- height: 50px !important;
- }
-
- ::ng-deep .mat-mdc-form-field-subscript-wrapper {
- display: none !important;
- }
-
- ::ng-deep .mat-mdc-text-field-wrapper {
- padding-left: 0px;
- padding-right: 10px;
- background-color: $ov-selection-color !important;
- border-radius: var(--ov-surface-radius);
- }
- ::ng-deep .mdc-button--unelevated {
- border-top-left-radius: var(--ov-surface-radius);
- border-bottom-left-radius: var(--ov-surface-radius);
- border-bottom-right-radius: 0px !important;
- border-top-right-radius: 0px !important;
- background-color: $ov-selection-color-btn !important;
- width: 48px !important;
- min-width: 48px !important;
- padding: 0;
- height: 50px;
- }
-
- ::ng-deep .mat-mdc-unelevated-button > .mat-icon {
- height: 24px;
- width: 24px;
- font-size: 24px !important;
- margin: auto;
- }
-
- ::ng-deep .mat-mdc-form-field-infix {
- padding: 0px !important;
- min-height: 100%;
- }
-
- .selected-text {
- padding-left: 5px;
- }
-
- .mat-icon {
- vertical-align: middle;
- display: inline-flex;
- }
-
- ::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
- border: 0px !important;
- }
-
- ::ng-deep .mat-mdc-button-touch-target {
- border-radius: var(--ov-surface-radius) !important;
- }
-
- .mute-btn {
- color: #ffffff !important;
- background-color: var(--ov-error-color) !important;
+ // Audio-specific overrides for normal mode
+ &:not(.compact) {
+ .normal-device-selector {
+ .device-input-selector {
+ &:not(.disabled) {
+ .selector-button {
+ // Audio-specific hover effect (simpler than video)
+ &:hover:not([disabled]) {
+ border-color: var(--ov-primary-action-color, #4285f4);
+ }
+ }
+ }
+ }
+ }
+ }
}
}
-::ng-deep .mat-mdc-select-panel {
- background-color: #ffffff !important;
-}
-::ng-deep .mat-mdc-option {
- padding: 10px 10px !important;
-}
-
-::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
- color: var(--ov-primary-action-color) !important;
-}
-::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
- border-bottom-color: var(--ov-primary-action-color) !important;
-}
-::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
- background-color: $ov-selection-color !important;
-}
+// Include shared device menu styles
+@include shared.device-menu-styles();
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts
index 6c482dcd..203f1b27 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/audio-devices/audio-devices.component.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
+import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service';
@@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
standalone: false
})
export class AudioDevicesComponent implements OnInit, OnDestroy {
+ @Input() compact: boolean = false;
@Output() onAudioDeviceChanged = new EventEmitter();
@Output() onAudioEnabledChanged = new EventEmitter();
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss
new file mode 100644
index 00000000..928bc499
--- /dev/null
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss
@@ -0,0 +1,245 @@
+// Shared styles for device selectors (video and audio)
+// This file contains common styling for both video-devices and audio-devices components
+
+@mixin device-selector-base() {
+ display: flex;
+ align-items: center;
+ width: 100%;
+
+ // Compact Mode - Unified Button
+ &.compact {
+ .unified-device-button {
+ display: flex;
+ background: var(--ov-secondary-action-color);
+ border-radius: 12px;
+ overflow: hidden;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ .toggle-section {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 50px;
+ width: 50px;
+ height: 48px;
+ border: none;
+ background: transparent;
+ border-radius: 0;
+ padding: 0;
+ transition: all 0.2s ease;
+
+ &.device-enabled {
+ color: var(--ov-primary-action-color, #4285f4);
+
+ mat-icon {
+ color: var(--ov-primary-action-color, #4285f4);
+ }
+ }
+
+ &.device-disabled {
+ background: rgba(244, 67, 54, 0.9);
+ color: white;
+
+ mat-icon {
+ color: white;
+ }
+ }
+
+ &[disabled] {
+ background: rgba(150, 150, 150, 0.5);
+ color: rgba(150, 150, 150, 0.8);
+ cursor: not-allowed;
+ }
+
+ mat-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ margin: 0;
+ }
+ }
+
+ .dropdown-section {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 30px;
+ width: 30px;
+ height: 48px;
+ border: none;
+ background: transparent;
+ border-left: 1px solid rgba(0, 0, 0, 0.1);
+ border-radius: 0;
+ padding: 0;
+ color: var(--ov-text-secondary-color, #666);
+ transition: all 0.2s ease;
+
+ mat-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin: 0;
+ }
+ }
+ }
+ }
+
+ // Normal Mode - Input Style Selector
+ &:not(.compact) {
+ .normal-device-selector {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+
+ .device-input-selector {
+ flex: 1;
+
+ &:not(.disabled) {
+ .selector-button {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ height: 48px;
+ padding: 0 16px;
+ background: var(--ov-surface-color, #ffffff);
+ border: 2px solid var(--ov-border-color, #e0e0e0);
+ border-radius: 8px;
+ color: var(--ov-text-surface-color);
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+ text-align: left;
+ justify-content: flex-start;
+
+ &[disabled] {
+ background: var(--ov-disabled-background, #f5f5f5);
+ color: var(--ov-disabled-text-color, #999);
+ cursor: not-allowed;
+ border-color: var(--ov-disabled-border-color, #ddd);
+ }
+
+ .device-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ color: var(--ov-text-secondary-color, #666);
+ flex-shrink: 0;
+ }
+
+ .selected-device-name {
+ flex: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .dropdown-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ color: var(--ov-text-secondary-color, #666);
+ flex-shrink: 0;
+ transition: transform 0.2s ease;
+ }
+
+ &[aria-expanded='true'] .dropdown-icon {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ &.disabled {
+ .selector-button.disabled {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ height: 48px;
+ padding: 0 16px;
+ background: var(--ov-disabled-background, #f5f5f5);
+ border: 2px solid var(--ov-disabled-border-color, #ddd);
+ border-radius: 8px;
+ color: var(--ov-disabled-text-color, #999);
+ font-size: 14px;
+ cursor: not-allowed;
+
+ .device-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ color: var(--ov-error-color, #d32f2f);
+ }
+
+ .selected-device-name {
+ flex: 1;
+ font-style: italic;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .no-device-message {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ background: rgba(255, 193, 7, 0.1);
+ border: 1px solid rgba(255, 193, 7, 0.3);
+ border-radius: 8px;
+ color: var(--ov-warning-color, #ff9800);
+ font-size: 12px;
+
+ .warning-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ }
+ }
+}
+
+// Shared device menu styles
+@mixin device-menu-styles() {
+ ::ng-deep .device-menu.mat-mdc-menu-panel {
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
+ border: 1px solid var(--ov-border-color, #e0e0e0);
+ overflow: hidden;
+ background-color: var(--ov-surface-color);
+
+ .mat-mdc-menu-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ transition: background-color 0.2s ease;
+ font-size: 14px;
+
+ &:hover {
+ background-color: var(--ov-hover-color, #f5f5f5);
+ }
+
+ &.selected {
+ background-color: rgba(66, 133, 244, 0.08);
+ color: var(--ov-primary-action-color, #4285f4);
+
+ mat-icon {
+ color: var(--ov-primary-action-color, #4285f4);
+ }
+ }
+
+ mat-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+
+ span {
+ flex: 1;
+ font-weight: 500;
+ }
+ }
+ }
+}
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html
index 80fbd5d7..720d19f5 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html
@@ -1,18 +1,39 @@
-
-
-
+
+
-
+
+
+
+
+
+
+
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss
index a74aea1f..629ec95b 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss
@@ -1,21 +1,120 @@
-$ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
+:host {
+ display: inline-block;
-.lang-button {
- background-color: var(--ov-primary-action-color) !important;
- color: var(--ov-secondary-action-color) !important;
-}
-.lang-button .mat-icon {
- color: var(--ov-secondary-action-color);
+ .language-selector-container {
+ .compact-lang-button {
+ width: 40px;
+ height: 40px;
+ background: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(10px);
+ border: 1px solid var(--ov-border-color, #e0e0e0);
+ border-radius: 10px;
+ transition: all 0.2s ease;
+ color: var(--ov-text-secondary-color, #666);
-}
-::ng-deep .mat-mdc-menu-panel {
- border-radius: var(--ov-surface-radius) !important;
- background-color: $ov-surface-color-lighter !important;
- box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
+ &:hover {
+ background: rgba(255, 255, 255, 1);
+ border-color: var(--ov-primary-action-color, #4285f4);
+ transform: scale(1.02);
+ }
+
+ mat-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ .full-lang-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 16px;
+ background: var(--ov-surface-color, #ffffff);
+ border: 2px solid var(--ov-border-color, #e0e0e0);
+ border-radius: 12px;
+ transition: all 0.2s ease;
+ color: var(--ov-text-primary-color, #333);
+ font-weight: 500;
+
+ &:hover {
+ border-color: var(--ov-primary-action-color, #4285f4);
+ box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
+ }
+
+ .lang-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ color: var(--ov-text-secondary-color, #666);
+ }
+
+ .lang-name {
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .expand-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ color: var(--ov-text-secondary-color, #666);
+ transition: transform 0.2s ease;
+ }
+
+ &[aria-expanded="true"] .expand-icon {
+ transform: rotate(180deg);
+ }
+ }
+ }
}
-::ng-deep .mat-mdc-menu-item,
-.mat-mdc-menu-item:visited,
-.mat-mdc-menu-item:link {
- color: var(--ov-text-surface-color) !important;
+::ng-deep .language-menu {
+ .mat-mdc-menu-panel {
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
+ border: 1px solid var(--ov-border-color, #e0e0e0);
+ overflow: hidden;
+ background: var(--ov-surface-color, #ffffff);
+ }
+
+ .language-option {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ transition: background-color 0.2s ease;
+ font-size: 14px;
+ min-height: 48px;
+
+ &:hover {
+ background-color: var(--ov-hover-color, #f5f5f5);
+ }
+
+ &.selected {
+ background-color: rgba(66, 133, 244, 0.08);
+ color: var(--ov-primary-action-color, #4285f4);
+
+ .check-icon {
+ color: var(--ov-primary-action-color, #4285f4);
+ }
+ }
+
+ .check-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+
+ .lang-option-name {
+ flex: 1;
+ font-weight: 500;
+ color: var(--ov-text-primary-color, #333);
+ }
+
+ &.selected .lang-option-name {
+ color: var(--ov-primary-action-color, #4285f4);
+ font-weight: 600;
+ }
+ }
}
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html
index 48be7868..1130f2db 100644
--- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html
+++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/participant-name-input/participant-name-input.component.html
@@ -1,20 +1,17 @@
-