diff --git a/apps/playground/src/app/app.config.ts b/apps/playground/src/app/app.config.ts index a423c2b..62ef223 100644 --- a/apps/playground/src/app/app.config.ts +++ b/apps/playground/src/app/app.config.ts @@ -31,6 +31,8 @@ const TRANSLATIONS: TranslationObject = { max: 'Máx', save: 'Guardar', reset: 'Borrar', + search: 'Buscar…', + noResults: 'Sin resultados', validator: { requiredField: 'Este campo es obligatorio', onlyNumbers: 'Sólo se admiten números', diff --git a/apps/playground/src/app/pages/catalog/catalog.page.html b/apps/playground/src/app/pages/catalog/catalog.page.html index ffc4afe..2afa7ee 100644 --- a/apps/playground/src/app/pages/catalog/catalog.page.html +++ b/apps/playground/src/app/pages/catalog/catalog.page.html @@ -81,6 +81,28 @@ Fecha de nacimiento + + Idioma + + + + País + + + + Países visitados + + + + Conocimientos + + [] = [ + { value: 'es', label: 'Español' }, + { value: 'en', label: 'Inglés' }, + { value: 'fr', label: 'Francés' }, + { value: 'de', label: 'Alemán', disabled: true }, + ]; + + /** Lista larga a propósito: es donde el buscador aporta algo. */ + protected readonly countryOptions: PiSelectFieldOption[] = [ + 'Alemania', + 'Andorra', + 'Argentina', + 'Australia', + 'Austria', + 'Bélgica', + 'Bolivia', + 'Brasil', + 'Canadá', + 'Chile', + 'China', + 'Colombia', + 'Corea del Sur', + 'Costa Rica', + 'Cuba', + 'Dinamarca', + 'Ecuador', + 'Egipto', + 'España', + 'Estados Unidos', + 'Finlandia', + 'Francia', + 'Grecia', + 'Guatemala', + 'Honduras', + 'Hungría', + 'India', + 'Irlanda', + 'Islandia', + 'Italia', + 'Japón', + 'Marruecos', + 'México', + 'Noruega', + 'Países Bajos', + 'Panamá', + 'Paraguay', + 'Perú', + 'Polonia', + 'Portugal', + 'Reino Unido', + 'República Dominicana', + 'Rumanía', + 'Suecia', + 'Suiza', + 'Uruguay', + 'Venezuela', + ].map((name) => ({ value: name.toLowerCase(), label: name })); + + protected readonly skillOptions: PiSelectFieldOption[] = [ + { value: 'angular', label: 'Angular' }, + { value: 'rxjs', label: 'RxJS' }, + { value: 'scss', label: 'SCSS' }, + { value: 'a11y', label: 'Accesibilidad' }, + ]; + protected readonly plans = [ { id: 1, name: 'Básico', description: '9 €/mes' }, { id: 2, name: 'Pro', description: '29 €/mes' }, diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-datepicker.scss b/libs/ui/src/lib/styles/common/vendors/_mat-datepicker.scss index 0656442..9ff03ed 100644 --- a/libs/ui/src/lib/styles/common/vendors/_mat-datepicker.scss +++ b/libs/ui/src/lib/styles/common/vendors/_mat-datepicker.scss @@ -47,3 +47,35 @@ display: none; } } + +// --- Superficie del panel ----------------------------------------------- +// +// El calendario venía con el gris y el radio de Material de serie. Se +// reconstruye con los mismos tokens que el panel del select y el resto de +// superficies de la librería. +.cdk-overlay-pane .mat-datepicker-content, +.mat-datepicker-content { + border-radius: var(--pi-theme-panel-radius); + border: 1px solid var(--pi-theme-field-border-color); + background-color: var(--pi-theme-field-surface); + box-shadow: var(--pi-box-shadow-panel); + + .mat-calendar-body-label, + .mat-calendar-table-header th { + color: var(--pi-color-grey-300); + } + + // Día de hoy y día seleccionado, con los colores de marca. + .mat-calendar-body-today:not(.mat-calendar-body-selected) { + border-color: var(--pi-color-primary); + } + + .mat-calendar-body-selected { + background-color: var(--pi-color-primary); + color: var(--pi-color-white); + } + + .mat-calendar-body-cell-content:hover:not(.mat-calendar-body-selected) { + background-color: var(--pi-color-primary-50); + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-select.scss b/libs/ui/src/lib/styles/common/vendors/_mat-select.scss new file mode 100644 index 0000000..a590128 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-select.scss @@ -0,0 +1,163 @@ +// Select +// +// Cubre lo que usan `pi-select-field` y `pi-multi-select-field`. Son dos cosas +// distintas y por eso están separadas: +// +// 1. El *trigger*, dentro del campo. `_mat-form-field.scss` estira el `` +// para que llene el alto de la caja, pero no hay equivalente para el select: +// su trigger mide lo que su contenido (24px) y quedaba pegado arriba, con el +// texto y la flecha ~10px por encima del centro. +// +// 2. El *panel*, que se monta en el overlay y por tanto fuera del campo. Venía +// con el gris y el radio de 4px de Material de serie, sin relación con la +// librería. Se reconstruye con los mismos tokens que el resto de superficies. + +// --- 1. Trigger: centrado vertical dentro del campo ---------------------- +.pi-field { + // El `` es `inline-block` y mide lo que su contenido (24px) + // dentro de un infix de 48px. Estirarlo es lo que de verdad centra: poner + // `height: 100%` solo en el trigger no servía, porque ese 100% se resolvía + // contra los 24px del propio select. + .mat-mdc-select { + height: 100%; + } + + .mat-mdc-select-trigger { + display: flex; + align-items: center; + height: 100%; + } + + // El valor deja de ser un bloque suelto arriba y se centra con el trigger. + .mat-mdc-select-value { + display: flex; + align-items: center; + } + + // La flecha va dentro del infix, mientras que los iconos sufijo del resto de + // campos van fuera de él. Entre ambos anclajes hay 28px: los 12 del + // `translateX(-0.75rem)` que `_mat-form-field.scss` aplica al infix, más la + // diferencia de padding entre el flex y el wrapper. Se posiciona respecto al + // infix (que ya es `position: relative`) sacándola esos 28px, para que quede + // al mismo inset que los demás iconos. + .mat-mdc-select-arrow-wrapper { + position: absolute; + right: -1.75rem; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + } + + // Material dimensiona `.mat-mdc-select-arrow` para el triángulo CSS antiguo + // (10x5) pero dentro pinta un SVG de 24x24, que se salía 7px por cada lado. + // Igualando la caja al glifo deja de desbordar y queda a la misma altura y + // color que los iconos sufijo del resto de campos. + .mat-mdc-select-arrow { + display: flex; + align-items: center; + justify-content: center; + width: var(--pi-spacing-large); + height: var(--pi-spacing-large); + color: var(--pi-color-grey-300); + + svg { + position: static; + transform: none; + width: var(--pi-spacing-large); + height: var(--pi-spacing-large); + } + } + + // Mismos estados que los afijos de icono en `_mat-form-field.scss`. + &.is-focused .mat-mdc-select-arrow { + color: var(--pi-color-primary); + } + + // Estados del propio campo, en línea con `_mat-form-field.scss`. + .mat-mdc-select-placeholder { + color: var(--pi-color-grey-300); + } + + &.has-error .mat-mdc-select-arrow { + color: var(--pi-color-warn); + } +} + +// --- 2. Panel desplegable ------------------------------------------------ +// +// Se ancla a `.cdk-overlay-pane`, no a `.cdk-overlay-container`: esta versión +// de Material monta el panel con la API de popover *dentro* del propio +// ``, así que el contenedor global no está en su ascendencia. +.cdk-overlay-pane { + .mat-mdc-select-panel { + // Radio de superficie, no el del campo: en el tema alternativo el campo es + // una pastilla (999px) y un panel con ese radio quedaría deforme. + border-radius: var(--pi-theme-panel-radius); + border: 1px solid var(--pi-theme-field-border-color); + background-color: var(--pi-theme-field-surface); + box-shadow: var(--pi-box-shadow-panel); + padding: var(--pi-spacing-xx-small); + } + + // Buscador de `pi-searchable-select-field`. Va pegado arriba para que siga + // visible al desplazar una lista larga. + .pi-select-search { + position: sticky; + top: 0; + z-index: 1; + display: flex; + align-items: center; + gap: var(--pi-spacing-xx-small); + padding: var(--pi-spacing-xx-small) var(--pi-spacing-small); + margin-bottom: var(--pi-spacing-xx-small); + border-bottom: 1px solid var(--pi-theme-field-border-color); + background-color: var(--pi-theme-field-surface); + + input { + flex: 1 1 auto; + border: none; + outline: none; + background: transparent; + color: var(--pi-color-black); + font-family: inherit; + font-size: var(--pi-font-size-small); + + &::placeholder { + color: var(--pi-color-grey-300); + } + } + + &__empty { + margin: 0; + padding: var(--pi-spacing-small); + color: var(--pi-color-grey-300); + font-size: var(--pi-font-size-small); + text-align: center; + } + } + + .mat-mdc-option { + border-radius: var(--pi-border-radius-small); + min-height: var(--pi-spacing-xxx-large); + + // Material pinta hover/foco/selección con una capa de estado propia, no + // con `background-color`; por eso se ajustan sus tokens en vez de + // sobreescribir el fondo, que quedaba tapado por esa capa. + --mat-option-hover-state-layer-color: var(--pi-color-primary-50); + --mat-option-focus-state-layer-color: var(--pi-color-primary-50); + --mat-option-selected-state-layer-color: var(--pi-color-primary-50); + --mat-option-label-text-color: var(--pi-color-black); + --mat-option-selected-state-label-text-color: var(--pi-color-primary-900); + + // Opción resaltada por teclado en `pi-searchable-select-field`, que lleva + // su propio índice activo (ver el componente). + &.is-active { + background-color: var(--pi-color-primary-50); + } + + &.mdc-list-item--disabled { + color: var(--pi-color-grey-300); + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/index.scss b/libs/ui/src/lib/styles/common/vendors/index.scss index 6683de8..949c524 100644 --- a/libs/ui/src/lib/styles/common/vendors/index.scss +++ b/libs/ui/src/lib/styles/common/vendors/index.scss @@ -9,6 +9,7 @@ @forward 'mat-progress-bar'; @forward 'mat-progress-spinner'; @forward 'mat-radio-button'; +@forward 'mat-select'; @forward 'mat-slider'; @forward 'mat-snackbar'; @forward 'mat-tooltip'; diff --git a/libs/ui/src/lib/styles/themes/_alternative.scss b/libs/ui/src/lib/styles/themes/_alternative.scss index c6681b4..ddaecb8 100644 --- a/libs/ui/src/lib/styles/themes/_alternative.scss +++ b/libs/ui/src/lib/styles/themes/_alternative.scss @@ -95,9 +95,3 @@ .pi-shell__alternative { @include alternativeThemeTokens; } - -// Los overlays de Material se montan fuera del shell: se les propaga el tema -// marcando el contenedor (lo hace la app al cambiar de tema). -.cdk-overlay-container.pi-shell__alternative { - @include alternativeThemeTokens; -} diff --git a/libs/ui/src/lib/styles/themes/_default.scss b/libs/ui/src/lib/styles/themes/_default.scss index 8402959..90d044a 100644 --- a/libs/ui/src/lib/styles/themes/_default.scss +++ b/libs/ui/src/lib/styles/themes/_default.scss @@ -6,9 +6,13 @@ // tokens (ver `_alternative.scss`). // // Los valores viven en `.pi-shell` (no en `:root`) para que puedan convivir -// varios temas en la misma página, y se repiten en `.cdk-overlay-container` -// porque los overlays de Material (diálogos, snackbars, menús) se montan fuera -// del árbol del shell y no heredarían nada. +// varios temas en la misma página. +// +// No hace falta repetirlos en `.cdk-overlay-container`: ese contenedor lo +// cuelga el CDK del propio ``, que es donde va la clase de tema, así que +// hereda los tokens. Declararlos ahí además sería contraproducente, porque el +// contenedor no lleva la clase del tema y sus valores por defecto pisarían por +// proximidad a los que hereda. @mixin defaultThemeTokens { // --- Botones --------------------------------------------------------- @@ -69,7 +73,6 @@ // Base para *todos* los shells: así un tema solo tiene que declarar lo que // cambia, y lo que no redefina sigue teniendo valor (incluido el puente con // Material). Los temas concretos se aplican encima; ver `_alternative.scss`. -.pi-shell, -.cdk-overlay-container { +.pi-shell { @include defaultThemeTokens; } diff --git a/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts b/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts index f3d7e3c..5d7bdba 100644 --- a/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts +++ b/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts @@ -5,6 +5,7 @@ import { TemplateRef, input, contentChild, + inject, } from '@angular/core'; import { status } from '../../../util/status'; @@ -25,18 +26,18 @@ export class PiAccordionHeaderDirective { // Accordion Item Header Content @Directive({ selector: '[pi-accordion-item-header-content]' }) export class PiAccordionItemHeaderDirective { - constructor(public templateRef: TemplateRef) {} + public readonly templateRef = inject(TemplateRef); } // Accordion Item Summary Content @Directive({ selector: '[pi-accordion-item-summary-content]' }) export class PiAccordionSummaryContentDirective { - constructor(public templateRef: TemplateRef) {} + readonly templateRef = inject>(TemplateRef); } // Accordion Item Expandable Content @Directive({ selector: '[pi-accordion-item-expandable-content]' }) export class PiAccordionItemContentDirective { - constructor(public templateRef: TemplateRef) {} + readonly templateRef = inject>(TemplateRef); } /** diff --git a/libs/ui/src/lib/ui/button/base-button.component.ts b/libs/ui/src/lib/ui/button/base-button.component.ts index 423125a..c423dc3 100644 --- a/libs/ui/src/lib/ui/button/base-button.component.ts +++ b/libs/ui/src/lib/ui/button/base-button.component.ts @@ -1,5 +1,5 @@ // Angular -import { Directive, OnInit, input, output } from '@angular/core'; +import { Directive, input, output } from '@angular/core'; import { buttonSize, styleType, @@ -9,7 +9,7 @@ import { // Style definitions @Directive() -export abstract class PiBaseButtonComponent implements OnInit { +export abstract class PiBaseButtonComponent { readonly size = input('medium'); readonly color = input('primary'); readonly appearance = input('fill'); @@ -19,11 +19,6 @@ export abstract class PiBaseButtonComponent implements OnInit { readonly execute = output(); - ngOnInit(): void { - this.sizeClass; - this.flexDirectionClass; - } - protected get colorClass(): string { return `pi-color__${this.color()}`; } diff --git a/libs/ui/src/lib/ui/card/base-card.component.ts b/libs/ui/src/lib/ui/card/base-card.component.ts index 81d0242..2a637a3 100644 --- a/libs/ui/src/lib/ui/card/base-card.component.ts +++ b/libs/ui/src/lib/ui/card/base-card.component.ts @@ -1,11 +1,11 @@ // Angular -import { Directive, HostBinding, OnInit, input, output } from '@angular/core'; +import { Directive, input, output } from '@angular/core'; // Style definitions import { alignment, context, gap, mode, opacity, padding } from '../../styles'; @Directive() -export abstract class PiBaseCardComponent implements OnInit { +export abstract class PiBaseCardComponent { readonly hasShadow = input(true); readonly isClickable = input(false); readonly isStroked = input(false); @@ -25,19 +25,6 @@ export abstract class PiBaseCardComponent implements OnInit { readonly execute = output(); - ngOnInit(): void { - this.colorClass; - this.contextClass; - this.gapClass; - this.hasEqualPaddingClass; - this.hasLinkLabelClass; - this.hasShadowClass; - this.isClickableClass; - this.modeClass; - this.opacityClass; - this.isStrokedClass; - } - protected get hasShadowClass(): string | boolean | undefined { return this.hasShadow() && 'has-shadow'; } diff --git a/libs/ui/src/lib/ui/carousel/carousel.directive.ts b/libs/ui/src/lib/ui/carousel/carousel.directive.ts index f1e7cf1..3b43d7c 100644 --- a/libs/ui/src/lib/ui/carousel/carousel.directive.ts +++ b/libs/ui/src/lib/ui/carousel/carousel.directive.ts @@ -1,12 +1,15 @@ -import { Directive, TemplateRef, ViewContainerRef } from '@angular/core'; +import { + Directive, + TemplateRef, + ViewContainerRef, + inject, +} from '@angular/core'; @Directive({ selector: '[pi-carousel-item]', host: { class: 'pi-carousel pi-carousel__item' }, }) export class PiCarouselItemDirective { - constructor( - public templateRef: TemplateRef, - public viewContainerRef: ViewContainerRef, - ) {} + public readonly templateRef = inject>(TemplateRef); + public readonly viewContainerRef = inject(ViewContainerRef); } diff --git a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts index b2ded9d..48bc8f7 100644 --- a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts +++ b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts @@ -7,15 +7,15 @@ import { Component, ElementRef, HostBinding, - OnChanges, OnDestroy, - OnInit, Renderer2, - SimpleChanges, input, output, viewChild, contentChildren, + inject, + effect, + untracked, } from '@angular/core'; // Shared @@ -34,7 +34,7 @@ import { PiIconButtonComponent } from '../../button/icon-button/icon-button.comp imports: [PiIconButtonComponent, NgTemplateOutlet], }) export class PiCarouselComponent - implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy + implements AfterViewInit, AfterViewChecked, OnDestroy { @HostBinding('class') get classes(): string { const carouselItems = this.carouselItems(); @@ -168,12 +168,26 @@ export class PiCarouselComponent private touchmoveListener!: () => void; private touchendListener!: () => void; - constructor( - private cdr: ChangeDetectorRef, - private el: ElementRef, - private renderer: Renderer2, - private deviceConfig: DeviceConfigsFacade, - ) {} + private readonly cdr = inject(ChangeDetectorRef); + private readonly el = inject(ElementRef); + private readonly renderer = inject(Renderer2); + private readonly deviceConfig = inject(DeviceConfigsFacade); + + constructor() { + // No depende de inputs: se puede suscribir ya en la construcción. + // Sustituye al `ngOnChanges`: recalcula al cambiar `itemWidth`, siempre + // que la vista ya esté montada. + effect(() => { + this.itemWidth(); + if (this.carouselContentWrapper()) { + untracked(() => this.setItemsPerPage()); + } + }); + + combineLatest([this.deviceConfig.device$, this.deviceConfig.size$]) + .pipe(skip(1), takeUntil(this.destroyed$)) + .subscribe(() => this.reloadComponent()); + } /** * Used for symmetric carousel to know how many items to show on each side of the current item. @@ -239,20 +253,6 @@ export class PiCarouselComponent return Math.trunc(this.currentItem / this.itemsPerPage); } - ngOnInit(): void { - combineLatest([this.deviceConfig.device$, this.deviceConfig.size$]) - .pipe(skip(1), takeUntil(this.destroyed$)) - .subscribe(() => { - this.reloadComponent(); - }); - } - - ngOnChanges(simpleChanges: SimpleChanges): void { - if (simpleChanges['itemWidth'] && !!this.carouselContentWrapper()) { - this.setItemsPerPage(); - } - } - ngAfterViewInit(): void { this.setItemsPerPage(); this.setPages(); diff --git a/libs/ui/src/lib/ui/chart/chart/chart.component.ts b/libs/ui/src/lib/ui/chart/chart/chart.component.ts index 2aacede..b6bac2b 100644 --- a/libs/ui/src/lib/ui/chart/chart/chart.component.ts +++ b/libs/ui/src/lib/ui/chart/chart/chart.component.ts @@ -4,9 +4,9 @@ import { ChangeDetectorRef, Component, ElementRef, - OnInit, input, viewChild, + inject, } from '@angular/core'; import { Device } from '../../../util/device-configs'; import { Dataset } from '../../../util/models'; @@ -48,14 +48,14 @@ registerChartComponents(); changeDetection: ChangeDetectionStrategy.OnPush, imports: [PiChartLegendComponent], }) -export class PiChartComponent implements OnInit, AfterViewInit { +export class PiChartComponent implements AfterViewInit { readonly type = input<'line' | 'bar'>('line'); readonly datasets = input.required>(); readonly device = input.required(); readonly xAxisMarks = input>(); readonly isComparativeChart = input(true); - private _hoveredDatasetIndex!: number; + private _hoveredDatasetIndex = -1; set hoveredDatasetIndex(index: number) { this._hoveredDatasetIndex = index; } @@ -69,10 +69,8 @@ export class PiChartComponent implements OnInit, AfterViewInit { chart!: Chart; - constructor( - private translate: TranslateService, - private cdr: ChangeDetectorRef, - ) {} + private readonly translate = inject(TranslateService); + private readonly cdr = inject(ChangeDetectorRef); get completedDatasets() { return this.datasets().map((dataset) => { @@ -87,10 +85,6 @@ export class PiChartComponent implements OnInit, AfterViewInit { }); } - ngOnInit(): void { - this.hoveredDatasetIndex = -1; - } - ngAfterViewInit(): void { this.getLineChartConfigs(); } diff --git a/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts b/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts index 7aad36a..4a9fa4b 100644 --- a/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts +++ b/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts @@ -3,7 +3,6 @@ import { Component, ElementRef, HostBinding, - OnInit, TemplateRef, input, output, diff --git a/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts b/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts index 8f2b339..a70cde6 100644 --- a/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts +++ b/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts @@ -4,6 +4,7 @@ import { ElementRef, HostBinding, input, + inject, } from '@angular/core'; import { itemType } from '../entities/data-grid.model'; @@ -24,7 +25,7 @@ export class PiCellDirective implements AfterViewInit { readonly index = input.required(); readonly textAlign = input<'left' | 'center' | 'right'>('left'); - constructor(private elRef: ElementRef) {} + private readonly elRef = inject(ElementRef); ngAfterViewInit(): void { const index = this.index(); diff --git a/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts b/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts index 588c431..6c7367c 100644 --- a/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts +++ b/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts @@ -5,6 +5,7 @@ import { HostListener, Renderer2, input, + inject, } from '@angular/core'; import { columnsNumber } from '../entities/data-grid.model'; @@ -32,10 +33,8 @@ export class PiGridItemDirective { readonly isVisited = input(false); - constructor( - private elRef: ElementRef, - private renderer: Renderer2, - ) {} + private readonly elRef = inject(ElementRef); + private readonly renderer = inject(Renderer2); @HostListener('click') selectItem(): void { const gridItem = this.elRef.nativeElement; diff --git a/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts index ba6c6a9..3aafee7 100644 --- a/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts +++ b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts @@ -2,8 +2,9 @@ import { ChangeDetectionStrategy, Component, HostBinding, - OnInit, input, + effect, + inject, } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -22,7 +23,7 @@ import { take } from 'rxjs/operators'; styleUrls: ['./base-data-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PiBaseDataTableComponent implements OnInit { +export class PiBaseDataTableComponent { @HostBinding('class') get classes(): string { return [ 'pi-base-data-table', @@ -36,12 +37,12 @@ export class PiBaseDataTableComponent implements OnInit { readonly type = input<'header' | 'data'>('data'); readonly hideValues = input(false); - constructor(private store: Store) {} + private readonly store = inject>(Store); - ngOnInit(): void { - this.dataDirectionClass; - this.typeClass; - this.hideHeader(); + constructor() { + // `hideHeader` lee inputs; las otras dos líneas del antiguo `ngOnInit` solo + // leían getters y no hacían nada. + effect(() => this.hideHeader()); } get dataDirectionClass(): string { diff --git a/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts index c4ba3b9..2223828 100644 --- a/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts +++ b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts @@ -8,15 +8,13 @@ import { input, contentChild, viewChild, + inject, } from '@angular/core'; import { textStyle, justify, borderType } from '../../../styles'; import { Store } from '@ngrx/store'; // Store -import { - DeviceConfigsState, - DeviceTypeSelectors, -} from '../../../util/device-configs'; +import { DeviceConfigsState } from '../../../util/device-configs'; // RxJS import { take } from 'rxjs/operators'; @@ -44,7 +42,7 @@ export class PiDataTableItemComponent implements AfterViewInit { readonly item = contentChild>('item'); readonly componentItem = viewChild('componentItem'); - constructor(private store: Store) {} + private readonly store = inject>(Store); ngAfterViewInit(): void { this.setCellClassByIndex(); diff --git a/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts b/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts index b04b5cd..4b97eea 100644 --- a/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts +++ b/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts @@ -3,6 +3,7 @@ import { Component, HostBinding, forwardRef, + inject, } from '@angular/core'; import { Location } from '@angular/common'; import { LayoutTypeClass } from '../../../styles'; @@ -39,7 +40,7 @@ import { TranslatePipe } from '@ngx-translate/core'; export class PiNotFoundComponent { @HostBinding('class') className = LayoutTypeClass.VerticallyCentered; - constructor(private location: Location) {} + private readonly location = inject(Location); previous(): void { this.location.back(); diff --git a/libs/ui/src/lib/ui/file-upload/file-upload/file-upload.component.ts b/libs/ui/src/lib/ui/file-upload/file-upload/file-upload.component.ts index 1ccf86a..74abe6d 100644 --- a/libs/ui/src/lib/ui/file-upload/file-upload/file-upload.component.ts +++ b/libs/ui/src/lib/ui/file-upload/file-upload/file-upload.component.ts @@ -166,7 +166,9 @@ export class PiFileUploadComponent implements FormValueControl { } // En modo simple el último sustituye al anterior. - let next = this.multiple() ? [...this.value(), ...accepted] : accepted.slice(-1); + let next = this.multiple() + ? [...this.value(), ...accepted] + : accepted.slice(-1); const maxFiles = this.maxFiles(); if (this.multiple() && maxFiles !== undefined && next.length > maxFiles) { diff --git a/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html b/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html index c280a42..630a021 100644 --- a/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html +++ b/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html @@ -20,12 +20,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/date-field/date-field.component.ts b/libs/ui/src/lib/ui/form-field/date-field/date-field.component.ts index 1c1273d..32612a9 100644 --- a/libs/ui/src/lib/ui/form-field/date-field/date-field.component.ts +++ b/libs/ui/src/lib/ui/form-field/date-field/date-field.component.ts @@ -1,4 +1,9 @@ -import { ChangeDetectionStrategy, Component, HostBinding, input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + HostBinding, + input, +} from '@angular/core'; import { MatFormField, MatLabel, diff --git a/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts b/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts index b92daf5..47c32f4 100644 --- a/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts +++ b/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts @@ -1,27 +1,37 @@ import { Directive, - OnChanges, - OnInit, - SimpleChanges, input, + afterNextRender, + inject, + effect, + untracked, } from '@angular/core'; import { MatInput } from '@angular/material/input'; @Directive({ selector: '[pi-autofocus]' }) -export class PiAutofocusDirective implements OnInit, OnChanges { +export class PiAutofocusDirective { readonly isAutofocusSelected = input(); - constructor(private matInput: MatInput) {} + private readonly matInput = inject(MatInput); - ngOnInit() { - if (this.isAutofocusSelected()) { - setTimeout(() => this.matInput.focus()); - } - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes) { + constructor() { + // `afterNextRender` sustituye al `ngOnInit` + `setTimeout`: ya corre con la + // vista pintada, así que el `focus()` no necesita aplazarse a mano. + // Cubre lo que hacían el `ngOnInit` (primer render) y el `ngOnChanges` + // (cada cambio de la entrada) en una sola pieza. + afterNextRender(() => { if (this.isAutofocusSelected()) this.matInput.focus(); - } + }); + + let first = true; + effect(() => { + const shouldFocus = this.isAutofocusSelected(); + // El primer paso lo cubre `afterNextRender`, con la vista ya pintada. + if (first) { + first = false; + return; + } + if (shouldFocus) untracked(() => this.matInput.focus()); + }); } } diff --git a/libs/ui/src/lib/ui/form-field/directives/label.directive.ts b/libs/ui/src/lib/ui/form-field/directives/label.directive.ts index a846e89..7affdc5 100644 --- a/libs/ui/src/lib/ui/form-field/directives/label.directive.ts +++ b/libs/ui/src/lib/ui/form-field/directives/label.directive.ts @@ -2,13 +2,14 @@ import { Directive, ElementRef, HostBinding, - OnInit, Renderer2, input, + afterNextRender, + inject, } from '@angular/core'; @Directive({ selector: 'pi-label' }) -export class PiLabelDirective implements OnInit { +export class PiLabelDirective { @HostBinding('class') get classes(): string { const validationClass = this.isRequired() ? 'is-required' : ''; return ['pi-label', validationClass].join(' '); @@ -17,12 +18,15 @@ export class PiLabelDirective implements OnInit { readonly isRequired = input(false); - constructor( - private renderer: Renderer2, - private el: ElementRef, - ) {} + private readonly renderer = inject(Renderer2); + private readonly el = inject(ElementRef); - ngOnInit() { + constructor() { + // Igual que en `pi-affix`: depende de un input y toca el DOM una vez. + afterNextRender(() => this.renderRequiredMark()); + } + + private renderRequiredMark(): void { if (this.isRequired()) { const span = this.renderer.createElement('span'); const text = this.renderer.createText('*'); diff --git a/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html b/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html index c45827c..1c2d46d 100644 --- a/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html +++ b/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html @@ -17,12 +17,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html index 7c3b6fc..fd1b100 100644 --- a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html +++ b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html @@ -18,12 +18,12 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss index 68889ba..d03378b 100644 --- a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss +++ b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss @@ -11,7 +11,4 @@ gap: var(--pi-spacing-medium); width: 100%; } - .mat-mdc-form-field-error { - margin-left: 3.8rem; - } } diff --git a/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html b/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html index 52b8583..0d8606d 100644 --- a/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html +++ b/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html @@ -38,12 +38,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/index.ts b/libs/ui/src/lib/ui/form-field/index.ts index d1cb199..4ed6059 100644 --- a/libs/ui/src/lib/ui/form-field/index.ts +++ b/libs/ui/src/lib/ui/form-field/index.ts @@ -28,3 +28,9 @@ export * from './base-form-field.component'; export * from './incremental-range-field/incremental-range-field.component'; export * from './slide-toggle/slide-toggle.component'; +export * from './select-field/select-field.model'; +export * from './select-field/select-field.component'; +export * from './select-field/multi-select-field.component'; +export * from './select-field/searchable-select-field.component'; +export * from './select-field/base-searchable-select-field.component'; +export * from './select-field/searchable-multi-select-field.component'; diff --git a/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html b/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html index 2aeb7fc..6788be6 100644 --- a/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html +++ b/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html @@ -20,12 +20,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html b/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html index 3489fdb..8ee154b 100644 --- a/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html +++ b/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html @@ -24,12 +24,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html b/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html index 289d269..b562a19 100644 --- a/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html +++ b/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html @@ -16,12 +16,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/radio-group/radio-group.component.ts b/libs/ui/src/lib/ui/form-field/radio-group/radio-group.component.ts index 12bdc01..22d2a27 100644 --- a/libs/ui/src/lib/ui/form-field/radio-group/radio-group.component.ts +++ b/libs/ui/src/lib/ui/form-field/radio-group/radio-group.component.ts @@ -39,9 +39,9 @@ export interface PiRadioOption { imports: [PiRadioButtonComponent, TranslatePipe], host: { role: 'radiogroup' }, }) -export class PiRadioGroupComponent - implements FormValueControl -{ +export class PiRadioGroupComponent< + TValue = unknown, +> implements FormValueControl { @HostBinding('class') get classes(): string { return [ ComponentFamilyClass.Field, diff --git a/libs/ui/src/lib/ui/form-field/select-field/base-searchable-select-field.component.ts b/libs/ui/src/lib/ui/form-field/select-field/base-searchable-select-field.component.ts new file mode 100644 index 0000000..8c1f4c6 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/base-searchable-select-field.component.ts @@ -0,0 +1,146 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + computed, + effect, + input, + signal, + viewChild, +} from '@angular/core'; +import { MatSelect } from '@angular/material/select'; + +import { PiBaseFormFieldComponent } from '../base-form-field.component'; +import { PiSelectFieldOption } from './select-field.model'; + +/** Quita acentos y pasa a minúsculas, para que «Peru» encuentre «Perú». */ +function normalize(text: string): string { + return text.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, ''); +} + +/** + * Parte común de los selects con buscador (simple y múltiple). + * + * Reúne el filtro, la navegación por teclado y el enganche del estado de + * error. Lo único que cambia entre ambos es qué hace `Enter` sobre la opción + * resaltada y cómo se resume el valor en el campo cerrado, así que eso queda + * abstracto. + * + * @template TValue Tipo de cada opción. + * @template TModel Lo que guarda el formulario: una opción o un array de ellas. + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', +}) +export abstract class PiBaseSearchableSelectFieldComponent< + TValue, + TModel, +> extends PiBaseFormFieldComponent { + readonly options = input.required[]>(); + + /** Texto de la caja de búsqueda. */ + readonly searchPlaceholder = input('common.search'); + + /** Se muestra cuando el filtro no deja ninguna opción. */ + readonly emptyLabel = input('common.noResults'); + + protected readonly search = signal(''); + + /** + * Índice resaltado dentro de la lista filtrada. Se gestiona aquí en vez de + * delegar en `mat-select`: con el foco en el buscador su gestor de teclas no + * llega a activarse, y enfocar la opción a mano no lo sincroniza (Enter + * acababa confirmando una opción distinta de la resaltada). + */ + protected readonly activeIndex = signal(0); + + protected readonly filteredOptions = computed(() => { + const term = normalize(this.search().trim()); + if (!term) return this.options(); + return this.options().filter((o) => normalize(o.label).includes(term)); + }); + + protected readonly matSelectRef = viewChild(MatSelect); + + private readonly searchInputRef = + viewChild>('searchInput'); + + constructor() { + super(); + // La clase base engancha el `errorStateMatcher` a `MatInput`, que aquí no + // hay; `MatSelect` expone la misma API. + effect(() => { + this.hasError(); + const select = this.matSelectRef(); + if (!select) return; + select.errorStateMatcher = this.errorStateMatcher; + select.updateErrorState(); + }); + } + + /** Qué hacer con la opción resaltada al pulsar Enter. */ + protected abstract commitActiveOption( + option: PiSelectFieldOption, + ): void; + + protected onSearchInput(term: string): void { + this.search.set(term); + this.activeIndex.set(0); + } + + protected isOptionActive(index: number): boolean { + return this.activeIndex() === index; + } + + protected onOpenedChange(opened: boolean): void { + if (opened) { + this.onFocus(); + // El panel se monta al abrir, así que hay que esperar al siguiente frame + // para que el input exista y se pueda enfocar. + requestAnimationFrame(() => this.searchInputRef()?.nativeElement.focus()); + } else { + // El filtro no se conserva entre aperturas: al reabrir se ve la lista + // completa, que es lo esperable. + this.search.set(''); + this.activeIndex.set(0); + this.onBlur(); + } + } + + /** + * `mat-select` salta a la opción que empieza por la letra pulsada, así que + * lo escrito en el buscador no debe llegarle. Las flechas y el Enter se + * atienden aquí, sobre la lista ya filtrada. + */ + protected onSearchKeydown(event: KeyboardEvent): void { + const options = this.filteredOptions(); + const last = options.length - 1; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + event.stopPropagation(); + this.activeIndex.set(Math.min(this.activeIndex() + 1, last)); + return; + case 'ArrowUp': + event.preventDefault(); + event.stopPropagation(); + this.activeIndex.set(Math.max(this.activeIndex() - 1, 0)); + return; + case 'Enter': { + const option = options[this.activeIndex()]; + event.preventDefault(); + event.stopPropagation(); + if (option && !option.disabled) this.commitActiveOption(option); + return; + } + case 'Escape': + case 'Tab': + // Las gestiona `mat-select` (cerrar el panel, salir del campo). + return; + default: + event.stopPropagation(); + } + } +} diff --git a/libs/ui/src/lib/ui/form-field/select-field/multi-select-field.component.html b/libs/ui/src/lib/ui/form-field/select-field/multi-select-field.component.html new file mode 100644 index 0000000..aae01e2 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/multi-select-field.component.html @@ -0,0 +1,32 @@ + + + + + + {{ displayText() }} + + @for (option of options(); track option.value) { + + {{ option.label | translate }} + + } + + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } + diff --git a/libs/ui/src/lib/ui/form-field/select-field/multi-select-field.component.ts b/libs/ui/src/lib/ui/form-field/select-field/multi-select-field.component.ts new file mode 100644 index 0000000..ed8dbf1 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/multi-select-field.component.ts @@ -0,0 +1,89 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + computed, + effect, + input, + viewChild, +} from '@angular/core'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatOption } from '@angular/material/core'; +import { MatSelect, MatSelectTrigger } from '@angular/material/select'; +import { TranslatePipe } from '@ngx-translate/core'; + +import { PiBaseFormFieldComponent } from '../base-form-field.component'; +import { PiSelectFieldOption } from './select-field.model'; + +/** + * Campo con desplegable de selección múltiple. + * + * Mismo planteamiento que `pi-select-field`; `mat-select` en modo `multiple` + * ya pinta las casillas dentro del panel, así que no hace falta componer nada + * a mano. + */ +@Component({ + selector: 'pi-multi-select-field', + templateUrl: './multi-select-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatFormField, + MatLabel, + MatSelect, + MatSelectTrigger, + MatOption, + MatError, + TranslatePipe, + ], +}) +export class PiMultiSelectFieldComponent< + TValue = unknown, +> extends PiBaseFormFieldComponent { + @HostBinding('class') className = 'pi-form-field__select'; + + readonly options = input.required[]>(); + + /** + * A partir de este número de opciones marcadas se resume como «N + * seleccionados» en vez de listarlas. Con `0` nunca resume. + */ + readonly summaryThreshold = input(3); + + /** Texto del resumen; recibe el número por el pipe `translate`. */ + readonly summaryLabel = input('seleccionados'); + + /** + * Lo que se ve en el campo cuando está cerrado. `mat-select` por defecto + * concatena los valores en crudo, que aquí serían objetos. + */ + protected readonly displayText = computed(() => { + const selected = this.value() ?? []; + if (!selected.length) return ''; + + const threshold = this.summaryThreshold(); + if (threshold > 0 && selected.length >= threshold) { + return `${selected.length} ${this.summaryLabel()}`; + } + + const byValue = new Map(this.options().map((o) => [o.value, o.label])); + return selected.map((v) => byValue.get(v) ?? String(v)).join(', '); + }); + + /** Mismo enganche de estado de error que en `pi-select-field`. */ + private readonly matSelectRef = viewChild(MatSelect); + + constructor() { + super(); + effect(() => { + this.hasError(); + const select = this.matSelectRef(); + if (!select) return; + select.errorStateMatcher = this.errorStateMatcher; + select.updateErrorState(); + }); + } + + protected onSelectionChange(next: TValue[] | null): void { + this.value.set(next ?? []); + } +} diff --git a/libs/ui/src/lib/ui/form-field/select-field/searchable-multi-select-field.component.html b/libs/ui/src/lib/ui/form-field/select-field/searchable-multi-select-field.component.html new file mode 100644 index 0000000..82df0d0 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/searchable-multi-select-field.component.html @@ -0,0 +1,51 @@ + + + + + + {{ displayText() }} + + + + @for (option of filteredOptions(); track option.value; let i = $index) { + + {{ option.label | translate }} + + } @empty { +

{{ emptyLabel() | translate }}

+ } +
+ + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } +
diff --git a/libs/ui/src/lib/ui/form-field/select-field/searchable-multi-select-field.component.ts b/libs/ui/src/lib/ui/form-field/select-field/searchable-multi-select-field.component.ts new file mode 100644 index 0000000..057f59b --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/searchable-multi-select-field.component.ts @@ -0,0 +1,92 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + computed, + input, +} from '@angular/core'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatOption } from '@angular/material/core'; +import { MatSelect, MatSelectTrigger } from '@angular/material/select'; +import { TranslatePipe } from '@ngx-translate/core'; + +import { PiIconComponent } from '../../icon/icon/icon.component'; +import { PiBaseSearchableSelectFieldComponent } from './base-searchable-select-field.component'; +import { PiSelectFieldOption } from './select-field.model'; + +/** + * Campo con desplegable, buscador y selección múltiple. + * + * Combina `pi-multi-select-field` y `pi-searchable-select-field`: casillas en + * el panel más caja de filtro, para listas largas de las que hay que elegir + * varias. + */ +@Component({ + selector: 'pi-searchable-multi-select-field', + templateUrl: './searchable-multi-select-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatFormField, + MatLabel, + MatSelect, + MatSelectTrigger, + MatOption, + MatError, + PiIconComponent, + TranslatePipe, + ], +}) +export class PiSearchableMultiSelectFieldComponent< + TValue = unknown, +> extends PiBaseSearchableSelectFieldComponent { + @HostBinding('class') className = 'pi-form-field__select'; + + /** + * A partir de este número de opciones marcadas se resume como «N + * seleccionados» en vez de listarlas. Con `0` nunca resume. + */ + readonly summaryThreshold = input(3); + + /** Texto del resumen; recibe el número por el pipe `translate`. */ + readonly summaryLabel = input('seleccionados'); + + /** + * Igual que en el simple, el texto sale del valor y no de las opciones + * pintadas: con el filtro activo puede haber selecciones fuera de la lista. + */ + protected readonly displayText = computed(() => { + const selected = this.value() ?? []; + if (!selected.length) return ''; + + const threshold = this.summaryThreshold(); + if (threshold > 0 && selected.length >= threshold) { + return `${selected.length} ${this.summaryLabel()}`; + } + + const byValue = new Map(this.options().map((o) => [o.value, o.label])); + return selected.map((v) => byValue.get(v) ?? String(v)).join(', '); + }); + + protected isSelected(option: PiSelectFieldOption): boolean { + return (this.value() ?? []).includes(option.value); + } + + /** + * En selección múltiple, Enter marca o desmarca la opción resaltada y deja + * el panel abierto, para poder seguir eligiendo sin reabrirlo. + */ + protected override commitActiveOption( + option: PiSelectFieldOption, + ): void { + const current = this.value() ?? []; + this.value.set( + current.includes(option.value) + ? current.filter((v) => v !== option.value) + : [...current, option.value], + ); + } + + protected onSelectionChange(next: TValue[] | null): void { + this.value.set(next ?? []); + } +} diff --git a/libs/ui/src/lib/ui/form-field/select-field/searchable-select-field.component.html b/libs/ui/src/lib/ui/form-field/select-field/searchable-select-field.component.html new file mode 100644 index 0000000..d04a18d --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/searchable-select-field.component.html @@ -0,0 +1,50 @@ + + + + + + {{ displayText() }} + + + + @for (option of filteredOptions(); track option.value; let i = $index) { + + {{ option.label | translate }} + + } @empty { +

{{ emptyLabel() | translate }}

+ } +
+ + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } +
diff --git a/libs/ui/src/lib/ui/form-field/select-field/searchable-select-field.component.ts b/libs/ui/src/lib/ui/form-field/select-field/searchable-select-field.component.ts new file mode 100644 index 0000000..a78cc35 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/searchable-select-field.component.ts @@ -0,0 +1,66 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + computed, +} from '@angular/core'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatOption } from '@angular/material/core'; +import { MatSelect, MatSelectTrigger } from '@angular/material/select'; +import { TranslatePipe } from '@ngx-translate/core'; + +import { PiIconComponent } from '../../icon/icon/icon.component'; +import { PiBaseSearchableSelectFieldComponent } from './base-searchable-select-field.component'; +import { PiSelectFieldOption } from './select-field.model'; + +/** + * Campo con desplegable y buscador (selección simple). + * + * Igual que `pi-select-field` pero con una caja de filtro dentro del panel, + * pensado para listas largas donde recorrer las opciones a mano no es viable. + */ +@Component({ + selector: 'pi-searchable-select-field', + templateUrl: './searchable-select-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatFormField, + MatLabel, + MatSelect, + MatSelectTrigger, + MatOption, + MatError, + PiIconComponent, + TranslatePipe, + ], +}) +export class PiSearchableSelectFieldComponent< + TValue = unknown, +> extends PiBaseSearchableSelectFieldComponent { + @HostBinding('class') className = 'pi-form-field__select'; + + /** + * El texto del campo cerrado se calcula del valor, no de la opción + * renderizada: al filtrar, la opción seleccionada puede no estar en la lista + * y `mat-select` se quedaría sin nada que mostrar. + */ + protected readonly displayText = computed(() => { + const current = this.value(); + if (current === null || current === undefined) return ''; + return ( + this.options().find((o) => o.value === current)?.label ?? String(current) + ); + }); + + /** En selección simple, Enter confirma y cierra. */ + protected override commitActiveOption( + option: PiSelectFieldOption, + ): void { + this.value.set(option.value); + this.matSelectRef()?.close(); + } + + protected onSelectionChange(next: TValue | null): void { + this.value.set(next); + } +} diff --git a/libs/ui/src/lib/ui/form-field/select-field/select-field.component.html b/libs/ui/src/lib/ui/form-field/select-field/select-field.component.html new file mode 100644 index 0000000..01ded68 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/select-field.component.html @@ -0,0 +1,25 @@ + + + + + @for (option of options(); track option.value) { + + {{ option.label | translate }} + + } + + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } + diff --git a/libs/ui/src/lib/ui/form-field/select-field/select-field.component.ts b/libs/ui/src/lib/ui/form-field/select-field/select-field.component.ts new file mode 100644 index 0000000..4c86ad4 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/select-field.component.ts @@ -0,0 +1,70 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + effect, + input, + viewChild, +} from '@angular/core'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatOption } from '@angular/material/core'; +import { MatSelect } from '@angular/material/select'; +import { TranslatePipe } from '@ngx-translate/core'; + +import { PiBaseFormFieldComponent } from '../base-form-field.component'; +import { PiSelectFieldOption } from './select-field.model'; + +/** + * Campo con desplegable (selección simple). + * + * Es un *campo de formulario*, no la familia `pi-select`: aquélla pinta una + * lista siempre visible y se usa para filtros y menús; ésta es un input con su + * panel desplegable, y encaja con el resto de campos (`pi-text-field`, + * `pi-email-field`...). + * + * Extiende `PiBaseFormFieldComponent` y envuelve `mat-form-field`, así hereda + * los tokens de tema (radio, borde, superficie, etiqueta) y el tratamiento de + * errores del resto de campos. + */ +@Component({ + selector: 'pi-select-field', + templateUrl: './select-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatFormField, + MatLabel, + MatSelect, + MatOption, + MatError, + TranslatePipe, + ], +}) +export class PiSelectFieldComponent< + TValue = unknown, +> extends PiBaseFormFieldComponent { + @HostBinding('class') className = 'pi-form-field__select'; + + readonly options = input.required[]>(); + + /** + * La clase base engancha el `errorStateMatcher` a `MatInput`, que aquí no + * hay. `MatSelect` expone la misma API, así que se repite el enganche para + * que el borde rojo y el mensaje también aparezcan en el desplegable. + */ + private readonly matSelectRef = viewChild(MatSelect); + + constructor() { + super(); + effect(() => { + this.hasError(); + const select = this.matSelectRef(); + if (!select) return; + select.errorStateMatcher = this.errorStateMatcher; + select.updateErrorState(); + }); + } + + protected onSelectionChange(next: TValue | null): void { + this.value.set(next); + } +} diff --git a/libs/ui/src/lib/ui/form-field/select-field/select-field.model.ts b/libs/ui/src/lib/ui/form-field/select-field/select-field.model.ts new file mode 100644 index 0000000..8435b0a --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/select-field/select-field.model.ts @@ -0,0 +1,7 @@ +/** Opción de `pi-select-field` y `pi-multi-select-field`. */ +export interface PiSelectFieldOption { + value: TValue; + /** Se pasa por el pipe `translate`, así que admite clave de i18n. */ + label: string; + disabled?: boolean; +} diff --git a/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html b/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html index df81f3b..0da89f2 100644 --- a/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html +++ b/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html @@ -15,12 +15,11 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" /> + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } - -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} diff --git a/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html b/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html index 84c71c0..49e1c85 100644 --- a/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html +++ b/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html @@ -17,14 +17,14 @@ pi-autofocus [isAutofocusSelected]="isAutofocus()" > + @if (enableErrorMessage() && hasError()) { + + @for (message of errorMessages(); track message) { + {{ message | translate }} + } + + } -@if (enableErrorMessage() && hasError()) { - - @for (message of errorMessages(); track message) { - {{ message | translate }} - } - -} @if (maxLength() !== 1e6) { {{ value().length }} / {{ maxLength() }} } diff --git a/libs/ui/src/lib/ui/image/background-image.directive.ts b/libs/ui/src/lib/ui/image/background-image.directive.ts index 6e0d38b..4f4d0ec 100644 --- a/libs/ui/src/lib/ui/image/background-image.directive.ts +++ b/libs/ui/src/lib/ui/image/background-image.directive.ts @@ -7,6 +7,7 @@ import { Renderer2, OnDestroy, input, + inject, } from '@angular/core'; // RxJS @@ -45,11 +46,9 @@ export class PiBackgroundImageDirective implements AfterViewInit, OnDestroy { private destroy$: Subject = new Subject(); - constructor( - private deviceConfigsFacade: DeviceConfigsFacade, - private element: ElementRef, - private renderer: Renderer2, - ) {} + private readonly deviceConfigsFacade = inject(DeviceConfigsFacade); + private readonly element = inject(ElementRef); + private readonly renderer = inject(Renderer2); ngAfterViewInit(): void { this.deviceConfigsFacade.device$ diff --git a/libs/ui/src/lib/ui/modal/modal/modal.component.ts b/libs/ui/src/lib/ui/modal/modal/modal.component.ts index 6f2bbdd..2b76050 100644 --- a/libs/ui/src/lib/ui/modal/modal/modal.component.ts +++ b/libs/ui/src/lib/ui/modal/modal/modal.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, HostBinding, - Inject, + inject, } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { @@ -58,10 +58,11 @@ export class PiModalComponent { title: string | undefined; subtitle: string | undefined; - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: CustomModalData | DialogModalData, - ) { + readonly dialogRef = inject>(MatDialogRef); + readonly data = inject(MAT_DIALOG_DATA); + + constructor() { + const data = this.data; this.title = data.title; this.subtitle = data.subtitle; if (data.type === ModalType.DIALOG) { diff --git a/libs/ui/src/lib/ui/note/note/note.component.html b/libs/ui/src/lib/ui/note/note/note.component.html index 9d56d1a..ca47a84 100644 --- a/libs/ui/src/lib/ui/note/note/note.component.html +++ b/libs/ui/src/lib/ui/note/note/note.component.html @@ -1,5 +1,5 @@
- + @if (title()) {
{{ title() }}
} diff --git a/libs/ui/src/lib/ui/note/note/note.component.ts b/libs/ui/src/lib/ui/note/note/note.component.ts index 30d1a4e..0aec559 100644 --- a/libs/ui/src/lib/ui/note/note/note.component.ts +++ b/libs/ui/src/lib/ui/note/note/note.component.ts @@ -3,8 +3,9 @@ import { Component, Directive, HostBinding, - OnInit, input, + computed, + inject, } from '@angular/core'; import { NoteDataService } from '../infrastructure/note-data.service'; import { NoteStatus } from '../entities/note.model'; @@ -37,7 +38,7 @@ export class PiNoteActionContentDirective {} changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatIcon, PiTextSmallDirective, PiTextXSmallDirective], }) -export class PiNoteComponent implements OnInit { +export class PiNoteComponent { @HostBinding('class') get classes(): string { const alignmentValue = this.alignment(); const alignmentClass = @@ -97,18 +98,16 @@ export class PiNoteComponent implements OnInit { readonly appearance = input<'fill' | 'ghost'>('fill'); readonly type = input<'status' | 'simple'>('status'); - noteStatus?: NoteStatus; + private readonly noteDataService = inject(NoteDataService); - constructor(private noteDataService: NoteDataService) {} - - ngOnInit(): void { - this.noteStatus = this.getNoteStatus(); - } - - getNoteStatus(): NoteStatus | undefined { + /** + * Antes se calculaba en `ngOnInit`, así que no seguía a `status` si cambiaba + * después. Como `computed`, se recalcula solo. + */ + readonly noteStatus = computed(() => { const noteStatusOptions = this.noteDataService.getNoteStatusData(); return noteStatusOptions.find( (noteStatus) => this.status() === noteStatus.statusType, ); - } + }); } diff --git a/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts b/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts index 3bbd35d..d127a50 100644 --- a/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts +++ b/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; // Material import { MatSnackBar } from '@angular/material/snack-bar'; @@ -10,7 +10,7 @@ import { PiSnackBarComponent } from '../snack-bar/snack-bar.component'; providedIn: 'root', }) export class NotificationService { - constructor(private _snackBar: MatSnackBar) {} + private readonly _snackBar = inject(MatSnackBar); success(message: string, description?: string): void { this._snackBar.openFromComponent(PiSnackBarComponent, { diff --git a/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts index 81ebedb..da6e7a7 100644 --- a/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts +++ b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts @@ -2,10 +2,9 @@ import { ChangeDetectionStrategy, Component, HostBinding, - Inject, - Optional, forwardRef, signal, + inject, } from '@angular/core'; import { MatSnackBarRef, @@ -73,7 +72,10 @@ export class PiSnackBarComponent { /** The instance of the component making up the content of the snack bar. */ private readonly snackBarRef!: MatSnackBarRef; - constructor(@Optional() @Inject(MAT_SNACK_BAR_DATA) public data: any) { + readonly data = inject(MAT_SNACK_BAR_DATA, { optional: true }); + + constructor() { + const data = this.data; if (data) { this.message.set(data.message); this.icon.set(data.icon); diff --git a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts index 2d04b75..a56fad5 100644 --- a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts +++ b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts @@ -1,12 +1,13 @@ import { ChangeDetectionStrategy, Component, - OnInit, HostBinding, forwardRef, input, model, output, + effect, + untracked, } from '@angular/core'; import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; @@ -23,7 +24,7 @@ import { PiIconComponent } from '../../icon/icon/icon.component'; forwardRef(() => PiPaginatorComponent), ], }) -export class PiPaginatorComponent implements OnInit { +export class PiPaginatorComponent { @HostBinding('class') className = 'pi-paginator'; readonly currentPage = model(1); readonly pageSize = input.required(); @@ -36,8 +37,13 @@ export class PiPaginatorComponent implements OnInit { firstPagesRange = 0; - ngOnInit(): void { - this.initFirstPages(); + constructor() { + // `initFirstPages` lee el input `totalPages`, que no está disponible en el + // constructor; con un efecto además se recalcula si cambia. + effect(() => { + this.totalPages(); + untracked(() => this.initFirstPages()); + }); } get pages() { diff --git a/libs/ui/src/lib/ui/panel/base-panel.component.ts b/libs/ui/src/lib/ui/panel/base-panel.component.ts index 35b72e6..4ec305c 100644 --- a/libs/ui/src/lib/ui/panel/base-panel.component.ts +++ b/libs/ui/src/lib/ui/panel/base-panel.component.ts @@ -1,5 +1,5 @@ // Angular -import { Directive, OnInit, input } from '@angular/core'; +import { Directive, input } from '@angular/core'; import { alignment, borderRadius, @@ -13,7 +13,7 @@ import { import { status } from '../../util/status'; @Directive() -export abstract class PiBasePanelComponent implements OnInit { +export abstract class PiBasePanelComponent { readonly alignment = input('center'); readonly borderRadius = input('large'); readonly color = input('white'); @@ -38,22 +38,6 @@ export abstract class PiBasePanelComponent implements OnInit { readonly status = input.required(); - ngOnInit(): void { - this.alignmentClass; - this.borderRadiusClass; - this.colorClass; - this.flexDirectionClass; - this.justifyClass; - this.widthClass; - this.outlineClass; - this.paddingClass; - this.gapClass; - this.statusClass; - this.isStrokedClass; - this.modeClass; - this.opacityClass; - } - protected get widthClass(): string | undefined { const width = this.width(); return width && `pi-panel__width--${width}`; diff --git a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts index 0943f8e..3a199ef 100644 --- a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts +++ b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts @@ -2,14 +2,13 @@ import { ChangeDetectionStrategy, Component, HostBinding, - OnChanges, - OnInit, - SimpleChanges, TemplateRef, input, model, output, contentChild, + effect, + untracked, } from '@angular/core'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { ComponentFamilyClass } from '../../../styles'; @@ -50,10 +49,7 @@ import { TranslatePipe } from '@ngx-translate/core'; TranslatePipe, ], }) -export class PiExpansionPanelComponent - extends PiBasePanelComponent - implements OnInit, OnChanges -{ +export class PiExpansionPanelComponent extends PiBasePanelComponent { @HostBinding('class') get classes(): string { const type = this.type(); const typeClass = type && `pi-panel__expansion--type-${type}`; @@ -90,12 +86,16 @@ export class PiExpansionPanelComponent readonly expandableContentRef = contentChild>('expandableContent'); - override ngOnInit(): void { - if (this.status() === 'disabled') this.isCollapsed.set(true); - } - - ngOnChanges({ status }: SimpleChanges): void { - if (status?.currentValue === 'disabled') this.isCollapsed.set(true); + constructor() { + super(); + // Antes eran un `ngOnInit` y un `ngOnChanges` que hacían lo mismo (uno para + // el valor inicial y otro para los cambios). Con `status` siendo un input + // de señal, un solo efecto cubre ambos casos. + effect(() => { + if (this.status() === 'disabled') { + untracked(() => this.isCollapsed.set(true)); + } + }); } toggle(): void { diff --git a/libs/ui/src/lib/ui/panel/panel.directive.ts b/libs/ui/src/lib/ui/panel/panel.directive.ts index e61aab1..aaf684a 100644 --- a/libs/ui/src/lib/ui/panel/panel.directive.ts +++ b/libs/ui/src/lib/ui/panel/panel.directive.ts @@ -1,18 +1,14 @@ -import { Directive, HostBinding, OnInit, input } from '@angular/core'; +import { Directive, HostBinding, input } from '@angular/core'; import { padding } from '../../styles'; @Directive() -export abstract class PiBasePanelContentDirective implements OnInit { +export abstract class PiBasePanelContentDirective { @HostBinding('class') get commonClasses(): string { return ['pi-panel-content', this.innerPaddingClass].join(' '); } readonly innerPadding = input(); - ngOnInit(): void { - this.innerPaddingClass; - } - protected get innerPaddingClass(): string { const innerPadding = this.innerPadding(); return innerPadding diff --git a/libs/ui/src/lib/ui/select/base-select.component.ts b/libs/ui/src/lib/ui/select/base-select.component.ts index 88976d9..0419e42 100644 --- a/libs/ui/src/lib/ui/select/base-select.component.ts +++ b/libs/ui/src/lib/ui/select/base-select.component.ts @@ -11,6 +11,7 @@ import { contentChildren, viewChildren, ChangeDetectionStrategy, + inject, } from '@angular/core'; import { FormValueControl, ValidationError } from '@angular/forms/signals'; import { PiSelectOptionComponent } from './select-option/select-option.component'; @@ -197,10 +198,8 @@ export abstract class PiBaseSelectComponent< read: ElementRef, }); - constructor( - protected el: ElementRef, - protected readonly _renderer: Renderer2, - ) {} + protected readonly el = inject(ElementRef); + protected readonly _renderer = inject(Renderer2); trackByFn(index: number): number | undefined { return index; diff --git a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts index 72fd71c..2a0bc88 100644 --- a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts +++ b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts @@ -5,9 +5,9 @@ import { Component, ElementRef, HostBinding, - Renderer2, effect, input, + inject, } from '@angular/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import _ from 'lodash'; @@ -78,13 +78,11 @@ export class PiSelectCarouselComponent * @param el - Element reference of the component. * @param _renderer - Renderer to modify native elements. */ - constructor( - private readonly translateService: TranslateService, - protected override el: ElementRef, - protected override readonly _renderer: Renderer2, - private _cdr: ChangeDetectorRef, - ) { - super(el, _renderer); + private readonly translateService = inject(TranslateService); + private readonly _cdr = inject(ChangeDetectorRef); + + constructor() { + super(); effect(() => { this.childrenOptionsComponents(); diff --git a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts index f9f1317..dd0aef7 100644 --- a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts +++ b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts @@ -5,9 +5,9 @@ import { Component, ElementRef, HostBinding, - Renderer2, effect, input, + inject, } from '@angular/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { PiBaseSelectComponent } from '../base-select.component'; @@ -88,13 +88,11 @@ export class PiSelectMultipleComponent */ readonly selectAllLabel = input(); - constructor( - private readonly translateService: TranslateService, - protected override el: ElementRef, - protected override readonly _renderer: Renderer2, - private _cdr: ChangeDetectorRef, - ) { - super(el, _renderer); + private readonly translateService = inject(TranslateService); + private readonly _cdr = inject(ChangeDetectorRef); + + constructor() { + super(); /** * Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` + diff --git a/libs/ui/src/lib/ui/select/select-period/select-period.component.ts b/libs/ui/src/lib/ui/select/select-period/select-period.component.ts index a068429..6e27bc1 100644 --- a/libs/ui/src/lib/ui/select/select-period/select-period.component.ts +++ b/libs/ui/src/lib/ui/select/select-period/select-period.component.ts @@ -6,8 +6,8 @@ import { ElementRef, HostBinding, HostListener, - Renderer2, effect, + inject, } from '@angular/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { PiBaseSelectComponent } from '../base-select.component'; @@ -84,13 +84,11 @@ export class PiSelectPeriodComponent * @param el - Element reference of the component. * @param _renderer - Renderer to modify native elements. */ - constructor( - private readonly translateService: TranslateService, - protected override el: ElementRef, - protected override readonly _renderer: Renderer2, - private _cdr: ChangeDetectorRef, - ) { - super(el, _renderer); + private readonly translateService = inject(TranslateService); + private readonly _cdr = inject(ChangeDetectorRef); + + constructor() { + super(); effect(() => { this.childrenOptionsComponents(); diff --git a/libs/ui/src/lib/ui/select/select/select.component.ts b/libs/ui/src/lib/ui/select/select/select.component.ts index 878e2c3..72495ba 100644 --- a/libs/ui/src/lib/ui/select/select/select.component.ts +++ b/libs/ui/src/lib/ui/select/select/select.component.ts @@ -6,10 +6,10 @@ import { ElementRef, HostBinding, HostListener, - Renderer2, effect, forwardRef, input, + inject, } from '@angular/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { PiBaseSelectComponent } from '../base-select.component'; @@ -92,13 +92,11 @@ export class PiSelectComponent * @param el - Element reference of the component. * @param _renderer - Renderer to modify native elements. */ - constructor( - private readonly translateService: TranslateService, - protected override el: ElementRef, - protected override readonly _renderer: Renderer2, - private _cdr: ChangeDetectorRef, - ) { - super(el, _renderer); + private readonly translateService = inject(TranslateService); + private readonly _cdr = inject(ChangeDetectorRef); + + constructor() { + super(); /** * Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` + diff --git a/libs/ui/src/lib/ui/tab/tab.directive.ts b/libs/ui/src/lib/ui/tab/tab.directive.ts index 9d9855b..39a3131 100644 --- a/libs/ui/src/lib/ui/tab/tab.directive.ts +++ b/libs/ui/src/lib/ui/tab/tab.directive.ts @@ -1,11 +1,11 @@ -import { Directive, TemplateRef } from '@angular/core'; +import { Directive, TemplateRef, inject } from '@angular/core'; @Directive({ selector: '[pi-tab-content]' }) export class PiTabContentDirective { - constructor(public templateRef: TemplateRef) {} + public readonly templateRef = inject(TemplateRef); } @Directive({ selector: '[pi-tab-element]' }) export class PiTabElementDirective { - constructor(public templateRef: TemplateRef) {} + readonly templateRef = inject>(TemplateRef); } diff --git a/libs/ui/src/lib/ui/text/base-text-viewer.component.ts b/libs/ui/src/lib/ui/text/base-text-viewer.component.ts index fc9a086..fb9811f 100644 --- a/libs/ui/src/lib/ui/text/base-text-viewer.component.ts +++ b/libs/ui/src/lib/ui/text/base-text-viewer.component.ts @@ -1,5 +1,5 @@ // Angular -import { Directive, HostBinding, OnInit, input } from '@angular/core'; +import { Directive, HostBinding, input } from '@angular/core'; // Style definitions import { @@ -17,7 +17,7 @@ import { } from '../../styles'; @Directive() -export abstract class PiBaseTextViewerComponent implements OnInit { +export abstract class PiBaseTextViewerComponent { @HostBinding('class') get classes(): string { return [ ComponentFamilyClass.Text, @@ -58,23 +58,6 @@ export abstract class PiBaseTextViewerComponent implements OnInit { readonly label = input(); readonly value = input(); - ngOnInit(): void { - this.alignmentClass; - this.borderTypeClass; - this.colorClass; - this.flexDirectionClass; - this.fontSizeClass; - this.fontStyleClass; - this.fontWeightClass; - this.justifyClass; - this.iconSizeClass; - this.iconColorClass; - this.isDisabledClass; - this.hasLabelClass; - this.hasValueClass; - this.iconAppearanceClass; - } - protected get alignmentClass(): string { const alignmentValue = this.alignment(); return alignmentValue && `pi-text-viewer__align--${alignmentValue}`; diff --git a/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts b/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts index 79878b7..beaef7e 100644 --- a/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts +++ b/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts @@ -3,9 +3,10 @@ import { ElementRef, HostBinding, OnDestroy, - OnInit, Renderer2, input, + effect, + inject, } from '@angular/core'; import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; @@ -13,7 +14,7 @@ import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; selector: '[pi-tooltip]', providers: [MatTooltip], }) -export class PiTooltipDirective implements OnInit, OnDestroy { +export class PiTooltipDirective implements OnDestroy { @HostBinding('class') className = 'pi-tooltip-dispatcher'; readonly tooltipText = input(); @@ -25,19 +26,24 @@ export class PiTooltipDirective implements OnInit, OnDestroy { private mouseoutListener!: () => void; private clickListener!: () => void; - constructor( - private tooltip: MatTooltip, - private el: ElementRef, - private renderer: Renderer2, - ) {} + private readonly tooltip = inject(MatTooltip); + private readonly el = inject(ElementRef); + private readonly renderer = inject(Renderer2); - ngOnInit(): void { - this.tooltip.tooltipClass = [ - 'pi-tooltip', - this.typeClass, - this.colorClass, - ].join(' '); + constructor() { + // La clase depende de inputs, así que va en un efecto; los listeners no + // dependen de nada y se registran una sola vez. + effect(() => { + this.tooltip.tooltipClass = [ + 'pi-tooltip', + this.typeClass, + this.colorClass, + ].join(' '); + }); + this.registerListeners(); + } + private registerListeners(): void { this.mouseoverListener = this.renderer.listen( this.el.nativeElement, 'mouseover', diff --git a/libs/ui/src/lib/ui/wrapper/base.wrapper.ts b/libs/ui/src/lib/ui/wrapper/base.wrapper.ts index a64f28d..6c37f4a 100644 --- a/libs/ui/src/lib/ui/wrapper/base.wrapper.ts +++ b/libs/ui/src/lib/ui/wrapper/base.wrapper.ts @@ -1,5 +1,5 @@ // Angular -import { Directive, OnInit, input } from '@angular/core'; +import { Directive, input } from '@angular/core'; import { itemsWidth } from '../../styles'; import { @@ -12,7 +12,7 @@ import { } from '../../styles'; @Directive() -export abstract class PiBaseWrapper implements OnInit { +export abstract class PiBaseWrapper { readonly alignment = input('center'); readonly flexDirection = input('row'); readonly gap = input.required(); @@ -23,17 +23,6 @@ export abstract class PiBaseWrapper implements OnInit { readonly maxWidth = input('edge-to-edge'); readonly itemsWidth = input(); - ngOnInit(): void { - this.alignmentClass; - this.flexDirectionClass; - this.gridClass; - this.hasBackgroundClass; - this.hasPaddingClass; - this.justifyClass; - this.itemsWidthClass; - this.maxWidthClass; - } - protected get maxWidthClass(): string | undefined { const maxWidthValue = this.maxWidth(); return maxWidthValue && `pi-wrapper__${maxWidthValue}`; diff --git a/libs/ui/src/lib/util/affix/affix.directive.ts b/libs/ui/src/lib/util/affix/affix.directive.ts index 04067af..fde4ce3 100644 --- a/libs/ui/src/lib/util/affix/affix.directive.ts +++ b/libs/ui/src/lib/util/affix/affix.directive.ts @@ -2,14 +2,15 @@ import { Directive, ElementRef, HostBinding, - OnInit, Renderer2, input, + afterNextRender, + inject, } from '@angular/core'; import { unit } from '../models'; @Directive() -export abstract class PiBaseAffixDirective implements OnInit { +export abstract class PiBaseAffixDirective { @HostBinding('class') get classes(): string { return ['pi-affix', `pi-affix__appearance--${this.appearance()}`].join(' '); } @@ -17,12 +18,17 @@ export abstract class PiBaseAffixDirective implements OnInit { readonly unit = input(); readonly appearance = input<'ghost' | 'fill'>('ghost'); - constructor( - private renderer: Renderer2, - private el: ElementRef, - ) {} + private readonly renderer = inject(Renderer2); + private readonly el = inject(ElementRef); - ngOnInit() { + constructor() { + // Pinta DOM a partir de un input, así que no puede ir en el cuerpo del + // constructor (los inputs aún no tienen valor). `afterNextRender` corre una + // sola vez tras el primer render, que es lo que hacía el `ngOnInit`. + afterNextRender(() => this.renderUnit()); + } + + private renderUnit(): void { const unitValue = this.unit(); if (unitValue) { const span = this.renderer.createElement('span'); diff --git a/libs/ui/src/lib/util/amplitude/amplitude.service.ts b/libs/ui/src/lib/util/amplitude/amplitude.service.ts index 5c6385f..ece5f4b 100644 --- a/libs/ui/src/lib/util/amplitude/amplitude.service.ts +++ b/libs/ui/src/lib/util/amplitude/amplitude.service.ts @@ -1,5 +1,5 @@ import { isPlatformBrowser } from '@angular/common'; -import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core'; @Injectable({ providedIn: 'root', @@ -8,7 +8,9 @@ export class AmplitudeService { private amp: any; private ampEnabled = false; - constructor(@Inject(PLATFORM_ID) public platformId: object) { + readonly platformId = inject(PLATFORM_ID); + + constructor() { if (isPlatformBrowser(this.platformId)) { this.amp = (window as any).amplitude; if (this.amp) this.ampEnabled = true; diff --git a/libs/ui/src/lib/util/color/directives/colors.directive.ts b/libs/ui/src/lib/util/color/directives/colors.directive.ts index 1541da3..2eb0ef6 100644 --- a/libs/ui/src/lib/util/color/directives/colors.directive.ts +++ b/libs/ui/src/lib/util/color/directives/colors.directive.ts @@ -1,8 +1,8 @@ -import { Directive, HostBinding, OnInit, input } from '@angular/core'; +import { Directive, HostBinding, input } from '@angular/core'; import { density, mode, opacity, styleType } from '../../../styles'; @Directive() -export abstract class PiBaseColor implements OnInit { +export abstract class PiBaseColor { @HostBinding('class') get commonClasses(): string { return [this.densityClass, this.modeClass].join(' '); } @@ -10,9 +10,6 @@ export abstract class PiBaseColor implements OnInit { readonly opacity = input('100'); readonly mode = input('dark'); - ngOnInit(): void { - this.densityClass; - } protected get densityClass(): string { const densityValue = this.density(); return !!densityValue ? `pi-color__density--${densityValue}` : ''; diff --git a/libs/ui/src/lib/util/common/common.ts b/libs/ui/src/lib/util/common/common.ts index 355de31..fd2090c 100644 --- a/libs/ui/src/lib/util/common/common.ts +++ b/libs/ui/src/lib/util/common/common.ts @@ -1,12 +1,12 @@ // Angular import { HttpParams } from '@angular/common/http'; -import { Inject, PLATFORM_ID } from '@angular/core'; +import { Inject, PLATFORM_ID, inject } from '@angular/core'; // Shared import { Params } from '../models'; import { timer } from 'rxjs'; export class Commons { - constructor(@Inject(PLATFORM_ID) public platformId: object) {} + readonly platformId = inject(PLATFORM_ID); static followUrl($event: MouseEvent, item: { url: string }): void { Commons.followExternalUrl($event, item); diff --git a/libs/ui/src/lib/util/common/sector.ts b/libs/ui/src/lib/util/common/sector.ts index bb85081..4625011 100644 --- a/libs/ui/src/lib/util/common/sector.ts +++ b/libs/ui/src/lib/util/common/sector.ts @@ -1,10 +1,10 @@ // Angular -import { Inject, PLATFORM_ID } from '@angular/core'; +import { Inject, PLATFORM_ID, inject } from '@angular/core'; import { Sector } from '../models'; import { SectorType, SectorUidMatchLocale } from '../models'; export class SectorCommons { - constructor(@Inject(PLATFORM_ID) public platformId: object) {} + readonly platformId = inject(PLATFORM_ID); static getSectorLocaleFromUid(uid: string): string { const sectorLocale: string = SectorUidMatchLocale[uid as SectorType]; diff --git a/libs/ui/src/lib/util/common/select-options.ts b/libs/ui/src/lib/util/common/select-options.ts index d1e2028..40c9329 100644 --- a/libs/ui/src/lib/util/common/select-options.ts +++ b/libs/ui/src/lib/util/common/select-options.ts @@ -1,8 +1,8 @@ -import { Inject, PLATFORM_ID } from '@angular/core'; +import { Inject, PLATFORM_ID, inject } from '@angular/core'; import { SelectOption } from '../../ui/select'; export class SelectOptionsCommons { - constructor(@Inject(PLATFORM_ID) public platformId: object) {} + readonly platformId = inject(PLATFORM_ID); static convertStringstoSelectOptions( values: Array, diff --git a/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts b/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts index 80ff349..79ea92d 100644 --- a/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts +++ b/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts @@ -1,6 +1,6 @@ import { BreakpointObserver } from '@angular/cdk/layout'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; -import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -32,12 +32,12 @@ import { DeviceConfigsState } from '../state/device-configs.reducer'; export class DeviceConfigsService { private htmlTag!: HTMLElement; - constructor( - private store: Store, - private breakpointObserver: BreakpointObserver, - @Inject(DOCUMENT) private document: Document, - @Inject(PLATFORM_ID) public platformId: Object, - ) { + private readonly store = inject>(Store); + private readonly breakpointObserver = inject(BreakpointObserver); + private readonly document = inject(DOCUMENT); + readonly platformId = inject(PLATFORM_ID); + + constructor() { this.deviceConfigsHandler(); } diff --git a/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts b/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts index 95c10cb..3473fac 100644 --- a/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts +++ b/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; // Rxjs import { of } from 'rxjs'; @@ -15,10 +15,8 @@ import { DeviceConfigsService } from '../services/device-configs.service'; @Injectable() export class DeviceConfigsEffects { - constructor( - private deviceConfigsService: DeviceConfigsService, - private actions: Actions, - ) {} + private readonly deviceConfigsService = inject(DeviceConfigsService); + private readonly actions = inject(Actions); getBrowser$ = createEffect(() => { return this.actions.pipe( diff --git a/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts b/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts index 56887f3..be9a0bc 100644 --- a/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts +++ b/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { customIcons } from './icons.data'; @@ -7,10 +7,8 @@ import { customIcons } from './icons.data'; providedIn: 'root', }) export class IconRegistryService { - constructor( - private matIconRegistry: MatIconRegistry, - private domSanitizer: DomSanitizer, - ) {} + private readonly matIconRegistry = inject(MatIconRegistry); + private readonly domSanitizer = inject(DomSanitizer); init(): void { this.registerIcons(customIcons, 'assets/icons/'); diff --git a/libs/ui/src/lib/util/local-storage/local-storage.service.ts b/libs/ui/src/lib/util/local-storage/local-storage.service.ts index c667575..6f6c533 100644 --- a/libs/ui/src/lib/util/local-storage/local-storage.service.ts +++ b/libs/ui/src/lib/util/local-storage/local-storage.service.ts @@ -1,10 +1,10 @@ // Angular import { isPlatformBrowser } from '@angular/common'; -import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class LocalStorageService { - constructor(@Inject(PLATFORM_ID) public platformId: Object) {} + readonly platformId = inject(PLATFORM_ID); getItem(tag: string) { if (isPlatformBrowser(this.platformId)) { diff --git a/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts b/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts index c77f8a8..42c3aab 100644 --- a/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts +++ b/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts @@ -4,16 +4,15 @@ import { HostBinding, HostListener, Renderer2, + inject, } from '@angular/core'; @Directive() export abstract class PiBaseMenuEventItem { @HostBinding('class') className = 'pi-menu-event-item'; - constructor( - protected el: ElementRef, - protected renderer: Renderer2, - ) {} + protected readonly el = inject(ElementRef); + protected readonly renderer = inject(Renderer2); @HostListener('click') onClick() { diff --git a/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts b/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts index 971ffba..434785c 100644 --- a/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts +++ b/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts @@ -4,15 +4,14 @@ import { HostBinding, HostListener, Renderer2, + inject, } from '@angular/core'; @Directive({ selector: '[pi-menu-event-children]' }) export class PiMenuEventChildrenDirective { @HostBinding('class') className = 'pi-menu pi-menu__children'; - constructor( - private el: ElementRef, - private renderer: Renderer2, - ) {} + private readonly el = inject(ElementRef); + private readonly renderer = inject(Renderer2); @HostListener('click') onClickMenuItem() { const menuChildren = this.el.nativeElement; diff --git a/libs/ui/src/lib/util/router-store/error-handler.ts b/libs/ui/src/lib/util/router-store/error-handler.ts index 8561417..f3c6878 100644 --- a/libs/ui/src/lib/util/router-store/error-handler.ts +++ b/libs/ui/src/lib/util/router-store/error-handler.ts @@ -1,9 +1,9 @@ -import { ErrorHandler, Injectable, Injector } from '@angular/core'; +import { ErrorHandler, Injectable, Injector, inject } from '@angular/core'; import { Router } from '@angular/router'; @Injectable() export class GlobalErrorHandler implements ErrorHandler { - constructor(private injector: Injector) {} + private readonly injector = inject(Injector); handleError(error: any) { const router = this.injector.get(Router); console.error(error.stack.toString()); diff --git a/libs/ui/src/lib/util/schema/schema.service.ts b/libs/ui/src/lib/util/schema/schema.service.ts index c0d29e7..cec8b83 100644 --- a/libs/ui/src/lib/util/schema/schema.service.ts +++ b/libs/ui/src/lib/util/schema/schema.service.ts @@ -1,6 +1,6 @@ // Angular import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable, Renderer2 } from '@angular/core'; +import { Inject, Injectable, Renderer2, inject } from '@angular/core'; @Injectable({ providedIn: 'root', @@ -8,7 +8,7 @@ import { Inject, Injectable, Renderer2 } from '@angular/core'; export class SchemaService { script!: HTMLScriptElement; - constructor(@Inject(DOCUMENT) private doc: Document) {} + private readonly doc = inject(DOCUMENT); loadJsScript(renderer: Renderer2, data: any): HTMLScriptElement { const script = renderer.createElement('script'); diff --git a/libs/ui/src/lib/util/translate/i18n.service.ts b/libs/ui/src/lib/util/translate/i18n.service.ts index c03ff3a..cd783a7 100644 --- a/libs/ui/src/lib/util/translate/i18n.service.ts +++ b/libs/ui/src/lib/util/translate/i18n.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; providedIn: 'root', }) export class I18nService { - constructor(private translateService: TranslateService) {} + private readonly translateService = inject(TranslateService); setLanguage(lang: string): Observable { return new Observable((observer) => { diff --git a/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts b/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts index 2153640..4de46a2 100644 --- a/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts +++ b/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts @@ -1,15 +1,13 @@ import { Observable } from 'rxjs'; import { TranslateLoader } from '@ngx-translate/core'; -import { makeStateKey, StateKey, TransferState } from '@angular/core'; +import { makeStateKey, StateKey, TransferState, inject } from '@angular/core'; import { join } from 'path'; import { readFileSync } from 'fs'; export class TranslateServerLoader implements TranslateLoader { - constructor( - private transferState: TransferState, - private prefix: string = 'i18n', - private suffix: string = '.json', - ) {} + private readonly transferState = inject(TransferState); + private readonly prefix = inject((string = 'i18n')); + private readonly suffix = inject((string = '.json')); public getTranslation(lang: string): Observable { return new Observable((observer) => { diff --git a/nx/core/package-json b/nx/core/package-json new file mode 100644 index 0000000..e69de29 diff --git a/nx/core/project-json b/nx/core/project-json new file mode 100644 index 0000000..e69de29 diff --git a/nx/js/dependencies-and-lockfile b/nx/js/dependencies-and-lockfile new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 2b5b019..5aab161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2698,7 +2698,6 @@ "version": "22.0.6", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.6.tgz", "integrity": "sha512-HPiceCIEyIcTmOuIFiyqu04kbLoJhSUavcOy/kzJkJ2OPXtPvGKaimC4lgs0C86uFy4P6rj6PDITBe3AVHkMTA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.29.7", @@ -2731,7 +2730,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -2762,14 +2760,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2779,7 +2775,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -4974,7 +4969,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.13.0.tgz", "integrity": "sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==", - "dev": true, + "devOptional": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@colordx/core": { @@ -5170,7 +5165,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5187,7 +5181,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5204,7 +5197,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5221,7 +5213,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5238,7 +5229,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5255,7 +5245,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5272,7 +5261,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5289,7 +5277,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5306,7 +5293,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5323,7 +5309,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5340,7 +5325,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5357,7 +5341,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5374,7 +5357,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5391,7 +5373,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5408,7 +5389,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5425,7 +5405,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5442,7 +5421,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5459,7 +5437,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5476,7 +5453,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5493,7 +5469,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5510,7 +5485,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5527,7 +5501,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5544,7 +5517,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5561,7 +5533,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5578,7 +5549,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5595,7 +5565,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10349,6 +10318,18 @@ } } }, + "node_modules/@nx/module-federation/node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@nx/module-federation/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -11400,6 +11381,43 @@ "node": ">=14.0.0" } }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -12353,7 +12371,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12370,7 +12387,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12387,7 +12403,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12404,7 +12419,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12421,7 +12435,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12438,7 +12451,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12455,7 +12467,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12472,7 +12483,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12489,7 +12499,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12506,7 +12515,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12523,7 +12531,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12540,7 +12547,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12557,7 +12563,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12573,7 +12578,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12585,7 +12589,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12596,7 +12599,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12607,7 +12609,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12626,7 +12627,6 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12640,7 +12640,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12657,7 +12656,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12671,14 +12669,13 @@ "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", - "dev": true, "license": "MIT" }, "node_modules/@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.1.0" @@ -12699,7 +12696,7 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -12722,7 +12719,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -13054,7 +13051,7 @@ "version": "4.62.3", "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.62.3.tgz", "integrity": "sha512-sEOQWamme1YgXeVcE1JkB1k44RwtPWcpYpGUW1Lso7bOj+Ay9lN2Pak/CcfNeC4xqIPNhYSabQNaWYkJJR7m8Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -13928,7 +13925,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -13965,7 +13962,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/eslint": { @@ -14979,7 +14976,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -14997,7 +14994,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/spy": "4.1.10", @@ -15024,7 +15021,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -15037,7 +15034,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/utils": "4.1.10", @@ -15051,7 +15048,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.10", @@ -15067,7 +15064,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" @@ -15099,7 +15096,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.10", @@ -15114,7 +15111,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@webassemblyjs/ast": { @@ -15714,7 +15711,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -16546,7 +16543,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -16602,7 +16599,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -16891,7 +16887,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/columnify": { @@ -16935,7 +16931,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/component-emitter": { @@ -17629,7 +17625,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -17649,7 +17645,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -18036,7 +18031,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -18503,7 +18498,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -18628,7 +18623,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -18998,7 +18993,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/find-cache-directory/-/find-cache-directory-6.0.0.tgz", "integrity": "sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "common-path-prefix": "^3.0.0", @@ -19015,7 +19010,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz", "integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "find-up-simple": "^1.0.0" @@ -19045,7 +19040,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -19464,7 +19459,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -20289,7 +20283,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.6.1.tgz", "integrity": "sha512-dbR5bdhi7TWDoCye9cByZqeg/gAfamm8Vu3G1KZOTYkOif8WkuM8CD0oeDPtZYMzT5YH76JAFB7bkmyY9OJi2A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -23565,7 +23559,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -24034,7 +24028,6 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -24067,7 +24060,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24088,7 +24080,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24109,7 +24100,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24130,7 +24120,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24151,7 +24140,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24172,7 +24160,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24193,7 +24180,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24214,7 +24200,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24235,7 +24220,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24256,7 +24240,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -24277,7 +24260,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -25283,7 +25265,7 @@ "version": "22.0.2", "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-22.0.2.tgz", "integrity": "sha512-8qQ+RWm0W3gK8Sch1kmo3EzIUiCTXIOP2gS7b+vtqCMBf4KBjBP0wFBikW5qRau9lGOzRTML0GPFGoARPR4+fw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -25774,7 +25756,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=20" @@ -25784,7 +25766,7 @@ "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -26535,7 +26517,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", - "dev": true, + "devOptional": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -27219,7 +27201,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { @@ -28343,7 +28325,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -28597,7 +28578,6 @@ "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.126.0", @@ -28631,7 +28611,6 @@ "version": "0.126.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -28685,7 +28664,7 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.4.1.tgz", "integrity": "sha512-l//F3Zf7ID5GoOfLfD8kroBjQKEKpy1qfhtAdnpibFZMffPaylrg1CoDC2vGkPeTeyxUe4bVFCln2EFuL7IGGg==", - "dev": true, + "devOptional": true, "license": "LGPL-3.0-only", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -28711,7 +28690,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/rollup/node_modules/@types/estree": { @@ -28850,7 +28829,7 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.100.0.tgz", "integrity": "sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.5.0", @@ -28898,7 +28877,6 @@ "!riscv64", "!x64" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -28909,7 +28887,6 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -28934,7 +28911,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -28951,7 +28927,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -28968,7 +28943,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -28985,7 +28959,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29002,7 +28975,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29019,7 +28991,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29036,7 +29007,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29053,7 +29023,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29070,7 +29039,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29087,7 +29055,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29104,7 +29071,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29121,7 +29087,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29138,7 +29103,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29155,7 +29119,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29169,7 +29132,6 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.100.0.tgz", "integrity": "sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A==", - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29186,7 +29148,6 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -29211,7 +29172,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29228,7 +29188,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -29242,7 +29201,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -29649,7 +29608,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/signal-exit": { @@ -29996,7 +29955,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/statuses": { @@ -30012,7 +29971,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/stdin-discarder": { @@ -30365,7 +30324,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "sync-message-port": "^1.0.0" @@ -30378,7 +30337,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=16.0.0" @@ -30700,14 +30659,14 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -30733,7 +30692,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -31160,7 +31119,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -31448,7 +31406,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/vary": { @@ -31464,7 +31422,6 @@ "version": "8.0.9", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", - "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -31580,11 +31537,27 @@ } } }, + "node_modules/vite-tsconfig-paths/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/expect": "4.1.10", @@ -32322,7 +32295,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", @@ -32555,7 +32528,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -32571,7 +32543,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -32587,7 +32559,6 @@ "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^9.0.1", @@ -32615,7 +32586,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -32628,7 +32598,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -32641,7 +32610,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^7.2.0", @@ -32656,14 +32624,12 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -32681,7 +32647,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -32697,7 +32662,6 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -32715,7 +32679,6 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23"