diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch
new file mode 100644
index 0000000000..64fe082fb8
--- /dev/null
+++ b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch
@@ -0,0 +1,26 @@
+diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs
+index 6c7f8b1..f390b3b 100644
+--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs
++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs
+@@ -173,7 +173,7 @@ export class Color {
+ const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l);
+ const value = (l + s) / 2;
+ const saturation = (2 * s) / (l + s) || 0;
+- return new Hsva(hue, saturation, value, color.alpha);
++ return new Hsva(hue, saturation * 100, value * 100, color.alpha);
+ }
+ rgbaToHsva(color) {
+ const red = color.red / 255;
+diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs
+index a3b270c..9a9b592 100644
+--- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs
++++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs
+@@ -505,7 +505,7 @@ class Color {
+ const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l);
+ const value = (l + s) / 2;
+ const saturation = (2 * s) / (l + s) || 0;
+- return new Hsva(hue, saturation, value, color.alpha);
++ return new Hsva(hue, saturation * 100, value * 100, color.alpha);
+ }
+ rgbaToHsva(color) {
+ const red = color.red / 255;
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss
new file mode 100644
index 0000000000..bae2e433aa
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+:host {
+ .color-input-container {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ }
+ .color-input {
+ max-width: 72px;
+ margin-bottom: 4px;
+ }
+}
+
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss
index f9db566be8..5e867f9e61 100644
--- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss
+++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss
@@ -16,7 +16,7 @@
@import "../scss/constants";
.tb-color-picker-panel {
- width: 342px;
+ width: 370px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 24px);
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html
index 676dafbfef..fcb1cb067d 100644
--- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html
+++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html
@@ -37,10 +37,8 @@
HSLA
-
-
+
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
index ed161a5b9f..799bcdf00c 100644
--- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
+++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
@@ -83,7 +83,7 @@
height: 56px;
display: flex;
align-items: center;
- gap: 20px;
+ gap: 8px;
.presentation-select {
font-size: 14px;
@@ -104,7 +104,7 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
- justify-content: space-between;
+ justify-content: center;
gap: 8px;
@media #{$mat-xs} {
flex-direction: column;
diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss
index 2f7a404c0d..73a6aff9f6 100644
--- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss
+++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss
@@ -19,11 +19,11 @@
gap: 8px;
}
.hex-input {
- max-width: 190px;
+ max-width: 220px;
}
.alpha-input {
- min-width: 60px;
- max-width: 60px;
+ min-width: 72px;
+ max-width: 72px;
}
::ng-deep {
diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html
new file mode 100644
index 0000000000..cf8e25eb51
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html
@@ -0,0 +1,54 @@
+
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts
new file mode 100644
index 0000000000..7965c48f28
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts
@@ -0,0 +1,71 @@
+///
+/// Copyright © 2016-2026 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Color } from '@iplab/ngx-color-picker';
+import { coerceBoolean } from '@shared/decorators/coercion';
+
+type Channel = 'H' | 'S' | 'L';
+
+@Component({
+ selector: 'tb-hsla-input',
+ templateUrl: './hsla-input.component.html',
+ styleUrl: './color-input.base.scss'
+})
+export class HslaInputComponent {
+
+ @Input()
+ public color: Color;
+
+ @Output()
+ public colorChange = new EventEmitter(false);
+
+ @Input()
+ @coerceBoolean()
+ public labelVisible = false;
+
+ @Input()
+ public suffixValue = '%';
+
+ public get value() {
+ return this.color.getHsla();
+ }
+
+ public get alphaValue(): number {
+ return this.color ? Math.round(this.color.getHsla().getAlpha() * 100) : 0;
+ }
+
+ public onAlphaInputChange(inputValue: number): void {
+ if (!this.color) return;
+ const hsla = this.color.getHsla();
+ const alpha = +inputValue / 100;
+ if (hsla.alpha !== alpha) {
+ const newColor = new Color().setHsla(hsla.getHue(), hsla.getSaturation(), hsla.getLightness(), alpha);
+ this.colorChange.emit(newColor);
+ }
+ }
+
+ public onInputChange(newValue: number, channel: Channel): void {
+ if (!this.color) return;
+ const hsla = this.value;
+ const hue = channel === 'H' ? +newValue : hsla.getHue();
+ const saturation = channel === 'S' ? +newValue : hsla.getSaturation();
+ const lightness = channel === 'L' ? +newValue : hsla.getLightness();
+ if (hue === hsla.getHue() && saturation === hsla.getSaturation() && lightness === hsla.getLightness()) return;
+ const newColor = new Color().setHsla(hue, saturation, lightness, hsla.getAlpha());
+ this.colorChange.emit(newColor);
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts
new file mode 100644
index 0000000000..0c493a2118
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts
@@ -0,0 +1,45 @@
+///
+/// Copyright © 2016-2026 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Directive, EventEmitter, HostBinding, HostListener, Input, numberAttribute, Output } from '@angular/core';
+
+@Directive({
+ selector: '[inputChange]'
+})
+export class InputChangeDirective {
+
+ @Input({transform: numberAttribute})
+ @HostBinding('attr.min')
+ min = 0;
+
+ @Input({transform: numberAttribute})
+ @HostBinding('attr.max')
+ max = 255;
+
+ @Output()
+ public inputChange = new EventEmitter();
+
+ @HostListener('input', ['$event'])
+ public inputChanges(event: any): void {
+ const element = event.target as HTMLInputElement || event.srcElement as HTMLInputElement;
+ const value = element.value;
+
+ const numeric = parseFloat(value);
+ if (!isNaN(numeric) && numeric >= this.min && numeric <= this.max) {
+ this.inputChange.emit(numeric);
+ }
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html
new file mode 100644
index 0000000000..3cb856f147
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html
@@ -0,0 +1,52 @@
+
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts
new file mode 100644
index 0000000000..1cf9ea5768
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts
@@ -0,0 +1,70 @@
+///
+/// Copyright © 2016-2026 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Color } from '@iplab/ngx-color-picker';
+import { coerceBoolean } from '@shared/decorators/coercion';
+
+type Channel = 'R' | 'G' | 'B' | 'A';
+
+@Component({
+ selector: 'tb-rgba-input',
+ templateUrl: './rgba-input.component.html',
+ styleUrl: './color-input.base.scss'
+})
+export class RgbaInputComponent {
+
+ @Input()
+ public color: Color;
+
+ @Output()
+ public colorChange = new EventEmitter(false);
+
+ @Input()
+ @coerceBoolean()
+ public labelVisible = false;
+
+ @Input()
+ public suffixValue = '%';
+
+ public get value() {
+ return this.color.getRgba();
+ }
+
+ public get alphaValue(): string {
+ return this.color ? Math.round(this.color.getRgba().getAlpha() * 100).toString() : '';
+ }
+
+ public onAlphaInputChange(inputValue: number): void {
+ if (!this.color) return;
+ const color = this.color.getRgba();
+ const alpha = +inputValue / 100;
+ if (color.getAlpha() !== alpha) {
+ const newColor = new Color().setRgba(color.getRed(), color.getGreen(), color.getBlue(), alpha).toRgbaString();
+ this.colorChange.emit(new Color(newColor));
+ }
+ }
+
+ onInputChange(newValue: number, channel: Channel) {
+ if (!this.color) return;
+ const rgba = this.value;
+ const red = channel === 'R' ? newValue : rgba.getRed();
+ const green = channel === 'G' ? newValue : rgba.getGreen();
+ const blue = channel === 'B' ? newValue : rgba.getBlue();
+ if (red === rgba.getRed() && green === rgba.getGreen() && blue === rgba.getBlue()) return;
+ this.colorChange.emit(new Color().setRgba(red, green, blue, rgba.alpha));
+ }
+}
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts
index dfe86539bc..cefc1c8824 100644
--- a/ui-ngx/src/app/shared/shared.module.ts
+++ b/ui-ngx/src/app/shared/shared.module.ts
@@ -239,6 +239,9 @@ import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe';
import { EntityLimitExceededDialogComponent } from '@shared/components/dialog/entity-limit-exceeded-dialog.component';
import { DynamicMatDialogModule } from '@shared/components/dialog/dynamic/dynamic-dialog.module';
import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle';
+import { RgbaInputComponent } from '@shared/components/color-picker/rgba-input.component';
+import { HslaInputComponent } from '@shared/components/color-picker/hsla-input.component';
+import { InputChangeDirective } from '@shared/components/color-picker/input-change.directive';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@@ -466,7 +469,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
MqttVersionSelectComponent,
PasswordRequirementsTooltipComponent,
TimeUnitInputComponent,
- StringPatternAutocompleteComponent
+ StringPatternAutocompleteComponent,
+ RgbaInputComponent,
+ HslaInputComponent,
+ InputChangeDirective
],
imports: [
CommonModule,