@maro-tech/form 0.0.17 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, Injectable, inject, InjectionToken, input, output, ViewChild, Component, ChangeDetectorRef, DestroyRef, EventEmitter, ElementRef, forwardRef, booleanAttribute, HostListener, Output, Input, ChangeDetectionStrategy, ViewContainerRef, computed } from '@angular/core';
2
+ import { signal, Injectable, inject, input, output, ViewChild, Component, EventEmitter, forwardRef, Output, Input, ChangeDetectorRef, ChangeDetectionStrategy, HostListener, DestroyRef, ElementRef, booleanAttribute, ViewContainerRef, computed } from '@angular/core';
3
3
  import { HttpClient } from '@angular/common/http';
4
4
  import * as i1$2 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import * as i2 from '@angular/forms';
7
- import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
8
- import { Subject, debounceTime, distinctUntilChanged, forkJoin, map, Observable, from, switchMap, of } from 'rxjs';
7
+ import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
8
+ import { UiButtonComponent, UiToastService } from '@maro-tech/core';
9
9
  import { BrowserMultiFormatOneDReader } from '@zxing/browser';
10
10
  import { DecodeHintType, BarcodeFormat } from '@zxing/library';
11
- import { UiButtonComponent, UiToastService } from '@maro-tech/core';
12
- import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
13
11
  import * as i1 from 'lucide-angular';
14
- import { X, ChevronDown, Check, LucideAngularModule, CalendarDays, ChevronLeft, ChevronRight, Eye, EyeOff } from 'lucide-angular';
12
+ import { CalendarDays, ChevronLeft, ChevronRight, LucideAngularModule, Eye, EyeOff, X, ChevronDown, Check } from 'lucide-angular';
15
13
  import * as i1$1 from '@angular/cdk/drag-drop';
16
14
  import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
15
+ import { forkJoin, map, Subject, debounceTime, distinctUntilChanged, of } from 'rxjs';
16
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
17
17
  import { Router } from '@angular/router';
18
18
 
19
19
  class UiFormContext {
@@ -79,8 +79,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
79
79
  type: Injectable
80
80
  }] });
81
81
 
82
- const UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS = new InjectionToken('UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS');
83
-
84
82
  const ACTION_PRINT = 'ACTION_PRINT';
85
83
  const ACTION_FORM = 'ACTION_FORM';
86
84
  const ACTION_API = 'ACTION_API';
@@ -101,7 +99,7 @@ const flattenDynamicFormFields = (fields) => {
101
99
  return result;
102
100
  };
103
101
 
