ov-components: Implement theming system

- Added THEME.md documentation detailing the theming system, including usage, service methods, and CSS variables reference.
- Created theme.scss with SCSS mixins for applying OpenVidu themes and integrating with Angular Material.
- Introduced theme.model.ts to define theme modes and variables.
- Developed theme.service.ts to manage theme switching, variable updates, and system theme detection.
- Updated public-api.ts to export new theme model and service.
- Enhanced styles.scss to incorporate OpenVidu theme integration with Angular Material.
- Added support for responsive theme detection based on system preferences.
master
Carlos Santos 2025-09-11 14:05:06 +02:00
parent 7821f3a75d
commit f0b3c2e2c6
29 changed files with 1117 additions and 140 deletions

View File

@ -19,7 +19,7 @@
max-width: 300px;
min-width: 300px;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
box-shadow: var(--ov-shadow-low);
}
.form-btn {

View File

@ -1,7 +1,7 @@
.poster {
height: 100%;
width: 100%;
background-color: #000000;
background-color: var(--ov-video-background);
position: absolute;
z-index: 888;
border-radius: var(--ov-video-radius);
@ -20,7 +20,7 @@
width: 70px;
border-radius: var(--ov-video-radius);
border: 2px solid var(--ov-text-primary-color);
color: #000000;
color: var(--ov-video-background);
}
#poster-text {

View File

@ -9,5 +9,5 @@ video {
border: 0;
font-size: 100%;
border-radius: var(--ov-video-radius);
background-color: #000000;
background-color: var(--ov-video-background);
}

View File

@ -1,5 +1,3 @@
$ov-activity-status-color: #afafaf;
:host {
.activities-body-container {
display: block !important;
@ -14,12 +12,12 @@ $ov-activity-status-color: #afafaf;
padding: 3px;
font-size: 11px;
border-radius: var(--ov-surface-radius);
background-color: $ov-activity-status-color;
background-color: var(--ov-primary-action-color);
}
.activity-icon {
display: inherit;
background-color: $ov-activity-status-color;;
background-color: var(--ov-primary-action-color);
border-radius: var(--ov-surface-radius);
margin: 10px 0px !important;
padding: 10px;

View File

@ -1,8 +1,3 @@
$ov-broadcasting-color: #5903ca;
$ov-input-color: #cccccc;
.time-container {
padding: 2px;
}
@ -14,7 +9,7 @@ $ov-input-color: #cccccc;
}
#broadcasting-icon {
color: $ov-broadcasting-color !important;
color: var(--ov-broadcasting-color) !important;
}
.broadcasting-duration {
@ -31,12 +26,12 @@ $ov-input-color: #cccccc;
}
.started {
background-color: $ov-broadcasting-color !important;
background-color: var(--ov-broadcasting-color) !important;
color: var(--ov-text-primary-color);
}
.activity-icon.started {
background-color: $ov-broadcasting-color !important;
background-color: var(--ov-broadcasting-color) !important;
color: var(--ov-text-primary-color);
}
@ -119,10 +114,11 @@ mat-expansion-panel {
.input-container {
height: 25px;
display: flex;
background-color: $ov-input-color;
background-color: var(--ov-input-background);
padding: 10px;
margin: 10px;
border-radius: var(--ov-surface-radius);
border: 1px solid var(--ov-border-color);
order: 3;
justify-content: space-evenly;
align-items: center;
@ -131,6 +127,7 @@ mat-expansion-panel {
.input-container input {
width: 100%;
height: 16px;
color: var(--ov-text-surface-color);
margin: auto;
background-color: transparent;
display: block;
@ -143,5 +140,19 @@ mat-expansion-panel {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
&::placeholder {
color: var(--ov-text-surface-color);
opacity: 1; /* Firefox */
}
}
#broadcasting-btn {
color: var(--ov-text-secondary-color);
&:disabled {
// background-color: var(--ov-disabled-color) !important;
color: var(--ov-text-disabled-color) !important;
cursor: not-allowed !important;
}
}

View File

@ -1,5 +1,4 @@
:host {
$ov-activity-status-color: #afafaf;
.recording-title,
.recording-subtitle {
color: var(--ov-text-surface-color);
@ -25,7 +24,7 @@
}
.recording-duration {
background-color: $ov-activity-status-color;
background-color: var(--ov-activity-status-color);
padding: 4px 8px;
border-radius: var(--ov-surface-radius);
font-weight: 500;
@ -120,6 +119,7 @@
font-weight: 600;
font-size: 15px;
margin-bottom: 2px;
color: var(--ov-text-surface-color);
}
.status-message {
@ -173,7 +173,7 @@
}
.recording-card {
background: var(--ov-surface-background-color);
background: var(--ov-surface-container-color);
border: 1px solid rgba(0, 102, 204, 0.1);
border-radius: var(--ov-surface-radius);
padding: 8px;
@ -261,7 +261,7 @@
font-weight: 500;
&.recording-live-text {
color: var(--ov-primary-action-color);
color: var(--ov-text-surface-color);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@ -317,22 +317,24 @@
color: var(--ov-accent-action-color);
&:hover {
background: rgba(0, 102, 204, 0.1);
color: var(--ov-accent-action-color);
background: transparent;
}
}
&.action-view {
color: var(--ov-accent-action-color);
border-radius: var(--ov-surface-radius);
&:hover {
background: transparent;
}
}
&.action-download {
color: #4caf50;
color: var(--ov-success-color);
&:hover {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
background: transparent;
}
}
@ -340,8 +342,7 @@
color: var(--ov-error-color);
&:hover {
background: rgba(244, 67, 54, 0.1);
color: var(--ov-error-color);
background: transparent;
}
}
}
@ -538,7 +539,7 @@
#start-recording-btn {
width: 100%;
background-color: var(--ov-primary-action-color);
color: var(--ov-secondary-action-color);
color: var(--ov-text-surface-color);
border-radius: var(--ov-surface-radius);
}
@ -562,7 +563,7 @@
#stop-recording-btn {
width: 100%;
background-color: var(--ov-error-color);
color: var(--ov-secondary-action-color);
color: #fff;
border-radius: var(--ov-surface-radius);
}

View File

@ -8,7 +8,7 @@
</button>
</div>
} @else {
<button class="pansel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>arrow_back</mat-icon>
</button>
}

View File

@ -2,10 +2,15 @@
margin: 0 10px 0px 10px;
max-height: 100%;
min-height: 100%;
.panel-close-button {
margin: 0;
}
}
.background-title {
color: var(--ov-text-surface-color);
margin: 10px 0;
font-weight: 300;
}
.effects-container {
display: block !important;
@ -17,8 +22,8 @@
.effect-button {
margin: 5px;
border-radius: var(--ov-surface-radius);
background-color: var(--ov-secondary-action-color);
color: var(--ov-primary-action-color);
background-color: var(--ov-primary-action-color);
color: var(--ov-secondary-action-color);
width: 60px;
height: 60px;
line-height: inherit;

View File

@ -1,5 +1,3 @@
$ov-selection-color: #d4d6d7;
.text-container {
color: var(--ov-text-primary-color);
text-align: center;
@ -29,8 +27,8 @@ $ov-selection-color: #d4d6d7;
.input-container {
height: 65px;
display: flex;
background-color: var(--ov-surface-color);
border: 1px solid $ov-selection-color;
background-color: var(--ov-input-background);
border: 1px solid var(--ov-border-color);
padding: 10px 5px 10px 10px;
margin: 10px;
border-radius: var(--ov-surface-radius);
@ -87,7 +85,7 @@ $ov-selection-color: #d4d6d7;
position: relative;
border-radius: var(--ov-surface-radius);
padding: 8px;
color: var(--ov-secondary-action-color);
color: var(--ov-text-surface-color);
width: auto;
max-width: 95%;
font-size: 14px;

View File

@ -37,12 +37,12 @@
}
::-webkit-scrollbar-thumb {
background: #a7a7a7;
background: var(--ov-selection-color-btn);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #7c7c7c;
background: var(--ov-text-disabled-color);
}
::-webkit-scrollbar-track {

View File

@ -6,8 +6,8 @@
align-items: center;
padding: 12px 16px;
border-radius: var(--ov-surface-radius, 8px);
background-color: var(--ov-surface-background, #ffffff);
border-bottom: 1px solid var(--ov-surface-border, #e0e0e0);
background-color: var(--ov-surface-color);
border-bottom: 1px solid var(--ov-border-color);
transition: all 0.2s ease-in-out;
min-height: 64px;
@ -57,7 +57,7 @@
border-radius: var(--ov-surface-radius);
margin-right: 12px;
padding: 0;
color: #ffffff;
color: var(--ov-text-primary-color);
font-weight: 500;
flex-shrink: 0;
position: relative;
@ -85,7 +85,7 @@
font-weight: 600 !important;
font-size: 14px;
line-height: 1.2;
color: var(--ov-text-primary, #212121);
color: var(--ov-text-surface-color);
margin: 0 0 4px 0;
display: flex;
align-items: center;
@ -98,14 +98,14 @@
.local-indicator {
font-size: 10px;
font-weight: 600;
color: var(--ov-primary-color, #1976d2);
background-color: var(--ov-primary-light, #e3f2fd);
color: var(--ov-focus-color);
background-color: var(--ov-surface-color);
padding: 4px 8px;
border-radius: var(--ov-surface-radius);
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
border: 1px solid var(--ov-primary-color, #1976d2);
border: 1px solid var(--ov-border-focus-color);
}
}
@ -115,7 +115,7 @@
font-size: 12px !important;
font-weight: 400;
margin: 0;
color: var(--ov-text-secondary, #757575);
color: var(--ov-text-secondary-color);
line-height: 1.3;
display: flex;
align-items: center;

View File

@ -11,7 +11,7 @@
.item-menu {
padding-right: 5px;
border-right: 1px solid var(--ov-secondary-action-color);
border-right: 1px solid var(--ov-border-color);
width: 170px;
}
.item-menu.mobile {

View File

@ -62,7 +62,7 @@
@if (backgroundEffectEnabled && hasVideoDevices) {
<div class="background-control">
<button
mat-icon-button
mat-flat-button
class="background-button"
(click)="toggleBackgroundPanel()"
[matTooltip]="'Virtual Backgrounds'"

View File

@ -109,7 +109,7 @@
aspect-ratio: 4/3;
border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0;
overflow: hidden;
background: #000;
background: var(--ov-video-background);
.video-frame {
width: 100%;
height: 100%;
@ -154,23 +154,16 @@
.background-button {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
min-width: 48px;
min-height: 48px;
background: var(--ov-primary-action-color);
color: var(--ov-secondary-action-color);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: #333333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transform: translateZ(0);
&:active {
transform: translateY(-1px);
transition: all 0.15s ease;
}
padding: 0;
&.mat-mdc-button-disabled {
background: rgba(255, 255, 255, 0.137);
color: rgba(233, 233, 233, 0.5);
background:var(--ov-disabled-background);
color: var(--ov-text-disabled-color);
cursor: not-allowed;
&:hover {
@ -184,10 +177,7 @@
height: 22px;
opacity: 0.9;
transition: opacity 0.2s ease;
}
&:hover mat-icon {
opacity: 1;
margin: 0;
}
}
}
@ -235,11 +225,11 @@
input {
font-size: 16px;
font-weight: 500;
color: var(--ov-text-primary-color, #333);
color: var(--ov-text-surface-color, #666);
padding: 16px;
&::placeholder {
color: var(--ov-text-secondary-color, #666);
color: var(--ov-text-surface-color, #666);
font-weight: 400;
}
}
@ -273,7 +263,7 @@
.join-button {
width: 100%;
height: 56px;
background: var(--ov-primary-action-color, #4285f4);
background: var(--ov-focus-color, #4285f4);
color: white;
border-radius: var(--ov-surface-radius);
font-size: 16px;

View File

@ -84,7 +84,7 @@
width: 100%;
height: 100%;
z-index: 1000;
background-color: black;
background-color: var(--ov-video-background);
opacity: 80%;
position: absolute;
}
@ -96,17 +96,15 @@
}
}
/* TODO(mdc-migration): The following rule targets internal classes of button that may no longer apply for the MDC version. */
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 1px !important;
}
::ng-deep .mat-mdc-input-element {
caret-color: #000000;
caret-color: var(--ov-video-background);
}
/* TODO(mdc-migration): The following rule targets internal classes of option that may no longer apply for the MDC version. */
::ng-deep .mat-primary .mat-mdc-option.mat-selected:not(.mat-option-disabled) {
color: #000000;
color: var(--ov-video-background);
}
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */

View File

@ -10,12 +10,12 @@
&.compact {
.unified-device-button {
display: flex;
background: rgba(255, 255, 255, 0.7);
background: var(--ov-primary-action-color);
backdrop-filter: blur(20px);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: var(--ov-shadow-low);
.toggle-section {
display: flex;
@ -31,25 +31,25 @@
transition: all 0.2s ease;
&.device-enabled {
color: var(--ov-primary-action-color, #4285f4);
color: var(--ov-focus-color);
mat-icon {
color: var(--ov-primary-action-color, #4285f4);
color: var(--ov-focus-color);
}
}
&.device-disabled {
background: rgba(244, 67, 54, 0.9);
color: white;
background: var(--ov-error-color);
color: var(--ov-surface-color);
mat-icon {
color: white;
color: var(--ov-surface-color);
}
}
&[disabled] {
background: rgba(150, 150, 150, 0.5);
color: rgba(150, 150, 150, 0.8);
background: var(--ov-gray-alpha-50);
color: var(--ov-gray-alpha-80);
cursor: not-allowed;
}
@ -70,7 +70,7 @@
height: 48px;
border: none;
background: transparent;
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid var(--ov-black-alpha-10);
border-radius: 0;
padding: 0;
color: var(--ov-text-secondary-color, #666);
@ -217,17 +217,17 @@
padding: 12px 16px;
transition: background-color 0.2s ease;
font-size: 14px;
color: var(--ov-text-surface-color);
&:hover {
background-color: var(--ov-hover-color, #f5f5f5);
background-color: var(--ov-hover-color);
}
&.selected {
background-color: rgba(66, 133, 244, 0.08);
color: var(--ov-primary-action-color, #4285f4);
background-color: var(--ov-active-color);
mat-icon {
color: var(--ov-primary-action-color, #4285f4);
color: var(--ov-text-surface-color);
}
}

View File

@ -53,9 +53,12 @@
.expand-icon {
font-size: 16px;
width: 16px;
min-width: 16px;
height: 16px;
min-height: 16px;
color: var(--ov-text-secondary-color, #666);
transition: transform 0.2s ease;
vertical-align: bottom;
}
&[aria-expanded='true'] .expand-icon {
@ -68,9 +71,9 @@
::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);
border: 1px solid var(--ov-border-color);
overflow: hidden;
background: var(--ov-surface-color, #ffffff);
background: var(--ov-surface-color);
.language-option {
display: flex;
@ -83,15 +86,14 @@
color: var(--ov-text-surface-color);
&:hover {
background-color: var(--ov-hover-color, #f5f5f5);
background-color: var(--ov-hover-color);
}
&.selected {
background-color: rgba(66, 133, 244, 0.08);
color: var(--ov-primary-action-color, #4285f4);
background-color: var(--ov-active-color);
.check-icon {
color: var(--ov-primary-action-color, #4285f4);
color: var(--ov-text-surface-color);
}
}
@ -105,9 +107,5 @@
flex: 1;
font-weight: 500;
}
&.selected .lang-option-name {
font-weight: 600;
}
}
}

View File

@ -27,7 +27,7 @@
justify-content: center;
width: 48px;
height: 56px;
background: var(--ov-surface-secondary, #f0f0f0);
background: var(--ov-surface-color, #f0f0f0);
color: var(--ov-text-secondary-color, #666);
font-size: 20px;
border-top-left-radius: 10px;
@ -44,6 +44,7 @@
background: transparent;
font-size: 16px;
font-weight: 500;
color: var(--ov-text-surface-color, #666);
&::placeholder {
color: var(--ov-text-secondary-color, #666);

View File

@ -1,8 +1,5 @@
@use '../device-selector-shared' as shared;
$ov-selection-color-btn: #afafaf;
$ov-selection-color: #cccccc;
:host {
display: flex;
align-items: center;
@ -27,7 +24,7 @@ $ov-selection-color: #cccccc;
.selector-button {
// Video-specific hover effect with box-shadow
&:hover:not([disabled]) {
background-color: white !important;
background-color: var(--ov-surface-color) !important;
border-color: var(--ov-primary-action-color);
}
}
@ -47,5 +44,5 @@ $ov-selection-color: #cccccc;
}
::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;
background-color: var(--ov-selection-color) !important;
}

View File

@ -1,4 +1,3 @@
$ov-video-elements-bg-color: var(--ov-primary-action-color);
:host {
/* Fixes layout bug. The OV_root is created with the entire layout width and it has a weird UX behaviour */
.no-size {
@ -7,10 +6,10 @@ $ov-video-elements-bg-color: var(--ov-primary-action-color);
}
.participant-name-container {
background-color: $ov-video-elements-bg-color;
background-color: var(--ov-primary-action-color);
padding: 5px;
color: var(--ov-secondary-action-color);
font-weight: bold;
font-weight: 400;
border-radius: var(--ov-video-radius);
}
.participant-name {
@ -46,7 +45,7 @@ $ov-video-elements-bg-color: var(--ov-primary-action-color);
}
.stream-video-controls {
background-color: $ov-video-elements-bg-color;
background-color: var(--ov-primary-action-color);
border-radius: var(--ov-video-radius);
width: fit-content;
height: 50px;

View File

@ -38,7 +38,7 @@
*ngIf="showCameraButton"
(click)="toggleCamera()"
[disabled]="isConnectionLost || !hasVideoDevices || cameraMuteChanging"
[class.warn-btn]="!isCameraEnabled"
[class.disabled]="!isCameraEnabled"
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
[matTooltipDisabled]="!hasVideoDevices"
>
@ -53,7 +53,7 @@
*ngIf="showMicrophoneButton"
(click)="toggleMicrophone()"
[disabled]="isConnectionLost || !hasAudioDevices || microphoneMuteChanging"
[class.warn-btn]="!isMicrophoneEnabled"
[class.disabled]="!isMicrophoneEnabled"
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
[matTooltipDisabled]="!hasAudioDevices"
>

View File

@ -1,6 +1,3 @@
$ov-broadcasting-blinking-color: #5903ca;
$ov-recording-blinking-color: #eb5144;
:host {
#toolbar {
height: 100%;
@ -58,9 +55,12 @@ $ov-recording-blinking-color: #eb5144;
#menu-buttons-container button {
border-radius: var(--ov-toolbar-buttons-radius);
color: var(--ov-secondary-action-color);
&.disabled {
background-color: var(--ov-error-color) !important;
color: #fff !important;
}
}
#media-buttons-container > button,
::ng-deep #media-buttons-container > button,
@ -74,9 +74,6 @@ $ov-recording-blinking-color: #eb5144;
margin: 6px;
}
.warn-btn {
background-color: var(--ov-error-color) !important;
}
#disable-screen-button > mat-icon {
color: var(--ov-error-color) !important;
}
@ -133,7 +130,7 @@ $ov-recording-blinking-color: #eb5144;
background-color: var(--ov-error-color);
}
.broadcasting-tag {
background-color: $ov-broadcasting-blinking-color;
background-color: var(--ov-broadcasting-color);
}
.recording-tag mat-icon,
@ -152,12 +149,13 @@ $ov-recording-blinking-color: #eb5144;
background-color: var(--ov-error-color) !important;
border-radius: var(--ov-leave-button-radius) !important;
width: 65px !important;
color: #ffffff !important;
}
.mat-mdc-icon-button[disabled] {
color: #fff;
}
::ng-deep .mat-badge-content{
::ng-deep .mat-badge-content {
background-color: var(--ov-warn-color);
}
.divider {
@ -186,14 +184,14 @@ $ov-recording-blinking-color: #eb5144;
/* Animation for recording blinking */
@keyframes blinker-recording {
50% {
background-color: $ov-recording-blinking-color;
background-color: var(--ov-recording-color);
}
}
/* Animation for broadcasting blinking */
@keyframes blinker-broadcasting {
50% {
background-color: $ov-broadcasting-blinking-color;
background-color: var(--ov-broadcasting-color);
}
}
@ -246,14 +244,14 @@ $ov-recording-blinking-color: #eb5144;
color: var(--ov-text-surface-color) !important;
}
::ng-deep #toolbar-broadcasting-btn > .mat-icon {
color: $ov-broadcasting-blinking-color !important;
color: var(--ov-broadcasting-color) !important;
}
::ng-deep #recording-btn > .mat-icon {
color: $ov-recording-blinking-color !important;
color: var(--ov-recording-color) !important;
}
::ng-deep .mat-mdc-menu-panel {
border-radius: var(--ov-surface-radius) !important;
background-color: var(--ov-surface-color) !important;
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
box-shadow: var(--ov-border-shadow) !important;
}

View File

@ -0,0 +1,396 @@
# OpenVidu Components Angular - Theme System
The OpenVidu Components Angular library provides a comprehensive theming system that allows you to customize the appearance of all components to match your application's design. The theme system is fully compatible with Angular Material themes and supports dynamic theme switching.
## Table of Contents
1. [Quick Start](#quick-start)
2. [Theme Service](#theme-service)
3. [CSS Variables Reference](#css-variables-reference)
4. [Angular Material Integration](#angular-material-integration)
5. [Custom Themes](#custom-themes)
6. [SCSS Mixins](#scss-mixins)
7. [Migration Guide](#migration-guide)
## Quick Start
### Basic Usage
To get started with theming, import and inject the `OpenViduThemeService`:
```typescript
import { Component } from '@angular/core';
import { OpenViduThemeService, OpenViduThemeMode } from 'openvidu-components-angular';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(private themeService: OpenViduThemeService) {}
setLightTheme() {
this.themeService.setTheme('light');
}
setDarkTheme() {
this.themeService.setTheme('dark');
}
toggleTheme() {
this.themeService.toggleTheme();
}
}
```
### CSS Variable Override
You can also customize themes by overriding CSS variables directly in your global styles:
```scss
:root {
--ov-primary-action-color: #ff5722;
--ov-accent-action-color: #4caf50;
--ov-background-color: #fafafa;
}
```
## Theme Service
The `OpenViduThemeService` provides methods to manage themes dynamically:
### Available Methods
#### `setTheme(theme: OpenViduThemeMode)`
Sets the global theme mode.
```typescript
// Set light theme
this.themeService.setTheme('light');
// Set dark theme
this.themeService.setTheme('dark');
// Set auto theme (follows system preference)
this.themeService.setTheme('auto');
```
#### `updateThemeVariables(variables: OpenViduThemeVariables)`
Updates specific theme variables without changing the overall theme.
```typescript
this.themeService.updateThemeVariables({
'--ov-primary-action-color': '#ff5722',
'--ov-accent-action-color': '#4caf50'
});
```
#### `applyThemeConfiguration(themeVariables: OpenViduThemeVariables)`
Applies a complete theme configuration.
```typescript
import { OPENVIDU_DARK_THEME } from 'openvidu-components-angular';
this.themeService.applyThemeConfiguration(OPENVIDU_DARK_THEME);
```
#### `getCurrentTheme(): OpenViduThemeMode`
Returns the current active theme mode.
```typescript
const currentTheme = this.themeService.getCurrentTheme();
console.log('Current theme:', currentTheme); // 'light', 'dark', 'auto' or 'none'
```
#### `toggleTheme()`
Toggles between light and dark themes.
```typescript
this.themeService.toggleTheme();
```
### Observables
Listen to theme changes using the provided observables:
```typescript
// Listen to theme mode changes
this.themeService.currentTheme$.subscribe(theme => {
console.log('Theme changed to:', theme);
});
// Listen to theme variable changes
this.themeService.currentVariables$.subscribe(variables => {
console.log('Theme variables updated:', variables);
});
```
## CSS Variables Reference
### Core Background Colors
| Variable | Description | Light Default | Dark Default |
|----------|-------------|---------------|--------------|
| `--ov-background-color` | Primary background color | `#ffffff` | `#1f2020` |
| `--ov-surface-color` | Surface/card background | `#ffffff` | `#2d2d2d` |
| `--ov-surface-container-color` | Container surfaces | `#f8f9fa` | `#3a3a3a` |
| `--ov-surface-container-high-color` | Elevated surfaces | `#f0f0f0` | `#474747` |
### Action Colors
| Variable | Description | Light Default | Dark Default |
|----------|-------------|---------------|--------------|
| `--ov-primary-action-color` | Primary buttons/actions | `#273235` | `#4a5a5d` |
| `--ov-primary-action-color-lighter` | Primary hover states | `#394649` | `#5a6a6d` |
| `--ov-secondary-action-color` | Secondary buttons | `#6c757d` | `#e1e1e1` |
| `--ov-accent-action-color` | Accent/highlight color | `#0089ab` | `#00b3d6` |
### State Colors
| Variable | Description | Light Default | Dark Default |
|----------|-------------|---------------|--------------|
| `--ov-error-color` | Error states | `#dc3545` | `#ff6b6b` |
| `--ov-warn-color` | Warning states | `#ffc107` | `#ffd93d` |
| `--ov-success-color` | Success states | `#28a745` | `#69db7c` |
### Text Colors
| Variable | Description | Light Default | Dark Default |
|----------|-------------|---------------|--------------|
| `--ov-text-primary-color` | Primary text | `#212529` | `#ffffff` |
| `--ov-text-surface-color` | Text on surfaces | `#212529` | `#ffffff` |
| `--ov-text-secondary-color` | Secondary text | `#6c757d` | `#b3b3b3` |
| `--ov-text-disabled-color` | Disabled text | `#adb5bd` | `#666666` |
### Interactive States
| Variable | Description | Light Default | Dark Default |
|----------|-------------|---------------|--------------|
| `--ov-hover-color` | Hover background | `#f8f9fa` | `#4a4a4a` |
| `--ov-active-color` | Active state | `rgba(66, 133, 244, 0.08)` | `rgba(66, 133, 244, 0.2)` |
| `--ov-focus-color` | Focus ring color | `#4285f4` | `#5294ff` |
| `--ov-disabled-background` | Disabled background | `#f8f9fa` | `#3a3a3a` |
| `--ov-disabled-border-color` | Disabled borders | `#dee2e6` | `#555555` |
### Input & Form Colors
| Variable | Description | Light Default | Dark Default |
|----------|-------------|---------------|--------------|
| `--ov-input-background` | Input backgrounds | `#ffffff` | `#3a3a3a` |
| `--ov-border-color` | Default borders | `#ced4da` | `#555555` |
| `--ov-border-focus-color` | Focused borders | `#4285f4` | `#5294ff` |
### Layout & Spacing
| Variable | Description | Default |
|----------|-------------|---------|
| `--ov-toolbar-buttons-radius` | Toolbar button radius | `50%` |
| `--ov-leave-button-radius` | Leave button radius | `10px` |
| `--ov-video-radius` | Video element radius | `5px` |
| `--ov-surface-radius` | Surface/card radius | `5px` |
| `--ov-input-radius` | Input field radius | `4px` |
### Special Colors
| Variable | Description | Default |
|----------|-------------|---------|
| `--ov-recording-color` | Recording indicator | `var(--ov-error-color)` |
| `--ov-broadcasting-color` | Broadcasting indicator | `#5903ca` |
| `--ov-selection-color` | Selection highlight | `#d4d6d7` |
| `--ov-selection-color-btn` | Button selection | `#afafaf` |
| `--ov-activity-status-color` | Activity status | `#afafaf` |
### Video/Media Specific
| Variable | Description | Default |
|----------|-------------|---------|
| `--ov-video-background` | Video element background | `#000000` |
| `--ov-audio-wave-color` | Audio wave visualization | `var(--ov-accent-action-color)` |
| `--ov-captions-height` | Captions panel height | `250px` |
### Shadow & Elevation
| Variable | Description | Default |
|----------|-------------|---------|
| `--ov-shadow-low` | Low elevation shadow | `0 2px 8px rgba(0, 0, 0, 0.1)` |
| `--ov-shadow-medium` | Medium elevation shadow | `0 4px 20px rgba(0, 0, 0, 0.1)` |
| `--ov-shadow-high` | High elevation shadow | `0 8px 32px rgba(0, 0, 0, 0.12)` |
| `--ov-border-shadow` | Border shadow | `1px 1px 5px 0px rgba(0, 0, 0, 0.2)` |
## Angular Material Integration
### Using SCSS Mixins
Import and use the provided SCSS mixins to integrate with Angular Material themes:
```scss
@use '@angular/material' as mat;
@use 'openvidu-components-angular/theme' as ovtheme;
// Define your Material theme
$my-theme: mat.define-theme();
// Apply the theme to OpenVidu components
@include ovtheme.apply-openvidu-theme($my-theme);
// Or apply responsive theme detection
@include ovtheme.openvidu-theme-responsive();
```
### Manual Integration
To manually integrate with Angular Material themes in your component:
```typescript
import { Injectable } from '@angular/core';
import { OpenViduThemeService } from 'openvidu-components-angular';
@Injectable()
export class MaterialThemeIntegration {
constructor(private themeService: OpenViduThemeService) {}
applyMaterialTheme(materialTheme: any) {
// Extract colors from Material theme and apply to OpenVidu
this.themeService.updateThemeVariables({
'--ov-primary-action-color': materialTheme.primary,
'--ov-accent-action-color': materialTheme.accent,
'--ov-background-color': materialTheme.background,
'--ov-surface-color': materialTheme.surface
});
}
}
```
## Custom Themes
### Creating a Custom Theme
Define a custom theme object:
```typescript
import { OpenViduThemeVariables } from 'openvidu-components-angular';
const myCustomTheme: OpenViduThemeVariables = {
'--ov-primary-action-color': '#ff5722',
'--ov-accent-action-color': '#4caf50',
'--ov-background-color': '#fafafa',
'--ov-surface-color': '#ffffff',
'--ov-text-primary-color': '#333333',
'--ov-text-secondary-color': '#666666',
// ... add more variables as needed
};
// Apply the custom theme
this.themeService.applyThemeConfiguration(myCustomTheme);
```
### Brand-Specific Themes
Create brand-specific themes for multi-tenant applications:
```typescript
const brandThemes = {
'brand-a': {
'--ov-primary-action-color': '#ff5722',
'--ov-accent-action-color': '#4caf50'
},
'brand-b': {
'--ov-primary-action-color': '#2196f3',
'--ov-accent-action-color': '#ff9800'
}
};
// Apply brand theme based on user/tenant
const userBrand = 'brand-a';
this.themeService.updateThemeVariables(brandThemes[userBrand]);
```
## SCSS Mixins
### Available Mixins
#### `apply-openvidu-theme($theme)`
Applies an Angular Material theme to OpenVidu components.
```scss
@include ovtheme.apply-openvidu-theme($my-material-theme);
```
#### `apply-openvidu-dark-theme()`
Applies the predefined dark theme.
```scss
@include ovtheme.apply-openvidu-dark-theme();
```
#### `apply-openvidu-light-theme()`
Applies the predefined light theme.
```scss
@include ovtheme.apply-openvidu-light-theme();
```
#### `openvidu-theme-responsive()`
Sets up responsive theme detection based on system preferences.
```scss
@include ovtheme.openvidu-theme-responsive();
```
## Examples
### Complete Theme Integration
```typescript
import { Component, OnInit } from '@angular/core';
import {
OpenViduThemeService,
OpenViduThemeMode,
OPENVIDU_LIGHT_THEME,
OPENVIDU_DARK_THEME
} from 'openvidu-components-angular';
@Component({
selector: 'app-theme-demo',
template: `
<div class="theme-controls">
<button (click)="setTheme('light')">Light</button>
<button (click)="setTheme('dark')">Dark</button>
<button (click)="setTheme('auto')">Auto</button>
<button (click)="toggleTheme()">Toggle</button>
<button (click)="applyCustomBrand()">Custom Brand</button>
</div>
<div class="current-theme">
Current theme: {{ currentTheme }}
</div>
`
})
export class ThemeDemoComponent implements OnInit {
currentTheme: OpenViduThemeMode = 'auto';
constructor(private themeService: OpenViduThemeService) {}
ngOnInit() {
this.themeService.currentTheme$.subscribe(theme => {
this.currentTheme = theme;
});
}
setTheme(theme: OpenViduThemeMode) {
this.themeService.setTheme(theme);
}
toggleTheme() {
this.themeService.toggleTheme();
}
applyCustomBrand() {
this.themeService.updateThemeVariables({
'--ov-primary-action-color': '#ff6b35',
'--ov-accent-action-color': '#f7931e',
'--ov-surface-radius': '12px'
});
}
}
```

View File

@ -0,0 +1,147 @@
@use '@angular/material' as mat;
// Mixin for applying OpenVidu CSS variables based on an Angular Material theme
@mixin apply-openvidu-theme($theme) {
:root {
// === Core Background Colors ===
--ov-background-color: #{mat.get-theme-color($theme, background)};
--ov-surface-color: #{mat.get-theme-color($theme, surface)};
--ov-surface-container-color: #{mat.get-theme-color($theme, surface-container)};
--ov-surface-container-high-color: #{mat.get-theme-color($theme, surface-container-high)};
// === Action Colors (Primary, Secondary, Accent) ===
--ov-primary-action-color: #{mat.get-theme-color($theme, primary)};
--ov-primary-action-color-lighter: #{mat.get-theme-color($theme, primary-container)};
--ov-secondary-action-color: #{mat.get-theme-color($theme, secondary)};
--ov-accent-action-color: #{mat.get-theme-color($theme, tertiary)};
// === State Colors ===
--ov-error-color: #{mat.get-theme-color($theme, error)};
--ov-warn-color: #{mat.get-theme-color($theme, error-container)};
--ov-success-color: #{mat.get-theme-color($theme, tertiary-container)};
// === Text Colors ===
--ov-text-primary-color: #{mat.get-theme-color($theme, on-background)};
--ov-text-surface-color: #{mat.get-theme-color($theme, on-surface)};
--ov-text-secondary-color: #{mat.get-theme-color($theme, on-surface-variant)};
--ov-text-disabled-color: #{mat.get-theme-color($theme, outline)};
// === Interactive States ===
--ov-hover-color: #{mat.get-theme-color($theme, surface-container-highest)};
--ov-active-color: #{mat.get-theme-color($theme, primary-container)};
--ov-focus-color: #{mat.get-theme-color($theme, primary)};
--ov-disabled-background: #{mat.get-theme-color($theme, surface-container-low)};
--ov-disabled-border-color: #{mat.get-theme-color($theme, outline-variant)};
// === Input & Form Colors ===
--ov-input-background: #{mat.get-theme-color($theme, surface-container)};
--ov-border-color: #{mat.get-theme-color($theme, outline-variant)};
--ov-border-focus-color: #{mat.get-theme-color($theme, primary)};
}
}
// Mixin for applying dark theme of OpenVidu
@mixin apply-openvidu-dark-theme() {
:root {
// === Core Background Colors ===
--ov-background-color: #1f2020;
--ov-surface-color: #2d2d2d;
--ov-surface-container-color: #3a3a3a;
--ov-surface-container-high-color: #474747;
// === Action Colors ===
--ov-primary-action-color: #4a5a5d;
--ov-primary-action-color-lighter: #5a6a6d;
--ov-secondary-action-color: #e1e1e1;
--ov-accent-action-color: #00b3d6;
// === State Colors ===
--ov-error-color: #ff6b6b;
--ov-warn-color: #ffd93d;
--ov-success-color: #69db7c;
// === Text Colors ===
--ov-text-primary-color: #ffffff;
--ov-text-surface-color: #ffffff;
--ov-text-secondary-color: #b3b3b3;
--ov-text-disabled-color: #666666;
// === Interactive States ===
--ov-hover-color: #4a4a4a;
--ov-active-color: rgba(66, 133, 244, 0.2);
--ov-focus-color: #5294ff;
--ov-disabled-background: #3a3a3a;
--ov-disabled-border-color: #555555;
// === Input & Form Colors ===
--ov-input-background: #3a3a3a;
--ov-border-color: #555555;
--ov-border-focus-color: #5294ff;
}
}
// Mixin for applying light theme of OpenVidu
@mixin apply-openvidu-light-theme() {
:root {
// === Core Background Colors ===
--ov-background-color: #ffffff;
--ov-surface-color: #ffffff;
--ov-surface-container-color: #f8f9fa;
--ov-surface-container-high-color: #f0f0f0;
// === Action Colors ===
--ov-primary-action-color: #273235;
--ov-primary-action-color-lighter: #394649;
--ov-secondary-action-color: #6c757d;
--ov-accent-action-color: #0089ab;
// === State Colors ===
--ov-error-color: #dc3545;
--ov-warn-color: #ffc107;
--ov-success-color: #28a745;
// === Text Colors ===
--ov-text-primary-color: #212529;
--ov-text-surface-color: #212529;
--ov-text-secondary-color: #6c757d;
--ov-text-disabled-color: #adb5bd;
// === Interactive States ===
--ov-hover-color: #f8f9fa;
--ov-active-color: rgba(66, 133, 244, 0.08);
--ov-focus-color: #4285f4;
--ov-disabled-background: #f8f9fa;
--ov-disabled-border-color: #dee2e6;
// === Input & Form Colors ===
--ov-input-background: #ffffff;
--ov-border-color: #ced4da;
--ov-border-focus-color: #4285f4;
}
}
// Mixin for establishing responsive theme properties
@mixin openvidu-theme-responsive() {
// Media query for detecting system dark theme preference
@media (prefers-color-scheme: dark) {
:root:not([data-ov-theme]) {
@include apply-openvidu-dark-theme();
}
}
// Media query for detecting system light theme preference
@media (prefers-color-scheme: light) {
:root:not([data-ov-theme]) {
@include apply-openvidu-light-theme();
}
}
// Apply specific theme when explicitly defined
:root[data-ov-theme='dark'] {
@include apply-openvidu-dark-theme();
}
:root[data-ov-theme='light'] {
@include apply-openvidu-light-theme();
}
}

View File

@ -0,0 +1,139 @@
/**
* Represents the possible theme modes for OpenVidu components
* @internal
*/
export enum OpenViduThemeMode {
Light = 'light',
Dark = 'dark',
None = 'none',
Auto = 'auto'
}
/**
* Interface representing the complete set of theme variables for OpenVidu components
* @internal
*/
export interface OpenViduThemeVariables {
// === Core Background Colors ===
'--ov-background-color'?: string;
'--ov-surface-color'?: string;
'--ov-surface-container-color'?: string;
'--ov-surface-container-high-color'?: string;
// === Action Colors ===
'--ov-primary-action-color'?: string;
'--ov-primary-action-color-lighter'?: string;
'--ov-secondary-action-color'?: string;
'--ov-accent-action-color'?: string;
// === State Colors ===
'--ov-error-color'?: string;
'--ov-warn-color'?: string;
'--ov-success-color'?: string;
// === Text Colors ===
'--ov-text-primary-color'?: string;
'--ov-text-surface-color'?: string;
'--ov-text-secondary-color'?: string;
'--ov-text-disabled-color'?: string;
// === Interactive States ===
'--ov-hover-color'?: string;
'--ov-active-color'?: string;
'--ov-focus-color'?: string;
'--ov-disabled-background'?: string;
'--ov-disabled-border-color'?: string;
// === Input & Form Colors ===
'--ov-input-background'?: string;
'--ov-border-color'?: string;
'--ov-border-focus-color'?: string;
// === Layout & Spacing ===
'--ov-toolbar-buttons-radius'?: string;
'--ov-leave-button-radius'?: string;
'--ov-video-radius'?: string;
'--ov-surface-radius'?: string;
'--ov-input-radius'?: string;
// === Special Colors ===
'--ov-recording-color'?: string;
'--ov-broadcasting-color'?: string;
'--ov-selection-color'?: string;
'--ov-selection-color-btn'?: string;
'--ov-activity-status-color'?: string;
// === Video/Media Specific ===
'--ov-video-background'?: string;
'--ov-audio-wave-color'?: string;
'--ov-captions-height'?: string;
// Allow for custom variables
[key: string]: string | undefined;
}
/**
* Predefined theme configurations
* @internal
*/
export const OPENVIDU_LIGHT_THEME: OpenViduThemeVariables = {
'--ov-background-color': '#f0f0f0',
'--ov-surface-color': '#ffffff',
'--ov-surface-container-color': '#f8f9fa',
'--ov-surface-container-high-color': '#f0f0f0',
'--ov-primary-action-color': '#d3d7d8ff',
'--ov-primary-action-color-lighter': '#c1cbceff',
'--ov-secondary-action-color': '#6e6d6dff',
'--ov-accent-action-color': '#bddfe7ff',
'--ov-error-color': '#dc3545',
'--ov-warn-color': '#eea300',
'--ov-success-color': '#28a745',
'--ov-text-primary-color': '#212529',
'--ov-text-surface-color': '#212529',
'--ov-text-secondary-color': '#6c757d',
'--ov-text-disabled-color': '#adb5bd',
'--ov-hover-color': '#f8f9fa',
'--ov-active-color': 'rgba(66, 133, 244, 0.08)',
'--ov-focus-color': '#4285f4',
'--ov-disabled-background': '#f8f9fa',
'--ov-disabled-border-color': '#dee2e6',
'--ov-input-background': '#ffffff',
'--ov-border-color': '#ced4da',
'--ov-border-focus-color': '#4285f4',
'--ov-activity-status-color': '#c8cdd6',
'--ov-broadcasting-color': '#8837f1',
'--ov-video-background': '#000000'
};
/**
* Predefined dark theme configuration
* @internal
*/
export const OPENVIDU_DARK_THEME: OpenViduThemeVariables = {
'--ov-background-color': '#1f2020',
'--ov-surface-color': '#2d2d2d',
'--ov-surface-container-color': '#3a3a3a',
'--ov-surface-container-high-color': '#474747',
'--ov-primary-action-color': '#4a4e4e',
'--ov-primary-action-color-lighter': '#93a5a8ff',
'--ov-secondary-action-color': '#e1e1e1',
'--ov-accent-action-color': '#009ab9ff',
'--ov-error-color': '#dc3545',
'--ov-warn-color': '#eea300',
'--ov-success-color': '#69db7c',
'--ov-text-primary-color': '#ffffff',
'--ov-text-surface-color': '#f0f0f0',
'--ov-text-secondary-color': '#b3b3b3',
'--ov-text-disabled-color': '#666666',
'--ov-hover-color': '#4a4a4a',
'--ov-active-color': '#4285f433',
'--ov-focus-color': '#5294ff',
'--ov-disabled-background': '#3a3a3a',
'--ov-disabled-border-color': '#555555',
'--ov-input-background': '#3a3a3a',
'--ov-border-color': '#555555',
'--ov-border-focus-color': '#5294ff',
'--ov-activity-status-color': '#c8cdd6ff',
'--ov-broadcasting-color': '#9d5af3ff',
'--ov-video-background': '#000000'
};

View File

@ -19,7 +19,7 @@ import { CustomDevice } from '../../models/device.model';
})
export class StorageService implements OnDestroy {
public log: ILogger;
protected readonly PREFIX_KEY = STORAGE_PREFIX;
readonly PREFIX_KEY = STORAGE_PREFIX;
private readonly tabId: string;
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
private readonly TAB_TIMEOUT_THRESHOLD = 60000; // 60 seconds

View File

@ -0,0 +1,238 @@
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { OPENVIDU_DARK_THEME, OPENVIDU_LIGHT_THEME, OpenViduThemeMode, OpenViduThemeVariables } from '../../models/theme.model';
/**
* Service for managing OpenVidu component themes dynamically
*
* This service allows you to:
* - Switch between light, dark, and auto themes
* - Apply custom theme variables
* - Listen to theme changes
* - Integrate with Angular Material themes
*
* @example
* ```typescript
* // Inject the service
* constructor(private themeService: OpenViduThemeService) {}
*
* // Switch to dark theme
* this.themeService.setTheme('dark');
*
* // Apply custom variables
* this.themeService.updateThemeVariables({
* '--ov-primary-action-color': '#ff5722',
* '--ov-accent-action-color': '#4caf50'
* });
*
* // Listen to theme changes
* this.themeService.currentTheme$.subscribe(theme => {
* console.log('Current theme:', theme);
* });
* ```
*
* @internal
*/
@Injectable({
providedIn: 'root'
})
export class OpenViduThemeService {
private readonly THEME_STORAGE_KEY = 'openvidu-theme';
private readonly THEME_ATTRIBUTE = 'data-ov-theme';
private currentThemeSubject = new BehaviorSubject<OpenViduThemeMode>(OpenViduThemeMode.None);
private currentVariablesSubject = new BehaviorSubject<OpenViduThemeVariables>({});
/**
* Observable that emits the current theme mode
*/
public readonly currentTheme$: Observable<OpenViduThemeMode> = this.currentThemeSubject.asObservable();
/**
* Observable that emits the current theme variables
*/
public readonly currentVariables$: Observable<OpenViduThemeVariables> = this.currentVariablesSubject.asObservable();
constructor(@Inject(DOCUMENT) private document: Document) {
this.initializeTheme();
this.setupSystemThemeListener();
}
/**
* Gets the current theme mode
*/
getCurrentTheme(): OpenViduThemeMode {
return this.currentThemeSubject.value;
}
/**
* Gets the current theme variables
*/
getCurrentVariables(): OpenViduThemeVariables {
return this.currentVariablesSubject.value;
}
/**
* Sets the theme mode (light, dark, or auto)
* @param theme The theme mode to apply
*/
setTheme(theme: OpenViduThemeMode): void {
this.currentThemeSubject.next(theme);
this.applyTheme(theme);
this.saveThemeToStorage(theme);
}
/**
* Updates specific theme variables
* @param variables Object containing CSS variables to update
*/
updateThemeVariables(variables: OpenViduThemeVariables): void {
const mergedVariables = { ...this.currentVariablesSubject.value, ...variables };
this.currentVariablesSubject.next(mergedVariables);
this.applyCSSVariables(variables);
}
/**
* Replaces all theme variables with a new set
* @param variables Complete set of theme variables
*/
setThemeVariables(variables: OpenViduThemeVariables): void {
this.currentVariablesSubject.next(variables);
this.applyCSSVariables(variables);
}
/**
* Resets theme variables to default values based on current theme
*/
resetThemeVariables(): void {
const currentTheme = this.getCurrentTheme();
const defaultVariables = this.getDefaultVariablesForTheme(currentTheme);
this.setThemeVariables(defaultVariables);
}
/**
* Applies a predefined theme configuration
* @param themeVariables Predefined theme configuration (e.g., OPENVIDU_LIGHT_THEME)
*/
applyThemeConfiguration(themeVariables: OpenViduThemeVariables): void {
this.setThemeVariables(themeVariables);
}
/**
* Toggles between light and dark themes
*/
toggleTheme(): void {
const currentTheme = this.getCurrentTheme();
if (currentTheme === OpenViduThemeMode.Light) {
this.setTheme(OpenViduThemeMode.Dark);
} else if (currentTheme === OpenViduThemeMode.Dark) {
this.setTheme(OpenViduThemeMode.Light);
} else {
// If auto, switch to opposite of system preference
const prefersDark = this.prefersDarkMode();
this.setTheme(prefersDark ? OpenViduThemeMode.Light : OpenViduThemeMode.Dark);
}
}
/**
* Gets a specific CSS variable value
* @param variableName The CSS variable name (with or without --)
*/
getThemeVariable(variableName: string): string {
const varName = variableName.startsWith('--') ? variableName : `--${variableName}`;
return getComputedStyle(this.document.documentElement).getPropertyValue(varName).trim();
}
/**
* Checks if the system prefers dark mode
*/
prefersDarkMode(): boolean {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
private initializeTheme(): void {
const savedTheme = this.getThemeFromStorage();
const initialTheme = savedTheme || OpenViduThemeMode.None;
this.applyTheme(initialTheme);
this.currentThemeSubject.next(initialTheme);
}
private applyTheme(theme: OpenViduThemeMode): void {
const documentElement = this.document.documentElement;
if (theme === OpenViduThemeMode.Auto || theme === OpenViduThemeMode.None) {
documentElement.removeAttribute(this.THEME_ATTRIBUTE);
} else {
documentElement.setAttribute(this.THEME_ATTRIBUTE, theme);
}
// Apply default variables for the theme
const defaultVariables = this.getDefaultVariablesForTheme(theme);
this.applyCSSVariables(defaultVariables);
}
private applyCSSVariables(variables: OpenViduThemeVariables): void {
const documentElement = this.document.documentElement;
Object.entries(variables).forEach(([property, value]) => {
if (value !== undefined) {
documentElement.style.setProperty(property, value);
}
});
}
private getDefaultVariablesForTheme(theme: OpenViduThemeMode): OpenViduThemeVariables {
switch (theme) {
case OpenViduThemeMode.Light:
return OPENVIDU_LIGHT_THEME;
case OpenViduThemeMode.Dark:
return OPENVIDU_DARK_THEME;
case OpenViduThemeMode.None:
return {};
case OpenViduThemeMode.Auto:
// Auto theme - use system preference
return this.prefersDarkMode() ? OPENVIDU_DARK_THEME : OPENVIDU_LIGHT_THEME;
default:
return {};
}
}
private setupSystemThemeListener(): void {
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
if (this.getCurrentTheme() === OpenViduThemeMode.Auto) {
const defaultVariables = this.getDefaultVariablesForTheme(OpenViduThemeMode.Auto);
this.applyCSSVariables(defaultVariables);
}
};
// Use the newer addEventListener if available, otherwise use the deprecated addListener
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
}
}
}
private saveThemeToStorage(theme: OpenViduThemeMode): void {
try {
localStorage.setItem(this.THEME_STORAGE_KEY, theme);
} catch (error) {
console.warn('Failed to save theme to localStorage:', error);
}
}
private getThemeFromStorage(): OpenViduThemeMode | null {
try {
const saved = localStorage.getItem(this.THEME_STORAGE_KEY) as OpenViduThemeMode;
if (saved && [OpenViduThemeMode.Light, OpenViduThemeMode.Dark, OpenViduThemeMode.Auto].includes(saved)) {
return saved;
}
} catch (error) {
console.warn('Failed to read theme from localStorage:', error);
}
return null;
}
}

View File

@ -40,6 +40,7 @@ export * from './lib/models/toolbar.model';
export * from './lib/models/logger.model'
export * from './lib/models/storage.model';
export * from './lib/models/lang.model';
export * from './lib/models/theme.model';
// Pipes
export * from './lib/pipes/participant.pipe';
export * from './lib/pipes/recording.pipe';
@ -57,6 +58,7 @@ export * from './lib/services/config/global-config.service';
export * from './lib/services/logger/logger.service';
export * from './lib/services/storage/storage.service';
export * from './lib/services/translate/translate.service';
export * from './lib/services/theme/theme.service';
//Modules
export * from './lib/openvidu-components-angular.module';
export * from './lib/openvidu-components-angular-ui.module';

View File

@ -1,4 +1,5 @@
@use '@angular/material' as mat;
@use '../projects/openvidu-components-angular/src/lib/config/theme' as ovtheme;
@include mat.elevation-classes();
@include mat.app-background();
@ -15,8 +16,14 @@ html {
@include mat.all-component-colors($openvidu-theme);
@include mat.all-component-typographies($openvidu-theme);
@include mat.all-component-densities($openvidu-theme);
// Apply OpenVidu theme integration with Angular Material
@include ovtheme.apply-openvidu-theme($openvidu-theme);
}
// Include responsive theme detection
@include ovtheme.openvidu-theme-responsive();
html,
body {
height: 100%;
@ -27,23 +34,77 @@ body {
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
}
// Custom openvidu-components styles
// Custom openvidu-components styles with Angular Material Theme support
:root {
--ov-background-color: #1f2020;
--ov-surface-color: #ffffff;
// === Core Background Colors ===
--ov-background-color: var(--mat-sys-background, #1f2020);
--ov-surface-color: var(--mat-sys-surface, #ffffff);
--ov-surface-container-color: var(--mat-sys-surface-container, #f3f3f3);
--ov-surface-container-high-color: var(--mat-sys-surface-container-high, #e6e6e6);
--ov-primary-action-color: #273235;
--ov-secondary-action-color: #f1f1f1;
--ov-accent-action-color: #0089ab;
// === Action Colors (Primary, Secondary, Accent) ===
--ov-primary-action-color: var(--mat-sys-primary, #273235);
--ov-primary-action-color-lighter: var(--mat-sys-primary-container, #394649);
--ov-secondary-action-color: var(--mat-sys-secondary, #f1f1f1);
--ov-accent-action-color: var(--mat-sys-tertiary, #0089ab);
--ov-error-color: #eb5144;
--ov-warn-color: #ffba53;
// === State Colors ===
--ov-error-color: var(--mat-sys-error, #eb5144);
--ov-warn-color: var(--mat-sys-error-container, #ffba53);
--ov-success-color: var(--mat-sys-tertiary-container, #8bffc9);
--ov-text-primary-color: #ffffff;
--ov-text-surface-color: #1d1d1d;
// === Text Colors ===
--ov-text-primary-color: var(--mat-sys-on-background, #ffffff);
--ov-text-surface-color: var(--mat-sys-on-surface, #1d1d1d);
--ov-text-secondary-color: var(--mat-sys-on-surface-variant, #666666);
--ov-text-disabled-color: var(--mat-sys-outline, #999999);
// === Interactive States ===
--ov-hover-color: var(--mat-sys-surface-container-highest, #f5f5f5);
--ov-active-color: var(--mat-sys-primary-container, rgba(66, 133, 244, 0.08));
--ov-focus-color: var(--mat-sys-primary, #4285f4);
--ov-disabled-background: var(--mat-sys-surface-container-low, #f5f5f5);
--ov-disabled-border-color: var(--mat-sys-outline-variant, #ddd);
// === Input & Form Colors ===
--ov-input-background: var(--mat-sys-surface-container, #f8f9fa);
--ov-border-color: var(--mat-sys-outline-variant, #e0e0e0);
--ov-border-focus-color: var(--mat-sys-primary, #4285f4);
// === Shadow & Elevation ===
--ov-shadow-low: 0 2px 8px rgba(0, 0, 0, 0.1);
--ov-shadow-medium: 0 4px 20px rgba(0, 0, 0, 0.1);
--ov-shadow-high: 0 8px 32px rgba(0, 0, 0, 0.12);
--ov-border-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2);
// === Layout & Spacing ===
--ov-toolbar-buttons-radius: 50%;
--ov-leave-button-radius: 10px;
--ov-video-radius: 5px;
--ov-surface-radius: 5px;
--ov-input-radius: 4px;
// === Special Colors (with fallbacks) ===
--ov-recording-color: var(--ov-error-color);
--ov-broadcasting-color: #5903ca;
--ov-selection-color: #d4d6d7;
--ov-selection-color-btn: #afafaf;
--ov-activity-status-color: #afafaf;
// === Alpha/Transparency Variants ===
--ov-primary-alpha-08: rgba(66, 133, 244, 0.08);
--ov-primary-alpha-10: rgba(66, 133, 244, 0.1);
--ov-error-alpha-10: rgba(211, 47, 47, 0.1);
--ov-warning-alpha-10: rgba(255, 193, 7, 0.1);
--ov-warning-alpha-30: rgba(255, 193, 7, 0.3);
--ov-black-alpha-10: rgba(0, 0, 0, 0.1);
--ov-white-alpha-70: rgba(255, 255, 255, 0.7);
--ov-white-alpha-90: rgba(255, 255, 255, 0.9);
--ov-gray-alpha-50: rgba(150, 150, 150, 0.5);
--ov-gray-alpha-80: rgba(150, 150, 150, 0.8);
// === Video/Media Specific ===
--ov-video-background: #000000;
--ov-audio-wave-color: var(--ov-accent-action-color);
--ov-captions-height: 250px;
}