diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch
index 211a3c8b0b..b0087a67e9 100644
--- a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch
+++ b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch
@@ -1,46 +1,13 @@
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 a372799..a3d709a 100644
+index a372799..f64a6f8 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
-@@ -1129,11 +1129,11 @@ class RgbaComponent {
- this.color.set(newColor);
+@@ -516,7 +516,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);
}
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
-- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
-+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
- }
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RgbaComponent, decorators: [{
- type: Component,
-- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
-+ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
- }] });
-
- class HslaComponent {
-@@ -1155,11 +1155,11 @@ class HslaComponent {
- this.color.set(newColor);
- }
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
-- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
-+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
- }
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HslaComponent, decorators: [{
- type: Component,
-- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
-+ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
- }] });
-
- class HexComponent {
-@@ -1190,11 +1190,11 @@ class HexComponent {
- }
- }
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
-- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
-+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
- }
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HexComponent, decorators: [{
- type: Component,
-- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
-+ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
- }] });
-
- const OpacityAnimation = trigger('opacityAnimation', [
+ 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..40ec056972
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts
@@ -0,0 +1,72 @@
+///
+/// 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',
+ standalone: false
+})
+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..55604df28e
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts
@@ -0,0 +1,46 @@
+///
+/// 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]',
+ standalone: false
+})
+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..04f2dbc44b
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/color-picker/rgba-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 = 'R' | 'G' | 'B';
+
+@Component({
+ selector: 'tb-rgba-input',
+ templateUrl: './rgba-input.component.html',
+ styleUrl: './color-input.base.scss',
+ standalone: false
+})
+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,