src/components/sq-textarea-form-control/sq-textarea-form-control.component.ts
Componente de textarea com Reactive Forms.
Substitui o componente legado sq-textarea com integração nativa de Reactive Forms,
ControlValueAccessor e Validator.
```html
<sq-textarea-form-control
[label]="'Descrição'"
[placeholder]="'Digite uma descrição...'"
[formControl]="descriptionControl"
[maxLength]="500"
></sq-textarea-form-control>```html
```html
<!-- Com validação required -->
<sq-textarea-form-control
[label]="'Comentário *'"
[formControl]="commentControl"
sqValidation
[fieldName]="'Comentário'"
></sq-textarea-form-control>
ControlValueAccessor
Validator
OnInit
OnDestroy
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
{
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SqTextareaFormControlComponent), multi: true,
}
{
provide: NG_VALIDATORS, useExisting: forwardRef(() => SqTextareaFormControlComponent), multi: true,
}
|
| selector | sq-textarea-form-control |
| standalone | true |
| imports |
NgClass
NgStyle
NgTemplateOutlet
ReactiveFormsModule
SqTooltipComponent
UniversalSafePipe
|
| styleUrls | ./sq-textarea-form-control.component.scss |
| templateUrl | ./sq-textarea-form-control.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
Accessors |
constructor()
|
|
Construtor do componente. |
| autoResize | |
Type : boolean
|
|
Default value : false
|
|
|
Se true, o textarea se expande automaticamente. |
|
| backgroundColor | |
Type : string
|
|
Default value : ''
|
|
|
Cor de fundo do textarea. |
|
| borderColor | |
Type : string
|
|
Default value : ''
|
|
|
Cor da borda do textarea. |
|
| customClass | |
Type : string
|
|
Default value : ''
|
|
|
Classe CSS customizada. |
|
| debounceTime | |
Type : number
|
|
Default value : 0
|
|
|
Debounce do valueChange em ms. |
|
| disabled | |
Type : boolean
|
|
Default value : false
|
|
|
Desabilita o textarea. |
|
| errorSpan | |
Type : boolean
|
|
Default value : true
|
|
|
Exibe o span de erro. |
|
| externalError | |
Type : string
|
|
Default value : ''
|
|
|
Erro externo para exibição. |
|
| externalIcon | |
Type : string
|
|
Default value : ''
|
|
|
Ícone externo. |
|
| id | |
Type : string
|
|
Default value : `textarea-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
|
ID do elemento textarea. |
|
| label | |
Type : string
|
|
Default value : ''
|
|
|
Label do campo. |
|
| labelColor | |
Type : string
|
|
Default value : ''
|
|
|
Cor da label. |
|
| maxHeight | |
Type : string
|
|
Default value : '300px'
|
|
|
Altura máxima do textarea (quando autoResize=true). |
|
| maxLength | |
Type : number | null
|
|
Default value : null
|
|
|
Comprimento máximo do texto. |
|
| minHeight | |
Type : string
|
|
Default value : '100px'
|
|
|
Altura mínima do textarea (quando autoResize=true). |
|
| minLength | |
Type : number | null
|
|
Default value : null
|
|
|
Comprimento mínimo do texto. |
|
| name | |
Type : string
|
|
Default value : ''
|
|
|
Nome do campo. |
|
| placeholder | |
Type : string
|
|
Default value : ''
|
|
|
Placeholder do textarea. |
|
| readonly | |
Type : boolean
|
|
Default value : false
|
|
|
Modo somente leitura. |
|
| required | |
Type : boolean
|
|
Default value : false
|
|
|
Campo obrigatório (adiciona validação required). |
|
| rows | |
Type : number
|
|
Default value : 4
|
|
|
Número de linhas do textarea. |
|
| tooltipColor | |
Type : string
|
|
Default value : 'inherit'
|
|
|
Cor do tooltip. |
|
| tooltipIcon | |
Type : string
|
|
Default value : ''
|
|
|
Ícone do tooltip. |
|
| tooltipMessage | |
Type : string
|
|
Default value : ''
|
|
|
Mensagem do tooltip. |
|
| tooltipPlacement | |
Type : "center top" | "center bottom" | "left center" | "right center"
|
|
Default value : 'right center'
|
|
|
Posição do tooltip. |
|
| blurred | |
Type : EventEmitter
|
|
|
Evento de blur. |
|
| focused | |
Type : EventEmitter
|
|
|
Evento de foco. |
|
| keyPressDown | |
Type : EventEmitter
|
|
|
Evento de tecla pressionada. |
|
| keyPressUp | |
Type : EventEmitter
|
|
|
Evento de tecla solta. |
|
| ngOnDestroy |
ngOnDestroy()
|
|
Cleanup do componente.
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Inicialização do componente.
Returns :
void
|
| onBlur | ||||||||
onBlur(event: FocusEvent)
|
||||||||
|
Handler de blur.
Parameters :
Returns :
void
|
| onFocus | ||||||||
onFocus(event: FocusEvent)
|
||||||||
|
Handler de focus.
Parameters :
Returns :
void
|
| onInput | ||||||||
onInput(event: Event)
|
||||||||
|
Handler para auto-resize.
Parameters :
Returns :
void
|
| onKeyDown | ||||||||
onKeyDown(event: KeyboardEvent)
|
||||||||
|
Handler de keydown.
Parameters :
Returns :
void
|
| onKeyUp | ||||||||
onKeyUp(event: KeyboardEvent)
|
||||||||
|
Handler de keyup.
Parameters :
Returns :
void
|
| registerOnChange | ||||||||
registerOnChange(fn: (value: string) => void)
|
||||||||
|
Registra callback de mudança de valor.
Parameters :
Returns :
void
|
| registerOnTouched | ||||||||
registerOnTouched(fn: () => void)
|
||||||||
|
Registra callback de touched.
Parameters :
Returns :
void
|
| registerOnValidatorChange | ||||||||
registerOnValidatorChange(fn: () => void)
|
||||||||
|
Registra callback para mudança de validação.
Parameters :
Returns :
void
|
| setDisabledState | ||||||||
setDisabledState(isDisabled: boolean)
|
||||||||
|
Define estado disabled.
Parameters :
Returns :
void
|
| Private setupValidators |
setupValidators()
|
|
Configura os validadores baseado nos inputs.
Returns :
void
|
| validate |
validate()
|
|
Valida o controle.
Returns :
ValidationErrors | null
Erros de validação ou null. |
| writeValue | ||||||||
writeValue(value: string | null)
|
||||||||
|
Escreve o valor no controle.
Parameters :
Returns :
void
|
| Private cdr |
Default value : inject(ChangeDetectorRef)
|
|
Referência ao ChangeDetectorRef. |
| control |
Default value : new FormControl<string>('')
|
|
FormControl interno. |
| Private destroy$ |
Default value : new Subject<void>()
|
|
Subject para cleanup. |
| Private elementRef |
Default value : inject(ElementRef)
|
|
Referência ao ElementRef. |
| labelTemplate |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('labelTemplate')
|
|
Template customizado para o label. |
| leftLabel |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('leftLabel')
|
|
Template para label à esquerda. |
| Private onChange |
Type : function
|
Default value : () => {...}
|
|
Callback de mudança de valor. |
| Private onTouched |
Type : function
|
Default value : () => {...}
|
|
Callback de touched. |
| Private onValidationChange |
Type : function
|
Default value : () => {...}
|
|
Callback de mudança de validação. |
| rightLabel |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('rightLabel')
|
|
Template para label à direita. |
| Private valueSubject |
Default value : new Subject<string>()
|
|
Subject para debounce. |
| value |
getvalue()
|
|
Retorna o valor atual.
Returns :
string
|
| remainingChars |
getremainingChars()
|
|
Retorna o número de caracteres restantes.
Returns :
number
|
| hasError |
gethasError()
|
|
Verifica se há erro.
Returns :
boolean
|
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
forwardRef,
inject,
Input,
OnDestroy,
OnInit,
Output,
TemplateRef,
} from '@angular/core';
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
import {
ControlValueAccessor,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
ValidationErrors,
Validator,
Validators,
} from '@angular/forms';
import { Subject, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs';
import { SqTooltipComponent } from '../sq-tooltip/sq-tooltip.component';
import { UniversalSafePipe } from '../../pipes/universal-safe/universal-safe.pipe';
/**
* Componente de textarea com Reactive Forms.
*
* Substitui o componente legado `sq-textarea` com integração nativa de Reactive Forms,
* ControlValueAccessor e Validator.
*
* @example
* ```html
* <sq-textarea-form-control
* [label]="'Descrição'"
* [placeholder]="'Digite uma descrição...'"
* [formControl]="descriptionControl"
* [maxLength]="500"
* ></sq-textarea-form-control>
* ```
*
* @example
* ```html
* <!-- Com validação required -->
* <sq-textarea-form-control
* [label]="'Comentário *'"
* [formControl]="commentControl"
* sqValidation
* [fieldName]="'Comentário'"
* ></sq-textarea-form-control>
* ```
*/
@Component({
selector: 'sq-textarea-form-control',
templateUrl: './sq-textarea-form-control.component.html',
styleUrls: ['./sq-textarea-form-control.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgClass, NgStyle, NgTemplateOutlet, ReactiveFormsModule, SqTooltipComponent, UniversalSafePipe],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SqTextareaFormControlComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SqTextareaFormControlComponent),
multi: true,
},
],
})
export class SqTextareaFormControlComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
// ============================================================
// Inputs - Identificação
// ============================================================
/**
* ID do elemento textarea.
*/
@Input() id = `textarea-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
/**
* Nome do campo.
*/
@Input() name = '';
// ============================================================
// Inputs - Aparência
// ============================================================
/**
* Label do campo.
*/
@Input() label = '';
/**
* Placeholder do textarea.
*/
@Input() placeholder = '';
/**
* Classe CSS customizada.
*/
@Input() customClass = '';
/**
* Cor de fundo do textarea.
*/
@Input() backgroundColor = '';
/**
* Cor da borda do textarea.
*/
@Input() borderColor = '';
/**
* Cor da label.
*/
@Input() labelColor = '';
/**
* Número de linhas do textarea.
*/
@Input() rows = 4;
/**
* Se true, o textarea se expande automaticamente.
*/
@Input() autoResize = false;
/**
* Altura mínima do textarea (quando autoResize=true).
*/
@Input() minHeight = '100px';
/**
* Altura máxima do textarea (quando autoResize=true).
*/
@Input() maxHeight = '300px';
// ============================================================
// Inputs - Comportamento
// ============================================================
/**
* Desabilita o textarea.
*/
@Input() disabled = false;
/**
* Modo somente leitura.
*/
@Input() readonly = false;
/**
* Campo obrigatório (adiciona validação required).
*/
@Input() required = false;
/**
* Comprimento máximo do texto.
*/
@Input() maxLength: number | null = null;
/**
* Comprimento mínimo do texto.
*/
@Input() minLength: number | null = null;
/**
* Debounce do valueChange em ms.
*/
@Input() debounceTime = 0;
/**
* Exibe o span de erro.
*/
@Input() errorSpan = true;
/**
* Erro externo para exibição.
*/
@Input() externalError = '';
/**
* Ícone externo.
*/
@Input() externalIcon = '';
// ============================================================
// Inputs - Tooltip
// ============================================================
/**
* Mensagem do tooltip.
*/
@Input() tooltipMessage = '';
/**
* Posição do tooltip.
*/
@Input() tooltipPlacement: 'center top' | 'center bottom' | 'left center' | 'right center' = 'right center';
/**
* Cor do tooltip.
*/
@Input() tooltipColor = 'inherit';
/**
* Ícone do tooltip.
*/
@Input() tooltipIcon = '';
// ============================================================
// Outputs
// ============================================================
/**
* Evento de tecla pressionada.
*/
@Output() keyPressDown = new EventEmitter<KeyboardEvent>();
/**
* Evento de tecla solta.
*/
@Output() keyPressUp = new EventEmitter<KeyboardEvent>();
/**
* Evento de foco.
*/
@Output() focused = new EventEmitter<FocusEvent>();
/**
* Evento de blur.
*/
@Output() blurred = new EventEmitter<FocusEvent>();
// ============================================================
// Templates
// ============================================================
/**
* Template para label à esquerda.
*/
@ContentChild('leftLabel') leftLabel: TemplateRef<HTMLElement> | null = null;
/**
* Template para label à direita.
*/
@ContentChild('rightLabel') rightLabel: TemplateRef<HTMLElement> | null = null;
/**
* Template customizado para o label.
*/
@ContentChild('labelTemplate') labelTemplate: TemplateRef<HTMLElement> | null = null;
// ============================================================
// Estado interno
// ============================================================
/**
* FormControl interno.
*/
control = new FormControl<string>('');
/**
* Subject para debounce.
*/
private valueSubject = new Subject<string>();
/**
* Subject para cleanup.
*/
private destroy$ = new Subject<void>();
/**
* Callback de mudança de valor.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
private onChange: (value: string) => void = () => {};
/**
* Callback de touched.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
private onTouched: () => void = () => {};
/**
* Callback de mudança de validação.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
private onValidationChange: () => void = () => {};
/**
* Referência ao ChangeDetectorRef.
*/
private cdr = inject(ChangeDetectorRef);
/**
* Referência ao ElementRef.
*/
private elementRef = inject(ElementRef);
/**
* Construtor do componente.
*/
constructor() {
// Subscription para mudança de valor com debounce opcional
this.valueSubject
.pipe(
debounceTime(this.debounceTime),
distinctUntilChanged(),
takeUntil(this.destroy$)
)
.subscribe(value => {
this.onChange(value);
});
}
/**
* Inicialização do componente.
*/
ngOnInit(): void {
this.setupValidators();
// Subscription para mudanças do controle interno
this.control.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
if (this.debounceTime > 0) {
this.valueSubject.next(value || '');
} else {
this.onChange(value || '');
}
});
}
/**
* Cleanup do componente.
*/
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
// ============================================================
// ControlValueAccessor
// ============================================================
/**
* Escreve o valor no controle.
*
* @param value - Valor a ser escrito.
*/
writeValue(value: string | null): void {
this.control.setValue(value || '', { emitEvent: false });
this.cdr.markForCheck();
}
/**
* Registra callback de mudança de valor.
*
* @param fn - Callback.
*/
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
/**
* Registra callback de touched.
*
* @param fn - Callback.
*/
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
/**
* Define estado disabled.
*
* @param isDisabled - Se está desabilitado.
*/
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.control.disable({ emitEvent: false });
} else {
this.control.enable({ emitEvent: false });
}
this.cdr.markForCheck();
}
// ============================================================
// Validator
// ============================================================
/**
* Valida o controle.
*
* @returns Erros de validação ou null.
*/
validate(): ValidationErrors | null {
return this.control.errors;
}
/**
* Registra callback para mudança de validação.
*
* @param fn - Callback.
*/
registerOnValidatorChange(fn: () => void): void {
this.onValidationChange = fn;
}
// ============================================================
// Getters
// ============================================================
/**
* Retorna o valor atual.
*/
get value(): string {
return this.control.value || '';
}
/**
* Retorna o número de caracteres restantes.
*/
get remainingChars(): number {
if (!this.maxLength) return 0;
return this.maxLength - (this.value?.length || 0);
}
/**
* Verifica se há erro.
*/
get hasError(): boolean {
return !!(this.externalError || (this.control.invalid && this.control.touched));
}
// ============================================================
// Métodos públicos - Eventos
// ============================================================
/**
* Handler de blur.
*
* @param event - Evento de blur.
*/
onBlur(event: FocusEvent): void {
this.onTouched();
this.blurred.emit(event);
}
/**
* Handler de focus.
*
* @param event - Evento de focus.
*/
onFocus(event: FocusEvent): void {
this.focused.emit(event);
}
/**
* Handler de keydown.
*
* @param event - Evento de teclado.
*/
onKeyDown(event: KeyboardEvent): void {
this.keyPressDown.emit(event);
}
/**
* Handler de keyup.
*
* @param event - Evento de teclado.
*/
onKeyUp(event: KeyboardEvent): void {
this.keyPressUp.emit(event);
}
/**
* Handler para auto-resize.
*
* @param event - Evento de input.
*/
onInput(event: Event): void {
if (this.autoResize) {
const textarea = event.target as HTMLTextAreaElement;
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
}
// ============================================================
// Métodos privados
// ============================================================
/**
* Configura os validadores baseado nos inputs.
*/
private setupValidators(): void {
const validators = [];
if (this.required) {
validators.push(Validators.required);
}
if (this.maxLength) {
validators.push(Validators.maxLength(this.maxLength));
}
if (this.minLength) {
validators.push(Validators.minLength(this.minLength));
}
if (validators.length > 0) {
this.control.setValidators(validators);
this.control.updateValueAndValidity({ emitEvent: false });
}
}
}
<div class="sq-textarea-form-control {{ customClass }}">
<!-- Label -->
@if (label?.length || tooltipMessage || labelTemplate) {
<div
class="display-flex align-items-center wrapper-label-input"
[ngClass]="{ disabled: disabled, readonly: readonly }"
[ngStyle]="{ color: labelColor }"
>
@if (label && !labelTemplate) {
<label class="label-input mr-1" [for]="id" [innerHtml]="label | universalSafe"></label>
}
@if (labelTemplate) {
<ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
}
@if (tooltipMessage) {
<sq-tooltip
[message]="tooltipMessage"
[placement]="tooltipPlacement"
[color]="tooltipColor"
[icon]="tooltipIcon"
></sq-tooltip>
}
</div>
}
<!-- Container do textarea -->
<div
class="textarea-container"
[ngClass]="{
error: hasError,
disabled: disabled,
readonly: readonly,
}"
>
<!-- Left label -->
@if (leftLabel) {
<span class="input-group-text left">
<ng-container *ngTemplateOutlet="leftLabel"></ng-container>
</span>
}
<!-- Textarea -->
<textarea
class="textarea scrollbar"
[ngStyle]="{
'background-color': backgroundColor,
'border-color': borderColor,
'min-height': autoResize ? minHeight : null,
'max-height': autoResize ? maxHeight : null,
}"
[ngClass]="{
error: hasError,
disabled: disabled,
readonly: readonly,
'has-icon-external': externalIcon,
'auto-resize': autoResize,
}"
[id]="id"
[name]="name"
[placeholder]="placeholder"
[rows]="rows"
[disabled]="disabled"
[readonly]="readonly"
[maxlength]="maxLength"
[formControl]="control"
(blur)="onBlur($event)"
(focus)="onFocus($event)"
(keydown)="onKeyDown($event)"
(keyup)="onKeyUp($event)"
(input)="onInput($event)"
></textarea>
<!-- Right label -->
@if (rightLabel) {
<span class="input-group-text right">
<ng-container *ngTemplateOutlet="rightLabel"></ng-container>
</span>
}
<!-- External icon -->
@if (externalIcon) {
<span
class="icon-external"
[ngClass]="{ 'no-label': !label?.length }"
[innerHtml]="externalIcon | universalSafe"
></span>
}
</div>
<!-- Error span e contador -->
@if (errorSpan) {
<div
class="box-validation box-invalid show"
[ngClass]="{ 'has-max-length': maxLength }"
>
@if (hasError) {
<i class="fa-solid fa-triangle-exclamation"></i>
}
<span class="error-text">
{{ externalError }}
</span>
@if (maxLength) {
<span
class="max-length-counter"
[ngClass]="{
'visibility-hidden-force': disabled || readonly,
warning: remainingChars < 20 && remainingChars > 0,
danger: remainingChars <= 0,
}"
>
{{ remainingChars }}
</span>
}
</div>
}
</div>
./sq-textarea-form-control.component.scss
.sq-textarea-form-control {
position: relative;
display: block;
}
// Label wrapper
.wrapper-label-input {
margin-bottom: 0.25rem;
&.disabled {
opacity: 0.6;
}
&.readonly {
opacity: 0.8;
}
.label-input {
font-size: 0.875rem;
font-weight: 500;
color: var(--text_color, #333);
}
}
// Container do textarea
.textarea-container {
position: relative;
display: flex;
border: 1px solid var(--border_color, #ccc);
border-radius: 5px;
background-color: var(--background, #fff);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&:focus-within:not(.disabled):not(.readonly) {
border-color: var(--primary_color, #007bff);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
}
&.error {
border-color: var(--color_error, #dc3545);
&:focus-within {
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.15);
}
}
&.disabled {
opacity: 0.6;
background-color: var(--color_bg_input_disabled, #f5f5f5);
cursor: not-allowed;
}
&.readonly {
background-color: var(--color_bg_input_readonly, #fafafa);
}
}
// Textarea
.textarea {
flex: 1;
width: 100%;
min-height: 100px;
padding: 0.625rem 1rem;
border: none;
border-radius: 5px;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.5;
color: var(--text_color, #333);
background-color: transparent;
resize: vertical;
outline: none;
transition: background-color 0.2s ease;
&::placeholder {
color: var(--color_text-icon_neutral_tertiary, #999);
}
&.disabled {
cursor: not-allowed;
resize: none;
}
&.readonly {
cursor: default;
resize: none;
}
&.has-icon-external {
padding-right: 2.5rem;
}
&.auto-resize {
resize: none;
overflow: hidden;
}
}
// Input group text (left/right labels)
.input-group-text {
display: flex;
align-items: flex-start;
padding: 0.625rem 0.75rem;
background-color: var(--color_bg_box_neutral_secondary, #f5f5f5);
border: none;
font-size: 0.875rem;
color: var(--text_color, #333);
&.left {
border-radius: 5px 0 0 5px;
border-right: 1px solid var(--border_color, #ccc);
}
&.right {
border-radius: 0 5px 5px 0;
border-left: 1px solid var(--border_color, #ccc);
}
}
// External icon
.icon-external {
position: absolute;
right: 1rem;
top: 0.625rem;
font-size: 1rem;
color: var(--color_text-icon_neutral_tertiary, #666);
&.no-label {
top: 0.625rem;
}
}
// Validation box
.box-validation {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 1.25rem;
margin-top: 0.25rem;
font-size: 0.75rem;
color: var(--color_error, #dc3545);
i {
margin-right: 0.25rem;
}
.error-text {
flex: 1;
}
}
// Max length counter
.max-length-counter {
margin-left: auto;
font-size: 0.75rem;
color: var(--color_text-icon_neutral_tertiary, #666);
font-weight: 500;
&.warning {
color: var(--color_warning, #ffc107);
}
&.danger {
color: var(--color_error, #dc3545);
}
}
.visibility-hidden-force {
visibility: hidden;
}