104
- class UiBarcodeScannerInputComponent {
102
+ class InputBarcodeComponent {
105
103
  value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
106
104
  label = input('Штрихкод', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
107
105
  placeholder = input('Відскануйте або введіть штрихкод', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
@@ -365,8 +363,8 @@ class UiBarcodeScannerInputComponent {
365
363
  return null;
366
364
  }
367
365
  }
368
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiBarcodeScannerInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
369
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiBarcodeScannerInputComponent, isStandalone: true, selector: "forms-barcode-scanner-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoOpen: { classPropertyName: "autoOpen", publicName: "autoOpen", isSignal: true, isRequired: false, transformFunction: null }, searchButtonLabel: { classPropertyName: "searchButtonLabel", publicName: "searchButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, openCameraLabel: { classPropertyName: "openCameraLabel", publicName: "openCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, closeCameraLabel: { classPropertyName: "closeCameraLabel", publicName: "closeCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraTitle: { classPropertyName: "cameraTitle", publicName: "cameraTitle", isSignal: true, isRequired: false, transformFunction: null }, cameraStartingLabel: { classPropertyName: "cameraStartingLabel", publicName: "cameraStartingLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraHint: { classPropertyName: "cameraHint", publicName: "cameraHint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", scanned: "scanned", searchRequested: "searchRequested", cameraStateChange: "cameraStateChange" }, viewQueries: [{ propertyName: "cameraVideo", first: true, predicate: ["cameraVideo"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
366
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputBarcodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
367
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputBarcodeComponent, isStandalone: true, selector: "input-barcode", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoOpen: { classPropertyName: "autoOpen", publicName: "autoOpen", isSignal: true, isRequired: false, transformFunction: null }, searchButtonLabel: { classPropertyName: "searchButtonLabel", publicName: "searchButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, openCameraLabel: { classPropertyName: "openCameraLabel", publicName: "openCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, closeCameraLabel: { classPropertyName: "closeCameraLabel", publicName: "closeCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraTitle: { classPropertyName: "cameraTitle", publicName: "cameraTitle", isSignal: true, isRequired: false, transformFunction: null }, cameraStartingLabel: { classPropertyName: "cameraStartingLabel", publicName: "cameraStartingLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraHint: { classPropertyName: "cameraHint", publicName: "cameraHint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", scanned: "scanned", searchRequested: "searchRequested", cameraStateChange: "cameraStateChange" }, viewQueries: [{ propertyName: "cameraVideo", first: true, predicate: ["cameraVideo"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
370
368
  <div class="space-y-2">
371
369
  @if (label()) {
372
370
  <label class="flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500" for="barcode-scanner-input">
@@ -460,10 +458,10 @@ class UiBarcodeScannerInputComponent {
460
458
  </div>
461
459
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }] });
462
460
  }
463
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiBarcodeScannerInputComponent, decorators: [{
461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputBarcodeComponent, decorators: [{
464
462
  type: Component,
465
463
  args: [{
466
- selector: 'forms-barcode-scanner-input',
464
+ selector: 'input-barcode',
467
465
  standalone: true,
468
466
  imports: [CommonModule, FormsModule, UiButtonComponent],
469
467
  template: `
@@ -565,6 +563,154 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
565
563
  args: ['cameraVideo']
566
564
  }] } });
567
565
 
566
+ class InputCheckboxComponent {
567
+ label;
568
+ disabled = false;
569
+ value = false;
570
+ valueChange = new EventEmitter();
571
+ onChange = () => undefined;
572
+ onTouched = () => undefined;
573
+ writeValue(value) { this.value = !!value; }
574
+ registerOnChange(fn) { this.onChange = fn; }
575
+ registerOnTouched(fn) { this.onTouched = fn; }
576
+ setDisabledState(isDisabled) { this.disabled = isDisabled; }
577
+ onValueChange(event) {
578
+ const value = event.target.checked;
579
+ this.value = value;
580
+ this.onChange(value);
581
+ this.valueChange.emit(value);
582
+ }
583
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputCheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: InputCheckboxComponent, isStandalone: true, selector: "input-checkbox", inputs: { label: "label", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputCheckboxComponent), multi: true }], ngImport: i0, template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span>{{ label }}</span>\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
585
+ }
586
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputCheckboxComponent, decorators: [{
587
+ type: Component,
588
+ args: [{ selector: 'input-checkbox', standalone: true, imports: [CommonModule], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputCheckboxComponent), multi: true }], template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span>{{ label }}</span>\n</label>\n" }]
589
+ }], propDecorators: { label: [{
590
+ type: Input
591
+ }], disabled: [{
592
+ type: Input
593
+ }], value: [{
594
+ type: Input
595
+ }], valueChange: [{
596
+ type: Output
597
+ }] } });
598
+
599
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
600
+ const validateFormControl = (control, required, pattern, email = false) => {
601
+ const value = control.value;
602
+ const empty = value == null || (typeof value === 'string' && value.trim() === '') || (Array.isArray(value) && value.length === 0);
603
+ if (required && empty)
604
+ return { required: true };
605
+ if (empty)
606
+ return null;
607
+ if (email && (typeof value !== 'string' || !EMAIL_PATTERN.test(value.trim())))
608
+ return { email: true };
609
+ if (pattern) {
610
+ try {
611
+ if (!new RegExp(pattern).test(String(value)))
612
+ return { pattern: true };
613
+ }
614
+ catch {
615
+ return null;
616
+ }
617
+ }
618
+ return null;
619
+ };
620
+
621
+ class InputColorComponent {
622
+ cdr = inject(ChangeDetectorRef);
623
+ label;
624
+ hint;
625
+ name;
626
+ id;
627
+ required = false;
628
+ fullWidth = false;
629
+ error;
630
+ disabled = false;
631
+ value = '#000000';
632
+ static nextId = 0;
633
+ generatedId = `input-color-${InputColorComponent.nextId++}`;
634
+ onChange = () => undefined;
635
+ onTouched = () => undefined;
636
+ get controlId() {
637
+ return this.id ?? this.generatedId;
638
+ }
639
+ get pickerValue() {
640
+ return this.isColorValue(this.value) ? this.value : '#000000';
641
+ }
642
+ writeValue(value) {
643
+ this.value = value?.trim() || '#000000';
644
+ this.cdr.markForCheck();
645
+ }
646
+ registerOnChange(fn) {
647
+ this.onChange = fn;
648
+ }
649
+ registerOnTouched(fn) {
650
+ this.onTouched = fn;
651
+ }
652
+ validate(control) { return validateFormControl(control, this.required); }
653
+ setDisabledState(isDisabled) {
654
+ this.disabled = isDisabled;
655
+ this.cdr.markForCheck();
656
+ }
657
+ onPickerChange(value) {
658
+ this.value = value;
659
+ this.onChange(value);
660
+ }
661
+ onHexInput(value) {
662
+ this.value = this.normalizeHexInput(value);
663
+ this.onChange(this.value);
664
+ }
665
+ markTouched() {
666
+ this.onTouched();
667
+ }
668
+ normalizeHexInput(value) {
669
+ const trimmed = value.trim();
670
+ if (!trimmed) {
671
+ return '';
672
+ }
673
+ return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
674
+ }
675
+ isColorValue(value) {
676
+ return /^#([0-9a-fA-F]{6})$/.test(value.trim());
677
+ }
678
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputColorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
679
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputColorComponent, isStandalone: true, selector: "input-color", inputs: { label: "label", hint: "hint", name: "name", id: "id", required: "required", fullWidth: "fullWidth", error: "error" }, providers: [
680
+ {
681
+ provide: NG_VALUE_ACCESSOR,
682
+ useExisting: forwardRef(() => InputColorComponent),
683
+ multi: true,
684
+ },
685
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputColorComponent), multi: true },
686
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200 disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
687
+ }
688
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputColorComponent, decorators: [{
689
+ type: Component,
690
+ args: [{ selector: 'input-color', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
691
+ {
692
+ provide: NG_VALUE_ACCESSOR,
693
+ useExisting: forwardRef(() => InputColorComponent),
694
+ multi: true,
695
+ },
696
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputColorComponent), multi: true },
697
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200 disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n" }]
698
+ }], propDecorators: { label: [{
699
+ type: Input
700
+ }], hint: [{
701
+ type: Input
702
+ }], name: [{
703
+ type: Input
704
+ }], id: [{
705
+ type: Input
706
+ }], required: [{
707
+ type: Input
708
+ }], fullWidth: [{
709
+ type: Input
710
+ }], error: [{
711
+ type: Input
712
+ }] } });
713
+
568
714
  class UiPopupService {
569
715
  activeClose = null;
570
716
  open(close) {
@@ -587,419 +733,285 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
587
733
  args: [{ providedIn: 'root' }]
588
734
  }] });
589
735
 
590
- class UiSelectComponent {
591
- closeIcon = X;
592
- chevronDownIcon = ChevronDown;
593
- checkIcon = Check;
736
+ class InputDateComponent {
594
737
  cdr = inject(ChangeDetectorRef);
595
- destroyRef = inject(DestroyRef);
596
- http = inject(HttpClient);
597
738
  popupService = inject(UiPopupService);
598
- closeActivePopup = () => this.closeDropdown();
739
+ closeActivePopup = () => this.closeCalendar();
599
740
  label;
741
+ hint;
600
742
  placeholder;
601
743
  name;
602
- data;
744
+ id;
745
+ type = 'date';
746
+ element = 'input';
747
+ calendarIcon = CalendarDays;
748
+ previousMonthIcon = ChevronLeft;
749
+ nextMonthIcon = ChevronRight;
750
+ rows = 4;
603
751
  required = false;
604
752
  fullWidth = false;
605
- disabled = false;
606
- multiple = false;
607
- includeBlankOption = false;
608
- blankOptionLabel = 'Оберіть…';
609
- emptyStateLabel = 'Немає варіантів для вибору';
610
- searchable = true;
611
- remoteSearch = false;
612
- searchPlaceholder = 'Пошук...';
613
- _options = [];
614
- set options(value) {
615
- this._options = value ?? [];
616
- this.updateSelectionFromValue();
617
- this.cdr.markForCheck();
618
- }
619
- get options() {
620
- return this._options;
621
- }
753
+ autocomplete = 'off';
754
+ pattern;
622
755
  error;
623
- hint;
624
- triggerClass = '';
625
- panelClass = '';
626
- valueChange = new EventEmitter();
627
- searchChange = new EventEmitter();
628
- popupMinWidth = 300;
629
- popupViewportMargin = 16;
630
- popupOffset = 8;
631
- dropdownTrigger;
632
- dropdownPositions = [
633
- {
634
- originX: 'start',
635
- originY: 'bottom',
636
- overlayX: 'start',
637
- overlayY: 'top',
638
- offsetY: this.popupOffset,
639
- },
640
- {
641
- originX: 'start',
642
- originY: 'top',
643
- overlayX: 'start',
644
- overlayY: 'bottom',
645
- offsetY: -this.popupOffset,
646
- },
647
- ];
648
- isOpen = false;
649
- popupWidth = 0;
756
+ actions = [];
757
+ disabled = false;
650
758
  value = '';
651
- multiValue = [];
652
- selectedOptions = [];
653
- searchTerm = '';
654
- isDictionaryLoading = false;
655
- searchChanges$ = new Subject();
656
- dictionaryRequestVersions = new Map();
657
- dictionaryOptions = [];
759
+ static nextId = 0;
760
+ generatedId = `input-date-${InputDateComponent.nextId++}`;
761
+ visibleMonth = this.startOfMonth(new Date());
762
+ calendarView = 'days';
763
+ isCalendarOpen = false;
764
+ actionTriggered = new EventEmitter();
765
+ get controlId() {
766
+ return this.id ?? this.generatedId;
767
+ }
658
768
  onChange = () => undefined;
659
769
  onTouched = () => undefined;
660
- constructor() {
661
- this.searchChanges$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((next) => {
662
- this.searchTerm = next;
663
- if (this.getDictionaryName()) {
664
- this.loadDictionaryOptions(next);
665
- return;
666
- }
667
- this.updateSelectionFromValue();
668
- this.cdr.markForCheck();
669
- });
670
- this.destroyRef.onDestroy(() => {
671
- this.searchChanges$.complete();
672
- this.dictionaryRequestVersions.clear();
673
- });
674
- }
675
- ngOnChanges(changes) {
676
- if ('data' in changes) {
677
- this.reloadDictionaryOptions();
678
- }
679
- }
680
770
  writeValue(value) {
681
- if (this.multiple) {
682
- this.multiValue = Array.isArray(value) ? value : [];
683
- }
684
- else {
685
- this.value = value ?? '';
771
+ this.value = value ?? '';
772
+ const selectedDate = this.parseDate(this.value);
773
+ if (selectedDate) {
774
+ this.visibleMonth = this.startOfMonth(selectedDate);
686
775
  }
687
- this.updateSelectionFromValue();
688
- this.ensureSelectedDictionaryOptionsLoaded();
689
776
  this.cdr.markForCheck();
690
777
  }
691
- registerOnChange(fn) {
692
- this.onChange = fn;
778
+ get monthLabel() {
779
+ return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long', year: 'numeric' });
693
780
  }
694
- registerOnTouched(fn) {
695
- this.onTouched = fn;
781
+ get monthName() {
782
+ return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' });
696
783
  }
697
- setDisabledState(isDisabled) {
698
- this.disabled = isDisabled;
699
- this.cdr.markForCheck();
784
+ get yearLabel() {
785
+ return this.visibleMonth.getFullYear();
700
786
  }
701
- toggleDropdown(event) {
702
- event.stopPropagation();
703
- if (this.disabled) {
704
- return;
705
- }
706
- if (this.isOpen) {
707
- this.closeDropdown();
708
- return;
709
- }
710
- const fieldWidth = this.dropdownTrigger.nativeElement.getBoundingClientRect().width;
711
- this.popupWidth = Math.max(fieldWidth, this.popupMinWidth);
712
- this.isOpen = true;
713
- this.popupService.open(this.closeActivePopup);
714
- this.searchTerm = '';
715
- this.searchChanges$.next('');
787
+ get monthOptions() {
788
+ return Array.from({ length: 12 }, (_, value) => ({
789
+ value,
790
+ label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }),
791
+ }));
792
+ }
793
+ get yearOptions() {
794
+ const year = this.visibleMonth.getFullYear();
795
+ return Array.from({ length: 12 }, (_, index) => year - 5 + index);
796
+ }
797
+ get calendarDays() {
798
+ const firstDay = this.startOfMonth(this.visibleMonth);
799
+ const mondayOffset = (firstDay.getDay() + 6) % 7;
800
+ const firstGridDay = new Date(firstDay);
801
+ firstGridDay.setDate(firstGridDay.getDate() - mondayOffset);
802
+ return Array.from({ length: 42 }, (_, index) => {
803
+ const date = new Date(firstGridDay);
804
+ date.setDate(firstGridDay.getDate() + index);
805
+ return {
806
+ date: this.toDateValue(date),
807
+ day: date.getDate(),
808
+ currentMonth: date.getMonth() === this.visibleMonth.getMonth(),
809
+ };
810
+ });
811
+ }
812
+ get displayValue() {
813
+ const date = this.selectedDate;
814
+ return date ? date.toLocaleDateString('uk-UA') : String(this.value ?? '');
815
+ }
816
+ get selectedDate() {
817
+ return this.parseDate(this.value);
818
+ }
819
+ openCalendar(event) {
820
+ event.stopPropagation();
821
+ if (this.disabled) {
822
+ return;
823
+ }
824
+ this.isCalendarOpen = true;
825
+ this.popupService.open(this.closeActivePopup);
826
+ this.calendarView = 'days';
716
827
  this.cdr.markForCheck();
717
828
  }
718
- selectOption(option, event) {
829
+ closeCalendar() {
830
+ this.isCalendarOpen = false;
831
+ this.popupService.close(this.closeActivePopup);
832
+ this.cdr.markForCheck();
833
+ }
834
+ showMonthPicker(event) {
719
835
  event.stopPropagation();
720
- const key = this.getOptionValue(option);
721
- if (this.multiple) {
722
- const normalized = this.multiValue.includes(key)
723
- ? this.multiValue.filter((item) => item !== key)
724
- : [...this.multiValue, key];
725
- this.multiValue = normalized;
726
- this.onChange(normalized);
727
- this.valueChange.emit(normalized);
836
+ this.calendarView = 'months';
837
+ this.cdr.markForCheck();
838
+ }
839
+ showYearPicker(event) {
840
+ event.stopPropagation();
841
+ this.calendarView = 'years';
842
+ this.cdr.markForCheck();
843
+ }
844
+ selectMonth(month, event) {
845
+ event.stopPropagation();
846
+ this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1);
847
+ this.calendarView = 'days';
848
+ this.cdr.markForCheck();
849
+ }
850
+ selectYear(year, event) {
851
+ event.stopPropagation();
852
+ this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1);
853
+ this.calendarView = 'months';
854
+ this.cdr.markForCheck();
855
+ }
856
+ changeMonth(offset, event) {
857
+ event.stopPropagation();
858
+ if (this.calendarView === 'years') {
859
+ this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset * 12, this.visibleMonth.getMonth(), 1);
860
+ }
861
+ else if (this.calendarView === 'months') {
862
+ this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset, this.visibleMonth.getMonth(), 1);
728
863
  }
729
864
  else {
730
- const nextValue = this.value === key ? null : key;
731
- this.value = nextValue;
732
- this.onChange(nextValue);
733
- this.valueChange.emit(nextValue);
734
- this.closeDropdown();
865
+ this.visibleMonth = new Date(this.visibleMonth.getFullYear(), this.visibleMonth.getMonth() + offset, 1);
735
866
  }
736
- this.updateSelectionFromValue();
737
- }
738
- markTouched() {
739
- this.onTouched();
867
+ this.cdr.markForCheck();
740
868
  }
741
- closeDropdown() {
742
- if (!this.isOpen) {
743
- return;
744
- }
745
- this.isOpen = false;
869
+ selectDate(dateValue, event) {
870
+ event.stopPropagation();
871
+ this.visibleMonth = this.startOfMonth(this.parseDate(dateValue));
872
+ this.value = dateValue;
873
+ this.onChange(dateValue);
874
+ this.closeCalendar();
746
875
  this.onTouched();
747
- this.searchTerm = '';
748
- this.searchChanges$.next('');
749
- this.popupService.close(this.closeActivePopup);
750
876
  this.cdr.markForCheck();
751
877
  }
878
+ isSelected(dateValue) {
879
+ return this.value === dateValue;
880
+ }
881
+ isSelectedMonth(month) {
882
+ return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear();
883
+ }
884
+ isSelectedYear(year) {
885
+ return this.selectedDate?.getFullYear() === year;
886
+ }
752
887
  onDocumentClick() {
753
- if (this.isOpen) {
754
- this.closeDropdown();
888
+ if (this.isCalendarOpen) {
889
+ this.closeCalendar();
755
890
  }
756
891
  }
757
892
  onEscape() {
758
- if (this.isOpen) {
759
- this.closeDropdown();
893
+ if (this.isCalendarOpen) {
894
+ this.closeCalendar();
760
895
  }
761
896
  }
762
- get displayLabel() {
763
- if (this.multiple) {
764
- if (!this.selectedOptions.length) {
765
- return this.placeholder ?? this.blankOptionLabel;
766
- }
767
- return `${this.selectedOptions.length}`;
768
- }
769
- return this.selectedOptions[0]?.label ?? this.placeholder ?? this.blankOptionLabel;
897
+ registerOnChange(fn) {
898
+ this.onChange = fn;
770
899
  }
771
- isSelected(option) {
772
- const value = this.getOptionValue(option);
773
- if (this.multiple) {
774
- return this.multiValue.includes(value);
900
+ registerOnTouched(fn) {
901
+ this.onTouched = fn;
902
+ }
903
+ validate(control) { return validateFormControl(control, this.required, this.pattern); }
904
+ setDisabledState(isDisabled) {
905
+ this.disabled = isDisabled;
906
+ this.cdr.markForCheck();
907
+ }
908
+ onValueChange(value) {
909
+ const maskedValue = this.applyDateMask(value);
910
+ const date = this.parseInputDate(maskedValue);
911
+ this.value = date ? this.toDateValue(date) : maskedValue;
912
+ this.onChange(this.value);
913
+ if (date) {
914
+ this.visibleMonth = this.startOfMonth(date);
775
915
  }
776
- return this.value === value;
916
+ this.cdr.markForCheck();
777
917
  }
778
- getOptionValue(option) {
779
- return option.value ?? '';
918
+ allowDateKey(event) {
919
+ if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
920
+ return;
921
+ if (!/^\d$/.test(event.key))
922
+ event.preventDefault();
780
923
  }
781
- trackOption(index, option) {
782
- return `${this.getOptionValue(option)}::${String(option.label || '').trim()}::${index}`;
924
+ applyDateMask(value) {
925
+ const digits = value.replace(/\D/g, '').slice(0, 8);
926
+ const day = digits.slice(0, 2);
927
+ const month = digits.slice(2, 4);
928
+ const year = digits.slice(4, 8);
929
+ return [day, month, year].filter(Boolean).join('.');
783
930
  }
784
- get visibleOptions() {
785
- const options = this.getRenderOptions();
786
- if (!this.searchable || this.remoteSearch || !this.searchTerm.trim()) {
787
- return options;
788
- }
789
- const search = this.searchTerm.trim().toLowerCase();
790
- return options.filter((option) => {
791
- if (this.includeBlankOption && !this.multiple && this.isBlankOption(option)) {
792
- return true;
793
- }
794
- return String(option.label ?? '').toLowerCase().includes(search);
795
- });
931
+ triggerAction(action) {
932
+ this.actionTriggered.emit(action);
796
933
  }
797
- onSearchChange(value) {
798
- const next = String(value ?? '');
799
- this.searchChanges$.next(next);
800
- this.searchChange.emit(next);
934
+ startOfMonth(date) {
935
+ return new Date(date.getFullYear(), date.getMonth(), 1);
801
936
  }
802
- removeOption(option, event) {
803
- event.stopPropagation();
804
- const key = this.getOptionValue(option);
805
- if (this.multiple) {
806
- this.multiValue = this.multiValue.filter((v) => v !== key);
807
- this.onChange(this.multiValue);
808
- this.valueChange.emit(this.multiValue);
809
- this.updateSelectionFromValue();
937
+ parseDate(value) {
938
+ if (!value) {
939
+ return null;
810
940
  }
811
- }
812
- updateSelectionFromValue() {
813
- const options = this.getRenderOptions();
814
- if (this.multiple) {
815
- this.selectedOptions = options.filter((option) => this.multiValue.includes(this.getOptionValue(option)));
816
- return;
941
+ const [year, month, day] = String(value).slice(0, 10).split('-').map(Number);
942
+ if (!year || !month || !day) {
943
+ return null;
817
944
  }
818
- const current = options.find((option) => this.getOptionValue(option) === (this.value ?? ''));
819
- this.selectedOptions = current ? [current] : [];
945
+ return new Date(year, month - 1, day);
820
946
  }
821
- getRenderOptions() {
822
- const merged = this.mergeOptions(this.options, this.dictionaryOptions);
823
- if (!this.includeBlankOption || this.multiple) {
824
- return merged;
947
+ parseInputDate(value) {
948
+ const normalized = value.trim();
949
+ const isoDate = this.parseDate(normalized);
950
+ if (isoDate) {
951
+ return isoDate;
825
952
  }
826
- return [{ label: this.blankOptionLabel, value: '' }, ...merged];
827
- }
828
- mergeOptions(...optionSets) {
829
- const merged = new Map();
830
- for (const options of optionSets) {
831
- for (const option of options || []) {
832
- const value = this.getOptionValue(option);
833
- const key = `${value}::${String(option.label || '').trim()}`;
834
- if (!merged.has(key)) {
835
- merged.set(key, option);
836
- }
837
- }
953
+ const match = normalized.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
954
+ if (!match) {
955
+ return null;
838
956
  }
839
- return [...merged.values()];
840
- }
841
- isBlankOption(option) {
842
- return String(option.value ?? '') === '';
957
+ const [, day, month, year] = match;
958
+ const date = new Date(Number(year), Number(month) - 1, Number(day));
959
+ return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day)
960
+ ? date
961
+ : null;
843
962
  }
844
- getDictionaryName() {
845
- return String(this.data || '').trim();
963
+ toDateValue(date) {
964
+ const year = date.getFullYear();
965
+ const month = String(date.getMonth() + 1).padStart(2, '0');
966
+ const day = String(date.getDate()).padStart(2, '0');
967
+ return `${year}-${month}-${day}`;
846
968
  }
847
- reloadDictionaryOptions() {
848
- const dictionaryName = this.getDictionaryName();
849
- this.dictionaryRequestVersions.clear();
850
- this.dictionaryOptions = [];
851
- this.isDictionaryLoading = false;
852
- if (!dictionaryName) {
853
- this.updateSelectionFromValue();
854
- this.cdr.markForCheck();
855
- return;
856
- }
857
- this.loadDictionaryOptions(this.searchTerm);
858
- }
859
- loadDictionaryOptions(query) {
860
- const dictionaryName = this.getDictionaryName();
861
- if (!dictionaryName) {
862
- return;
863
- }
864
- const requestVersion = (this.dictionaryRequestVersions.get(dictionaryName) || 0) + 1;
865
- this.dictionaryRequestVersions.set(dictionaryName, requestVersion);
866
- this.isDictionaryLoading = true;
867
- this.loadSuggestItems(dictionaryName, String(query || '').trim()).subscribe({
868
- next: (items) => {
869
- if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
870
- return;
871
- }
872
- this.dictionaryOptions = this.normalizeSuggestItems(items);
873
- this.isDictionaryLoading = false;
874
- this.ensureSelectedDictionaryOptionsLoaded();
875
- this.updateSelectionFromValue();
876
- this.cdr.markForCheck();
877
- },
878
- error: () => {
879
- if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
880
- return;
881
- }
882
- this.dictionaryOptions = [];
883
- this.isDictionaryLoading = false;
884
- this.updateSelectionFromValue();
885
- this.cdr.markForCheck();
886
- },
887
- });
888
- }
889
- ensureSelectedDictionaryOptionsLoaded() {
890
- const dictionaryName = this.getDictionaryName();
891
- if (!dictionaryName) {
892
- return;
893
- }
894
- const existingValues = new Set(this.getRenderOptions().map((option) => this.getOptionValue(option)));
895
- const selectedValues = this.multiple ? this.multiValue : [this.value];
896
- const missingValues = selectedValues
897
- .map((item) => String(item || '').trim())
898
- .filter((item) => !!item && !existingValues.has(item));
899
- if (!missingValues.length) {
900
- return;
901
- }
902
- forkJoin(missingValues.map((value) => this.loadSuggestItem(dictionaryName, value))).subscribe({
903
- next: (items) => {
904
- const nextOptions = [...this.dictionaryOptions];
905
- for (const item of items) {
906
- const normalized = this.normalizeSuggestItem(item);
907
- if (!nextOptions.some((option) => this.getOptionValue(option) === normalized.value)) {
908
- nextOptions.push(normalized);
909
- }
910
- }
911
- this.dictionaryOptions = nextOptions;
912
- this.updateSelectionFromValue();
913
- this.cdr.markForCheck();
914
- },
915
- });
916
- }
917
- normalizeSuggestItems(items) {
918
- return (items || [])
919
- .map((item) => this.normalizeSuggestItem(item))
920
- .filter((item) => !!item.label || item.value !== null);
921
- }
922
- normalizeSuggestItem(item) {
923
- return {
924
- label: String(item?.label ?? ''),
925
- value: item?.key != null ? String(item.key) : null,
926
- imageUrl: item?.imageId ? `/api/files/${item.imageId}/preview?scale=25` : undefined,
927
- };
928
- }
929
- loadSuggestItems(name, query) {
930
- const params = new URLSearchParams();
931
- if (query?.trim())
932
- params.set('q', query.trim());
933
- const suffix = params.toString() ? `?${params.toString()}` : '';
934
- return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
935
- }
936
- loadSuggestItem(name, key) {
937
- return this.http.get(`/api/suggest/${encodeURIComponent(name)}/${encodeURIComponent(key)}`);
938
- }
939
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
940
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiSelectComponent, isStandalone: true, selector: "forms-select", inputs: { label: "label", placeholder: "placeholder", name: "name", data: "data", required: "required", fullWidth: "fullWidth", disabled: "disabled", multiple: ["multiple", "multiple", booleanAttribute], includeBlankOption: "includeBlankOption", blankOptionLabel: "blankOptionLabel", emptyStateLabel: "emptyStateLabel", searchable: "searchable", remoteSearch: "remoteSearch", searchPlaceholder: "searchPlaceholder", options: "options", error: "error", hint: "hint", triggerClass: "triggerClass", panelClass: "panelClass" }, outputs: { valueChange: "valueChange", searchChange: "searchChange" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
969
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
970
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputDateComponent, isStandalone: true, selector: "input-date", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
941
971
  {
942
972
  provide: NG_VALUE_ACCESSOR,
943
- useExisting: forwardRef(() => UiSelectComponent),
973
+ useExisting: forwardRef(() => InputDateComponent),
944
974
  multi: true,
945
975
  },
946
- ], viewQueries: [{ propertyName: "dropdownTrigger", first: true, predicate: ["dropdownTrigger"], descendants: true, read: ElementRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex min-h-10 w-full flex-nowrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\" [class.-ml-2]=\"multiple\">\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md bg-slate-100 px-2 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n (keydown.enter)=\"removeOption(option, $event)\"\n (keydown.space)=\"$event.preventDefault(); removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"h-4 w-4 shrink-0\"></lucide-icon>\n </span>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"min-w-0 truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n </span>\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"shrink-0 text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"false\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayViewportMargin]=\"popupViewportMargin\"\n [cdkConnectedOverlayFlexibleDimensions]=\"true\"\n [cdkConnectedOverlayPush]=\"true\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 w-full max-w-[calc(100vw-2rem)] rounded-xl border border-slate-200 bg-white shadow-lg\"\n [style.width.px]=\"popupWidth\"\n [style.max-width]=\"'calc(100vw - 2rem)'\"\n [class]=\"panelClass\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-900 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n <div class=\"max-h-60 overflow-auto\">\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-start justify-between gap-3 whitespace-normal break-words px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"whitespace-normal break-words\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n </div>\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n", dependencies: [{ kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
976
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDateComponent), multi: true },
977
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
947
978
  }
948
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, decorators: [{
979
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDateComponent, decorators: [{
949
980
  type: Component,
950
- args: [{ selector: 'forms-select', standalone: true, imports: [LucideAngularModule, CommonModule, FormsModule, CdkConnectedOverlay, CdkOverlayOrigin], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
981
+ args: [{ selector: 'input-date', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
951
982
  {
952
983
  provide: NG_VALUE_ACCESSOR,
953
- useExisting: forwardRef(() => UiSelectComponent),
984
+ useExisting: forwardRef(() => InputDateComponent),
954
985
  multi: true,
955
986
  },
956
- ], template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex min-h-10 w-full flex-nowrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\" [class.-ml-2]=\"multiple\">\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md bg-slate-100 px-2 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n (keydown.enter)=\"removeOption(option, $event)\"\n (keydown.space)=\"$event.preventDefault(); removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"h-4 w-4 shrink-0\"></lucide-icon>\n </span>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"min-w-0 truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n </span>\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"shrink-0 text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"false\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayViewportMargin]=\"popupViewportMargin\"\n [cdkConnectedOverlayFlexibleDimensions]=\"true\"\n [cdkConnectedOverlayPush]=\"true\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 w-full max-w-[calc(100vw-2rem)] rounded-xl border border-slate-200 bg-white shadow-lg\"\n [style.width.px]=\"popupWidth\"\n [style.max-width]=\"'calc(100vw - 2rem)'\"\n [class]=\"panelClass\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-900 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n <div class=\"max-h-60 overflow-auto\">\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-start justify-between gap-3 whitespace-normal break-words px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"whitespace-normal break-words\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n </div>\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n" }]
957
- }], ctorParameters: () => [], propDecorators: { label: [{
987
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDateComponent), multi: true },
988
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n" }]
989
+ }], propDecorators: { label: [{
990
+ type: Input
991
+ }], hint: [{
958
992
  type: Input
959
993
  }], placeholder: [{
960
994
  type: Input
961
995
  }], name: [{
962
996
  type: Input
963
- }], data: [{
997
+ }], id: [{
998
+ type: Input
999
+ }], rows: [{
964
1000
  type: Input
965
1001
  }], required: [{
966
1002
  type: Input
967
1003
  }], fullWidth: [{
968
1004
  type: Input
969
- }], disabled: [{
970
- type: Input
971
- }], multiple: [{
972
- type: Input,
973
- args: [{ transform: booleanAttribute }]
974
- }], includeBlankOption: [{
975
- type: Input
976
- }], blankOptionLabel: [{
977
- type: Input
978
- }], emptyStateLabel: [{
979
- type: Input
980
- }], searchable: [{
981
- type: Input
982
- }], remoteSearch: [{
983
- type: Input
984
- }], searchPlaceholder: [{
1005
+ }], autocomplete: [{
985
1006
  type: Input
986
- }], options: [{
1007
+ }], pattern: [{
987
1008
  type: Input
988
1009
  }], error: [{
989
1010
  type: Input
990
- }], hint: [{
991
- type: Input
992
- }], triggerClass: [{
993
- type: Input
994
- }], panelClass: [{
1011
+ }], actions: [{
995
1012
  type: Input
996
- }], valueChange: [{
997
- type: Output
998
- }], searchChange: [{
1013
+ }], actionTriggered: [{
999
1014
  type: Output
1000
- }], dropdownTrigger: [{
1001
- type: ViewChild,
1002
- args: ['dropdownTrigger', { static: true, read: ElementRef }]
1003
1015
  }], onDocumentClick: [{
1004
1016
  type: HostListener,
1005
1017
  args: ['document:click']
@@ -1008,7 +1020,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1008
1020
  args: ['document:keydown.escape']
1009
1021
  }] } });
1010
1022
 
1011
- class UiDateInputComponent {
1023
+ class InputDatetimeComponent {
1012
1024
  cdr = inject(ChangeDetectorRef);
1013
1025
  popupService = inject(UiPopupService);
1014
1026
  closeActivePopup = () => this.closeCalendar();
@@ -1017,248 +1029,210 @@ class UiDateInputComponent {
1017
1029
  placeholder;
1018
1030
  name;
1019
1031
  id;
1020
- type = 'date';
1021
- element = 'input';
1022
- calendarIcon = CalendarDays;
1023
- previousMonthIcon = ChevronLeft;
1024
- nextMonthIcon = ChevronRight;
1025
1032
  rows = 4;
1026
1033
  required = false;
1027
1034
  fullWidth = false;
1028
1035
  autocomplete = 'off';
1029
- readonly = false;
1030
1036
  pattern;
1031
1037
  error;
1032
1038
  actions = [];
1039
+ type = 'datetime-local';
1040
+ calendarIcon = CalendarDays;
1041
+ previousMonthIcon = ChevronLeft;
1042
+ nextMonthIcon = ChevronRight;
1033
1043
  disabled = false;
1034
1044
  value = '';
1035
- static nextId = 0;
1036
- generatedId = `ui-input-${UiDateInputComponent.nextId++}`;
1037
- visibleMonth = this.startOfMonth(new Date());
1038
- calendarView = 'days';
1039
1045
  isCalendarOpen = false;
1046
+ calendarView = 'days';
1047
+ visibleMonth = this.startOfMonth(new Date());
1048
+ selectedHour = 12;
1049
+ selectedMinute = 0;
1050
+ static nextId = 0;
1051
+ generatedId = `input-datetime-${InputDatetimeComponent.nextId++}`;
1040
1052
  actionTriggered = new EventEmitter();
1041
- get controlId() {
1042
- return this.id ?? this.generatedId;
1043
- }
1044
1053
  onChange = () => undefined;
1045
1054
  onTouched = () => undefined;
1046
- writeValue(value) {
1047
- this.value = value ?? '';
1048
- const selectedDate = this.parseDate(this.value);
1049
- if (selectedDate) {
1050
- this.visibleMonth = this.startOfMonth(selectedDate);
1051
- }
1052
- this.cdr.markForCheck();
1053
- }
1054
- get monthLabel() {
1055
- return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long', year: 'numeric' });
1056
- }
1057
- get monthName() {
1058
- return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' });
1059
- }
1060
- get yearLabel() {
1061
- return this.visibleMonth.getFullYear();
1062
- }
1063
- get monthOptions() {
1064
- return Array.from({ length: 12 }, (_, value) => ({
1065
- value,
1066
- label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }),
1067
- }));
1068
- }
1069
- get yearOptions() {
1070
- const year = this.visibleMonth.getFullYear();
1071
- return Array.from({ length: 12 }, (_, index) => year - 5 + index);
1072
- }
1055
+ get controlId() { return this.id ?? this.generatedId; }
1056
+ get selectedDate() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? new Date(+match[1], +match[2] - 1, +match[3]) : null; }
1057
+ get displayValue() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? `${match[3]}.${match[2]}.${match[1]} ${match[4]}:${match[5]}` : String(this.value ?? ''); }
1058
+ get monthName() { return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' }); }
1059
+ get yearLabel() { return this.visibleMonth.getFullYear(); }
1060
+ get monthOptions() { return Array.from({ length: 12 }, (_, value) => ({ value, label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }) })); }
1061
+ get yearOptions() { const year = this.visibleMonth.getFullYear(); return Array.from({ length: 12 }, (_, index) => year - 5 + index); }
1073
1062
  get calendarDays() {
1074
- const firstDay = this.startOfMonth(this.visibleMonth);
1075
- const mondayOffset = (firstDay.getDay() + 6) % 7;
1076
- const firstGridDay = new Date(firstDay);
1077
- firstGridDay.setDate(firstGridDay.getDate() - mondayOffset);
1078
- return Array.from({ length: 42 }, (_, index) => {
1079
- const date = new Date(firstGridDay);
1080
- date.setDate(firstGridDay.getDate() + index);
1081
- return {
1082
- date: this.toDateValue(date),
1083
- day: date.getDate(),
1084
- currentMonth: date.getMonth() === this.visibleMonth.getMonth(),
1085
- };
1086
- });
1087
- }
1088
- get displayValue() {
1089
- const date = this.selectedDate;
1090
- return date ? date.toLocaleDateString('uk-UA') : String(this.value ?? '');
1091
- }
1092
- get selectedDate() {
1093
- return this.parseDate(this.value);
1063
+ const first = this.startOfMonth(this.visibleMonth);
1064
+ const offset = (first.getDay() + 6) % 7;
1065
+ const start = new Date(first);
1066
+ start.setDate(start.getDate() - offset);
1067
+ return Array.from({ length: 42 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return { date: this.toDateValue(date), day: date.getDate(), currentMonth: date.getMonth() === this.visibleMonth.getMonth() }; });
1094
1068
  }
1095
- openCalendar(event) {
1096
- event.stopPropagation();
1097
- if (this.disabled) {
1098
- return;
1069
+ writeValue(value) { this.value = value ?? ''; const date = this.selectedDate; if (date) {
1070
+ this.visibleMonth = this.startOfMonth(date);
1071
+ this.selectedHour = +(String(value).slice(11, 13));
1072
+ this.selectedMinute = +(String(value).slice(14, 16));
1073
+ } this.cdr.markForCheck(); }
1074
+ registerOnChange(fn) { this.onChange = fn; }
1075
+ registerOnTouched(fn) { this.onTouched = fn; }
1076
+ validate(control) { return validateFormControl(control, this.required, this.pattern); }
1077
+ setDisabledState(disabled) { this.disabled = disabled; this.cdr.markForCheck(); }
1078
+ openCalendar(event) { event.stopPropagation(); if (this.disabled)
1079
+ return; this.isCalendarOpen = true; this.calendarView = 'days'; this.popupService.open(this.closeActivePopup); this.cdr.markForCheck(); }
1080
+ closeCalendar() { this.isCalendarOpen = false; this.popupService.close(this.closeActivePopup); this.cdr.markForCheck(); }
1081
+ changeMonth(offset, event) { event.stopPropagation(); const year = this.visibleMonth.getFullYear(); const month = this.visibleMonth.getMonth(); this.visibleMonth = this.calendarView === 'years' ? new Date(year + offset * 12, month, 1) : new Date(year + (this.calendarView === 'months' ? offset : 0), month + (this.calendarView === 'days' ? offset : 0), 1); this.cdr.markForCheck(); }
1082
+ showMonthPicker(event) { event.stopPropagation(); this.calendarView = 'months'; this.cdr.markForCheck(); }
1083
+ showYearPicker(event) { event.stopPropagation(); this.calendarView = 'years'; this.cdr.markForCheck(); }
1084
+ selectMonth(month, event) { event.stopPropagation(); this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1); this.calendarView = 'days'; this.cdr.markForCheck(); }
1085
+ selectYear(year, event) { event.stopPropagation(); this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1); this.calendarView = 'months'; this.cdr.markForCheck(); }
1086
+ selectDate(dateValue, event) { event.stopPropagation(); const date = this.parseDate(dateValue); this.visibleMonth = this.startOfMonth(date); this.setDateTime(dateValue); }
1087
+ setDateTime(dateValue, close = true) { const hour = String(this.selectedHour).padStart(2, '0'); const minute = String(this.selectedMinute).padStart(2, '0'); this.value = `${dateValue}T${hour}:${minute}`; this.onChange(this.value); if (close)
1088
+ this.closeCalendar(); if (close)
1089
+ this.onTouched(); this.cdr.markForCheck(); }
1090
+ onTimeChange() { const date = this.selectedDate; if (date) {
1091
+ this.setDateTime(this.toDateValue(date), false);
1092
+ } }
1093
+ onValueChange(input) { const masked = this.applyMask(input); const match = masked.match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/); if (match) {
1094
+ const date = new Date(+match[3], +match[2] - 1, +match[1]);
1095
+ if (date.getFullYear() === +match[3] && date.getMonth() === +match[2] - 1 && date.getDate() === +match[1] && +match[4] < 24 && +match[5] < 60) {
1096
+ this.value = `${match[3]}-${match[2]}-${match[1]}T${match[4]}:${match[5]}`;
1097
+ this.visibleMonth = this.startOfMonth(date);
1098
+ this.selectedHour = +match[4];
1099
+ this.selectedMinute = +match[5];
1099
1100
  }
1100
- this.isCalendarOpen = true;
1101
- this.popupService.open(this.closeActivePopup);
1102
- this.calendarView = 'days';
1103
- this.cdr.markForCheck();
1104
- }
1105
- closeCalendar() {
1106
- this.isCalendarOpen = false;
1107
- this.popupService.close(this.closeActivePopup);
1108
- this.cdr.markForCheck();
1101
+ else
1102
+ this.value = masked;
1109
1103
  }
1110
- showMonthPicker(event) {
1111
- event.stopPropagation();
1112
- this.calendarView = 'months';
1113
- this.cdr.markForCheck();
1114
- }
1115
- showYearPicker(event) {
1116
- event.stopPropagation();
1117
- this.calendarView = 'years';
1118
- this.cdr.markForCheck();
1119
- }
1120
- selectMonth(month, event) {
1121
- event.stopPropagation();
1122
- this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1);
1123
- this.calendarView = 'days';
1124
- this.cdr.markForCheck();
1125
- }
1126
- selectYear(year, event) {
1127
- event.stopPropagation();
1128
- this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1);
1129
- this.calendarView = 'months';
1130
- this.cdr.markForCheck();
1131
- }
1132
- changeMonth(offset, event) {
1133
- event.stopPropagation();
1134
- if (this.calendarView === 'years') {
1135
- this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset * 12, this.visibleMonth.getMonth(), 1);
1136
- }
1137
- else if (this.calendarView === 'months') {
1138
- this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset, this.visibleMonth.getMonth(), 1);
1139
- }
1140
- else {
1141
- this.visibleMonth = new Date(this.visibleMonth.getFullYear(), this.visibleMonth.getMonth() + offset, 1);
1142
- }
1143
- this.cdr.markForCheck();
1104
+ else
1105
+ this.value = masked; this.onChange(this.value); this.cdr.markForCheck(); }
1106
+ allowDateKey(event) { if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
1107
+ return; if (!/^\d$/.test(event.key))
1108
+ event.preventDefault(); }
1109
+ applyMask(value) { const digits = value.replace(/\D/g, '').slice(0, 12); const date = [digits.slice(0, 2), digits.slice(2, 4), digits.slice(4, 8)].filter(Boolean).join('.'); const time = [digits.slice(8, 10), digits.slice(10, 12)].filter(Boolean).join(':'); return [date, time].filter(Boolean).join(' '); }
1110
+ isSelected(date) { return this.selectedDate ? this.toDateValue(this.selectedDate) === date : false; }
1111
+ isSelectedMonth(month) { return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear(); }
1112
+ isSelectedYear(year) { return this.selectedDate?.getFullYear() === year; }
1113
+ onDocumentClick() { if (this.isCalendarOpen)
1114
+ this.closeCalendar(); }
1115
+ onEscape() { if (this.isCalendarOpen)
1116
+ this.closeCalendar(); }
1117
+ triggerAction(action) { this.actionTriggered.emit(action); }
1118
+ startOfMonth(date) { return new Date(date.getFullYear(), date.getMonth(), 1); }
1119
+ parseDate(value) { const [year, month, day] = value.split('-').map(Number); return year && month && day ? new Date(year, month - 1, day) : null; }
1120
+ toDateValue(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; }
1121
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDatetimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1122
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputDatetimeComponent, isStandalone: true, selector: "input-datetime", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
1123
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
1124
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
1125
+ ], ngImport: i0, template: "<div class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\" [class.w-full]=\"fullWidth\">\n @if (label) { <span class=\"text-[0.7rem] font-semibold text-slate-600\">{{ label }} @if (required) { <span class=\"text-rose-500\"> *</span> }</span> }\n <div class=\"flex items-end gap-2\"><div class=\"min-w-0 flex-1\"><div class=\"relative\">\n <input type=\"text\" [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420 \u0413\u0413:\u0425\u0425'\" maxlength=\"16\" inputmode=\"numeric\" [attr.name]=\"name\" [attr.autocomplete]=\"autocomplete\" [disabled]=\"disabled\" class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" [attr.id]=\"controlId\" [value]=\"displayValue\" (click)=\"openCalendar($event)\" (keydown)=\"allowDateKey($event)\" (input)=\"onValueChange($any($event.target).value)\" (blur)=\"onTouched()\" />\n <lucide-icon [img]=\"calendarIcon\" class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"></lucide-icon>\n @if (isCalendarOpen) { <div class=\"absolute left-0 top-full z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\" (click)=\"$event.stopPropagation()\">\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(-1, $event)\"><lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n <div class=\"flex items-center gap-1\"><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 hover:bg-slate-100\" (click)=\"showMonthPicker($event)\">{{ monthName }}</button><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 hover:bg-slate-100\" (click)=\"showYearPicker($event)\">{{ yearLabel }}</button></div>\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(1, $event)\"><lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n </div>\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">@for (weekday of ['\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431','\u041D\u0434']; track weekday) { <span class=\"py-1\">{{ weekday }}</span> }</div>\n <div class=\"grid grid-cols-7 gap-1\">@for (day of calendarDays; track day.date) { <button type=\"button\" class=\"aspect-square rounded-lg text-sm hover:bg-slate-100 hover:text-slate-900\" [class.text-slate-300]=\"!day.currentMonth\" [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\" [class.bg-slate-900]=\"isSelected(day.date)\" [class.text-white]=\"isSelected(day.date)\" (click)=\"selectDate(day.date, $event)\">{{ day.day }}</button> }</div>\n <div class=\"mt-4 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3\"><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0413\u043E\u0434\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"23\" [value]=\"selectedHour\" (input)=\"selectedHour = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0425\u0432\u0438\u043B\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"59\" [value]=\"selectedMinute\" (input)=\"selectedMinute = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label></div>\n } @else if (calendarView === 'months') { <div class=\"grid grid-cols-3 gap-2\">@for (month of monthOptions; track month.value) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedMonth(month.value)\" [class.text-white]=\"isSelectedMonth(month.value)\" (click)=\"selectMonth(month.value, $event)\">{{ month.label }}</button> }</div>\n } @else { <div class=\"grid grid-cols-3 gap-2\">@for (year of yearOptions; track year) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedYear(year)\" [class.text-white]=\"isSelectedYear(year)\" (click)=\"selectYear(year, $event)\">{{ year }}</button> }</div> }\n </div> }\n </div></div>\n @if (actions.length) { <div class=\"flex flex-none items-center gap-2 pb-0.5\">@for (action of actions; track action.id) { <ui-button type=\"button\" [variant]=\"action.variant ?? 'secondary'\" [icon]=\"action.icon || ''\" size=\"sm\" [disabled]=\"disabled\" (click)=\"triggerAction(action)\">{{ action.label }}</ui-button> }</div> }\n </div>\n @if (hint) { <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p> } @if (error) { <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p> }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1126
+ }
1127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDatetimeComponent, decorators: [{
1128
+ type: Component,
1129
+ args: [{ selector: 'input-datetime', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1130
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
1131
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
1132
+ ], template: "<div class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\" [class.w-full]=\"fullWidth\">\n @if (label) { <span class=\"text-[0.7rem] font-semibold text-slate-600\">{{ label }} @if (required) { <span class=\"text-rose-500\"> *</span> }</span> }\n <div class=\"flex items-end gap-2\"><div class=\"min-w-0 flex-1\"><div class=\"relative\">\n <input type=\"text\" [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420 \u0413\u0413:\u0425\u0425'\" maxlength=\"16\" inputmode=\"numeric\" [attr.name]=\"name\" [attr.autocomplete]=\"autocomplete\" [disabled]=\"disabled\" class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" [attr.id]=\"controlId\" [value]=\"displayValue\" (click)=\"openCalendar($event)\" (keydown)=\"allowDateKey($event)\" (input)=\"onValueChange($any($event.target).value)\" (blur)=\"onTouched()\" />\n <lucide-icon [img]=\"calendarIcon\" class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"></lucide-icon>\n @if (isCalendarOpen) { <div class=\"absolute left-0 top-full z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\" (click)=\"$event.stopPropagation()\">\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(-1, $event)\"><lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n <div class=\"flex items-center gap-1\"><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 hover:bg-slate-100\" (click)=\"showMonthPicker($event)\">{{ monthName }}</button><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 hover:bg-slate-100\" (click)=\"showYearPicker($event)\">{{ yearLabel }}</button></div>\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(1, $event)\"><lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n </div>\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">@for (weekday of ['\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431','\u041D\u0434']; track weekday) { <span class=\"py-1\">{{ weekday }}</span> }</div>\n <div class=\"grid grid-cols-7 gap-1\">@for (day of calendarDays; track day.date) { <button type=\"button\" class=\"aspect-square rounded-lg text-sm hover:bg-slate-100 hover:text-slate-900\" [class.text-slate-300]=\"!day.currentMonth\" [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\" [class.bg-slate-900]=\"isSelected(day.date)\" [class.text-white]=\"isSelected(day.date)\" (click)=\"selectDate(day.date, $event)\">{{ day.day }}</button> }</div>\n <div class=\"mt-4 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3\"><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0413\u043E\u0434\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"23\" [value]=\"selectedHour\" (input)=\"selectedHour = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0425\u0432\u0438\u043B\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"59\" [value]=\"selectedMinute\" (input)=\"selectedMinute = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label></div>\n } @else if (calendarView === 'months') { <div class=\"grid grid-cols-3 gap-2\">@for (month of monthOptions; track month.value) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedMonth(month.value)\" [class.text-white]=\"isSelectedMonth(month.value)\" (click)=\"selectMonth(month.value, $event)\">{{ month.label }}</button> }</div>\n } @else { <div class=\"grid grid-cols-3 gap-2\">@for (year of yearOptions; track year) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedYear(year)\" [class.text-white]=\"isSelectedYear(year)\" (click)=\"selectYear(year, $event)\">{{ year }}</button> }</div> }\n </div> }\n </div></div>\n @if (actions.length) { <div class=\"flex flex-none items-center gap-2 pb-0.5\">@for (action of actions; track action.id) { <ui-button type=\"button\" [variant]=\"action.variant ?? 'secondary'\" [icon]=\"action.icon || ''\" size=\"sm\" [disabled]=\"disabled\" (click)=\"triggerAction(action)\">{{ action.label }}</ui-button> }</div> }\n </div>\n @if (hint) { <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p> } @if (error) { <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p> }\n</div>\n" }]
1133
+ }], propDecorators: { label: [{
1134
+ type: Input
1135
+ }], hint: [{
1136
+ type: Input
1137
+ }], placeholder: [{
1138
+ type: Input
1139
+ }], name: [{
1140
+ type: Input
1141
+ }], id: [{
1142
+ type: Input
1143
+ }], rows: [{
1144
+ type: Input
1145
+ }], required: [{
1146
+ type: Input
1147
+ }], fullWidth: [{
1148
+ type: Input
1149
+ }], autocomplete: [{
1150
+ type: Input
1151
+ }], pattern: [{
1152
+ type: Input
1153
+ }], error: [{
1154
+ type: Input
1155
+ }], actions: [{
1156
+ type: Input
1157
+ }], actionTriggered: [{
1158
+ type: Output
1159
+ }], onDocumentClick: [{
1160
+ type: HostListener,
1161
+ args: ['document:click']
1162
+ }], onEscape: [{
1163
+ type: HostListener,
1164
+ args: ['document:keydown.escape']
1165
+ }] } });
1166
+
1167
+ class InputEmailComponent {
1168
+ cdr = inject(ChangeDetectorRef);
1169
+ label;
1170
+ hint;
1171
+ placeholder;
1172
+ name;
1173
+ id;
1174
+ type = 'email';
1175
+ element = 'input';
1176
+ rows = 4;
1177
+ required = false;
1178
+ fullWidth = false;
1179
+ autocomplete;
1180
+ pattern;
1181
+ error;
1182
+ actions = [];
1183
+ disabled = false;
1184
+ value = '';
1185
+ static nextId = 0;
1186
+ generatedId = `input-email-${InputEmailComponent.nextId++}`;
1187
+ actionTriggered = new EventEmitter();
1188
+ get controlId() {
1189
+ return this.id ?? this.generatedId;
1144
1190
  }
1145
- selectDate(dateValue, event) {
1146
- event.stopPropagation();
1147
- this.visibleMonth = this.startOfMonth(this.parseDate(dateValue));
1148
- this.value = dateValue;
1149
- this.onChange(dateValue);
1150
- this.closeCalendar();
1151
- this.onTouched();
1191
+ onChange = () => undefined;
1192
+ onTouched = () => undefined;
1193
+ writeValue(value) {
1194
+ this.value = value ?? '';
1152
1195
  this.cdr.markForCheck();
1153
1196
  }
1154
- isSelected(dateValue) {
1155
- return this.value === dateValue;
1156
- }
1157
- isSelectedMonth(month) {
1158
- return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear();
1159
- }
1160
- isSelectedYear(year) {
1161
- return this.selectedDate?.getFullYear() === year;
1162
- }
1163
- onDocumentClick() {
1164
- if (this.isCalendarOpen) {
1165
- this.closeCalendar();
1166
- }
1167
- }
1168
- onEscape() {
1169
- if (this.isCalendarOpen) {
1170
- this.closeCalendar();
1171
- }
1172
- }
1173
1197
  registerOnChange(fn) {
1174
1198
  this.onChange = fn;
1175
1199
  }
1176
1200
  registerOnTouched(fn) {
1177
1201
  this.onTouched = fn;
1178
1202
  }
1203
+ validate(control) { return validateFormControl(control, this.required, this.pattern, true); }
1179
1204
  setDisabledState(isDisabled) {
1180
1205
  this.disabled = isDisabled;
1181
1206
  this.cdr.markForCheck();
1182
1207
  }
1183
1208
  onValueChange(value) {
1184
- const maskedValue = this.applyDateMask(value);
1185
- const date = this.parseInputDate(maskedValue);
1186
- this.value = date ? this.toDateValue(date) : maskedValue;
1187
- this.onChange(this.value);
1188
- if (date) {
1189
- this.visibleMonth = this.startOfMonth(date);
1190
- }
1191
- this.cdr.markForCheck();
1192
- }
1193
- allowDateKey(event) {
1194
- if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
1195
- return;
1196
- if (!/^\d$/.test(event.key))
1197
- event.preventDefault();
1198
- }
1199
- applyDateMask(value) {
1200
- const digits = value.replace(/\D/g, '').slice(0, 8);
1201
- const day = digits.slice(0, 2);
1202
- const month = digits.slice(2, 4);
1203
- const year = digits.slice(4, 8);
1204
- return [day, month, year].filter(Boolean).join('.');
1209
+ const parsed = value;
1210
+ this.value = parsed;
1211
+ this.onChange(parsed);
1205
1212
  }
1206
1213
  triggerAction(action) {
1207
1214
  this.actionTriggered.emit(action);
1208
1215
  }
1209
- startOfMonth(date) {
1210
- return new Date(date.getFullYear(), date.getMonth(), 1);
1211
- }
1212
- parseDate(value) {
1213
- if (!value) {
1214
- return null;
1215
- }
1216
- const [year, month, day] = String(value).slice(0, 10).split('-').map(Number);
1217
- if (!year || !month || !day) {
1218
- return null;
1219
- }
1220
- return new Date(year, month - 1, day);
1221
- }
1222
- parseInputDate(value) {
1223
- const normalized = value.trim();
1224
- const isoDate = this.parseDate(normalized);
1225
- if (isoDate) {
1226
- return isoDate;
1227
- }
1228
- const match = normalized.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
1229
- if (!match) {
1230
- return null;
1231
- }
1232
- const [, day, month, year] = match;
1233
- const date = new Date(Number(year), Number(month) - 1, Number(day));
1234
- return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day)
1235
- ? date
1236
- : null;
1237
- }
1238
- toDateValue(date) {
1239
- const year = date.getFullYear();
1240
- const month = String(date.getMonth() + 1).padStart(2, '0');
1241
- const day = String(date.getDate()).padStart(2, '0');
1242
- return `${year}-${month}-${day}`;
1243
- }
1244
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1245
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDateInputComponent, isStandalone: true, selector: "forms-date-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
1216
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputEmailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1217
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputEmailComponent, isStandalone: true, selector: "input-email", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1246
1218
  {
1247
1219
  provide: NG_VALUE_ACCESSOR,
1248
- useExisting: forwardRef(() => UiDateInputComponent),
1220
+ useExisting: forwardRef(() => InputEmailComponent),
1249
1221
  multi: true,
1250
1222
  },
1251
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1223
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputEmailComponent), multi: true },
1224
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1252
1225
  }
1253
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, decorators: [{
1226
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputEmailComponent, decorators: [{
1254
1227
  type: Component,
1255
- args: [{ selector: 'forms-date-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1228
+ args: [{ selector: 'input-email', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1256
1229
  {
1257
1230
  provide: NG_VALUE_ACCESSOR,
1258
- useExisting: forwardRef(() => UiDateInputComponent),
1231
+ useExisting: forwardRef(() => InputEmailComponent),
1259
1232
  multi: true,
1260
1233
  },
1261
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n" }]
1234
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputEmailComponent), multi: true },
1235
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
1262
1236
  }], propDecorators: { label: [{
1263
1237
  type: Input
1264
1238
  }], hint: [{
@@ -1277,8 +1251,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1277
1251
  type: Input
1278
1252
  }], autocomplete: [{
1279
1253
  type: Input
1280
- }], readonly: [{
1281
- type: Input
1282
1254
  }], pattern: [{
1283
1255
  type: Input
1284
1256
  }], error: [{
@@ -1287,281 +1259,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1287
1259
  type: Input
1288
1260
  }], actionTriggered: [{
1289
1261
  type: Output
1290
- }], onDocumentClick: [{
1291
- type: HostListener,
1292
- args: ['document:click']
1293
- }], onEscape: [{
1294
- type: HostListener,
1295
- args: ['document:keydown.escape']
1296
1262
  }] } });
1297
1263
 
1298
- class UiDatetimeInputComponent {
1264
+ class InputNumberComponent {
1299
1265
  cdr = inject(ChangeDetectorRef);
1300
- popupService = inject(UiPopupService);
1301
- closeActivePopup = () => this.closeCalendar();
1302
1266
  label;
1303
1267
  hint;
1304
1268
  placeholder;
1305
1269
  name;
1306
1270
  id;
1271
+ type = 'number';
1272
+ element = 'input';
1307
1273
  rows = 4;
1308
1274
  required = false;
1309
1275
  fullWidth = false;
1310
- autocomplete = 'off';
1311
- readonly = false;
1276
+ autocomplete;
1277
+ disabled = false;
1312
1278
  pattern;
1313
1279
  error;
1314
1280
  actions = [];
1315
- type = 'datetime-local';
1316
- calendarIcon = CalendarDays;
1317
- previousMonthIcon = ChevronLeft;
1318
- nextMonthIcon = ChevronRight;
1319
- disabled = false;
1320
1281
  value = '';
1321
- isCalendarOpen = false;
1322
- calendarView = 'days';
1323
- visibleMonth = this.startOfMonth(new Date());
1324
- selectedHour = 12;
1325
- selectedMinute = 0;
1326
1282
  static nextId = 0;
1327
- generatedId = `ui-input-${UiDatetimeInputComponent.nextId++}`;
1283
+ generatedId = `input-number-${InputNumberComponent.nextId++}`;
1328
1284
  actionTriggered = new EventEmitter();
1285
+ get controlId() {
1286
+ return this.id ?? this.generatedId;
1287
+ }
1329
1288
  onChange = () => undefined;
1330
1289
  onTouched = () => undefined;
1331
- get controlId() { return this.id ?? this.generatedId; }
1332
- get selectedDate() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? new Date(+match[1], +match[2] - 1, +match[3]) : null; }
1333
- get displayValue() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? `${match[3]}.${match[2]}.${match[1]} ${match[4]}:${match[5]}` : String(this.value ?? ''); }
1334
- get monthName() { return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' }); }
1335
- get yearLabel() { return this.visibleMonth.getFullYear(); }
1336
- get monthOptions() { return Array.from({ length: 12 }, (_, value) => ({ value, label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }) })); }
1337
- get yearOptions() { const year = this.visibleMonth.getFullYear(); return Array.from({ length: 12 }, (_, index) => year - 5 + index); }
1338
- get calendarDays() {
1339
- const first = this.startOfMonth(this.visibleMonth);
1340
- const offset = (first.getDay() + 6) % 7;
1341
- const start = new Date(first);
1342
- start.setDate(start.getDate() - offset);
1343
- return Array.from({ length: 42 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return { date: this.toDateValue(date), day: date.getDate(), currentMonth: date.getMonth() === this.visibleMonth.getMonth() }; });
1344
- }
1345
- writeValue(value) { this.value = value ?? ''; const date = this.selectedDate; if (date) {
1346
- this.visibleMonth = this.startOfMonth(date);
1347
- this.selectedHour = +(String(value).slice(11, 13));
1348
- this.selectedMinute = +(String(value).slice(14, 16));
1349
- } this.cdr.markForCheck(); }
1350
- registerOnChange(fn) { this.onChange = fn; }
1351
- registerOnTouched(fn) { this.onTouched = fn; }
1352
- setDisabledState(disabled) { this.disabled = disabled; this.cdr.markForCheck(); }
1353
- openCalendar(event) { event.stopPropagation(); if (this.disabled)
1354
- return; this.isCalendarOpen = true; this.calendarView = 'days'; this.popupService.open(this.closeActivePopup); this.cdr.markForCheck(); }
1355
- closeCalendar() { this.isCalendarOpen = false; this.popupService.close(this.closeActivePopup); this.cdr.markForCheck(); }
1356
- changeMonth(offset, event) { event.stopPropagation(); const year = this.visibleMonth.getFullYear(); const month = this.visibleMonth.getMonth(); this.visibleMonth = this.calendarView === 'years' ? new Date(year + offset * 12, month, 1) : new Date(year + (this.calendarView === 'months' ? offset : 0), month + (this.calendarView === 'days' ? offset : 0), 1); this.cdr.markForCheck(); }
1357
- showMonthPicker(event) { event.stopPropagation(); this.calendarView = 'months'; this.cdr.markForCheck(); }
1358
- showYearPicker(event) { event.stopPropagation(); this.calendarView = 'years'; this.cdr.markForCheck(); }
1359
- selectMonth(month, event) { event.stopPropagation(); this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1); this.calendarView = 'days'; this.cdr.markForCheck(); }
1360
- selectYear(year, event) { event.stopPropagation(); this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1); this.calendarView = 'months'; this.cdr.markForCheck(); }
1361
- selectDate(dateValue, event) { event.stopPropagation(); const date = this.parseDate(dateValue); this.visibleMonth = this.startOfMonth(date); this.setDateTime(dateValue); }
1362
- setDateTime(dateValue, close = true) { const hour = String(this.selectedHour).padStart(2, '0'); const minute = String(this.selectedMinute).padStart(2, '0'); this.value = `${dateValue}T${hour}:${minute}`; this.onChange(this.value); if (close)
1363
- this.closeCalendar(); if (close)
1364
- this.onTouched(); this.cdr.markForCheck(); }
1365
- onTimeChange() { const date = this.selectedDate; if (date) {
1366
- this.setDateTime(this.toDateValue(date), false);
1367
- } }
1368
- onValueChange(input) { const masked = this.applyMask(input); const match = masked.match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/); if (match) {
1369
- const date = new Date(+match[3], +match[2] - 1, +match[1]);
1370
- if (date.getFullYear() === +match[3] && date.getMonth() === +match[2] - 1 && date.getDate() === +match[1] && +match[4] < 24 && +match[5] < 60) {
1371
- this.value = `${match[3]}-${match[2]}-${match[1]}T${match[4]}:${match[5]}`;
1372
- this.visibleMonth = this.startOfMonth(date);
1373
- this.selectedHour = +match[4];
1374
- this.selectedMinute = +match[5];
1375
- }
1376
- else
1377
- this.value = masked;
1378
- }
1379
- else
1380
- this.value = masked; this.onChange(this.value); this.cdr.markForCheck(); }
1381
- allowDateKey(event) { if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
1382
- return; if (!/^\d$/.test(event.key))
1383
- event.preventDefault(); }
1384
- applyMask(value) { const digits = value.replace(/\D/g, '').slice(0, 12); const date = [digits.slice(0, 2), digits.slice(2, 4), digits.slice(4, 8)].filter(Boolean).join('.'); const time = [digits.slice(8, 10), digits.slice(10, 12)].filter(Boolean).join(':'); return [date, time].filter(Boolean).join(' '); }
1385
- isSelected(date) { return this.selectedDate ? this.toDateValue(this.selectedDate) === date : false; }
1386
- isSelectedMonth(month) { return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear(); }
1387
- isSelectedYear(year) { return this.selectedDate?.getFullYear() === year; }
1388
- onDocumentClick() { if (this.isCalendarOpen)
1389
- this.closeCalendar(); }
1390
- onEscape() { if (this.isCalendarOpen)
1391
- this.closeCalendar(); }
1392
- triggerAction(action) { this.actionTriggered.emit(action); }
1393
- startOfMonth(date) { return new Date(date.getFullYear(), date.getMonth(), 1); }
1394
- parseDate(value) { const [year, month, day] = value.split('-').map(Number); return year && month && day ? new Date(year, month - 1, day) : null; }
1395
- toDateValue(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; }
1396
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1397
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDatetimeInputComponent, isStandalone: true, selector: "forms-datetime-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiDatetimeInputComponent), multi: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\" [class.w-full]=\"fullWidth\">\n @if (label) { <span class=\"text-[0.7rem] font-semibold text-slate-600\">{{ label }} @if (required) { <span class=\"text-rose-500\"> *</span> }</span> }\n <div class=\"flex items-end gap-2\"><div class=\"min-w-0 flex-1\"><div class=\"relative\">\n <input type=\"text\" [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420 \u0413\u0413:\u0425\u0425'\" maxlength=\"16\" inputmode=\"numeric\" [attr.name]=\"name\" [attr.autocomplete]=\"autocomplete\" [readonly]=\"readonly\" [disabled]=\"disabled\" class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" [attr.id]=\"controlId\" [value]=\"displayValue\" (click)=\"openCalendar($event)\" (keydown)=\"allowDateKey($event)\" (input)=\"onValueChange($any($event.target).value)\" (blur)=\"onTouched()\" />\n <lucide-icon [img]=\"calendarIcon\" class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"></lucide-icon>\n @if (isCalendarOpen) { <div class=\"absolute left-0 top-full z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\" (click)=\"$event.stopPropagation()\">\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(-1, $event)\"><lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n <div class=\"flex items-center gap-1\"><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 hover:bg-slate-100\" (click)=\"showMonthPicker($event)\">{{ monthName }}</button><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 hover:bg-slate-100\" (click)=\"showYearPicker($event)\">{{ yearLabel }}</button></div>\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(1, $event)\"><lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n </div>\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">@for (weekday of ['\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431','\u041D\u0434']; track weekday) { <span class=\"py-1\">{{ weekday }}</span> }</div>\n <div class=\"grid grid-cols-7 gap-1\">@for (day of calendarDays; track day.date) { <button type=\"button\" class=\"aspect-square rounded-lg text-sm hover:bg-slate-100 hover:text-slate-900\" [class.text-slate-300]=\"!day.currentMonth\" [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\" [class.bg-slate-900]=\"isSelected(day.date)\" [class.text-white]=\"isSelected(day.date)\" (click)=\"selectDate(day.date, $event)\">{{ day.day }}</button> }</div>\n <div class=\"mt-4 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3\"><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0413\u043E\u0434\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"23\" [value]=\"selectedHour\" (input)=\"selectedHour = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0425\u0432\u0438\u043B\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"59\" [value]=\"selectedMinute\" (input)=\"selectedMinute = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label></div>\n } @else if (calendarView === 'months') { <div class=\"grid grid-cols-3 gap-2\">@for (month of monthOptions; track month.value) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedMonth(month.value)\" [class.text-white]=\"isSelectedMonth(month.value)\" (click)=\"selectMonth(month.value, $event)\">{{ month.label }}</button> }</div>\n } @else { <div class=\"grid grid-cols-3 gap-2\">@for (year of yearOptions; track year) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedYear(year)\" [class.text-white]=\"isSelectedYear(year)\" (click)=\"selectYear(year, $event)\">{{ year }}</button> }</div> }\n </div> }\n </div></div>\n @if (actions.length) { <div class=\"flex flex-none items-center gap-2 pb-0.5\">@for (action of actions; track action.id) { <ui-button type=\"button\" [variant]=\"action.variant ?? 'secondary'\" [icon]=\"action.icon || ''\" size=\"sm\" [disabled]=\"disabled\" (click)=\"triggerAction(action)\">{{ action.label }}</ui-button> }</div> }\n </div>\n @if (hint) { <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p> } @if (error) { <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p> }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1398
- }
1399
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, decorators: [{
1400
- type: Component,
1401
- args: [{ selector: 'forms-datetime-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiDatetimeInputComponent), multi: true }], template: "<div class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\" [class.w-full]=\"fullWidth\">\n @if (label) { <span class=\"text-[0.7rem] font-semibold text-slate-600\">{{ label }} @if (required) { <span class=\"text-rose-500\"> *</span> }</span> }\n <div class=\"flex items-end gap-2\"><div class=\"min-w-0 flex-1\"><div class=\"relative\">\n <input type=\"text\" [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420 \u0413\u0413:\u0425\u0425'\" maxlength=\"16\" inputmode=\"numeric\" [attr.name]=\"name\" [attr.autocomplete]=\"autocomplete\" [readonly]=\"readonly\" [disabled]=\"disabled\" class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" [attr.id]=\"controlId\" [value]=\"displayValue\" (click)=\"openCalendar($event)\" (keydown)=\"allowDateKey($event)\" (input)=\"onValueChange($any($event.target).value)\" (blur)=\"onTouched()\" />\n <lucide-icon [img]=\"calendarIcon\" class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"></lucide-icon>\n @if (isCalendarOpen) { <div class=\"absolute left-0 top-full z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\" (click)=\"$event.stopPropagation()\">\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(-1, $event)\"><lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n <div class=\"flex items-center gap-1\"><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 hover:bg-slate-100\" (click)=\"showMonthPicker($event)\">{{ monthName }}</button><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 hover:bg-slate-100\" (click)=\"showYearPicker($event)\">{{ yearLabel }}</button></div>\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(1, $event)\"><lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n </div>\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">@for (weekday of ['\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431','\u041D\u0434']; track weekday) { <span class=\"py-1\">{{ weekday }}</span> }</div>\n <div class=\"grid grid-cols-7 gap-1\">@for (day of calendarDays; track day.date) { <button type=\"button\" class=\"aspect-square rounded-lg text-sm hover:bg-slate-100 hover:text-slate-900\" [class.text-slate-300]=\"!day.currentMonth\" [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\" [class.bg-slate-900]=\"isSelected(day.date)\" [class.text-white]=\"isSelected(day.date)\" (click)=\"selectDate(day.date, $event)\">{{ day.day }}</button> }</div>\n <div class=\"mt-4 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3\"><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0413\u043E\u0434\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"23\" [value]=\"selectedHour\" (input)=\"selectedHour = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0425\u0432\u0438\u043B\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"59\" [value]=\"selectedMinute\" (input)=\"selectedMinute = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label></div>\n } @else if (calendarView === 'months') { <div class=\"grid grid-cols-3 gap-2\">@for (month of monthOptions; track month.value) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedMonth(month.value)\" [class.text-white]=\"isSelectedMonth(month.value)\" (click)=\"selectMonth(month.value, $event)\">{{ month.label }}</button> }</div>\n } @else { <div class=\"grid grid-cols-3 gap-2\">@for (year of yearOptions; track year) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedYear(year)\" [class.text-white]=\"isSelectedYear(year)\" (click)=\"selectYear(year, $event)\">{{ year }}</button> }</div> }\n </div> }\n </div></div>\n @if (actions.length) { <div class=\"flex flex-none items-center gap-2 pb-0.5\">@for (action of actions; track action.id) { <ui-button type=\"button\" [variant]=\"action.variant ?? 'secondary'\" [icon]=\"action.icon || ''\" size=\"sm\" [disabled]=\"disabled\" (click)=\"triggerAction(action)\">{{ action.label }}</ui-button> }</div> }\n </div>\n @if (hint) { <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p> } @if (error) { <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p> }\n</div>\n" }]
1402
- }], propDecorators: { label: [{
1403
- type: Input
1404
- }], hint: [{
1405
- type: Input
1406
- }], placeholder: [{
1407
- type: Input
1408
- }], name: [{
1409
- type: Input
1410
- }], id: [{
1411
- type: Input
1412
- }], rows: [{
1413
- type: Input
1414
- }], required: [{
1415
- type: Input
1416
- }], fullWidth: [{
1417
- type: Input
1418
- }], autocomplete: [{
1419
- type: Input
1420
- }], readonly: [{
1421
- type: Input
1422
- }], pattern: [{
1423
- type: Input
1424
- }], error: [{
1425
- type: Input
1426
- }], actions: [{
1427
- type: Input
1428
- }], actionTriggered: [{
1429
- type: Output
1430
- }], onDocumentClick: [{
1431
- type: HostListener,
1432
- args: ['document:click']
1433
- }], onEscape: [{
1434
- type: HostListener,
1435
- args: ['document:keydown.escape']
1436
- }] } });
1437
-
1438
- class UiEmailInputComponent {
1439
- cdr = inject(ChangeDetectorRef);
1440
- label;
1441
- hint;
1442
- placeholder;
1443
- name;
1444
- id;
1445
- type = 'email';
1446
- element = 'input';
1447
- rows = 4;
1448
- required = false;
1449
- fullWidth = false;
1450
- autocomplete;
1451
- readonly = false;
1452
- pattern;
1453
- error;
1454
- actions = [];
1455
- disabled = false;
1456
- value = '';
1457
- static nextId = 0;
1458
- generatedId = `ui-input-${UiEmailInputComponent.nextId++}`;
1459
- actionTriggered = new EventEmitter();
1460
- get controlId() {
1461
- return this.id ?? this.generatedId;
1462
- }
1463
- onChange = () => undefined;
1464
- onTouched = () => undefined;
1465
- writeValue(value) {
1466
- this.value = value ?? '';
1467
- this.cdr.markForCheck();
1468
- }
1469
- registerOnChange(fn) {
1470
- this.onChange = fn;
1471
- }
1472
- registerOnTouched(fn) {
1473
- this.onTouched = fn;
1474
- }
1475
- setDisabledState(isDisabled) {
1476
- this.disabled = isDisabled;
1477
- this.cdr.markForCheck();
1478
- }
1479
- onValueChange(value) {
1480
- const parsed = value;
1481
- this.value = parsed;
1482
- this.onChange(parsed);
1483
- }
1484
- triggerAction(action) {
1485
- this.actionTriggered.emit(action);
1486
- }
1487
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1488
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiEmailInputComponent, isStandalone: true, selector: "forms-email-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1489
- {
1490
- provide: NG_VALUE_ACCESSOR,
1491
- useExisting: forwardRef(() => UiEmailInputComponent),
1492
- multi: true,
1493
- },
1494
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1495
- }
1496
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, decorators: [{
1497
- type: Component,
1498
- args: [{ selector: 'forms-email-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1499
- {
1500
- provide: NG_VALUE_ACCESSOR,
1501
- useExisting: forwardRef(() => UiEmailInputComponent),
1502
- multi: true,
1503
- },
1504
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1505
- }], propDecorators: { label: [{
1506
- type: Input
1507
- }], hint: [{
1508
- type: Input
1509
- }], placeholder: [{
1510
- type: Input
1511
- }], name: [{
1512
- type: Input
1513
- }], id: [{
1514
- type: Input
1515
- }], rows: [{
1516
- type: Input
1517
- }], required: [{
1518
- type: Input
1519
- }], fullWidth: [{
1520
- type: Input
1521
- }], autocomplete: [{
1522
- type: Input
1523
- }], readonly: [{
1524
- type: Input
1525
- }], pattern: [{
1526
- type: Input
1527
- }], error: [{
1528
- type: Input
1529
- }], actions: [{
1530
- type: Input
1531
- }], actionTriggered: [{
1532
- type: Output
1533
- }] } });
1534
-
1535
- class UiNumberInputComponent {
1536
- cdr = inject(ChangeDetectorRef);
1537
- label;
1538
- hint;
1539
- placeholder;
1540
- name;
1541
- id;
1542
- type = 'number';
1543
- element = 'input';
1544
- rows = 4;
1545
- required = false;
1546
- fullWidth = false;
1547
- autocomplete;
1548
- readonly = false;
1549
- disabled = false;
1550
- pattern;
1551
- error;
1552
- actions = [];
1553
- value = '';
1554
- static nextId = 0;
1555
- generatedId = `ui-input-${UiNumberInputComponent.nextId++}`;
1556
- actionTriggered = new EventEmitter();
1557
- get controlId() {
1558
- return this.id ?? this.generatedId;
1559
- }
1560
- onChange = () => undefined;
1561
- onTouched = () => undefined;
1562
- writeValue(value) {
1563
- this.value = value ?? '';
1564
- this.cdr.markForCheck();
1290
+ writeValue(value) {
1291
+ this.value = value ?? '';
1292
+ this.cdr.markForCheck();
1565
1293
  }
1566
1294
  registerOnChange(fn) {
1567
1295
  this.onChange = fn;
@@ -1569,6 +1297,7 @@ class UiNumberInputComponent {
1569
1297
  registerOnTouched(fn) {
1570
1298
  this.onTouched = fn;
1571
1299
  }
1300
+ validate(control) { return validateFormControl(control, this.required, this.pattern); }
1572
1301
  setDisabledState(isDisabled) {
1573
1302
  this.disabled = isDisabled;
1574
1303
  this.cdr.markForCheck();
@@ -1584,24 +1313,26 @@ class UiNumberInputComponent {
1584
1313
  triggerAction(action) {
1585
1314
  this.actionTriggered.emit(action);
1586
1315
  }
1587
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiNumberInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1588
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiNumberInputComponent, isStandalone: true, selector: "forms-number-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", disabled: "disabled", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1316
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputNumberComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1317
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputNumberComponent, isStandalone: true, selector: "input-number", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", disabled: "disabled", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1589
1318
  {
1590
1319
  provide: NG_VALUE_ACCESSOR,
1591
- useExisting: forwardRef(() => UiNumberInputComponent),
1320
+ useExisting: forwardRef(() => InputNumberComponent),
1592
1321
  multi: true,
1593
1322
  },
1594
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full appearance-none rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (wheel)=\"preventWheelChange($event)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", styles: ["input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;-webkit-appearance:none;margin:0}\n", "input[type=number]{-moz-appearance:textfield}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1323
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputNumberComponent), multi: true },
1324
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full appearance-none rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (wheel)=\"preventWheelChange($event)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", styles: ["input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;-webkit-appearance:none;margin:0}\n", "input[type=number]{-moz-appearance:textfield}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1595
1325
  }
1596
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiNumberInputComponent, decorators: [{
1326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputNumberComponent, decorators: [{
1597
1327
  type: Component,
1598
- args: [{ selector: 'forms-number-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1328
+ args: [{ selector: 'input-number', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1599
1329
  {
1600
1330
  provide: NG_VALUE_ACCESSOR,
1601
- useExisting: forwardRef(() => UiNumberInputComponent),
1331
+ useExisting: forwardRef(() => InputNumberComponent),
1602
1332
  multi: true,
1603
1333
  },
1604
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full appearance-none rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (wheel)=\"preventWheelChange($event)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", styles: ["input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;-webkit-appearance:none;margin:0}\n", "input[type=number]{-moz-appearance:textfield}\n"] }]
1334
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputNumberComponent), multi: true },
1335
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full appearance-none rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (wheel)=\"preventWheelChange($event)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", styles: ["input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;-webkit-appearance:none;margin:0}\n", "input[type=number]{-moz-appearance:textfield}\n"] }]
1605
1336
  }], propDecorators: { label: [{
1606
1337
  type: Input
1607
1338
  }], hint: [{
@@ -1620,8 +1351,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1620
1351
  type: Input
1621
1352
  }], autocomplete: [{
1622
1353
  type: Input
1623
- }], readonly: [{
1624
- type: Input
1625
1354
  }], disabled: [{
1626
1355
  type: Input
1627
1356
  }], pattern: [{
@@ -1634,7 +1363,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1634
1363
  type: Output
1635
1364
  }] } });
1636
1365
 
1637
- class UiPasswordInputComponent {
1366
+ class InputPasswordComponent {
1638
1367
  cdr = inject(ChangeDetectorRef);
1639
1368
  label;
1640
1369
  hint;
@@ -1649,7 +1378,6 @@ class UiPasswordInputComponent {
1649
1378
  required = false;
1650
1379
  fullWidth = false;
1651
1380
  autocomplete;
1652
- readonly = false;
1653
1381
  pattern;
1654
1382
  error;
1655
1383
  actions = [];
@@ -1657,7 +1385,7 @@ class UiPasswordInputComponent {
1657
1385
  isPasswordVisible = false;
1658
1386
  value = '';
1659
1387
  static nextId = 0;
1660
- generatedId = `ui-input-${UiPasswordInputComponent.nextId++}`;
1388
+ generatedId = `input-password-${InputPasswordComponent.nextId++}`;
1661
1389
  actionTriggered = new EventEmitter();
1662
1390
  get controlId() {
1663
1391
  return this.id ?? this.generatedId;
@@ -1674,6 +1402,7 @@ class UiPasswordInputComponent {
1674
1402
  registerOnTouched(fn) {
1675
1403
  this.onTouched = fn;
1676
1404
  }
1405
+ validate(control) { return validateFormControl(control, this.required, this.pattern); }
1677
1406
  setDisabledState(isDisabled) {
1678
1407
  this.disabled = isDisabled;
1679
1408
  this.cdr.markForCheck();
@@ -1691,24 +1420,26 @@ class UiPasswordInputComponent {
1691
1420
  triggerAction(action) {
1692
1421
  this.actionTriggered.emit(action);
1693
1422
  }
1694
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPasswordInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1695
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPasswordInputComponent, isStandalone: true, selector: "forms-password-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1423
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPasswordComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1424
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputPasswordComponent, isStandalone: true, selector: "input-password", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1696
1425
  {
1697
1426
  provide: NG_VALUE_ACCESSOR,
1698
- useExisting: forwardRef(() => UiPasswordInputComponent),
1427
+ useExisting: forwardRef(() => InputPasswordComponent),
1699
1428
  multi: true,
1700
1429
  },
1701
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n [attr.type]=\"isPasswordVisible ? 'text' : type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <button\n type=\"button\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.aria-label]=\"isPasswordVisible ? '\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C' : '\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C'\"\n [disabled]=\"disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"togglePassword($event)\"\n >\n <lucide-icon\n [img]=\"isPasswordVisible ? hidePasswordIcon : showPasswordIcon\"\n class=\"h-4 w-4\"\n aria-hidden=\"true\"\n ></lucide-icon>\n </button>\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1430
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputPasswordComponent), multi: true },
1431
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n [attr.type]=\"isPasswordVisible ? 'text' : type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <button\n type=\"button\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.aria-label]=\"isPasswordVisible ? '\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C' : '\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C'\"\n [disabled]=\"disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"togglePassword($event)\"\n >\n <lucide-icon\n [img]=\"isPasswordVisible ? hidePasswordIcon : showPasswordIcon\"\n class=\"h-4 w-4\"\n aria-hidden=\"true\"\n ></lucide-icon>\n </button>\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1702
1432
  }
1703
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPasswordInputComponent, decorators: [{
1433
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPasswordComponent, decorators: [{
1704
1434
  type: Component,
1705
- args: [{ selector: 'forms-password-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1435
+ args: [{ selector: 'input-password', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1706
1436
  {
1707
1437
  provide: NG_VALUE_ACCESSOR,
1708
- useExisting: forwardRef(() => UiPasswordInputComponent),
1438
+ useExisting: forwardRef(() => InputPasswordComponent),
1709
1439
  multi: true,
1710
1440
  },
1711
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n [attr.type]=\"isPasswordVisible ? 'text' : type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <button\n type=\"button\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.aria-label]=\"isPasswordVisible ? '\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C' : '\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C'\"\n [disabled]=\"disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"togglePassword($event)\"\n >\n <lucide-icon\n [img]=\"isPasswordVisible ? hidePasswordIcon : showPasswordIcon\"\n class=\"h-4 w-4\"\n aria-hidden=\"true\"\n ></lucide-icon>\n </button>\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
1441
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputPasswordComponent), multi: true },
1442
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n [attr.type]=\"isPasswordVisible ? 'text' : type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <button\n type=\"button\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.aria-label]=\"isPasswordVisible ? '\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C' : '\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C'\"\n [disabled]=\"disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"togglePassword($event)\"\n >\n <lucide-icon\n [img]=\"isPasswordVisible ? hidePasswordIcon : showPasswordIcon\"\n class=\"h-4 w-4\"\n aria-hidden=\"true\"\n ></lucide-icon>\n </button>\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n" }]
1712
1443
  }], propDecorators: { label: [{
1713
1444
  type: Input
1714
1445
  }], hint: [{
@@ -1727,8 +1458,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1727
1458
  type: Input
1728
1459
  }], autocomplete: [{
1729
1460
  type: Input
1730
- }], readonly: [{
1731
- type: Input
1732
1461
  }], pattern: [{
1733
1462
  type: Input
1734
1463
  }], error: [{
@@ -1739,7 +1468,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1739
1468
  type: Output
1740
1469
  }] } });
1741
1470
 
1742
- class UiTextInputComponent {
1471
+ class InputTextComponent {
1743
1472
  cdr = inject(ChangeDetectorRef);
1744
1473
  label;
1745
1474
  hint;
@@ -1752,14 +1481,13 @@ class UiTextInputComponent {
1752
1481
  required = false;
1753
1482
  fullWidth = false;
1754
1483
  autocomplete;
1755
- readonly = false;
1756
1484
  pattern;
1757
1485
  error;
1758
1486
  actions = [];
1759
1487
  disabled = false;
1760
1488
  value = '';
1761
1489
  static nextId = 0;
1762
- generatedId = `ui-input-${UiTextInputComponent.nextId++}`;
1490
+ generatedId = `input-text-${InputTextComponent.nextId++}`;
1763
1491
  actionTriggered = new EventEmitter();
1764
1492
  get controlId() {
1765
1493
  return this.id ?? this.generatedId;
@@ -1776,6 +1504,7 @@ class UiTextInputComponent {
1776
1504
  registerOnTouched(fn) {
1777
1505
  this.onTouched = fn;
1778
1506
  }
1507
+ validate(control) { return validateFormControl(control, this.required, this.pattern); }
1779
1508
  setDisabledState(isDisabled) {
1780
1509
  this.disabled = isDisabled;
1781
1510
  this.cdr.markForCheck();
@@ -1788,24 +1517,26 @@ class UiTextInputComponent {
1788
1517
  triggerAction(action) {
1789
1518
  this.actionTriggered.emit(action);
1790
1519
  }
1791
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1792
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiTextInputComponent, isStandalone: true, selector: "forms-text-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1520
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1521
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputTextComponent, isStandalone: true, selector: "input-text", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1793
1522
  {
1794
1523
  provide: NG_VALUE_ACCESSOR,
1795
- useExisting: forwardRef(() => UiTextInputComponent),
1524
+ useExisting: forwardRef(() => InputTextComponent),
1796
1525
  multi: true,
1797
1526
  },
1798
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1527
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextComponent), multi: true },
1528
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1799
1529
  }
1800
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextInputComponent, decorators: [{
1530
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextComponent, decorators: [{
1801
1531
  type: Component,
1802
- args: [{ selector: 'forms-text-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1532
+ args: [{ selector: 'input-text', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1803
1533
  {
1804
1534
  provide: NG_VALUE_ACCESSOR,
1805
- useExisting: forwardRef(() => UiTextInputComponent),
1535
+ useExisting: forwardRef(() => InputTextComponent),
1806
1536
  multi: true,
1807
1537
  },
1808
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1538
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextComponent), multi: true },
1539
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
1809
1540
  }], propDecorators: { label: [{
1810
1541
  type: Input
1811
1542
  }], hint: [{
@@ -1824,8 +1555,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1824
1555
  type: Input
1825
1556
  }], autocomplete: [{
1826
1557
  type: Input
1827
- }], readonly: [{
1828
- type: Input
1829
1558
  }], pattern: [{
1830
1559
  type: Input
1831
1560
  }], error: [{
@@ -1836,7 +1565,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1836
1565
  type: Output
1837
1566
  }] } });
1838
1567
 
1839
- class UiTextareaInputComponent {
1568
+ class InputTextareaComponent {
1840
1569
  cdr = inject(ChangeDetectorRef);
1841
1570
  label;
1842
1571
  hint;
@@ -1849,14 +1578,13 @@ class UiTextareaInputComponent {
1849
1578
  required = false;
1850
1579
  fullWidth = false;
1851
1580
  autocomplete;
1852
- readonly = false;
1853
1581
  pattern;
1854
1582
  error;
1855
1583
  actions = [];
1856
1584
  disabled = false;
1857
1585
  value = '';
1858
1586
  static nextId = 0;
1859
- generatedId = `ui-input-${UiTextareaInputComponent.nextId++}`;
1587
+ generatedId = `input-textarea-${InputTextareaComponent.nextId++}`;
1860
1588
  actionTriggered = new EventEmitter();
1861
1589
  get controlId() {
1862
1590
  return this.id ?? this.generatedId;
@@ -1873,6 +1601,7 @@ class UiTextareaInputComponent {
1873
1601
  registerOnTouched(fn) {
1874
1602
  this.onTouched = fn;
1875
1603
  }
1604
+ validate(control) { return validateFormControl(control, this.required, this.pattern); }
1876
1605
  setDisabledState(isDisabled) {
1877
1606
  this.disabled = isDisabled;
1878
1607
  this.cdr.markForCheck();
@@ -1885,24 +1614,26 @@ class UiTextareaInputComponent {
1885
1614
  triggerAction(action) {
1886
1615
  this.actionTriggered.emit(action);
1887
1616
  }
1888
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextareaInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1889
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiTextareaInputComponent, isStandalone: true, selector: "forms-textarea-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1617
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1618
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputTextareaComponent, isStandalone: true, selector: "input-textarea", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1890
1619
  {
1891
1620
  provide: NG_VALUE_ACCESSOR,
1892
- useExisting: forwardRef(() => UiTextareaInputComponent),
1621
+ useExisting: forwardRef(() => InputTextareaComponent),
1893
1622
  multi: true,
1894
1623
  },
1895
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1624
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextareaComponent), multi: true },
1625
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1896
1626
  }
1897
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextareaInputComponent, decorators: [{
1627
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextareaComponent, decorators: [{
1898
1628
  type: Component,
1899
- args: [{ selector: 'forms-textarea-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1629
+ args: [{ selector: 'input-textarea', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1900
1630
  {
1901
1631
  provide: NG_VALUE_ACCESSOR,
1902
- useExisting: forwardRef(() => UiTextareaInputComponent),
1632
+ useExisting: forwardRef(() => InputTextareaComponent),
1903
1633
  multi: true,
1904
1634
  },
1905
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1635
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextareaComponent), multi: true },
1636
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
1906
1637
  }], propDecorators: { label: [{
1907
1638
  type: Input
1908
1639
  }], hint: [{
@@ -1921,8 +1652,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1921
1652
  type: Input
1922
1653
  }], autocomplete: [{
1923
1654
  type: Input
1924
- }], readonly: [{
1925
- type: Input
1926
1655
  }], pattern: [{
1927
1656
  type: Input
1928
1657
  }], error: [{
@@ -1933,96 +1662,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1933
1662
  type: Output
1934
1663
  }] } });
1935
1664
 
1936
- const UI_FORM_INPUT_REGISTRY = {
1937
- text: UiTextInputComponent,
1938
- number: UiNumberInputComponent,
1939
- date: UiDateInputComponent,
1940
- datetime: UiDatetimeInputComponent,
1941
- email: UiEmailInputComponent,
1942
- password: UiPasswordInputComponent,
1943
- textarea: UiTextareaInputComponent,
1944
- };
1945
-
1946
- class UiFormInputHostComponent {
1947
- field;
1948
- value;
1949
- error = '';
1950
- disabled = false;
1951
- actions = [];
1952
- options = [];
1953
- valueChange = new EventEmitter();
1954
- actionTriggered = new EventEmitter();
1955
- viewContainer = inject(ViewContainerRef);
1956
- componentRef;
1957
- ngOnChanges(changes) {
1958
- if (changes['field']) {
1959
- this.render();
1960
- return;
1961
- }
1962
- this.syncInputs();
1963
- }
1964
- render() {
1965
- this.viewContainer.clear();
1966
- const inputType = String(this.field?.type || 'text');
1967
- const componentType = UI_FORM_INPUT_REGISTRY[inputType] || UI_FORM_INPUT_REGISTRY.text;
1968
- this.componentRef = this.viewContainer.createComponent(componentType);
1969
- this.syncInputs();
1970
- const instance = this.componentRef.instance;
1971
- instance.registerOnChange?.((nextValue) => this.valueChange.emit(nextValue));
1972
- instance.registerOnTouched?.(() => undefined);
1973
- instance.actionTriggered?.subscribe((action) => this.actionTriggered.emit(action));
1974
- }
1975
- syncInputs() {
1976
- if (!this.componentRef || !this.field)
1977
- return;
1978
- const field = this.field;
1979
- const setInput = (name, value) => this.componentRef?.setInput(name, value);
1980
- setInput('name', field.id);
1981
- setInput('id', field.id);
1982
- setInput('label', field.label);
1983
- setInput('placeholder', field.placeholder);
1984
- setInput('hint', field.hint);
1985
- setInput('required', !!field.required);
1986
- setInput('readonly', !!field.readonly);
1987
- setInput('disabled', this.disabled || !!field.disabled);
1988
- setInput('pattern', field.pattern);
1989
- setInput('rows', field.rows ?? 4);
1990
- setInput('fullWidth', field.fullWidth !== false);
1991
- setInput('error', this.error);
1992
- setInput('actions', this.actions);
1993
- const instance = this.componentRef.instance;
1994
- instance.writeValue?.(this.value);
1995
- instance.setDisabledState?.(this.disabled || !!field.disabled);
1996
- }
1997
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1998
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiFormInputHostComponent, isStandalone: true, selector: "forms-form-input-host", inputs: { field: "field", value: "value", error: "error", disabled: "disabled", actions: "actions", options: "options" }, outputs: { valueChange: "valueChange", actionTriggered: "actionTriggered" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
1999
- }
2000
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, decorators: [{
2001
- type: Component,
2002
- args: [{
2003
- selector: 'forms-form-input-host',
2004
- standalone: true,
2005
- template: '',
2006
- }]
2007
- }], propDecorators: { field: [{
2008
- type: Input
2009
- }], value: [{
2010
- type: Input
2011
- }], error: [{
2012
- type: Input
2013
- }], disabled: [{
2014
- type: Input
2015
- }], actions: [{
2016
- type: Input
2017
- }], options: [{
2018
- type: Input
2019
- }], valueChange: [{
2020
- type: Output
2021
- }], actionTriggered: [{
2022
- type: Output
2023
- }] } });
2024
-
2025
- class UiFileInputComponent {
1665
+ class InputFileComponent {
2026
1666
  label;
2027
1667
  hint;
2028
1668
  accept;
@@ -2031,6 +1671,12 @@ class UiFileInputComponent {
2031
1671
  value = null;
2032
1672
  valueChange = new EventEmitter();
2033
1673
  filesChange = new EventEmitter();
1674
+ onChange = () => undefined;
1675
+ onTouched = () => undefined;
1676
+ writeValue(value) { this.value = value; }
1677
+ registerOnChange(fn) { this.onChange = fn; }
1678
+ registerOnTouched(fn) { this.onTouched = fn; }
1679
+ setDisabledState(isDisabled) { this.disabled = isDisabled; }
2034
1680
  get selectedFiles() {
2035
1681
  if (this.value instanceof File)
2036
1682
  return [this.value];
@@ -2040,6 +1686,8 @@ class UiFileInputComponent {
2040
1686
  const input = event.target;
2041
1687
  const files = Array.from(input.files ?? []);
2042
1688
  const value = this.multiple ? files : (files[0] ?? null);
1689
+ this.value = value;
1690
+ this.onChange(value);
2043
1691
  this.valueChange.emit(value);
2044
1692
  this.filesChange.emit(files);
2045
1693
  input.value = '';
@@ -2047,15 +1695,17 @@ class UiFileInputComponent {
2047
1695
  remove(index) {
2048
1696
  const files = this.selectedFiles.filter((_file, fileIndex) => fileIndex !== index);
2049
1697
  const value = this.multiple ? files : (files[0] ?? null);
1698
+ this.value = value;
1699
+ this.onChange(value);
2050
1700
  this.valueChange.emit(value);
2051
1701
  this.filesChange.emit(files);
2052
1702
  }
2053
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2054
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiFileInputComponent, isStandalone: true, selector: "forms-file-input", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange" }, ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1703
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1704
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputFileComponent, isStandalone: true, selector: "input-file", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputFileComponent), multi: true }], ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n (blur)=\"onTouched()\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
2055
1705
  }
2056
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFileInputComponent, decorators: [{
1706
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputFileComponent, decorators: [{
2057
1707
  type: Component,
2058
- args: [{ selector: 'forms-file-input', standalone: true, imports: [CommonModule], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
1708
+ args: [{ selector: 'input-file', standalone: true, imports: [CommonModule], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputFileComponent), multi: true }], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n (blur)=\"onTouched()\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
2059
1709
  }], propDecorators: { label: [{
2060
1710
  type: Input
2061
1711
  }], hint: [{
@@ -2074,7 +1724,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
2074
1724
  type: Output
2075
1725
  }] } });
2076
1726
 
2077
- class UiPhotoInputComponent {
1727
+ class InputPhotoComponent {
2078
1728
  http = inject(HttpClient);
2079
1729
  label;
2080
1730
  hint;
@@ -2090,7 +1740,16 @@ class UiPhotoInputComponent {
2090
1740
  previews = [];
2091
1741
  uploading = false;
2092
1742
  fileTypes = new Map();
2093
- ngOnChanges(changes) {
1743
+ onChange = () => undefined;
1744
+ onTouched = () => undefined;
1745
+ writeValue(value) {
1746
+ this.value = value;
1747
+ this.rebuildPreviews();
1748
+ }
1749
+ registerOnChange(fn) { this.onChange = fn; }
1750
+ registerOnTouched(fn) { this.onTouched = fn; }
1751
+ setDisabledState(isDisabled) { this.disabled = isDisabled; }
1752
+ ngOnChanges(changes) {
2094
1753
  if (changes['value'] || changes['multiple'])
2095
1754
  this.rebuildPreviews();
2096
1755
  }
@@ -2140,6 +1799,8 @@ class UiPhotoInputComponent {
2140
1799
  return Number.isInteger(Number(this.limit)) && Number(this.limit) > 0 ? Number(this.limit) : Number.MAX_SAFE_INTEGER;
2141
1800
  }
2142
1801
  emitValue(value) {
1802
+ this.value = value;
1803
+ this.onChange(value);
2143
1804
  this.valueChange.emit(value);
2144
1805
  this.rebuildPreviews(value);
2145
1806
  }
@@ -2200,12 +1861,12 @@ class UiPhotoInputComponent {
2200
1861
  const normalized = value.toLowerCase();
2201
1862
  return normalized.startsWith('image/') || normalized === 'application/pdf';
2202
1863
  }
2203
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2204
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPhotoInputComponent, isStandalone: true, selector: "forms-photo-input", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", limit: "limit", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange", uploadingChange: "uploadingChange" }, usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
1864
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1865
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputPhotoComponent, isStandalone: true, selector: "input-photo", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", limit: "limit", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange", uploadingChange: "uploadingChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputPhotoComponent), multi: true }], usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
2205
1866
  }
2206
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoInputComponent, decorators: [{
1867
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoComponent, decorators: [{
2207
1868
  type: Component,
2208
- args: [{ selector: 'forms-photo-input', standalone: true, imports: [CommonModule, DragDropModule], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
1869
+ args: [{ selector: 'input-photo', standalone: true, imports: [CommonModule, DragDropModule], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputPhotoComponent), multi: true }], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
2209
1870
  }], propDecorators: { label: [{
2210
1871
  type: Input
2211
1872
  }], hint: [{
@@ -2228,53 +1889,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
2228
1889
  type: Output
2229
1890
  }] } });
2230
1891
 
2231
- class UiCheckboxInputComponent {
2232
- label;
2233
- disabled = false;
2234
- value = false;
2235
- valueChange = new EventEmitter();
2236
- onValueChange(event) {
2237
- this.valueChange.emit(event.target.checked);
2238
- }
2239
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2240
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiCheckboxInputComponent, isStandalone: true, selector: "forms-checkbox-input", inputs: { label: "label", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n />\n <span>{{ label }}</span>\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
2241
- }
2242
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, decorators: [{
2243
- type: Component,
2244
- args: [{ selector: 'forms-checkbox-input', standalone: true, imports: [CommonModule], template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n />\n <span>{{ label }}</span>\n</label>\n" }]
2245
- }], propDecorators: { label: [{
2246
- type: Input
2247
- }], disabled: [{
2248
- type: Input
2249
- }], value: [{
2250
- type: Input
2251
- }], valueChange: [{
2252
- type: Output
2253
- }] } });
2254
-
2255
- class UiColorInputComponent {
1892
+ class InputSelectComponent {
1893
+ closeIcon = X;
1894
+ chevronDownIcon = ChevronDown;
1895
+ checkIcon = Check;
2256
1896
  cdr = inject(ChangeDetectorRef);
1897
+ destroyRef = inject(DestroyRef);
1898
+ http = inject(HttpClient);
1899
+ popupService = inject(UiPopupService);
1900
+ closeActivePopup = () => this.closeDropdown();
2257
1901
  label;
2258
- hint;
1902
+ placeholder;
2259
1903
  name;
2260
- id;
1904
+ data;
2261
1905
  required = false;
2262
1906
  fullWidth = false;
2263
- error;
2264
1907
  disabled = false;
2265
- value = '#000000';
2266
- static nextId = 0;
2267
- generatedId = `ui-color-input-${UiColorInputComponent.nextId++}`;
1908
+ multiple = false;
1909
+ includeBlankOption = false;
1910
+ blankOptionLabel = 'Оберіть…';
1911
+ emptyStateLabel = 'Немає варіантів для вибору';
1912
+ searchable = true;
1913
+ remoteSearch = false;
1914
+ searchPlaceholder = 'Пошук...';
1915
+ _options = [];
1916
+ set options(value) {
1917
+ this._options = value ?? [];
1918
+ this.updateSelectionFromValue();
1919
+ this.cdr.markForCheck();
1920
+ }
1921
+ get options() {
1922
+ return this._options;
1923
+ }
1924
+ error;
1925
+ hint;
1926
+ triggerClass = '';
1927
+ panelClass = '';
1928
+ valueChange = new EventEmitter();
1929
+ searchChange = new EventEmitter();
1930
+ popupMinWidth = 300;
1931
+ popupViewportMargin = 16;
1932
+ popupOffset = 8;
1933
+ dropdownTrigger;
1934
+ dropdownPositions = [
1935
+ {
1936
+ originX: 'start',
1937
+ originY: 'bottom',
1938
+ overlayX: 'start',
1939
+ overlayY: 'top',
1940
+ offsetY: this.popupOffset,
1941
+ },
1942
+ {
1943
+ originX: 'start',
1944
+ originY: 'top',
1945
+ overlayX: 'start',
1946
+ overlayY: 'bottom',
1947
+ offsetY: -this.popupOffset,
1948
+ },
1949
+ ];
1950
+ isOpen = false;
1951
+ popupWidth = 0;
1952
+ value = '';
1953
+ multiValue = [];
1954
+ selectedOptions = [];
1955
+ searchTerm = '';
1956
+ isDictionaryLoading = false;
1957
+ searchChanges$ = new Subject();
1958
+ dictionaryRequestVersions = new Map();
1959
+ dictionaryOptions = [];
2268
1960
  onChange = () => undefined;
2269
1961
  onTouched = () => undefined;
2270
- get controlId() {
2271
- return this.id ?? this.generatedId;
1962
+ constructor() {
1963
+ this.searchChanges$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((next) => {
1964
+ this.searchTerm = next;
1965
+ if (this.getDictionaryName()) {
1966
+ this.loadDictionaryOptions(next);
1967
+ return;
1968
+ }
1969
+ this.updateSelectionFromValue();
1970
+ this.cdr.markForCheck();
1971
+ });
1972
+ this.destroyRef.onDestroy(() => {
1973
+ this.searchChanges$.complete();
1974
+ this.dictionaryRequestVersions.clear();
1975
+ });
2272
1976
  }
2273
- get pickerValue() {
2274
- return this.isColorValue(this.value) ? this.value : '#000000';
1977
+ ngOnChanges(changes) {
1978
+ if ('data' in changes) {
1979
+ this.reloadDictionaryOptions();
1980
+ }
2275
1981
  }
2276
1982
  writeValue(value) {
2277
- this.value = value?.trim() || '#000000';
1983
+ if (this.multiple) {
1984
+ this.multiValue = Array.isArray(value) ? value : [];
1985
+ }
1986
+ else {
1987
+ this.value = value ?? '';
1988
+ }
1989
+ this.updateSelectionFromValue();
1990
+ this.ensureSelectedDictionaryOptionsLoaded();
2278
1991
  this.cdr.markForCheck();
2279
1992
  }
2280
1993
  registerOnChange(fn) {
@@ -2283,66 +1996,324 @@ class UiColorInputComponent {
2283
1996
  registerOnTouched(fn) {
2284
1997
  this.onTouched = fn;
2285
1998
  }
1999
+ validate(control) { return validateFormControl(control, this.required); }
2286
2000
  setDisabledState(isDisabled) {
2287
2001
  this.disabled = isDisabled;
2288
2002
  this.cdr.markForCheck();
2289
2003
  }
2290
- onPickerChange(value) {
2291
- this.value = value;
2292
- this.onChange(value);
2004
+ toggleDropdown(event) {
2005
+ event.stopPropagation();
2006
+ if (this.disabled) {
2007
+ return;
2008
+ }
2009
+ if (this.isOpen) {
2010
+ this.closeDropdown();
2011
+ return;
2012
+ }
2013
+ const fieldWidth = this.dropdownTrigger.nativeElement.getBoundingClientRect().width;
2014
+ this.popupWidth = Math.max(fieldWidth, this.popupMinWidth);
2015
+ this.isOpen = true;
2016
+ this.popupService.open(this.closeActivePopup);
2017
+ this.searchTerm = '';
2018
+ this.searchChanges$.next('');
2019
+ this.cdr.markForCheck();
2293
2020
  }
2294
- onHexInput(value) {
2295
- this.value = this.normalizeHexInput(value);
2296
- this.onChange(this.value);
2021
+ selectOption(option, event) {
2022
+ event.stopPropagation();
2023
+ const key = this.getOptionValue(option);
2024
+ if (this.multiple) {
2025
+ const normalized = this.multiValue.includes(key)
2026
+ ? this.multiValue.filter((item) => item !== key)
2027
+ : [...this.multiValue, key];
2028
+ this.multiValue = normalized;
2029
+ this.onChange(normalized);
2030
+ this.valueChange.emit(normalized);
2031
+ }
2032
+ else {
2033
+ const nextValue = this.value === key ? null : key;
2034
+ this.value = nextValue;
2035
+ this.onChange(nextValue);
2036
+ this.valueChange.emit(nextValue);
2037
+ this.closeDropdown();
2038
+ }
2039
+ this.updateSelectionFromValue();
2040
+ }
2041
+ markTouched() {
2042
+ this.onTouched();
2043
+ }
2044
+ closeDropdown() {
2045
+ if (!this.isOpen) {
2046
+ return;
2047
+ }
2048
+ this.isOpen = false;
2049
+ this.onTouched();
2050
+ this.searchTerm = '';
2051
+ this.searchChanges$.next('');
2052
+ this.popupService.close(this.closeActivePopup);
2053
+ this.cdr.markForCheck();
2054
+ }
2055
+ onDocumentClick() {
2056
+ if (this.isOpen) {
2057
+ this.closeDropdown();
2058
+ }
2059
+ }
2060
+ onEscape() {
2061
+ if (this.isOpen) {
2062
+ this.closeDropdown();
2063
+ }
2064
+ }
2065
+ get displayLabel() {
2066
+ if (this.multiple) {
2067
+ if (!this.selectedOptions.length) {
2068
+ return this.placeholder ?? this.blankOptionLabel;
2069
+ }
2070
+ return `${this.selectedOptions.length}`;
2071
+ }
2072
+ return this.selectedOptions[0]?.label ?? this.placeholder ?? this.blankOptionLabel;
2073
+ }
2074
+ isSelected(option) {
2075
+ const value = this.getOptionValue(option);
2076
+ if (this.multiple) {
2077
+ return this.multiValue.includes(value);
2078
+ }
2079
+ return this.value === value;
2080
+ }
2081
+ getOptionValue(option) {
2082
+ return option.value ?? '';
2083
+ }
2084
+ trackOption(index, option) {
2085
+ return `${this.getOptionValue(option)}::${String(option.label || '').trim()}::${index}`;
2086
+ }
2087
+ get visibleOptions() {
2088
+ const options = this.getRenderOptions();
2089
+ if (!this.searchable || this.remoteSearch || !this.searchTerm.trim()) {
2090
+ return options;
2091
+ }
2092
+ const search = this.searchTerm.trim().toLowerCase();
2093
+ return options.filter((option) => {
2094
+ if (this.includeBlankOption && !this.multiple && this.isBlankOption(option)) {
2095
+ return true;
2096
+ }
2097
+ return String(option.label ?? '').toLowerCase().includes(search);
2098
+ });
2099
+ }
2100
+ onSearchChange(value) {
2101
+ const next = String(value ?? '');
2102
+ this.searchChanges$.next(next);
2103
+ this.searchChange.emit(next);
2104
+ }
2105
+ removeOption(option, event) {
2106
+ event.stopPropagation();
2107
+ const key = this.getOptionValue(option);
2108
+ if (this.multiple) {
2109
+ this.multiValue = this.multiValue.filter((v) => v !== key);
2110
+ this.onChange(this.multiValue);
2111
+ this.valueChange.emit(this.multiValue);
2112
+ this.updateSelectionFromValue();
2113
+ }
2114
+ }
2115
+ updateSelectionFromValue() {
2116
+ const options = this.getRenderOptions();
2117
+ if (this.multiple) {
2118
+ this.selectedOptions = options.filter((option) => this.multiValue.includes(this.getOptionValue(option)));
2119
+ return;
2120
+ }
2121
+ const current = options.find((option) => this.getOptionValue(option) === (this.value ?? ''));
2122
+ this.selectedOptions = current ? [current] : [];
2123
+ }
2124
+ getRenderOptions() {
2125
+ const merged = this.mergeOptions(this.options, this.dictionaryOptions);
2126
+ if (!this.includeBlankOption || this.multiple) {
2127
+ return merged;
2128
+ }
2129
+ return [{ label: this.blankOptionLabel, value: '' }, ...merged];
2130
+ }
2131
+ mergeOptions(...optionSets) {
2132
+ const merged = new Map();
2133
+ for (const options of optionSets) {
2134
+ for (const option of options || []) {
2135
+ const value = this.getOptionValue(option);
2136
+ const key = `${value}::${String(option.label || '').trim()}`;
2137
+ if (!merged.has(key)) {
2138
+ merged.set(key, option);
2139
+ }
2140
+ }
2141
+ }
2142
+ return [...merged.values()];
2143
+ }
2144
+ isBlankOption(option) {
2145
+ return String(option.value ?? '') === '';
2146
+ }
2147
+ getDictionaryName() {
2148
+ return String(this.data || '').trim();
2149
+ }
2150
+ reloadDictionaryOptions() {
2151
+ const dictionaryName = this.getDictionaryName();
2152
+ this.dictionaryRequestVersions.clear();
2153
+ this.dictionaryOptions = [];
2154
+ this.isDictionaryLoading = false;
2155
+ if (!dictionaryName) {
2156
+ this.updateSelectionFromValue();
2157
+ this.cdr.markForCheck();
2158
+ return;
2159
+ }
2160
+ this.loadDictionaryOptions(this.searchTerm);
2161
+ }
2162
+ loadDictionaryOptions(query) {
2163
+ const dictionaryName = this.getDictionaryName();
2164
+ if (!dictionaryName) {
2165
+ return;
2166
+ }
2167
+ const requestVersion = (this.dictionaryRequestVersions.get(dictionaryName) || 0) + 1;
2168
+ this.dictionaryRequestVersions.set(dictionaryName, requestVersion);
2169
+ this.isDictionaryLoading = true;
2170
+ this.loadSuggestItems(dictionaryName, String(query || '').trim()).subscribe({
2171
+ next: (items) => {
2172
+ if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
2173
+ return;
2174
+ }
2175
+ this.dictionaryOptions = this.normalizeSuggestItems(items);
2176
+ this.isDictionaryLoading = false;
2177
+ this.ensureSelectedDictionaryOptionsLoaded();
2178
+ this.updateSelectionFromValue();
2179
+ this.cdr.markForCheck();
2180
+ },
2181
+ error: () => {
2182
+ if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
2183
+ return;
2184
+ }
2185
+ this.dictionaryOptions = [];
2186
+ this.isDictionaryLoading = false;
2187
+ this.updateSelectionFromValue();
2188
+ this.cdr.markForCheck();
2189
+ },
2190
+ });
2191
+ }
2192
+ ensureSelectedDictionaryOptionsLoaded() {
2193
+ const dictionaryName = this.getDictionaryName();
2194
+ if (!dictionaryName) {
2195
+ return;
2196
+ }
2197
+ const existingValues = new Set(this.getRenderOptions().map((option) => this.getOptionValue(option)));
2198
+ const selectedValues = this.multiple ? this.multiValue : [this.value];
2199
+ const missingValues = selectedValues
2200
+ .map((item) => String(item || '').trim())
2201
+ .filter((item) => !!item && !existingValues.has(item));
2202
+ if (!missingValues.length) {
2203
+ return;
2204
+ }
2205
+ forkJoin(missingValues.map((value) => this.loadSuggestItem(dictionaryName, value))).subscribe({
2206
+ next: (items) => {
2207
+ const nextOptions = [...this.dictionaryOptions];
2208
+ for (const item of items) {
2209
+ const normalized = this.normalizeSuggestItem(item);
2210
+ if (!nextOptions.some((option) => this.getOptionValue(option) === normalized.value)) {
2211
+ nextOptions.push(normalized);
2212
+ }
2213
+ }
2214
+ this.dictionaryOptions = nextOptions;
2215
+ this.updateSelectionFromValue();
2216
+ this.cdr.markForCheck();
2217
+ },
2218
+ });
2219
+ }
2220
+ normalizeSuggestItems(items) {
2221
+ return (items || [])
2222
+ .map((item) => this.normalizeSuggestItem(item))
2223
+ .filter((item) => !!item.label || item.value !== null);
2297
2224
  }
2298
- markTouched() {
2299
- this.onTouched();
2225
+ normalizeSuggestItem(item) {
2226
+ return {
2227
+ label: String(item?.label ?? ''),
2228
+ value: item?.key != null ? String(item.key) : null,
2229
+ imageUrl: item?.imageId ? `/api/files/${item.imageId}/preview?scale=25` : undefined,
2230
+ };
2300
2231
  }
2301
- normalizeHexInput(value) {
2302
- const trimmed = value.trim();
2303
- if (!trimmed) {
2304
- return '';
2305
- }
2306
- return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
2232
+ loadSuggestItems(name, query) {
2233
+ const params = new URLSearchParams();
2234
+ if (query?.trim())
2235
+ params.set('q', query.trim());
2236
+ const suffix = params.toString() ? `?${params.toString()}` : '';
2237
+ return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
2307
2238
  }
2308
- isColorValue(value) {
2309
- return /^#([0-9a-fA-F]{6})$/.test(value.trim());
2239
+ loadSuggestItem(name, key) {
2240
+ return this.http.get(`/api/suggest/${encodeURIComponent(name)}/${encodeURIComponent(key)}`);
2310
2241
  }
2311
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiColorInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2312
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiColorInputComponent, isStandalone: true, selector: "forms-color-input", inputs: { label: "label", hint: "hint", name: "name", id: "id", required: "required", fullWidth: "fullWidth", error: "error" }, providers: [
2242
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2243
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputSelectComponent, isStandalone: true, selector: "input-select", inputs: { label: "label", placeholder: "placeholder", name: "name", data: "data", required: "required", fullWidth: "fullWidth", disabled: "disabled", multiple: ["multiple", "multiple", booleanAttribute], includeBlankOption: "includeBlankOption", blankOptionLabel: "blankOptionLabel", emptyStateLabel: "emptyStateLabel", searchable: "searchable", remoteSearch: "remoteSearch", searchPlaceholder: "searchPlaceholder", options: "options", error: "error", hint: "hint", triggerClass: "triggerClass", panelClass: "panelClass" }, outputs: { valueChange: "valueChange", searchChange: "searchChange" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
2313
2244
  {
2314
2245
  provide: NG_VALUE_ACCESSOR,
2315
- useExisting: forwardRef(() => UiColorInputComponent),
2246
+ useExisting: forwardRef(() => InputSelectComponent),
2316
2247
  multi: true,
2317
2248
  },
2318
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200 disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2249
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSelectComponent), multi: true },
2250
+ ], viewQueries: [{ propertyName: "dropdownTrigger", first: true, predicate: ["dropdownTrigger"], descendants: true, read: ElementRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex min-h-10 w-full flex-nowrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\" [class.-ml-2]=\"multiple\">\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md bg-slate-100 px-2 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n (keydown.enter)=\"removeOption(option, $event)\"\n (keydown.space)=\"$event.preventDefault(); removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"h-4 w-4 shrink-0\"></lucide-icon>\n </span>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"min-w-0 truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n </span>\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"shrink-0 text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"false\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayViewportMargin]=\"popupViewportMargin\"\n [cdkConnectedOverlayFlexibleDimensions]=\"true\"\n [cdkConnectedOverlayPush]=\"true\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 w-full max-w-[calc(100vw-2rem)] rounded-xl border border-slate-200 bg-white shadow-lg\"\n [style.width.px]=\"popupWidth\"\n [style.max-width]=\"'calc(100vw - 2rem)'\"\n [class]=\"panelClass\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-900 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n <div class=\"max-h-60 overflow-auto\">\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-start justify-between gap-3 whitespace-normal break-words px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"whitespace-normal break-words\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n </div>\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n", dependencies: [{ kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2319
2251
  }
2320
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiColorInputComponent, decorators: [{
2252
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSelectComponent, decorators: [{
2321
2253
  type: Component,
2322
- args: [{ selector: 'forms-color-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2254
+ args: [{ selector: 'input-select', standalone: true, imports: [LucideAngularModule, CommonModule, FormsModule, CdkConnectedOverlay, CdkOverlayOrigin], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2323
2255
  {
2324
2256
  provide: NG_VALUE_ACCESSOR,
2325
- useExisting: forwardRef(() => UiColorInputComponent),
2257
+ useExisting: forwardRef(() => InputSelectComponent),
2326
2258
  multi: true,
2327
2259
  },
2328
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200 disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n" }]
2329
- }], propDecorators: { label: [{
2260
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSelectComponent), multi: true },
2261
+ ], template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex min-h-10 w-full flex-nowrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\" [class.-ml-2]=\"multiple\">\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md bg-slate-100 px-2 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n (keydown.enter)=\"removeOption(option, $event)\"\n (keydown.space)=\"$event.preventDefault(); removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"h-4 w-4 shrink-0\"></lucide-icon>\n </span>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"min-w-0 truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n </span>\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"shrink-0 text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"false\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayViewportMargin]=\"popupViewportMargin\"\n [cdkConnectedOverlayFlexibleDimensions]=\"true\"\n [cdkConnectedOverlayPush]=\"true\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 w-full max-w-[calc(100vw-2rem)] rounded-xl border border-slate-200 bg-white shadow-lg\"\n [style.width.px]=\"popupWidth\"\n [style.max-width]=\"'calc(100vw - 2rem)'\"\n [class]=\"panelClass\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-900 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n <div class=\"max-h-60 overflow-auto\">\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-start justify-between gap-3 whitespace-normal break-words px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"whitespace-normal break-words\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n </div>\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n" }]
2262
+ }], ctorParameters: () => [], propDecorators: { label: [{
2330
2263
  type: Input
2331
- }], hint: [{
2264
+ }], placeholder: [{
2332
2265
  type: Input
2333
2266
  }], name: [{
2334
2267
  type: Input
2335
- }], id: [{
2268
+ }], data: [{
2336
2269
  type: Input
2337
2270
  }], required: [{
2338
2271
  type: Input
2339
2272
  }], fullWidth: [{
2340
2273
  type: Input
2274
+ }], disabled: [{
2275
+ type: Input
2276
+ }], multiple: [{
2277
+ type: Input,
2278
+ args: [{ transform: booleanAttribute }]
2279
+ }], includeBlankOption: [{
2280
+ type: Input
2281
+ }], blankOptionLabel: [{
2282
+ type: Input
2283
+ }], emptyStateLabel: [{
2284
+ type: Input
2285
+ }], searchable: [{
2286
+ type: Input
2287
+ }], remoteSearch: [{
2288
+ type: Input
2289
+ }], searchPlaceholder: [{
2290
+ type: Input
2291
+ }], options: [{
2292
+ type: Input
2341
2293
  }], error: [{
2342
2294
  type: Input
2295
+ }], hint: [{
2296
+ type: Input
2297
+ }], triggerClass: [{
2298
+ type: Input
2299
+ }], panelClass: [{
2300
+ type: Input
2301
+ }], valueChange: [{
2302
+ type: Output
2303
+ }], searchChange: [{
2304
+ type: Output
2305
+ }], dropdownTrigger: [{
2306
+ type: ViewChild,
2307
+ args: ['dropdownTrigger', { static: true, read: ElementRef }]
2308
+ }], onDocumentClick: [{
2309
+ type: HostListener,
2310
+ args: ['document:click']
2311
+ }], onEscape: [{
2312
+ type: HostListener,
2313
+ args: ['document:keydown.escape']
2343
2314
  }] } });
2344
2315
 
2345
- class UiSlugInputComponent {
2316
+ class InputSlugComponent {
2346
2317
  label;
2347
2318
  hint;
2348
2319
  placeholder;
@@ -2351,13 +2322,12 @@ class UiSlugInputComponent {
2351
2322
  required = false;
2352
2323
  fullWidth = false;
2353
2324
  autocomplete = 'off';
2354
- readonly = false;
2355
2325
  error;
2356
2326
  invalidInput = new EventEmitter();
2357
2327
  disabled = false;
2358
2328
  value = '';
2359
2329
  static nextId = 0;
2360
- generatedId = `ui-slug-input-${UiSlugInputComponent.nextId++}`;
2330
+ generatedId = `input-slug-${InputSlugComponent.nextId++}`;
2361
2331
  onChange = () => undefined;
2362
2332
  onTouched = () => undefined;
2363
2333
  get controlId() {
@@ -2372,6 +2342,7 @@ class UiSlugInputComponent {
2372
2342
  registerOnTouched(fn) {
2373
2343
  this.onTouched = fn;
2374
2344
  }
2345
+ validate(control) { return validateFormControl(control, this.required); }
2375
2346
  setDisabledState(isDisabled) {
2376
2347
  this.disabled = isDisabled;
2377
2348
  }
@@ -2390,24 +2361,26 @@ class UiSlugInputComponent {
2390
2361
  .replace(/\s+/g, '_')
2391
2362
  .replace(/[^a-z0-9_]/g, '');
2392
2363
  }
2393
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSlugInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2394
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiSlugInputComponent, isStandalone: true, selector: "forms-slug-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", error: "error" }, outputs: { invalidInput: "invalidInput" }, providers: [
2364
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSlugComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2365
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputSlugComponent, isStandalone: true, selector: "input-slug", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", error: "error" }, outputs: { invalidInput: "invalidInput" }, providers: [
2395
2366
  {
2396
2367
  provide: NG_VALUE_ACCESSOR,
2397
- useExisting: forwardRef(() => UiSlugInputComponent),
2368
+ useExisting: forwardRef(() => InputSlugComponent),
2398
2369
  multi: true,
2399
2370
  },
2400
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2371
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSlugComponent), multi: true },
2372
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2401
2373
  }
2402
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSlugInputComponent, decorators: [{
2374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSlugComponent, decorators: [{
2403
2375
  type: Component,
2404
- args: [{ selector: 'forms-slug-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2376
+ args: [{ selector: 'input-slug', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2405
2377
  {
2406
2378
  provide: NG_VALUE_ACCESSOR,
2407
- useExisting: forwardRef(() => UiSlugInputComponent),
2379
+ useExisting: forwardRef(() => InputSlugComponent),
2408
2380
  multi: true,
2409
2381
  },
2410
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n" }]
2382
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSlugComponent), multi: true },
2383
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n" }]
2411
2384
  }], propDecorators: { label: [{
2412
2385
  type: Input
2413
2386
  }], hint: [{
@@ -2424,19 +2397,439 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
2424
2397
  type: Input
2425
2398
  }], autocomplete: [{
2426
2399
  type: Input
2427
- }], readonly: [{
2428
- type: Input
2429
2400
  }], error: [{
2430
2401
  type: Input
2431
2402
  }], invalidInput: [{
2432
2403
  type: Output
2433
2404
  }] } });
2434
2405
 
2435
- class UiDynamicFormComponent {
2436
- formsApi = inject(MaroFormsApiService);
2406
+ const UI_FORM_INPUT_REGISTRY = {
2407
+ text: InputTextComponent,
2408
+ number: InputNumberComponent,
2409
+ date: InputDateComponent,
2410
+ datetime: InputDatetimeComponent,
2411
+ email: InputEmailComponent,
2412
+ password: InputPasswordComponent,
2413
+ textarea: InputTextareaComponent,
2414
+ select: InputSelectComponent,
2415
+ checkbox: InputCheckboxComponent,
2416
+ color: InputColorComponent,
2417
+ slug: InputSlugComponent,
2418
+ file: InputFileComponent,
2419
+ photo: InputPhotoComponent,
2420
+ barcode: InputBarcodeComponent,
2421
+ };
2422
+
2423
+ class FieldInputComponent {
2424
+ field;
2425
+ value;
2426
+ error = '';
2427
+ disabled = false;
2428
+ actions = [];
2429
+ valueChange = new EventEmitter();
2430
+ actionTriggered = new EventEmitter();
2431
+ lookupRequested = new EventEmitter();
2432
+ barcodeScanned = new EventEmitter();
2433
+ filesChange = new EventEmitter();
2434
+ uploadingChange = new EventEmitter();
2435
+ inputContainer;
2436
+ componentRef;
2437
+ fallbackViewContainer = inject(ViewContainerRef);
2438
+ viewReady = false;
2439
+ ngAfterViewInit() {
2440
+ this.viewReady = true;
2441
+ if (this.field && !this.componentRef)
2442
+ this.render();
2443
+ }
2444
+ ngOnChanges(changes) {
2445
+ if (changes['field']) {
2446
+ if (this.viewReady)
2447
+ this.render();
2448
+ return;
2449
+ }
2450
+ if (this.viewReady)
2451
+ this.syncInputs();
2452
+ }
2453
+ render() {
2454
+ const viewContainer = this.inputContainer || this.fallbackViewContainer;
2455
+ viewContainer.clear();
2456
+ const inputType = String(this.field?.type || 'text');
2457
+ const componentType = UI_FORM_INPUT_REGISTRY[inputType] || UI_FORM_INPUT_REGISTRY.text;
2458
+ this.componentRef = viewContainer.createComponent(componentType);
2459
+ const hostElement = this.componentRef.location.nativeElement;
2460
+ hostElement.style.display = 'block';
2461
+ hostElement.style.width = '100%';
2462
+ this.syncInputs();
2463
+ const instance = this.componentRef.instance;
2464
+ const hasCva = !!instance.registerOnChange;
2465
+ instance.registerOnChange?.((nextValue) => this.valueChange.emit(nextValue));
2466
+ instance.registerOnTouched?.(() => undefined);
2467
+ instance.actionTriggered?.subscribe((action) => this.actionTriggered.emit(action));
2468
+ if (!hasCva)
2469
+ instance.valueChange?.subscribe((value) => this.valueChange.emit(value));
2470
+ instance.scanned?.subscribe((value) => this.barcodeScanned.emit(value));
2471
+ instance.searchRequested?.subscribe(() => this.lookupRequested.emit());
2472
+ instance.filesChange?.subscribe((files) => this.filesChange.emit(files));
2473
+ instance.uploadingChange?.subscribe((uploading) => this.uploadingChange.emit(uploading));
2474
+ }
2475
+ syncInputs() {
2476
+ if (!this.componentRef || !this.field)
2477
+ return;
2478
+ const field = this.field;
2479
+ const setInput = (name, value) => this.componentRef?.setInput(name, value);
2480
+ const inputType = String(field.type || 'text');
2481
+ const fullWidth = field.fullWidth !== false;
2482
+ // Common presentation belongs to this wrapper, not to the concrete control.
2483
+ // Only pass inputs that are actually declared by the selected component.
2484
+ switch (inputType) {
2485
+ case 'text':
2486
+ case 'number':
2487
+ case 'date':
2488
+ case 'datetime':
2489
+ case 'email':
2490
+ case 'password':
2491
+ case 'textarea':
2492
+ setInput('name', field.id);
2493
+ setInput('id', field.id);
2494
+ setInput('placeholder', field.placeholder);
2495
+ setInput('pattern', field.pattern);
2496
+ setInput('rows', field.rows ?? 4);
2497
+ setInput('fullWidth', fullWidth);
2498
+ break;
2499
+ case 'slug':
2500
+ setInput('name', field.id);
2501
+ setInput('id', field.id);
2502
+ setInput('placeholder', field.placeholder);
2503
+ setInput('fullWidth', fullWidth);
2504
+ break;
2505
+ case 'select':
2506
+ setInput('name', field.id);
2507
+ setInput('placeholder', field.placeholder);
2508
+ setInput('data', field.data);
2509
+ setInput('options', field.options || []);
2510
+ setInput('multiple', !!field.multiple);
2511
+ setInput('searchable', field.searchable ?? !!field.data);
2512
+ setInput('remoteSearch', !!field.data);
2513
+ setInput('searchPlaceholder', field.placeholder || 'Пошук...');
2514
+ setInput('fullWidth', fullWidth);
2515
+ break;
2516
+ case 'color':
2517
+ setInput('name', field.id);
2518
+ setInput('id', field.id);
2519
+ setInput('required', false);
2520
+ setInput('fullWidth', fullWidth);
2521
+ break;
2522
+ case 'file':
2523
+ setInput('accept', field.accept);
2524
+ setInput('multiple', !!field.multiple);
2525
+ break;
2526
+ case 'photo':
2527
+ setInput('accept', field.accept || 'image/*');
2528
+ setInput('multiple', !!field.multiple);
2529
+ setInput('limit', field.limit ?? 0);
2530
+ break;
2531
+ case 'barcode':
2532
+ setInput('label', '');
2533
+ setInput('hint', '');
2534
+ setInput('placeholder', field.placeholder);
2535
+ setInput('disabled', this.disabled);
2536
+ setInput('autoOpen', field.autoOpen !== false);
2537
+ setInput('searchButtonLabel', field.searchButtonLabel || 'Знайти');
2538
+ setInput('openCameraLabel', field.openCameraLabel || 'Відкрити камеру');
2539
+ setInput('closeCameraLabel', field.closeCameraLabel || 'Закрити камеру');
2540
+ setInput('cameraTitle', field.cameraTitle || 'Камера');
2541
+ setInput('cameraStartingLabel', field.cameraStartingLabel || 'Запускаємо камеру…');
2542
+ setInput('cameraHint', field.cameraHint || 'Наведіть камеру на штрихкод. Після зчитування код підставиться автоматично.');
2543
+ setInput('value', this.value ?? '');
2544
+ break;
2545
+ case 'checkbox':
2546
+ break;
2547
+ }
2548
+ const instance = this.componentRef.instance;
2549
+ instance.writeValue?.(this.value);
2550
+ instance.setDisabledState?.(this.disabled);
2551
+ }
2552
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FieldInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2553
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FieldInputComponent, isStandalone: true, selector: "field-input", inputs: { field: "field", value: "value", error: "error", disabled: "disabled", actions: "actions" }, outputs: { valueChange: "valueChange", actionTriggered: "actionTriggered", lookupRequested: "lookupRequested", barcodeScanned: "barcodeScanned", filesChange: "filesChange", uploadingChange: "uploadingChange" }, viewQueries: [{ propertyName: "inputContainer", first: true, predicate: ["inputContainer"], descendants: true, read: ViewContainerRef }], usesOnChanges: true, ngImport: i0, template: `
2554
+ <div class="flex flex-col gap-1" [class.w-full]="field.fullWidth !== false">
2555
+ @if (field.label) {
2556
+ <label class="text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600" [attr.for]="field.id">
2557
+ {{ field.label }}
2558
+ @if (field.required) { <span aria-hidden="true" class="text-rose-500"> *</span> }
2559
+ </label>
2560
+ }
2561
+
2562
+ <div class="flex items-start gap-2" [class.rounded-xl]="!!error" [class.ring-2]="!!error" [class.ring-rose-200]="!!error">
2563
+ <div class="min-w-0 w-full flex-1">
2564
+ <ng-container #inputContainer></ng-container>
2565
+ </div>
2566
+
2567
+ @if (actions.length) {
2568
+ <div class="flex flex-none items-center gap-2 pt-0.5">
2569
+ @for (action of actions; track action.id) {
2570
+ <ui-button
2571
+ type="button"
2572
+ [variant]="action.variant ?? 'secondary'"
2573
+ [icon]="action.icon || ''"
2574
+ [size]="action.size || 'sm'"
2575
+ [disabled]="disabled"
2576
+ (click)="actionTriggered.emit(action)"
2577
+ >
2578
+ {{ action.label }}
2579
+ </ui-button>
2580
+ }
2581
+ </div>
2582
+ }
2583
+ </div>
2584
+
2585
+ @if (field.hint) { <p class="text-[0.5rem] text-slate-500">{{ field.hint }}</p> }
2586
+ @if (error) { <p class="text-[0.65rem] text-rose-500">{{ error }}</p> }
2587
+ </div>
2588
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }] });
2589
+ }
2590
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FieldInputComponent, decorators: [{
2591
+ type: Component,
2592
+ args: [{
2593
+ selector: 'field-input',
2594
+ standalone: true,
2595
+ imports: [CommonModule, UiButtonComponent],
2596
+ template: `
2597
+ <div class="flex flex-col gap-1" [class.w-full]="field.fullWidth !== false">
2598
+ @if (field.label) {
2599
+ <label class="text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600" [attr.for]="field.id">
2600
+ {{ field.label }}
2601
+ @if (field.required) { <span aria-hidden="true" class="text-rose-500"> *</span> }
2602
+ </label>
2603
+ }
2604
+
2605
+ <div class="flex items-start gap-2" [class.rounded-xl]="!!error" [class.ring-2]="!!error" [class.ring-rose-200]="!!error">
2606
+ <div class="min-w-0 w-full flex-1">
2607
+ <ng-container #inputContainer></ng-container>
2608
+ </div>
2609
+
2610
+ @if (actions.length) {
2611
+ <div class="flex flex-none items-center gap-2 pt-0.5">
2612
+ @for (action of actions; track action.id) {
2613
+ <ui-button
2614
+ type="button"
2615
+ [variant]="action.variant ?? 'secondary'"
2616
+ [icon]="action.icon || ''"
2617
+ [size]="action.size || 'sm'"
2618
+ [disabled]="disabled"
2619
+ (click)="actionTriggered.emit(action)"
2620
+ >
2621
+ {{ action.label }}
2622
+ </ui-button>
2623
+ }
2624
+ </div>
2625
+ }
2626
+ </div>
2627
+
2628
+ @if (field.hint) { <p class="text-[0.5rem] text-slate-500">{{ field.hint }}</p> }
2629
+ @if (error) { <p class="text-[0.65rem] text-rose-500">{{ error }}</p> }
2630
+ </div>
2631
+ `,
2632
+ }]
2633
+ }], propDecorators: { field: [{
2634
+ type: Input
2635
+ }], value: [{
2636
+ type: Input
2637
+ }], error: [{
2638
+ type: Input
2639
+ }], disabled: [{
2640
+ type: Input
2641
+ }], actions: [{
2642
+ type: Input
2643
+ }], valueChange: [{
2644
+ type: Output
2645
+ }], actionTriggered: [{
2646
+ type: Output
2647
+ }], lookupRequested: [{
2648
+ type: Output
2649
+ }], barcodeScanned: [{
2650
+ type: Output
2651
+ }], filesChange: [{
2652
+ type: Output
2653
+ }], uploadingChange: [{
2654
+ type: Output
2655
+ }], inputContainer: [{
2656
+ type: ViewChild,
2657
+ args: ['inputContainer', { read: ViewContainerRef }]
2658
+ }] } });
2659
+
2660
+ const isIdentifierStart = (value) => /[A-Za-z_$]/.test(value);
2661
+ const isIdentifierPart = (value) => /[A-Za-z0-9_.$-]/.test(value);
2662
+ const tokenize = (source) => {
2663
+ const tokens = [];
2664
+ let index = 0;
2665
+ while (index < source.length) {
2666
+ const char = source[index];
2667
+ if (/\s/.test(char)) {
2668
+ index += 1;
2669
+ continue;
2670
+ }
2671
+ const three = source.slice(index, index + 3);
2672
+ const two = source.slice(index, index + 2);
2673
+ if (three === '===' || three === '!==') {
2674
+ tokens.push({ type: 'operator', value: three });
2675
+ index += 3;
2676
+ continue;
2677
+ }
2678
+ if (two === '==' || two === '!=' || two === '&&' || two === '||') {
2679
+ tokens.push({ type: 'operator', value: two });
2680
+ index += 2;
2681
+ continue;
2682
+ }
2683
+ if (char === '!' || char === '(' || char === ')') {
2684
+ tokens.push({ type: 'operator', value: char });
2685
+ index += 1;
2686
+ continue;
2687
+ }
2688
+ if (char === '"' || char === "'") {
2689
+ const quote = char;
2690
+ let value = '';
2691
+ let escaped = false;
2692
+ index += 1;
2693
+ while (index < source.length) {
2694
+ const current = source[index++];
2695
+ if (escaped) {
2696
+ value += current;
2697
+ escaped = false;
2698
+ }
2699
+ else if (current === '\\') {
2700
+ escaped = true;
2701
+ }
2702
+ else if (current === quote) {
2703
+ tokens.push({ type: 'value', value });
2704
+ break;
2705
+ }
2706
+ else {
2707
+ value += current;
2708
+ }
2709
+ }
2710
+ if (source[index - 1] !== quote)
2711
+ throw new Error('Unterminated string');
2712
+ continue;
2713
+ }
2714
+ if (/[0-9.-]/.test(char)) {
2715
+ const match = source.slice(index).match(/^-?\d+(?:\.\d+)?/);
2716
+ if (match) {
2717
+ tokens.push({ type: 'value', value: Number(match[0]) });
2718
+ index += match[0].length;
2719
+ continue;
2720
+ }
2721
+ }
2722
+ if (isIdentifierStart(char)) {
2723
+ let value = char;
2724
+ index += 1;
2725
+ while (index < source.length && isIdentifierPart(source[index]))
2726
+ value += source[index++];
2727
+ if (value === 'true')
2728
+ tokens.push({ type: 'value', value: true });
2729
+ else if (value === 'false')
2730
+ tokens.push({ type: 'value', value: false });
2731
+ else if (value === 'null')
2732
+ tokens.push({ type: 'value', value: null });
2733
+ else
2734
+ tokens.push({ type: 'identifier', value });
2735
+ continue;
2736
+ }
2737
+ throw new Error(`Unexpected token: ${char}`);
2738
+ }
2739
+ return tokens;
2740
+ };
2741
+ const getPathValue = (model, path) => {
2742
+ return path.split('.').reduce((current, part) => {
2743
+ if (current == null || typeof current !== 'object')
2744
+ return undefined;
2745
+ return current[part];
2746
+ }, model);
2747
+ };
2748
+ const evaluateFormCondition = (source, model) => {
2749
+ const tokens = tokenize(String(source || '').trim());
2750
+ let index = 0;
2751
+ const peek = () => tokens[index];
2752
+ const consume = () => {
2753
+ const token = tokens[index++];
2754
+ if (!token)
2755
+ throw new Error('Unexpected end of condition');
2756
+ return token;
2757
+ };
2758
+ const matchOperator = (...values) => {
2759
+ const token = peek();
2760
+ if (token?.type !== 'operator' || !values.includes(token.value))
2761
+ return false;
2762
+ index += 1;
2763
+ return true;
2764
+ };
2765
+ const toBoolean = (value) => typeof value === 'string' ? value.trim().length > 0 : Boolean(value);
2766
+ const parseExpression = () => parseOr();
2767
+ const parseOr = () => {
2768
+ let value = parseAnd();
2769
+ while (matchOperator('||'))
2770
+ value = toBoolean(value) || toBoolean(parseAnd());
2771
+ return value;
2772
+ };
2773
+ const parseAnd = () => {
2774
+ let value = parseEquality();
2775
+ while (matchOperator('&&'))
2776
+ value = toBoolean(value) && toBoolean(parseEquality());
2777
+ return value;
2778
+ };
2779
+ const parseEquality = () => {
2780
+ let value = parseUnary();
2781
+ while (true) {
2782
+ if (matchOperator('==')) {
2783
+ value = value == parseUnary();
2784
+ continue;
2785
+ }
2786
+ if (matchOperator('!=')) {
2787
+ value = value != parseUnary();
2788
+ continue;
2789
+ }
2790
+ if (matchOperator('===')) {
2791
+ value = value === parseUnary();
2792
+ continue;
2793
+ }
2794
+ if (matchOperator('!==')) {
2795
+ value = value !== parseUnary();
2796
+ continue;
2797
+ }
2798
+ return value;
2799
+ }
2800
+ };
2801
+ const parseUnary = () => matchOperator('!') ? !toBoolean(parseUnary()) : parsePrimary();
2802
+ const parsePrimary = () => {
2803
+ if (matchOperator('(')) {
2804
+ const value = parseExpression();
2805
+ if (!matchOperator(')'))
2806
+ throw new Error('Missing closing parenthesis');
2807
+ return value;
2808
+ }
2809
+ const token = consume();
2810
+ if (token.type === 'value')
2811
+ return token.value;
2812
+ if (token.type !== 'identifier')
2813
+ throw new Error('Expected value');
2814
+ if (token.value === 'Number' && matchOperator('(')) {
2815
+ const value = Number(parseExpression());
2816
+ if (!matchOperator(')'))
2817
+ throw new Error('Missing closing parenthesis');
2818
+ return Number.isNaN(value) ? undefined : value;
2819
+ }
2820
+ return getPathValue(model, token.value);
2821
+ };
2822
+ if (!tokens.length)
2823
+ return true;
2824
+ const result = parseExpression();
2825
+ if (index !== tokens.length)
2826
+ throw new Error('Unexpected tokens after condition');
2827
+ return toBoolean(result);
2828
+ };
2829
+
2830
+ class DynamicFormComponent {
2437
2831
  lookupService = inject(UiFormLookupService);
2438
2832
  formContext = inject(UiFormContext);
2439
- optionsSourceResolvers = inject(UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, { optional: true }) ?? [];
2440
2833
  fields = [];
2441
2834
  value = {};
2442
2835
  errors = {};
@@ -2450,15 +2843,8 @@ class UiDynamicFormComponent {
2450
2843
  uploadingChange = new EventEmitter();
2451
2844
  fieldActionTriggered = new EventEmitter();
2452
2845
  modelSignal = signal({}, ...(ngDevMode ? [{ debugName: "modelSignal" }] : /* istanbul ignore next */ []));
2453
- dictionaryOptionsSignal = signal({}, ...(ngDevMode ? [{ debugName: "dictionaryOptionsSignal" }] : /* istanbul ignore next */ []));
2454
2846
  lookupLoadingSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupLoadingSignal" }] : /* istanbul ignore next */ []));
2455
2847
  lookupErrorsSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupErrorsSignal" }] : /* istanbul ignore next */ []));
2456
- selectSearchChanges = new Map();
2457
- selectSearchSubscriptions = new Map();
2458
- optionsSourceRequestVersions = new Map();
2459
- ngOnInit() {
2460
- this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
2461
- }
2462
2848
  ngOnChanges(changes) {
2463
2849
  if ('fields' in changes || 'value' in changes) {
2464
2850
  const next = { ...this.value };
@@ -2476,19 +2862,12 @@ class UiDynamicFormComponent {
2476
2862
  this.formContext.setErrors(this.errors);
2477
2863
  this.formContext.disabled.set(this.disabled);
2478
2864
  }
2479
- if ('fields' in changes || 'value' in changes) {
2480
- this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
2481
- }
2482
- }
2483
- ngOnDestroy() {
2484
- this.cleanupSelectSearchStreams();
2485
2865
  }
2486
2866
  updateField(fieldId, nextValue) {
2487
2867
  this.modelSignal.update((current) => {
2488
2868
  const updated = { ...current, [fieldId]: nextValue };
2489
2869
  this.formContext.setValue(updated);
2490
2870
  this.valueChange.emit(updated);
2491
- this.ensureOptionsSourceOptionsLoaded(updated);
2492
2871
  return updated;
2493
2872
  });
2494
2873
  }
@@ -2503,18 +2882,17 @@ class UiDynamicFormComponent {
2503
2882
  this.updateField(field.id, value);
2504
2883
  this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
2505
2884
  }
2506
- onBarcodeScanned(field, value) {
2885
+ onFieldValueChange(field, value) {
2507
2886
  this.updateField(field.id, value);
2508
- this.runFieldLookup(field, value);
2887
+ this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
2888
+ if (field.type === 'select' && field.lookupApi) {
2889
+ const lookupValue = Array.isArray(value) ? '' : String(value || '').trim();
2890
+ this.runFieldLookup(field, lookupValue);
2891
+ }
2509
2892
  }
2510
- onSelectValueChange(field, value) {
2893
+ onBarcodeScanned(field, value) {
2511
2894
  this.updateField(field.id, value);
2512
- const nextValue = Array.isArray(value) ? '' : String(value || '').trim();
2513
- if (field.lookupApi) {
2514
- this.runFieldLookup(field, nextValue);
2515
- return;
2516
- }
2517
- this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
2895
+ this.runFieldLookup(field, value);
2518
2896
  }
2519
2897
  runBarcodeLookup(field) {
2520
2898
  this.runFieldLookup(field);
@@ -2561,8 +2939,7 @@ class UiDynamicFormComponent {
2561
2939
  return true;
2562
2940
  }
2563
2941
  try {
2564
- const evaluator = new Function('model', `with (model) { return !!(${condition}); }`);
2565
- return evaluator(model);
2942
+ return evaluateFormCondition(condition, model);
2566
2943
  }
2567
2944
  catch {
2568
2945
  return true;
@@ -2582,7 +2959,7 @@ class UiDynamicFormComponent {
2582
2959
  return false;
2583
2960
  const condition = raw.startsWith('${') && raw.endsWith('}') ? raw.slice(2, -1).trim() : raw;
2584
2961
  try {
2585
- return Boolean(Function('model', `with (model) { return Boolean(${condition}); }`)(this.modelSignal()));
2962
+ return evaluateFormCondition(condition, this.modelSignal());
2586
2963
  }
2587
2964
  catch {
2588
2965
  return false;
@@ -2594,13 +2971,6 @@ class UiDynamicFormComponent {
2594
2971
  isTextArea(field) {
2595
2972
  return (field.type ?? 'text') === 'textarea';
2596
2973
  }
2597
- isInputField(field) {
2598
- const type = field.type ?? 'text';
2599
- return type === 'text' || type === 'number' || type === 'date' || type === 'datetime' || type === 'email' || type === 'password' || type === 'textarea';
2600
- }
2601
- isBarcodeField(field) {
2602
- return field.type === 'barcode';
2603
- }
2604
2974
  getStringValue(fieldId) {
2605
2975
  return String(this.modelSignal()[fieldId] || '');
2606
2976
  }
@@ -2632,8 +3002,7 @@ class UiDynamicFormComponent {
2632
3002
  : hidden;
2633
3003
  try {
2634
3004
  const model = this.modelSignal();
2635
- const evaluator = new Function('model', `with (model) { return !!(${expression}); }`);
2636
- return !evaluator(model);
3005
+ return !evaluateFormCondition(expression, model);
2637
3006
  }
2638
3007
  catch {
2639
3008
  return false;
@@ -2648,16 +3017,6 @@ class UiDynamicFormComponent {
2648
3017
  return 'datetime-local';
2649
3018
  return 'text';
2650
3019
  }
2651
- resolveSelectOptions(field) {
2652
- if (field.options?.length) {
2653
- return field.options;
2654
- }
2655
- const sourceKey = String(field.optionsSource || '').trim();
2656
- if (!sourceKey) {
2657
- return [];
2658
- }
2659
- return this.dictionaryOptionsSignal()[sourceKey] ?? [];
2660
- }
2661
3020
  renderTemplate(field) {
2662
3021
  const template = this.isTemplateField(field) ? field.template : '';
2663
3022
  if (!template) {
@@ -2730,103 +3089,6 @@ class UiDynamicFormComponent {
2730
3089
  };
2731
3090
  return classes[span] || classes[12];
2732
3091
  }
2733
- onSelectSearchChange(field, query) {
2734
- const key = String(field.optionsSource || '').trim();
2735
- if (!key)
2736
- return;
2737
- this.getSelectSearchStream(field, key).next(String(query ?? ''));
2738
- }
2739
- ensureOptionsSourceOptionsLoaded(model) {
2740
- const sourceKeys = [...new Set(this.getLeafFields()
2741
- .filter((field) => (field.type ?? 'text') === 'select' && !!field.optionsSource)
2742
- .map((field) => String(field.optionsSource || '').trim())
2743
- .filter(Boolean))];
2744
- if (!sourceKeys.length) {
2745
- return;
2746
- }
2747
- for (const sourceKey of sourceKeys) {
2748
- const resolver = this.optionsSourceResolvers.find((item) => item.source === sourceKey);
2749
- if (!resolver) {
2750
- continue;
2751
- }
2752
- const sourceFields = this.getLeafFields().filter((field) => String(field.optionsSource || '').trim() === sourceKey);
2753
- if (!sourceFields.length) {
2754
- continue;
2755
- }
2756
- const version = (this.optionsSourceRequestVersions.get(sourceKey) || 0) + 1;
2757
- this.optionsSourceRequestVersions.set(sourceKey, version);
2758
- forkJoin(sourceFields.map((field) => {
2759
- const resolved = resolver
2760
- ? resolver.resolve({ source: sourceKey, field, model })
2761
- : this.formsApi.getSuggestByName(sourceKey);
2762
- return resolved instanceof Observable ? resolved : from(Promise.resolve(resolved));
2763
- })).subscribe({
2764
- next: (results) => {
2765
- if ((this.optionsSourceRequestVersions.get(sourceKey) || 0) !== version) {
2766
- return;
2767
- }
2768
- const options = results
2769
- .flat()
2770
- .map((item) => ({
2771
- label: String(item?.label ?? ''),
2772
- value: item?.value == null ? null : String(item.value),
2773
- disabled: typeof item?.disabled === 'boolean' ? item.disabled : undefined,
2774
- imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
2775
- }))
2776
- .filter((item) => !!item.label || item.value !== null);
2777
- this.dictionaryOptionsSignal.update((current) => ({
2778
- ...current,
2779
- [sourceKey]: options,
2780
- }));
2781
- },
2782
- });
2783
- }
2784
- }
2785
- getSelectSearchStream(field, key) {
2786
- const normalizedKey = String(key || '').trim();
2787
- const existing = this.selectSearchChanges.get(normalizedKey);
2788
- if (existing) {
2789
- return existing;
2790
- }
2791
- const search$ = new Subject();
2792
- this.selectSearchChanges.set(normalizedKey, search$);
2793
- const subscription = search$
2794
- .pipe(debounceTime(300), distinctUntilChanged(), switchMap((query) => {
2795
- const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
2796
- if (resolver) {
2797
- return resolver.resolve({ source: normalizedKey, field, model: this.modelSignal(), query });
2798
- }
2799
- return this.formsApi.getSuggestByName(normalizedKey, query);
2800
- }))
2801
- .subscribe({
2802
- next: (items) => {
2803
- const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
2804
- this.dictionaryOptionsSignal.update((current) => ({
2805
- ...current,
2806
- [normalizedKey]: resolver
2807
- ? (items ?? []).map((item) => ({
2808
- label: String(item?.label ?? ''),
2809
- value: item?.value == null ? null : String(item.value),
2810
- disabled: item?.disabled,
2811
- imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
2812
- }))
2813
- : (items ?? []).map((item) => ({
2814
- label: String(item?.label ?? ''),
2815
- value: item?.['key'] != null ? String(item['key']) : null,
2816
- })),
2817
- }));
2818
- },
2819
- });
2820
- this.selectSearchSubscriptions.set(normalizedKey, subscription);
2821
- return search$;
2822
- }
2823
- cleanupSelectSearchStreams() {
2824
- for (const subscription of this.selectSearchSubscriptions.values()) {
2825
- subscription.unsubscribe();
2826
- }
2827
- this.selectSearchSubscriptions.clear();
2828
- this.selectSearchChanges.clear();
2829
- }
2830
3092
  applyLookupResponse(field, response) {
2831
3093
  const lookupMap = field.lookupMap || {};
2832
3094
  const mapped = Object.entries(lookupMap).reduce((acc, [targetField, sourcePath]) => {
@@ -2837,7 +3099,6 @@ class UiDynamicFormComponent {
2837
3099
  const updated = { ...current, ...mapped };
2838
3100
  this.formContext.setValue(updated);
2839
3101
  this.valueChange.emit(updated);
2840
- this.ensureOptionsSourceOptionsLoaded(updated);
2841
3102
  return updated;
2842
3103
  });
2843
3104
  }
@@ -2859,12 +3120,12 @@ class UiDynamicFormComponent {
2859
3120
  getLeafFields() {
2860
3121
  return this.getFlatFields().filter((field) => field.type !== 'container' && field.type !== 'template');
2861
3122
  }
2862
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2863
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDynamicFormComponent, isStandalone: true, selector: "forms-dynamic-form", inputs: { fields: "fields", value: "value", errors: "errors", submitLabel: "submitLabel", showSubmit: "showSubmit", disabled: "disabled" }, outputs: { valueChange: "valueChange", lookupCompleted: "lookupCompleted", submitted: "submitted", filesChange: "filesChange", uploadingChange: "uploadingChange", fieldActionTriggered: "fieldActionTriggered" }, providers: [UiFormContext, UiFormLookupService], usesOnChanges: true, ngImport: i0, template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n @if (isBarcodeField(field)) {\n <forms-barcode-scanner-input\n [label]=\"field.label || '\u0428\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [placeholder]=\"field.placeholder || '\u0412\u0456\u0434\u0441\u043A\u0430\u043D\u0443\u0439\u0442\u0435 \u0430\u0431\u043E \u0432\u0432\u0435\u0434\u0456\u0442\u044C \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [hint]=\"field.hint || '\u0421\u043A\u0430\u043D\u0435\u0440 \u043C\u043E\u0436\u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0432\u0432\u0435\u0441\u0442\u0438 \u043A\u043E\u0434 \u0456 \u043D\u0430\u0442\u0438\u0441\u043D\u0443\u0442\u0438 Enter.'\"\n [disabled]=\"isFieldDisabled(field) || isLookupLoading(field.id)\"\n [autoOpen]=\"field.autoOpen !== false\"\n [value]=\"getStringValue(field.id)\"\n [searchButtonLabel]=\"field.searchButtonLabel || '\u0417\u043D\u0430\u0439\u0442\u0438'\"\n [openCameraLabel]=\"field.openCameraLabel || '\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [closeCameraLabel]=\"field.closeCameraLabel || '\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [cameraTitle]=\"field.cameraTitle || '\u041A\u0430\u043C\u0435\u0440\u0430'\"\n [cameraStartingLabel]=\"field.cameraStartingLabel || '\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u0454\u043C\u043E \u043A\u0430\u043C\u0435\u0440\u0443\u2026'\"\n [cameraHint]=\"field.cameraHint || '\u041D\u0430\u0432\u0435\u0434\u0456\u0442\u044C \u043A\u0430\u043C\u0435\u0440\u0443 \u043D\u0430 \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434. \u041F\u0456\u0441\u043B\u044F \u0437\u0447\u0438\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0434 \u043F\u0456\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E.'\"\n (valueChange)=\"onBarcodeValueChange(field, $event)\"\n (searchRequested)=\"runBarcodeLookup(field)\"\n (scanned)=\"onBarcodeScanned(field, $event)\"\n ></forms-barcode-scanner-input>\n @if (getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (isInputField(field)) {\n <forms-form-input-host\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled\"\n [error]=\"errors[field.id] ?? ''\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"updateField(field.id, $any($event))\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n ></forms-form-input-host>\n }\n\n @if (field.type === 'select') {\n <forms-select\n [name]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [data]=\"field.data\"\n [required]=\"!!field.required\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [options]=\"resolveSelectOptions(field)\"\n [searchable]=\"(field.searchable ?? !!field.data) || !!field.optionsSource\"\n [remoteSearch]=\"!!field.optionsSource\"\n (searchChange)=\"field.optionsSource ? onSelectSearchChange(field, $event) : null\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"onSelectValueChange(field, $event)\">\n </forms-select>\n @if (field.lookupApi && getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (field.type === 'checkbox') {\n <forms-checkbox-input\n [label]=\"field.label\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"!!modelSignal()[field.id]\"\n (valueChange)=\"updateField(field.id, $event)\"\n ></forms-checkbox-input>\n }\n\n @if (field.type === 'color') {\n <forms-color-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-color-input>\n }\n\n @if (field.type === 'slug') {\n <forms-slug-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [readonly]=\"!!field.readonly\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-slug-input>\n }\n\n @if (field.type === 'photo') {\n <forms-photo-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept ?? 'image/*'\"\n [multiple]=\"!!field.multiple\"\n [limit]=\"field.limit ?? 0\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></forms-photo-input>\n }\n @if (field.type === 'file') {\n <forms-file-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n ></forms-file-input>\n }\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiFormInputHostComponent, selector: "forms-form-input-host", inputs: ["field", "value", "error", "disabled", "actions", "options"], outputs: ["valueChange", "actionTriggered"] }, { kind: "component", type: UiFileInputComponent, selector: "forms-file-input", inputs: ["label", "hint", "accept", "multiple", "disabled", "value"], outputs: ["valueChange", "filesChange"] }, { kind: "component", type: UiPhotoInputComponent, selector: "forms-photo-input", inputs: ["label", "hint", "accept", "multiple", "limit", "disabled", "value"], outputs: ["valueChange", "filesChange", "uploadingChange"] }, { kind: "component", type: UiCheckboxInputComponent, selector: "forms-checkbox-input", inputs: ["label", "disabled", "value"], outputs: ["valueChange"] }, { kind: "component", type: UiColorInputComponent, selector: "forms-color-input", inputs: ["label", "hint", "name", "id", "required", "fullWidth", "error"] }, { kind: "component", type: UiSlugInputComponent, selector: "forms-slug-input", inputs: ["label", "hint", "placeholder", "name", "id", "required", "fullWidth", "autocomplete", "readonly", "error"], outputs: ["invalidInput"] }, { kind: "component", type: UiBarcodeScannerInputComponent, selector: "forms-barcode-scanner-input", inputs: ["value", "label", "placeholder", "hint", "disabled", "autoOpen", "searchButtonLabel", "openCameraLabel", "closeCameraLabel", "cameraTitle", "cameraStartingLabel", "cameraHint"], outputs: ["valueChange", "scanned", "searchRequested", "cameraStateChange"] }, { kind: "component", type: UiSelectComponent, selector: "forms-select", inputs: ["label", "placeholder", "name", "data", "required", "fullWidth", "disabled", "multiple", "includeBlankOption", "blankOptionLabel", "emptyStateLabel", "searchable", "remoteSearch", "searchPlaceholder", "options", "error", "hint", "triggerClass", "panelClass"], outputs: ["valueChange", "searchChange"] }] });
3123
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3124
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: DynamicFormComponent, isStandalone: true, selector: "dynamic-form", inputs: { fields: "fields", value: "value", errors: "errors", submitLabel: "submitLabel", showSubmit: "showSubmit", disabled: "disabled" }, outputs: { valueChange: "valueChange", lookupCompleted: "lookupCompleted", submitted: "submitted", filesChange: "filesChange", uploadingChange: "uploadingChange", fieldActionTriggered: "fieldActionTriggered" }, providers: [UiFormContext, UiFormLookupService], usesOnChanges: true, ngImport: i0, template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n <field-input\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled || isFieldDisabled(field) || isLookupLoading(field.id)\"\n [error]=\"errors[field.id] ?? getLookupError(field.id)\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"onFieldValueChange(field, $event)\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n (lookupRequested)=\"runBarcodeLookup(field)\"\n (barcodeScanned)=\"onBarcodeScanned(field, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></field-input>\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: FieldInputComponent, selector: "field-input", inputs: ["field", "value", "error", "disabled", "actions"], outputs: ["valueChange", "actionTriggered", "lookupRequested", "barcodeScanned", "filesChange", "uploadingChange"] }] });
2864
3125
  }
2865
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDynamicFormComponent, decorators: [{
3126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DynamicFormComponent, decorators: [{
2866
3127
  type: Component,
2867
- args: [{ selector: 'forms-dynamic-form', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, UiFormInputHostComponent, UiFileInputComponent, UiPhotoInputComponent, UiCheckboxInputComponent, UiColorInputComponent, UiSlugInputComponent, UiBarcodeScannerInputComponent, UiSelectComponent], providers: [UiFormContext, UiFormLookupService], template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n @if (isBarcodeField(field)) {\n <forms-barcode-scanner-input\n [label]=\"field.label || '\u0428\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [placeholder]=\"field.placeholder || '\u0412\u0456\u0434\u0441\u043A\u0430\u043D\u0443\u0439\u0442\u0435 \u0430\u0431\u043E \u0432\u0432\u0435\u0434\u0456\u0442\u044C \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [hint]=\"field.hint || '\u0421\u043A\u0430\u043D\u0435\u0440 \u043C\u043E\u0436\u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0432\u0432\u0435\u0441\u0442\u0438 \u043A\u043E\u0434 \u0456 \u043D\u0430\u0442\u0438\u0441\u043D\u0443\u0442\u0438 Enter.'\"\n [disabled]=\"isFieldDisabled(field) || isLookupLoading(field.id)\"\n [autoOpen]=\"field.autoOpen !== false\"\n [value]=\"getStringValue(field.id)\"\n [searchButtonLabel]=\"field.searchButtonLabel || '\u0417\u043D\u0430\u0439\u0442\u0438'\"\n [openCameraLabel]=\"field.openCameraLabel || '\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [closeCameraLabel]=\"field.closeCameraLabel || '\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [cameraTitle]=\"field.cameraTitle || '\u041A\u0430\u043C\u0435\u0440\u0430'\"\n [cameraStartingLabel]=\"field.cameraStartingLabel || '\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u0454\u043C\u043E \u043A\u0430\u043C\u0435\u0440\u0443\u2026'\"\n [cameraHint]=\"field.cameraHint || '\u041D\u0430\u0432\u0435\u0434\u0456\u0442\u044C \u043A\u0430\u043C\u0435\u0440\u0443 \u043D\u0430 \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434. \u041F\u0456\u0441\u043B\u044F \u0437\u0447\u0438\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0434 \u043F\u0456\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E.'\"\n (valueChange)=\"onBarcodeValueChange(field, $event)\"\n (searchRequested)=\"runBarcodeLookup(field)\"\n (scanned)=\"onBarcodeScanned(field, $event)\"\n ></forms-barcode-scanner-input>\n @if (getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (isInputField(field)) {\n <forms-form-input-host\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled\"\n [error]=\"errors[field.id] ?? ''\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"updateField(field.id, $any($event))\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n ></forms-form-input-host>\n }\n\n @if (field.type === 'select') {\n <forms-select\n [name]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [data]=\"field.data\"\n [required]=\"!!field.required\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [options]=\"resolveSelectOptions(field)\"\n [searchable]=\"(field.searchable ?? !!field.data) || !!field.optionsSource\"\n [remoteSearch]=\"!!field.optionsSource\"\n (searchChange)=\"field.optionsSource ? onSelectSearchChange(field, $event) : null\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"onSelectValueChange(field, $event)\">\n </forms-select>\n @if (field.lookupApi && getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (field.type === 'checkbox') {\n <forms-checkbox-input\n [label]=\"field.label\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"!!modelSignal()[field.id]\"\n (valueChange)=\"updateField(field.id, $event)\"\n ></forms-checkbox-input>\n }\n\n @if (field.type === 'color') {\n <forms-color-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-color-input>\n }\n\n @if (field.type === 'slug') {\n <forms-slug-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [readonly]=\"!!field.readonly\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-slug-input>\n }\n\n @if (field.type === 'photo') {\n <forms-photo-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept ?? 'image/*'\"\n [multiple]=\"!!field.multiple\"\n [limit]=\"field.limit ?? 0\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></forms-photo-input>\n }\n @if (field.type === 'file') {\n <forms-file-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n ></forms-file-input>\n }\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n" }]
3128
+ args: [{ selector: 'dynamic-form', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, FieldInputComponent], providers: [UiFormContext, UiFormLookupService], template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n <field-input\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled || isFieldDisabled(field) || isLookupLoading(field.id)\"\n [error]=\"errors[field.id] ?? getLookupError(field.id)\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"onFieldValueChange(field, $event)\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n (lookupRequested)=\"runBarcodeLookup(field)\"\n (barcodeScanned)=\"onBarcodeScanned(field, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></field-input>\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n" }]
2868
3129
  }], propDecorators: { fields: [{
2869
3130
  type: Input
2870
3131
  }], value: [{
@@ -2891,7 +3152,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
2891
3152
  type: Output
2892
3153
  }] } });
2893
3154
 
2894
- class UiActionFormModalComponent {
3155
+ class FormModalComponent {
2895
3156
  formId = input('', ...(ngDevMode ? [{ debugName: "formId" }] : /* istanbul ignore next */ []));
2896
3157
  recordId = input('', ...(ngDevMode ? [{ debugName: "recordId" }] : /* istanbul ignore next */ []));
2897
3158
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
@@ -2902,6 +3163,7 @@ class UiActionFormModalComponent {
2902
3163
  completed = output();
2903
3164
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
2904
3165
  isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : /* istanbul ignore next */ []));
3166
+ isActionRunning = signal(false, ...(ngDevMode ? [{ debugName: "isActionRunning" }] : /* istanbul ignore next */ []));
2905
3167
  isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : /* istanbul ignore next */ []));
2906
3168
  formConfig = signal(null, ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
2907
3169
  formValue = signal({}, ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
@@ -2921,12 +3183,8 @@ class UiActionFormModalComponent {
2921
3183
  this.loadForm();
2922
3184
  }
2923
3185
  }
2924
- close() {
2925
- this.closed.emit();
2926
- }
2927
- onValueChange(value) {
2928
- this.formValue.set(value);
2929
- }
3186
+ close() { this.closed.emit(); }
3187
+ onValueChange(value) { this.formValue.set(value); }
2930
3188
  onLookupCompleted(event) {
2931
3189
  const redirectTo = String(this.formConfig()?.redirectTo || '').trim();
2932
3190
  if (!redirectTo)
@@ -2938,24 +3196,30 @@ class UiActionFormModalComponent {
2938
3196
  void this.router.navigateByUrl(target.startsWith('/') ? target : `/${target}`);
2939
3197
  }
2940
3198
  }
2941
- onUploadingChange(uploading) {
2942
- this.isUploading.set(uploading);
2943
- }
3199
+ onUploadingChange(uploading) { this.isUploading.set(uploading); }
2944
3200
  onFieldActionTriggered(event) {
3201
+ if (this.isActionRunning() || this.isSubmitting())
3202
+ return;
2945
3203
  const payload = this.normalizePayload({ ...this.formValue(), ...(event.action.payload || {}) });
2946
3204
  const endpoint = this.resolveTemplate(this.resolveEndpoint(event.action.api), payload);
2947
3205
  if (!endpoint)
2948
3206
  return;
3207
+ this.isActionRunning.set(true);
2949
3208
  this.http.request(this.resolveMethod(event.action.method), endpoint, { body: payload }).subscribe({
2950
3209
  next: (response) => {
2951
3210
  const data = this.extractData(response);
2952
3211
  if (data)
2953
3212
  this.formValue.update((current) => ({ ...current, ...data }));
3213
+ this.isActionRunning.set(false);
3214
+ },
3215
+ error: (error) => {
3216
+ this.isActionRunning.set(false);
3217
+ this.toast.show(error?.error?.message || 'Не вдалося виконати дію', 'error', 4000);
2954
3218
  },
2955
3219
  });
2956
3220
  }
2957
3221
  confirm() {
2958
- if (this.isSubmitting())
3222
+ if (this.isSubmitting() || this.isActionRunning() || this.isUploading())
2959
3223
  return;
2960
3224
  const action = this.submitAction();
2961
3225
  const endpointTemplate = this.formConfig()?.api || action?.api;
@@ -3003,10 +3267,7 @@ class UiActionFormModalComponent {
3003
3267
  const config = {
3004
3268
  ...raw,
3005
3269
  title: raw.title || raw.meta?.title,
3006
- actions: (raw.actions || []).map((action) => ({
3007
- ...action,
3008
- type: this.normalizeActionType(action.type),
3009
- })),
3270
+ actions: (raw.actions || []).map((action) => ({ ...action, type: this.normalizeActionType(action.type) })),
3010
3271
  };
3011
3272
  this.formConfig.set(config);
3012
3273
  this.formValue.set({ ...(config.defaults || {}), ...(config.item || {}), ...(this.initialValue() || {}), ...(recordId ? { id: recordId } : {}) });
@@ -3031,9 +3292,8 @@ class UiActionFormModalComponent {
3031
3292
  if (field.type !== 'photo' || !field.multiple)
3032
3293
  continue;
3033
3294
  const value = payload[field.id];
3034
- if (typeof value === 'string') {
3295
+ if (typeof value === 'string')
3035
3296
  payload[field.id] = value.split(',').map((item) => item.trim()).filter(Boolean);
3036
- }
3037
3297
  }
3038
3298
  return payload;
3039
3299
  }
@@ -3085,15 +3345,15 @@ class UiActionFormModalComponent {
3085
3345
  return value == null || value === '' ? '' : encodeURIComponent(String(value));
3086
3346
  });
3087
3347
  }
3088
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiActionFormModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3089
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiActionFormModalComponent, isStandalone: true, selector: "forms-action-form-modal", inputs: { formId: { classPropertyName: "formId", publicName: "formId", isSignal: true, isRequired: false, transformFunction: null }, recordId: { classPropertyName: "recordId", publicName: "recordId", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, cancelLabel: { classPropertyName: "cancelLabel", publicName: "cancelLabel", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", completed: "completed" }, usesOnChanges: true, ngImport: i0, template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiDynamicFormComponent, selector: "forms-dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
3348
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3349
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormModalComponent, isStandalone: true, selector: "form-modal", inputs: { formId: { classPropertyName: "formId", publicName: "formId", isSignal: true, isRequired: false, transformFunction: null }, recordId: { classPropertyName: "recordId", publicName: "recordId", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, cancelLabel: { classPropertyName: "cancelLabel", publicName: "cancelLabel", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", completed: "completed" }, usesOnChanges: true, ngImport: i0, template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [disabled]=\"isLoading() || isSubmitting() || isActionRunning()\" [showSubmit]=\"false\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: DynamicFormComponent, selector: "dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
3090
3350
  }
3091
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiActionFormModalComponent, decorators: [{
3351
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormModalComponent, decorators: [{
3092
3352
  type: Component,
3093
- args: [{ selector: 'forms-action-form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiDynamicFormComponent], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
3353
+ args: [{ selector: 'form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, DynamicFormComponent], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [disabled]=\"isLoading() || isSubmitting() || isActionRunning()\" [showSubmit]=\"false\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
3094
3354
  }], propDecorators: { formId: [{ type: i0.Input, args: [{ isSignal: true, alias: "formId", required: false }] }], recordId: [{ type: i0.Input, args: [{ isSignal: true, alias: "recordId", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], cancelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelLabel", required: false }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], completed: [{ type: i0.Output, args: ["completed"] }] } });
3095
3355
 
3096
- class UiInputComponent {
3356
+ class InputComponent {
3097
3357
  cdr = inject(ChangeDetectorRef);
3098
3358
  label;
3099
3359
  hint;
@@ -3106,14 +3366,13 @@ class UiInputComponent {
3106
3366
  required = false;
3107
3367
  fullWidth = false;
3108
3368
  autocomplete;
3109
- readonly = false;
3110
3369
  pattern;
3111
3370
  error;
3112
3371
  actions = [];
3113
3372
  disabled = false;
3114
3373
  value = '';
3115
3374
  static nextId = 0;
3116
- generatedId = `ui-input-${UiInputComponent.nextId++}`;
3375
+ generatedId = `input-${InputComponent.nextId++}`;
3117
3376
  actionTriggered = new EventEmitter();
3118
3377
  get controlId() {
3119
3378
  return this.id ?? this.generatedId;
@@ -3142,24 +3401,24 @@ class UiInputComponent {
3142
3401
  triggerAction(action) {
3143
3402
  this.actionTriggered.emit(action);
3144
3403
  }
3145
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3146
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiInputComponent, isStandalone: true, selector: "forms-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", type: "type", element: "element", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
3404
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3405
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputComponent, isStandalone: true, selector: "input-base", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", type: "type", element: "element", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
3147
3406
  {
3148
3407
  provide: NG_VALUE_ACCESSOR,
3149
- useExisting: forwardRef(() => UiInputComponent),
3408
+ useExisting: forwardRef(() => InputComponent),
3150
3409
  multi: true,
3151
3410
  },
3152
- ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3411
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3153
3412
  }
3154
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiInputComponent, decorators: [{
3413
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputComponent, decorators: [{
3155
3414
  type: Component,
3156
- args: [{ selector: 'forms-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
3415
+ args: [{ selector: 'input-base', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
3157
3416
  {
3158
3417
  provide: NG_VALUE_ACCESSOR,
3159
- useExisting: forwardRef(() => UiInputComponent),
3418
+ useExisting: forwardRef(() => InputComponent),
3160
3419
  multi: true,
3161
3420
  },
3162
- ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
3421
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n" }]
3163
3422
  }], propDecorators: { label: [{
3164
3423
  type: Input
3165
3424
  }], hint: [{
@@ -3182,8 +3441,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
3182
3441
  type: Input
3183
3442
  }], autocomplete: [{
3184
3443
  type: Input
3185
- }], readonly: [{
3186
- type: Input
3187
3444
  }], pattern: [{
3188
3445
  type: Input
3189
3446
  }], error: [{
@@ -3194,7 +3451,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
3194
3451
  type: Output
3195
3452
  }] } });
3196
3453
 
3197
- class UiPhotoGalleryFieldComponent {
3454
+ class InputPhotoGalleryComponent {
3198
3455
  http = inject(HttpClient);
3199
3456
  label = 'Фото';
3200
3457
  orderLabel = 'Порядок фото (перетягуй)';
@@ -3336,12 +3593,12 @@ class UiPhotoGalleryFieldComponent {
3336
3593
  formData.append('file', file);
3337
3594
  return this.http.post('/api/files/', formData).pipe(map((response) => ({ imageId: response.id })));
3338
3595
  }
3339
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoGalleryFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3340
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPhotoGalleryFieldComponent, isStandalone: true, selector: "forms-photo-gallery-field", inputs: { label: "label", orderLabel: "orderLabel", imageIds: "imageIds", disabled: "disabled", afterUpload: "afterUpload", resolveImageUrl: "resolveImageUrl", notify: "notify" }, outputs: { imageIdsChange: "imageIdsChange" }, usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
3596
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoGalleryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3597
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputPhotoGalleryComponent, isStandalone: true, selector: "input-photo-gallery", inputs: { label: "label", orderLabel: "orderLabel", imageIds: "imageIds", disabled: "disabled", afterUpload: "afterUpload", resolveImageUrl: "resolveImageUrl", notify: "notify" }, outputs: { imageIdsChange: "imageIdsChange" }, usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
3341
3598
  }
3342
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoGalleryFieldComponent, decorators: [{
3599
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoGalleryComponent, decorators: [{
3343
3600
  type: Component,
3344
- args: [{ selector: 'forms-photo-gallery-field', standalone: true, imports: [CommonModule, DragDropModule], template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"] }]
3601
+ args: [{ selector: 'input-photo-gallery', standalone: true, imports: [CommonModule, DragDropModule], template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"] }]
3345
3602
  }], propDecorators: { label: [{
3346
3603
  type: Input
3347
3604
  }], orderLabel: [{
@@ -3360,7 +3617,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
3360
3617
  type: Output
3361
3618
  }] } });
3362
3619
 
3363
- class UiRangeSliderComponent {
3620
+ class InputRangeSliderComponent {
3364
3621
  min = 0;
3365
3622
  max = 100;
3366
3623
  step = 1;
@@ -3449,8 +3706,8 @@ class UiRangeSliderComponent {
3449
3706
  input.step = String(this.safeStep);
3450
3707
  input.value = String(value);
3451
3708
  }
3452
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiRangeSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3453
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiRangeSliderComponent, isStandalone: true, selector: "forms-range-slider", inputs: { min: "min", max: "max", step: "step", startValue: "startValue", endValue: "endValue", startLabel: "startLabel", endLabel: "endLabel" }, outputs: { startValueChange: "startValueChange", endValueChange: "endValueChange", interactionEnd: "interactionEnd" }, viewQueries: [{ propertyName: "startInput", first: true, predicate: ["startInput"], descendants: true }, { propertyName: "endInput", first: true, predicate: ["endInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
3709
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputRangeSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3710
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: InputRangeSliderComponent, isStandalone: true, selector: "input-range-slider", inputs: { min: "min", max: "max", step: "step", startValue: "startValue", endValue: "endValue", startLabel: "startLabel", endLabel: "endLabel" }, outputs: { startValueChange: "startValueChange", endValueChange: "endValueChange", interactionEnd: "interactionEnd" }, viewQueries: [{ propertyName: "startInput", first: true, predicate: ["startInput"], descendants: true }, { propertyName: "endInput", first: true, predicate: ["endInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
3454
3711
  <div class="range-slider">
3455
3712
  <div class="range-slider__track"></div>
3456
3713
  <div
@@ -3485,9 +3742,9 @@ class UiRangeSliderComponent {
3485
3742
  </div>
3486
3743
  `, isInline: true, styles: [".range-slider{position:relative;height:2rem;width:100%;display:flex;align-items:center}.range-slider__track,.range-slider__range{position:absolute;left:0;right:0;height:6px;border-radius:999px}.range-slider__track{background:#dbe1ea}.range-slider__range{background:linear-gradient(90deg,#334155,#0f172a)}.range-slider__input{pointer-events:none;position:absolute;left:0;width:100%;height:2rem;margin:0;background:transparent;appearance:none;-webkit-appearance:none}.range-slider__input::-webkit-slider-runnable-track{height:6px;background:transparent}.range-slider__input::-moz-range-track{height:6px;background:transparent}.range-slider__input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:20px;height:20px;margin-top:-7px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}.range-slider__input::-moz-range-thumb{pointer-events:auto;width:20px;height:20px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
3487
3744
  }
3488
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiRangeSliderComponent, decorators: [{
3745
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputRangeSliderComponent, decorators: [{
3489
3746
  type: Component,
3490
- args: [{ selector: 'forms-range-slider', standalone: true, imports: [CommonModule], template: `
3747
+ args: [{ selector: 'input-range-slider', standalone: true, imports: [CommonModule], template: `
3491
3748
  <div class="range-slider">
3492
3749
  <div class="range-slider__track"></div>
3493
3750
  <div
@@ -3553,5 +3810,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
3553
3810
  * Generated bundle index. Do not edit.
3554
3811
  */
3555
3812
 
3556
- export { ACTION_API, ACTION_FORM, ACTION_PRINT, ACTION_SUBMIT, MaroFormsApiService, UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, UI_FORM_INPUT_REGISTRY, UiActionFormModalComponent, UiBarcodeScannerInputComponent, UiCheckboxInputComponent, UiColorInputComponent, UiDateInputComponent, UiDatetimeInputComponent, UiDynamicFormComponent, UiEmailInputComponent, UiFileInputComponent, UiFormContext, UiFormInputHostComponent, UiFormLookupService, UiInputComponent, UiNumberInputComponent, UiPasswordInputComponent, UiPhotoGalleryFieldComponent, UiPhotoInputComponent, UiRangeSliderComponent, UiSelectComponent, UiSlugInputComponent, UiTextInputComponent, UiTextareaInputComponent, flattenDynamicFormFields, isDynamicFormContainerField, isDynamicFormTemplateField };
3813
+ export { ACTION_API, ACTION_FORM, ACTION_PRINT, ACTION_SUBMIT, DynamicFormComponent, FieldInputComponent, FormModalComponent, InputBarcodeComponent, InputCheckboxComponent, InputColorComponent, InputComponent, InputDateComponent, InputDatetimeComponent, InputEmailComponent, InputFileComponent, InputNumberComponent, InputPasswordComponent, InputPhotoComponent, InputPhotoGalleryComponent, InputRangeSliderComponent, InputSelectComponent, InputSlugComponent, InputTextComponent, InputTextareaComponent, MaroFormsApiService, UI_FORM_INPUT_REGISTRY, UiFormContext, UiFormLookupService, flattenDynamicFormFields, isDynamicFormContainerField, isDynamicFormTemplateField };
3557
3814
  //# sourceMappingURL=maro-tech-form.mjs.map