@keepui/ui 0.3.0 → 0.5.0

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.
Files changed (32) hide show
  1. package/README.md +741 -276
  2. package/fesm2022/keepui-ui.mjs +1509 -2
  3. package/fesm2022/keepui-ui.mjs.map +1 -1
  4. package/lib/components/signal-dropdown/signal-dropdown.component.d.ts +91 -0
  5. package/lib/components/signal-dropdown/signal-dropdown.component.d.ts.map +1 -0
  6. package/lib/components/signal-dropdown/signal-dropdown.types.d.ts +12 -0
  7. package/lib/components/signal-dropdown/signal-dropdown.types.d.ts.map +1 -0
  8. package/lib/components/signal-text-input/signal-text-input.component.d.ts +101 -0
  9. package/lib/components/signal-text-input/signal-text-input.component.d.ts.map +1 -0
  10. package/lib/components/signal-text-input/signal-text-input.types.d.ts +8 -0
  11. package/lib/components/signal-text-input/signal-text-input.types.d.ts.map +1 -0
  12. package/lib/components/signal-textarea/signal-textarea.component.d.ts +70 -0
  13. package/lib/components/signal-textarea/signal-textarea.component.d.ts.map +1 -0
  14. package/lib/components/signal-textarea/signal-textarea.types.d.ts +11 -0
  15. package/lib/components/signal-textarea/signal-textarea.types.d.ts.map +1 -0
  16. package/lib/components/stepper/stepper.component.d.ts +67 -0
  17. package/lib/components/stepper/stepper.component.d.ts.map +1 -0
  18. package/lib/components/stepper/stepper.types.d.ts +16 -0
  19. package/lib/components/stepper/stepper.types.d.ts.map +1 -0
  20. package/lib/components/tab-group/tab-group.component.d.ts +52 -0
  21. package/lib/components/tab-group/tab-group.component.d.ts.map +1 -0
  22. package/lib/components/tab-group/tab-group.types.d.ts +14 -0
  23. package/lib/components/tab-group/tab-group.types.d.ts.map +1 -0
  24. package/lib/i18n/keep-ui-translations.d.ts +4 -0
  25. package/lib/i18n/keep-ui-translations.d.ts.map +1 -1
  26. package/lib/i18n/translation-keys.d.ts +4 -0
  27. package/lib/i18n/translation-keys.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/public-api.d.ts +10 -0
  30. package/public-api.d.ts.map +1 -1
  31. package/styles/index.css +1 -1
  32. package/styles/prebuilt.css +1 -1
@@ -1,6 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, input, output, computed, ChangeDetectionStrategy, Component, inject, signal, makeEnvironmentProviders } from '@angular/core';
2
+ import { InjectionToken, Injectable, input, output, computed, ChangeDetectionStrategy, Component, inject, signal, DestroyRef, viewChild, model, effect, viewChildren, makeEnvironmentProviders } from '@angular/core';
3
3
  import { TranslocoPipe, TRANSLOCO_SCOPE, TranslocoService, provideTransloco } from '@jsverse/transloco';
4
+ import { NgTemplateOutlet } from '@angular/common';
4
5
  import { of } from 'rxjs';
5
6
 
6
7
  /**
@@ -551,6 +552,10 @@ const KEEPUI_TRANSLATION_KEYS = {
551
552
  PREVIEW_ALT: 'imagePreview.previewAlt',
552
553
  ERROR_UNEXPECTED: 'imagePreview.errorUnexpected',
553
554
  },
555
+ SIGNAL_TEXT_INPUT: {
556
+ SHOW_PASSWORD: 'signalTextInput.showPassword',
557
+ HIDE_PASSWORD: 'signalTextInput.hidePassword',
558
+ },
554
559
  };
555
560
  /** Ordered list of languages available in KeepUI. */
556
561
  const KEEPUI_AVAILABLE_LANGUAGES = [
@@ -690,6 +695,1496 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
690
695
  }]
691
696
  }] });
692
697
 
698
+ const DROPDOWN_ITEM_HEIGHT = 40;
699
+ const DROPDOWN_PADDING = 8;
700
+ const DROPDOWN_GAP = 8;
701
+ /**
702
+ * Signal-based accessible dropdown / select component.
703
+ *
704
+ * Fully platform-agnostic — no native API usage. The panel opens in `fixed`
705
+ * position so it is never clipped by overflow-hidden ancestors. It repositions
706
+ * itself automatically on scroll and resize.
707
+ *
708
+ * The `value` and `touched` properties are `model()` signals so the component
709
+ * integrates seamlessly with Angular signal-based forms.
710
+ *
711
+ * ```html
712
+ * <keepui-signal-dropdown
713
+ * label="País"
714
+ * placeholder="Selecciona un país"
715
+ * [options]="countries"
716
+ * [(value)]="selectedCountry"
717
+ * />
718
+ * ```
719
+ *
720
+ * @typeParam T – type of each option's `value` property. Defaults to `string`.
721
+ */
722
+ class SignalDropdownComponent {
723
+ constructor() {
724
+ this.destroyRef = inject(DestroyRef);
725
+ this.isOpen = signal(false);
726
+ this.openUpwards = signal(false);
727
+ this.panelTopPx = signal(0);
728
+ this.panelLeftPx = signal(0);
729
+ this.panelWidthPx = signal(0);
730
+ this.dropdownButton = viewChild('dropdownButton');
731
+ this.dropdownContainer = viewChild('dropdownContainer');
732
+ /** Optional label text rendered above the dropdown. */
733
+ this.label = input('');
734
+ /** Placeholder shown when no value is selected. */
735
+ this.placeholder = input('');
736
+ /** Array of options to display in the panel. */
737
+ this.options = input.required();
738
+ /** Layout width of the wrapper. @default 'full' */
739
+ this.width = input('full');
740
+ /** Marks the field as required. Adds `aria-required` and a visual asterisk. @default false */
741
+ this.required = input(false);
742
+ /** Human-readable error message shown below the dropdown. Takes precedence over `errors[0]`. */
743
+ this.errorMessage = input('');
744
+ /**
745
+ * Array of error strings. The first item is displayed when `errorMessage` is empty.
746
+ * Set together with `invalid=true` to trigger the error state.
747
+ */
748
+ this.errors = input([]);
749
+ /**
750
+ * Stable `id` used to link the `<label>` with the trigger `<button>`.
751
+ * A random suffix is generated by default.
752
+ */
753
+ this.selectId = input(`ku-dropdown-${Math.random().toString(36).slice(2, 8)}`);
754
+ /** Disables the dropdown. @default false */
755
+ this.disabled = input(false);
756
+ /**
757
+ * Forces the error visual state regardless of the `touched` model.
758
+ * Useful for external form validation. @default false
759
+ */
760
+ this.invalid = input(false);
761
+ /** Currently selected value. Use `[(value)]` for two-way binding. */
762
+ this.value = model(null);
763
+ /** Whether the control has been interacted with. Use `[(touched)]` for two-way binding. */
764
+ this.touched = model(false);
765
+ /** Emitted when the selected value changes. */
766
+ this.valueChange = output();
767
+ this.errorId = computed(() => `${this.selectId()}-error`);
768
+ this.panelStyle = computed(() => `top:${this.panelTopPx()}px;left:${this.panelLeftPx()}px;width:${this.panelWidthPx()}px`);
769
+ this.widthClass = computed(() => {
770
+ const map = {
771
+ full: 'w-full',
772
+ half: 'w-1/2',
773
+ auto: 'w-auto',
774
+ };
775
+ return map[this.width()];
776
+ });
777
+ this.selectedLabel = computed(() => {
778
+ const current = this.value();
779
+ if (current === null)
780
+ return this.placeholder();
781
+ return this.options().find(o => o.value === current)?.label ?? this.placeholder();
782
+ });
783
+ this.selectedBadges = computed(() => {
784
+ const current = this.value();
785
+ if (current === null)
786
+ return [];
787
+ return this.options().find(o => o.value === current)?.badges ?? [];
788
+ });
789
+ this.showError = computed(() => this.touched() && (this.invalid() || this.errorMessage().length > 0 || this.errors().length > 0));
790
+ this.recalculatePosition = () => {
791
+ if (this.isOpen())
792
+ this.calculateDropdownPosition();
793
+ };
794
+ effect(() => {
795
+ if (this.isOpen()) {
796
+ this.calculateDropdownPosition();
797
+ this.addViewportListeners();
798
+ }
799
+ else {
800
+ this.removeViewportListeners();
801
+ }
802
+ });
803
+ this.destroyRef.onDestroy(() => this.removeViewportListeners());
804
+ }
805
+ onDocumentClick(event) {
806
+ const container = this.dropdownContainer()?.nativeElement;
807
+ if (container && !container.contains(event.target)) {
808
+ this.isOpen.set(false);
809
+ }
810
+ }
811
+ toggleDropdown() {
812
+ if (this.disabled())
813
+ return;
814
+ this.isOpen.update(open => !open);
815
+ }
816
+ close() {
817
+ if (this.isOpen()) {
818
+ this.isOpen.set(false);
819
+ this.dropdownButton()?.nativeElement.focus();
820
+ }
821
+ }
822
+ selectOption(option) {
823
+ this.value.set(option.value);
824
+ this.isOpen.set(false);
825
+ this.dropdownButton()?.nativeElement.focus();
826
+ this.valueChange.emit(option.value);
827
+ }
828
+ onBlur() {
829
+ this.touched.set(true);
830
+ }
831
+ isSelected(optionValue) {
832
+ return this.value() === optionValue;
833
+ }
834
+ onButtonKeydown(event) {
835
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
836
+ event.preventDefault();
837
+ if (!this.isOpen()) {
838
+ this.isOpen.set(true);
839
+ }
840
+ }
841
+ }
842
+ onOptionKeydown(event, option, index) {
843
+ const panel = this.dropdownContainer()?.nativeElement;
844
+ const optionButtons = panel?.querySelectorAll('[role="option"]');
845
+ if (!optionButtons)
846
+ return;
847
+ if (event.key === 'ArrowDown') {
848
+ event.preventDefault();
849
+ optionButtons[Math.min(index + 1, optionButtons.length - 1)]?.focus();
850
+ }
851
+ else if (event.key === 'ArrowUp') {
852
+ event.preventDefault();
853
+ if (index === 0) {
854
+ this.dropdownButton()?.nativeElement.focus();
855
+ }
856
+ else {
857
+ optionButtons[index - 1]?.focus();
858
+ }
859
+ }
860
+ else if (event.key === 'Enter' || event.key === ' ') {
861
+ event.preventDefault();
862
+ this.selectOption(option);
863
+ }
864
+ }
865
+ calculateDropdownPosition() {
866
+ const button = this.dropdownButton()?.nativeElement;
867
+ if (!button)
868
+ return;
869
+ const rect = button.getBoundingClientRect();
870
+ const estimatedHeight = Math.min(this.options().length * DROPDOWN_ITEM_HEIGHT + DROPDOWN_PADDING, 256);
871
+ const spaceBelow = window.innerHeight - rect.bottom;
872
+ const spaceAbove = rect.top;
873
+ const shouldOpenUpwards = spaceBelow < estimatedHeight + DROPDOWN_GAP &&
874
+ spaceAbove > estimatedHeight + DROPDOWN_GAP;
875
+ this.openUpwards.set(shouldOpenUpwards);
876
+ this.panelWidthPx.set(rect.width);
877
+ this.panelLeftPx.set(rect.left);
878
+ this.panelTopPx.set(shouldOpenUpwards
879
+ ? rect.top - estimatedHeight - DROPDOWN_GAP
880
+ : rect.bottom + DROPDOWN_GAP);
881
+ }
882
+ addViewportListeners() {
883
+ this.removeViewportListeners();
884
+ window.addEventListener('scroll', this.recalculatePosition, true);
885
+ window.addEventListener('resize', this.recalculatePosition);
886
+ }
887
+ removeViewportListeners() {
888
+ window.removeEventListener('scroll', this.recalculatePosition, true);
889
+ window.removeEventListener('resize', this.recalculatePosition);
890
+ }
891
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
892
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: SignalDropdownComponent, isStandalone: true, selector: "keepui-signal-dropdown", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, selectId: { classPropertyName: "selectId", publicName: "selectId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", valueChange: "valueChange" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "close()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "dropdownButton", first: true, predicate: ["dropdownButton"], descendants: true, isSignal: true }, { propertyName: "dropdownContainer", first: true, predicate: ["dropdownContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
893
+ <div class="flex flex-col gap-1" [class]="widthClass()">
894
+
895
+ @if (label()) {
896
+ <label [for]="selectId()" class="text-sm text-ku-gray-text">
897
+ {{ label() }}
898
+ @if (required()) {
899
+ <span class="text-ku-error-primary" aria-hidden="true"> *</span>
900
+ }
901
+ </label>
902
+ }
903
+
904
+ <div class="relative" #dropdownContainer>
905
+
906
+ <button
907
+ #dropdownButton
908
+ [id]="selectId()"
909
+ type="button"
910
+ [disabled]="disabled()"
911
+ [attr.aria-disabled]="disabled() ? true : null"
912
+ [attr.aria-expanded]="isOpen()"
913
+ [attr.aria-haspopup]="'listbox'"
914
+ [attr.aria-required]="required() ? true : null"
915
+ [attr.aria-invalid]="showError() ? true : null"
916
+ [attr.aria-describedby]="showError() ? errorId() : null"
917
+ (click)="toggleDropdown()"
918
+ (blur)="onBlur()"
919
+ (keydown)="onButtonKeydown($event)"
920
+ class="w-full inline-flex items-center gap-2 bg-ku-primary border rounded-xl
921
+ px-4 py-2.5 text-sm transition-all cursor-pointer min-h-[2.75rem]
922
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ku-action-primary
923
+ focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
924
+ [class]="showError() ? 'border-ku-error-primary' : 'border-ku-secondary-border'"
925
+ >
926
+ <span
927
+ class="flex-1 text-left flex items-center justify-between gap-2 min-w-0"
928
+ [class]="value() === null ? 'text-ku-gray-text opacity-70' : 'text-ku-primary-text'"
929
+ >
930
+ <span class="truncate">{{ selectedLabel() }}</span>
931
+
932
+ @if (selectedBadges().length) {
933
+ <span class="flex flex-wrap items-center gap-1.5 shrink-0">
934
+ @for (badge of selectedBadges(); track badge) {
935
+ <span class="inline-flex items-center rounded-full bg-ku-action-background
936
+ px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
937
+ {{ badge }}
938
+ </span>
939
+ }
940
+ </span>
941
+ }
942
+ </span>
943
+
944
+ <keepui-icon
945
+ name="chevron-down-icon"
946
+ [size]="18"
947
+ aria-hidden="true"
948
+ class="text-ku-gray-text transition-transform duration-200 shrink-0"
949
+ [class.rotate-180]="isOpen()"
950
+ />
951
+ </button>
952
+
953
+ @if (isOpen()) {
954
+ <div
955
+ role="listbox"
956
+ [attr.aria-label]="label() || null"
957
+ class="fixed rounded-xl shadow-lg bg-ku-primary border border-ku-secondary-border
958
+ z-[1000] overflow-y-auto max-h-64"
959
+ [style]="panelStyle()"
960
+ >
961
+ @for (option of options(); track option.value; let i = $index) {
962
+ <button
963
+ type="button"
964
+ role="option"
965
+ [attr.aria-selected]="isSelected(option.value)"
966
+ (click)="selectOption(option)"
967
+ (keydown)="onOptionKeydown($event, option, i)"
968
+ class="w-full text-left cursor-pointer px-4 py-2 text-sm text-ku-primary-text
969
+ flex items-center gap-3 transition-colors min-h-[2.75rem]
970
+ hover:bg-ku-primary-hover focus-visible:outline-none
971
+ focus-visible:bg-ku-primary-hover"
972
+ [class]="isSelected(option.value) ? 'bg-ku-primary-hover' : ''"
973
+ >
974
+ <span class="flex-1 flex items-center justify-between gap-2 min-w-0">
975
+ <span class="truncate">{{ option.label }}</span>
976
+
977
+ @if (option.badges?.length) {
978
+ <span class="flex flex-wrap items-center gap-1.5 shrink-0">
979
+ @for (badge of option.badges ?? []; track badge) {
980
+ <span class="inline-flex items-center rounded-full bg-ku-action-background
981
+ px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
982
+ {{ badge }}
983
+ </span>
984
+ }
985
+ </span>
986
+ }
987
+ </span>
988
+
989
+ @if (isSelected(option.value)) {
990
+ <keepui-icon
991
+ name="check-icon"
992
+ [size]="16"
993
+ aria-hidden="true"
994
+ class="text-ku-action-primary shrink-0"
995
+ />
996
+ }
997
+ </button>
998
+ }
999
+ </div>
1000
+ }
1001
+
1002
+ </div>
1003
+
1004
+ @if (showError()) {
1005
+ <span
1006
+ [id]="errorId()"
1007
+ class="text-sm text-ku-error-primary"
1008
+ role="alert"
1009
+ >
1010
+ @if (errorMessage()) {
1011
+ {{ errorMessage() }}
1012
+ } @else if (errors().length > 0) {
1013
+ {{ errors()[0] }}
1014
+ }
1015
+ </span>
1016
+ }
1017
+
1018
+ </div>
1019
+ `, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1020
+ }
1021
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalDropdownComponent, decorators: [{
1022
+ type: Component,
1023
+ args: [{
1024
+ selector: 'keepui-signal-dropdown',
1025
+ standalone: true,
1026
+ changeDetection: ChangeDetectionStrategy.OnPush,
1027
+ imports: [IconComponent],
1028
+ host: {
1029
+ class: 'block',
1030
+ '(document:click)': 'onDocumentClick($event)',
1031
+ '(document:keydown.escape)': 'close()',
1032
+ },
1033
+ template: `
1034
+ <div class="flex flex-col gap-1" [class]="widthClass()">
1035
+
1036
+ @if (label()) {
1037
+ <label [for]="selectId()" class="text-sm text-ku-gray-text">
1038
+ {{ label() }}
1039
+ @if (required()) {
1040
+ <span class="text-ku-error-primary" aria-hidden="true"> *</span>
1041
+ }
1042
+ </label>
1043
+ }
1044
+
1045
+ <div class="relative" #dropdownContainer>
1046
+
1047
+ <button
1048
+ #dropdownButton
1049
+ [id]="selectId()"
1050
+ type="button"
1051
+ [disabled]="disabled()"
1052
+ [attr.aria-disabled]="disabled() ? true : null"
1053
+ [attr.aria-expanded]="isOpen()"
1054
+ [attr.aria-haspopup]="'listbox'"
1055
+ [attr.aria-required]="required() ? true : null"
1056
+ [attr.aria-invalid]="showError() ? true : null"
1057
+ [attr.aria-describedby]="showError() ? errorId() : null"
1058
+ (click)="toggleDropdown()"
1059
+ (blur)="onBlur()"
1060
+ (keydown)="onButtonKeydown($event)"
1061
+ class="w-full inline-flex items-center gap-2 bg-ku-primary border rounded-xl
1062
+ px-4 py-2.5 text-sm transition-all cursor-pointer min-h-[2.75rem]
1063
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ku-action-primary
1064
+ focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
1065
+ [class]="showError() ? 'border-ku-error-primary' : 'border-ku-secondary-border'"
1066
+ >
1067
+ <span
1068
+ class="flex-1 text-left flex items-center justify-between gap-2 min-w-0"
1069
+ [class]="value() === null ? 'text-ku-gray-text opacity-70' : 'text-ku-primary-text'"
1070
+ >
1071
+ <span class="truncate">{{ selectedLabel() }}</span>
1072
+
1073
+ @if (selectedBadges().length) {
1074
+ <span class="flex flex-wrap items-center gap-1.5 shrink-0">
1075
+ @for (badge of selectedBadges(); track badge) {
1076
+ <span class="inline-flex items-center rounded-full bg-ku-action-background
1077
+ px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
1078
+ {{ badge }}
1079
+ </span>
1080
+ }
1081
+ </span>
1082
+ }
1083
+ </span>
1084
+
1085
+ <keepui-icon
1086
+ name="chevron-down-icon"
1087
+ [size]="18"
1088
+ aria-hidden="true"
1089
+ class="text-ku-gray-text transition-transform duration-200 shrink-0"
1090
+ [class.rotate-180]="isOpen()"
1091
+ />
1092
+ </button>
1093
+
1094
+ @if (isOpen()) {
1095
+ <div
1096
+ role="listbox"
1097
+ [attr.aria-label]="label() || null"
1098
+ class="fixed rounded-xl shadow-lg bg-ku-primary border border-ku-secondary-border
1099
+ z-[1000] overflow-y-auto max-h-64"
1100
+ [style]="panelStyle()"
1101
+ >
1102
+ @for (option of options(); track option.value; let i = $index) {
1103
+ <button
1104
+ type="button"
1105
+ role="option"
1106
+ [attr.aria-selected]="isSelected(option.value)"
1107
+ (click)="selectOption(option)"
1108
+ (keydown)="onOptionKeydown($event, option, i)"
1109
+ class="w-full text-left cursor-pointer px-4 py-2 text-sm text-ku-primary-text
1110
+ flex items-center gap-3 transition-colors min-h-[2.75rem]
1111
+ hover:bg-ku-primary-hover focus-visible:outline-none
1112
+ focus-visible:bg-ku-primary-hover"
1113
+ [class]="isSelected(option.value) ? 'bg-ku-primary-hover' : ''"
1114
+ >
1115
+ <span class="flex-1 flex items-center justify-between gap-2 min-w-0">
1116
+ <span class="truncate">{{ option.label }}</span>
1117
+
1118
+ @if (option.badges?.length) {
1119
+ <span class="flex flex-wrap items-center gap-1.5 shrink-0">
1120
+ @for (badge of option.badges ?? []; track badge) {
1121
+ <span class="inline-flex items-center rounded-full bg-ku-action-background
1122
+ px-2 py-0.5 text-[11px] font-medium text-ku-action-primary">
1123
+ {{ badge }}
1124
+ </span>
1125
+ }
1126
+ </span>
1127
+ }
1128
+ </span>
1129
+
1130
+ @if (isSelected(option.value)) {
1131
+ <keepui-icon
1132
+ name="check-icon"
1133
+ [size]="16"
1134
+ aria-hidden="true"
1135
+ class="text-ku-action-primary shrink-0"
1136
+ />
1137
+ }
1138
+ </button>
1139
+ }
1140
+ </div>
1141
+ }
1142
+
1143
+ </div>
1144
+
1145
+ @if (showError()) {
1146
+ <span
1147
+ [id]="errorId()"
1148
+ class="text-sm text-ku-error-primary"
1149
+ role="alert"
1150
+ >
1151
+ @if (errorMessage()) {
1152
+ {{ errorMessage() }}
1153
+ } @else if (errors().length > 0) {
1154
+ {{ errors()[0] }}
1155
+ }
1156
+ </span>
1157
+ }
1158
+
1159
+ </div>
1160
+ `,
1161
+ }]
1162
+ }], ctorParameters: () => [] });
1163
+
1164
+ /**
1165
+ * Signal-based accessible text input supporting all common HTML input types,
1166
+ * leading/trailing icons, a trailing content slot, and a built-in
1167
+ * password-visibility toggle (when `type="password"`).
1168
+ *
1169
+ * The `value` and `touched` properties are `model()` signals so the component
1170
+ * integrates seamlessly with Angular signal-based forms.
1171
+ *
1172
+ * Password toggle labels are translated via Transloco (scope `'keepui'`).
1173
+ * Call `provideKeepUiI18n()` in your `app.config.ts` to activate i18n.
1174
+ *
1175
+ * ```html
1176
+ * <keepui-signal-text-input
1177
+ * label="Email"
1178
+ * type="email"
1179
+ * placeholder="usuario@ejemplo.com"
1180
+ * leadingIcon="mail-icon"
1181
+ * [(value)]="email"
1182
+ * />
1183
+ *
1184
+ * <!-- Password with toggle -->
1185
+ * <keepui-signal-text-input
1186
+ * label="Contraseña"
1187
+ * type="password"
1188
+ * [(value)]="password"
1189
+ * />
1190
+ * ```
1191
+ */
1192
+ class SignalTextInputComponent {
1193
+ constructor() {
1194
+ this.isPasswordVisible = signal(false);
1195
+ this.inputRef = viewChild('inputRef');
1196
+ /** Translation key references (typed via KEEPUI_TRANSLATION_KEYS). */
1197
+ this.keys = KEEPUI_TRANSLATION_KEYS.SIGNAL_TEXT_INPUT;
1198
+ /** Optional label text rendered above the input. */
1199
+ this.label = input('');
1200
+ /** Placeholder passed to the underlying `<input>`. */
1201
+ this.placeholder = input('');
1202
+ /** HTML `type` attribute. Use `'password'` to enable the visibility toggle. @default 'text' */
1203
+ this.type = input('text');
1204
+ /** Layout width of the wrapper. @default 'full' */
1205
+ this.width = input('full');
1206
+ /** Name of the leading icon (SVG symbol ID). Leave empty for no icon. */
1207
+ this.leadingIcon = input('');
1208
+ /** Name of the trailing icon (SVG symbol ID). Ignored when `type="password"`. */
1209
+ this.trailingIcon = input('');
1210
+ /**
1211
+ * When `true`, a slot for custom trailing content is enabled
1212
+ * (projects `[trailingSlot]` content). Overrides `trailingIcon`.
1213
+ * @default false
1214
+ */
1215
+ this.hasTrailingSlot = input(false);
1216
+ /** Marks the field as required. Adds `aria-required` and a visual asterisk. @default false */
1217
+ this.required = input(false);
1218
+ /** When `false`, hides the visual asterisk even if `required=true`. @default true */
1219
+ this.showRequiredIndicator = input(true);
1220
+ /** Human-readable error message. Takes precedence over `errors[0]`. */
1221
+ this.errorMessage = input('');
1222
+ /**
1223
+ * Array of error strings. The first item is displayed when `errorMessage` is empty.
1224
+ * Set together with `invalid=true` to trigger the error state.
1225
+ */
1226
+ this.errors = input([]);
1227
+ /**
1228
+ * Stable `id` used to link the `<label>` with the `<input>`.
1229
+ * A random suffix is generated by default.
1230
+ */
1231
+ this.inputId = input(`ku-text-input-${Math.random().toString(36).slice(2, 8)}`);
1232
+ /** Disables the input. @default false */
1233
+ this.disabled = input(false);
1234
+ /**
1235
+ * Forces the error visual state regardless of the `touched` model.
1236
+ * Useful for external form validation. @default false
1237
+ */
1238
+ this.invalid = input(false);
1239
+ /** Current text value. Use `[(value)]` for two-way binding. */
1240
+ this.value = model('');
1241
+ /** Whether the control has been interacted with. Use `[(touched)]` for two-way binding. */
1242
+ this.touched = model(false);
1243
+ this.errorId = computed(() => `${this.inputId()}-error`);
1244
+ this.isPasswordType = computed(() => this.type() === 'password');
1245
+ this.resolvedType = computed(() => {
1246
+ if (this.isPasswordType()) {
1247
+ return this.isPasswordVisible() ? 'text' : 'password';
1248
+ }
1249
+ return this.type();
1250
+ });
1251
+ this.widthClass = computed(() => {
1252
+ const map = {
1253
+ full: 'w-full',
1254
+ half: 'w-1/2',
1255
+ auto: 'w-auto',
1256
+ };
1257
+ return map[this.width()];
1258
+ });
1259
+ this.hasLeading = computed(() => this.leadingIcon().length > 0);
1260
+ this.hasTrailing = computed(() => this.trailingIcon().length > 0 && !this.isPasswordType());
1261
+ this.showError = computed(() => this.touched() &&
1262
+ (this.invalid() || this.errorMessage().length > 0 || this.errors().length > 0));
1263
+ this.isDateEmpty = computed(() => this.type() === 'date' && !this.value());
1264
+ this.inputClasses = computed(() => {
1265
+ const textColor = this.isDateEmpty()
1266
+ ? 'text-ku-gray-text'
1267
+ : 'text-ku-primary-text';
1268
+ const paddingLeft = this.hasLeading() ? 'pl-9' : 'pl-4';
1269
+ const paddingRight = this.hasTrailingSlot()
1270
+ ? 'pr-24'
1271
+ : this.hasTrailing() || this.isPasswordType()
1272
+ ? 'pr-10'
1273
+ : 'pr-4';
1274
+ const borderColor = this.showError()
1275
+ ? 'border-ku-error-primary'
1276
+ : 'border-ku-secondary-border focus-visible:border-ku-action-primary';
1277
+ const cursor = this.type() === 'date' ? 'cursor-pointer' : '';
1278
+ return `${textColor} ${paddingLeft} ${paddingRight} ${borderColor} ${cursor}`;
1279
+ });
1280
+ }
1281
+ onInput(event) {
1282
+ const target = event.target;
1283
+ this.value.set(target.value);
1284
+ }
1285
+ onBlur() {
1286
+ this.touched.set(true);
1287
+ }
1288
+ openDatePicker() {
1289
+ if (this.type() === 'date') {
1290
+ try {
1291
+ this.inputRef()?.nativeElement.showPicker();
1292
+ }
1293
+ catch { /* showPicker not supported in all browsers */ }
1294
+ }
1295
+ }
1296
+ togglePasswordVisibility() {
1297
+ this.isPasswordVisible.update(v => !v);
1298
+ }
1299
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1300
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: SignalTextInputComponent, isStandalone: true, selector: "keepui-signal-text-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, leadingIcon: { classPropertyName: "leadingIcon", publicName: "leadingIcon", isSignal: true, isRequired: false, transformFunction: null }, trailingIcon: { classPropertyName: "trailingIcon", publicName: "trailingIcon", isSignal: true, isRequired: false, transformFunction: null }, hasTrailingSlot: { classPropertyName: "hasTrailingSlot", publicName: "hasTrailingSlot", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, showRequiredIndicator: { classPropertyName: "showRequiredIndicator", publicName: "showRequiredIndicator", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { classAttribute: "block" }, providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'keepui' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
1301
+ <div class="flex flex-col gap-1" [class]="widthClass()">
1302
+
1303
+ @if (label()) {
1304
+ <label [for]="inputId()" class="text-sm text-ku-gray-text">
1305
+ {{ label() }}
1306
+ @if (required() && showRequiredIndicator()) {
1307
+ <span class="text-ku-error-primary" aria-hidden="true"> *</span>
1308
+ }
1309
+ </label>
1310
+ }
1311
+
1312
+ <div class="relative flex items-center">
1313
+
1314
+ @if (hasLeading()) {
1315
+ <span
1316
+ class="absolute left-3 flex items-center pointer-events-none text-ku-gray-text"
1317
+ aria-hidden="true"
1318
+ >
1319
+ <keepui-icon [name]="leadingIcon()" [size]="18" />
1320
+ </span>
1321
+ }
1322
+
1323
+ <input
1324
+ #inputRef
1325
+ [id]="inputId()"
1326
+ [type]="resolvedType()"
1327
+ [value]="value()"
1328
+ [placeholder]="placeholder()"
1329
+ [disabled]="disabled()"
1330
+ [attr.required]="required() ? true : null"
1331
+ [attr.aria-required]="required() ? true : null"
1332
+ [attr.aria-invalid]="showError() ? true : null"
1333
+ [attr.aria-describedby]="showError() ? errorId() : null"
1334
+ class="bg-ku-primary border rounded-xl py-2.5 text-sm text-ku-primary-text
1335
+ placeholder:text-ku-gray-text outline-none transition-colors w-full
1336
+ disabled:opacity-50 disabled:cursor-not-allowed min-h-[2.75rem]
1337
+ dark:[color-scheme:dark]"
1338
+ [class]="inputClasses()"
1339
+ (click)="openDatePicker()"
1340
+ (input)="onInput($event)"
1341
+ (blur)="onBlur()"
1342
+ />
1343
+
1344
+ @if (hasTrailingSlot()) {
1345
+ <div class="absolute right-1 flex items-center">
1346
+ <ng-content select="[trailingSlot]" />
1347
+ </div>
1348
+ } @else if (isPasswordType()) {
1349
+ <button
1350
+ type="button"
1351
+ (click)="togglePasswordVisibility()"
1352
+ class="absolute right-3 flex items-center justify-center text-ku-gray-text
1353
+ hover:text-ku-primary-text transition-colors cursor-pointer
1354
+ focus-visible:outline-none focus-visible:ring-2
1355
+ focus-visible:ring-ku-action-primary rounded min-h-[2.75rem] min-w-[2.75rem]"
1356
+ [attr.aria-label]="(isPasswordVisible()
1357
+ ? keys.HIDE_PASSWORD
1358
+ : keys.SHOW_PASSWORD) | transloco"
1359
+ >
1360
+ <keepui-icon
1361
+ [name]="isPasswordVisible() ? 'eye-off-icon' : 'eye-icon'"
1362
+ [size]="18"
1363
+ aria-hidden="true"
1364
+ />
1365
+ </button>
1366
+ } @else if (hasTrailing()) {
1367
+ <span
1368
+ class="absolute right-3 flex items-center pointer-events-none text-ku-gray-text"
1369
+ aria-hidden="true"
1370
+ >
1371
+ <keepui-icon [name]="trailingIcon()" [size]="18" />
1372
+ </span>
1373
+ }
1374
+
1375
+ </div>
1376
+
1377
+ @if (showError()) {
1378
+ <span
1379
+ [id]="errorId()"
1380
+ class="text-sm text-ku-error-primary"
1381
+ role="alert"
1382
+ >
1383
+ @if (errorMessage()) {
1384
+ {{ errorMessage() }}
1385
+ } @else if (errors().length > 0) {
1386
+ {{ errors()[0] }}
1387
+ }
1388
+ </span>
1389
+ }
1390
+
1391
+ </div>
1392
+ `, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1393
+ }
1394
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextInputComponent, decorators: [{
1395
+ type: Component,
1396
+ args: [{
1397
+ selector: 'keepui-signal-text-input',
1398
+ standalone: true,
1399
+ changeDetection: ChangeDetectionStrategy.OnPush,
1400
+ imports: [IconComponent, TranslocoPipe],
1401
+ providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'keepui' }],
1402
+ host: { class: 'block' },
1403
+ template: `
1404
+ <div class="flex flex-col gap-1" [class]="widthClass()">
1405
+
1406
+ @if (label()) {
1407
+ <label [for]="inputId()" class="text-sm text-ku-gray-text">
1408
+ {{ label() }}
1409
+ @if (required() && showRequiredIndicator()) {
1410
+ <span class="text-ku-error-primary" aria-hidden="true"> *</span>
1411
+ }
1412
+ </label>
1413
+ }
1414
+
1415
+ <div class="relative flex items-center">
1416
+
1417
+ @if (hasLeading()) {
1418
+ <span
1419
+ class="absolute left-3 flex items-center pointer-events-none text-ku-gray-text"
1420
+ aria-hidden="true"
1421
+ >
1422
+ <keepui-icon [name]="leadingIcon()" [size]="18" />
1423
+ </span>
1424
+ }
1425
+
1426
+ <input
1427
+ #inputRef
1428
+ [id]="inputId()"
1429
+ [type]="resolvedType()"
1430
+ [value]="value()"
1431
+ [placeholder]="placeholder()"
1432
+ [disabled]="disabled()"
1433
+ [attr.required]="required() ? true : null"
1434
+ [attr.aria-required]="required() ? true : null"
1435
+ [attr.aria-invalid]="showError() ? true : null"
1436
+ [attr.aria-describedby]="showError() ? errorId() : null"
1437
+ class="bg-ku-primary border rounded-xl py-2.5 text-sm text-ku-primary-text
1438
+ placeholder:text-ku-gray-text outline-none transition-colors w-full
1439
+ disabled:opacity-50 disabled:cursor-not-allowed min-h-[2.75rem]
1440
+ dark:[color-scheme:dark]"
1441
+ [class]="inputClasses()"
1442
+ (click)="openDatePicker()"
1443
+ (input)="onInput($event)"
1444
+ (blur)="onBlur()"
1445
+ />
1446
+
1447
+ @if (hasTrailingSlot()) {
1448
+ <div class="absolute right-1 flex items-center">
1449
+ <ng-content select="[trailingSlot]" />
1450
+ </div>
1451
+ } @else if (isPasswordType()) {
1452
+ <button
1453
+ type="button"
1454
+ (click)="togglePasswordVisibility()"
1455
+ class="absolute right-3 flex items-center justify-center text-ku-gray-text
1456
+ hover:text-ku-primary-text transition-colors cursor-pointer
1457
+ focus-visible:outline-none focus-visible:ring-2
1458
+ focus-visible:ring-ku-action-primary rounded min-h-[2.75rem] min-w-[2.75rem]"
1459
+ [attr.aria-label]="(isPasswordVisible()
1460
+ ? keys.HIDE_PASSWORD
1461
+ : keys.SHOW_PASSWORD) | transloco"
1462
+ >
1463
+ <keepui-icon
1464
+ [name]="isPasswordVisible() ? 'eye-off-icon' : 'eye-icon'"
1465
+ [size]="18"
1466
+ aria-hidden="true"
1467
+ />
1468
+ </button>
1469
+ } @else if (hasTrailing()) {
1470
+ <span
1471
+ class="absolute right-3 flex items-center pointer-events-none text-ku-gray-text"
1472
+ aria-hidden="true"
1473
+ >
1474
+ <keepui-icon [name]="trailingIcon()" [size]="18" />
1475
+ </span>
1476
+ }
1477
+
1478
+ </div>
1479
+
1480
+ @if (showError()) {
1481
+ <span
1482
+ [id]="errorId()"
1483
+ class="text-sm text-ku-error-primary"
1484
+ role="alert"
1485
+ >
1486
+ @if (errorMessage()) {
1487
+ {{ errorMessage() }}
1488
+ } @else if (errors().length > 0) {
1489
+ {{ errors()[0] }}
1490
+ }
1491
+ </span>
1492
+ }
1493
+
1494
+ </div>
1495
+ `,
1496
+ }]
1497
+ }] });
1498
+
1499
+ /**
1500
+ * Signal-based accessible textarea component with character counter,
1501
+ * resize control, and integrated error display.
1502
+ *
1503
+ * The `value` and `touched` properties are `model()` signals so the component
1504
+ * integrates seamlessly with Angular signal-based forms.
1505
+ *
1506
+ * ```html
1507
+ * <keepui-signal-textarea
1508
+ * label="Descripción"
1509
+ * placeholder="Escribe aquí…"
1510
+ * [rows]="5"
1511
+ * [maxLength]="500"
1512
+ * [(value)]="description"
1513
+ * />
1514
+ * ```
1515
+ */
1516
+ class SignalTextareaComponent {
1517
+ constructor() {
1518
+ /** Optional label text rendered above the textarea. */
1519
+ this.label = input('');
1520
+ /** Placeholder passed to the underlying `<textarea>`. */
1521
+ this.placeholder = input('');
1522
+ /** Number of visible text rows. @default 4 */
1523
+ this.rows = input(4);
1524
+ /** Layout width of the wrapper. @default 'full' */
1525
+ this.width = input('full');
1526
+ /** CSS `resize` behaviour. @default 'none' */
1527
+ this.resize = input('none');
1528
+ /** Marks the field as required. @default false */
1529
+ this.required = input(false);
1530
+ /** Human-readable error message. Takes precedence over `errors[0]`. */
1531
+ this.errorMessage = input('');
1532
+ /**
1533
+ * Array of error strings. The first item is displayed when `errorMessage` is empty.
1534
+ * Set together with `invalid=true` to trigger the error state.
1535
+ */
1536
+ this.errors = input([]);
1537
+ /**
1538
+ * Stable `id` used to link the `<label>` with the `<textarea>`.
1539
+ * A random suffix is generated by default.
1540
+ */
1541
+ this.textareaId = input(`ku-textarea-${Math.random().toString(36).slice(2, 8)}`);
1542
+ /** Disables the textarea. @default false */
1543
+ this.disabled = input(false);
1544
+ /** Maximum number of characters allowed. Shows a character counter when set. */
1545
+ this.maxLength = input(undefined);
1546
+ /**
1547
+ * Forces the error visual state regardless of the `touched` model.
1548
+ * Useful for external form validation. @default false
1549
+ */
1550
+ this.invalid = input(false);
1551
+ /** Current text value. Use `[(value)]` for two-way binding. */
1552
+ this.value = model('');
1553
+ /** Whether the control has been interacted with. Use `[(touched)]` for two-way binding. */
1554
+ this.touched = model(false);
1555
+ this.errorId = computed(() => `${this.textareaId()}-error`);
1556
+ this.charCountId = computed(() => `${this.textareaId()}-count`);
1557
+ this.charCount = computed(() => this.value().length);
1558
+ this.showError = computed(() => this.touched() &&
1559
+ (this.invalid() || this.errorMessage().length > 0 || this.errors().length > 0));
1560
+ this.widthClass = computed(() => {
1561
+ const map = {
1562
+ full: 'w-full',
1563
+ half: 'w-1/2',
1564
+ auto: 'w-auto',
1565
+ };
1566
+ return map[this.width()];
1567
+ });
1568
+ this.resizeClass = computed(() => {
1569
+ const map = {
1570
+ none: 'resize-none',
1571
+ vertical: 'resize-y',
1572
+ horizontal: 'resize-x',
1573
+ both: 'resize',
1574
+ };
1575
+ return map[this.resize()];
1576
+ });
1577
+ this.textareaClasses = computed(() => {
1578
+ const borderColor = this.showError()
1579
+ ? 'border-ku-error-primary'
1580
+ : 'border-ku-secondary-border focus-visible:border-ku-action-primary';
1581
+ return `${this.resizeClass()} ${borderColor}`;
1582
+ });
1583
+ }
1584
+ onInput(event) {
1585
+ const target = event.target;
1586
+ this.value.set(target.value);
1587
+ }
1588
+ onBlur() {
1589
+ this.touched.set(true);
1590
+ }
1591
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1592
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: SignalTextareaComponent, isStandalone: true, selector: "keepui-signal-textarea", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, resize: { classPropertyName: "resize", publicName: "resize", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, textareaId: { classPropertyName: "textareaId", publicName: "textareaId", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { classAttribute: "block" }, ngImport: i0, template: `
1593
+ <div class="flex flex-col gap-1" [class]="widthClass()">
1594
+
1595
+ @if (label()) {
1596
+ <label [for]="textareaId()" class="text-sm text-ku-gray-text">
1597
+ {{ label() }}
1598
+ @if (required()) {
1599
+ <span class="text-ku-error-primary" aria-hidden="true"> *</span>
1600
+ }
1601
+ </label>
1602
+ }
1603
+
1604
+ <textarea
1605
+ [id]="textareaId()"
1606
+ [value]="value()"
1607
+ [placeholder]="placeholder()"
1608
+ [rows]="rows()"
1609
+ [disabled]="disabled()"
1610
+ [attr.maxlength]="maxLength() ?? null"
1611
+ [attr.required]="required() ? true : null"
1612
+ [attr.aria-required]="required() ? true : null"
1613
+ [attr.aria-invalid]="showError() ? true : null"
1614
+ [attr.aria-describedby]="showError() ? errorId() : (maxLength() ? charCountId() : null)"
1615
+ class="bg-ku-primary border rounded-xl p-3 text-sm text-ku-primary-text
1616
+ placeholder:text-ku-gray-text outline-none transition-colors w-full
1617
+ disabled:opacity-50 disabled:cursor-not-allowed"
1618
+ [class]="textareaClasses()"
1619
+ (input)="onInput($event)"
1620
+ (blur)="onBlur()"
1621
+ ></textarea>
1622
+
1623
+ <div class="flex items-start justify-between gap-2">
1624
+
1625
+ <span
1626
+ [id]="errorId()"
1627
+ class="text-sm text-ku-error-primary"
1628
+ role="alert"
1629
+ >
1630
+ @if (showError()) {
1631
+ @if (errorMessage()) {
1632
+ {{ errorMessage() }}
1633
+ } @else if (errors().length > 0) {
1634
+ {{ errors()[0] }}
1635
+ }
1636
+ }
1637
+ </span>
1638
+
1639
+ @if (maxLength()) {
1640
+ <span
1641
+ [id]="charCountId()"
1642
+ class="text-xs text-ku-gray-text ml-auto shrink-0"
1643
+ aria-live="polite"
1644
+ aria-atomic="true"
1645
+ >
1646
+ {{ charCount() }}&thinsp;/&thinsp;{{ maxLength() }}
1647
+ </span>
1648
+ }
1649
+
1650
+ </div>
1651
+
1652
+ </div>
1653
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1654
+ }
1655
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SignalTextareaComponent, decorators: [{
1656
+ type: Component,
1657
+ args: [{
1658
+ selector: 'keepui-signal-textarea',
1659
+ standalone: true,
1660
+ changeDetection: ChangeDetectionStrategy.OnPush,
1661
+ host: { class: 'block' },
1662
+ template: `
1663
+ <div class="flex flex-col gap-1" [class]="widthClass()">
1664
+
1665
+ @if (label()) {
1666
+ <label [for]="textareaId()" class="text-sm text-ku-gray-text">
1667
+ {{ label() }}
1668
+ @if (required()) {
1669
+ <span class="text-ku-error-primary" aria-hidden="true"> *</span>
1670
+ }
1671
+ </label>
1672
+ }
1673
+
1674
+ <textarea
1675
+ [id]="textareaId()"
1676
+ [value]="value()"
1677
+ [placeholder]="placeholder()"
1678
+ [rows]="rows()"
1679
+ [disabled]="disabled()"
1680
+ [attr.maxlength]="maxLength() ?? null"
1681
+ [attr.required]="required() ? true : null"
1682
+ [attr.aria-required]="required() ? true : null"
1683
+ [attr.aria-invalid]="showError() ? true : null"
1684
+ [attr.aria-describedby]="showError() ? errorId() : (maxLength() ? charCountId() : null)"
1685
+ class="bg-ku-primary border rounded-xl p-3 text-sm text-ku-primary-text
1686
+ placeholder:text-ku-gray-text outline-none transition-colors w-full
1687
+ disabled:opacity-50 disabled:cursor-not-allowed"
1688
+ [class]="textareaClasses()"
1689
+ (input)="onInput($event)"
1690
+ (blur)="onBlur()"
1691
+ ></textarea>
1692
+
1693
+ <div class="flex items-start justify-between gap-2">
1694
+
1695
+ <span
1696
+ [id]="errorId()"
1697
+ class="text-sm text-ku-error-primary"
1698
+ role="alert"
1699
+ >
1700
+ @if (showError()) {
1701
+ @if (errorMessage()) {
1702
+ {{ errorMessage() }}
1703
+ } @else if (errors().length > 0) {
1704
+ {{ errors()[0] }}
1705
+ }
1706
+ }
1707
+ </span>
1708
+
1709
+ @if (maxLength()) {
1710
+ <span
1711
+ [id]="charCountId()"
1712
+ class="text-xs text-ku-gray-text ml-auto shrink-0"
1713
+ aria-live="polite"
1714
+ aria-atomic="true"
1715
+ >
1716
+ {{ charCount() }}&thinsp;/&thinsp;{{ maxLength() }}
1717
+ </span>
1718
+ }
1719
+
1720
+ </div>
1721
+
1722
+ </div>
1723
+ `,
1724
+ }]
1725
+ }] });
1726
+
1727
+ /**
1728
+ * Accessible pill-style tab group with icon support and two visual variants.
1729
+ *
1730
+ * Implements the WAI-ARIA `tablist` pattern: keyboard navigation with arrow keys,
1731
+ * `Home`, and `End`. Each tab carries `role="tab"` and `aria-selected`.
1732
+ *
1733
+ * Works identically on **web** and **Angular + Capacitor**.
1734
+ *
1735
+ * ```html
1736
+ * <!-- Basic usage -->
1737
+ * <keepui-tab-group
1738
+ * [tabs]="tabs"
1739
+ * [selectedTabId]="activeTab"
1740
+ * (tabChange)="activeTab = $event"
1741
+ * />
1742
+ *
1743
+ * <!-- Filled variant -->
1744
+ * <keepui-tab-group
1745
+ * variant="filled"
1746
+ * [tabs]="tabs"
1747
+ * [selectedTabId]="activeTab"
1748
+ * (tabChange)="activeTab = $event"
1749
+ * />
1750
+ * ```
1751
+ */
1752
+ class TabGroupComponent {
1753
+ constructor() {
1754
+ /** List of tabs to render. */
1755
+ this.tabs = input.required();
1756
+ /** ID of the currently selected tab. */
1757
+ this.selectedTabId = input.required();
1758
+ /** Visual style variant. @default 'default' */
1759
+ this.variant = input('default');
1760
+ /**
1761
+ * Accessible label for the tablist container.
1762
+ * Provide a meaningful description when multiple tab groups exist on the page.
1763
+ */
1764
+ this.ariaLabel = input('');
1765
+ /** Emits the `id` of the tab the user has selected. */
1766
+ this.tabChange = output();
1767
+ this.tabButtons = viewChildren('tabButton');
1768
+ this.containerClass = computed(() => this.variant() === 'filled'
1769
+ ? 'bg-ku-action-primary/10 border-ku-action-primary/20'
1770
+ : 'bg-ku-secondary border-ku-secondary-border');
1771
+ }
1772
+ buttonClass(tabId) {
1773
+ const isActive = this.selectedTabId() === tabId;
1774
+ if (this.variant() === 'filled') {
1775
+ return isActive
1776
+ ? 'bg-ku-action-primary text-white'
1777
+ : 'text-ku-gray-text hover:text-ku-primary-text hover:bg-ku-action-primary/10';
1778
+ }
1779
+ return isActive
1780
+ ? 'bg-ku-primary text-ku-primary-text shadow-sm border border-ku-primary-border'
1781
+ : 'text-ku-gray-text hover:text-ku-primary-text hover:bg-ku-secondary-hover';
1782
+ }
1783
+ iconClass(tabId) {
1784
+ const isActive = this.selectedTabId() === tabId;
1785
+ if (this.variant() === 'filled') {
1786
+ return isActive ? 'text-white' : 'text-ku-gray-text';
1787
+ }
1788
+ return isActive ? 'text-ku-action-primary' : 'text-ku-gray-text';
1789
+ }
1790
+ onTabClick(tab) {
1791
+ if (!tab.disabled && tab.id !== this.selectedTabId()) {
1792
+ this.tabChange.emit(tab.id);
1793
+ }
1794
+ }
1795
+ onKeydown(event) {
1796
+ const tabs = this.tabs().filter(t => !t.disabled);
1797
+ const activeIndex = tabs.findIndex(t => t.id === this.selectedTabId());
1798
+ let nextIndex = activeIndex;
1799
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
1800
+ nextIndex = (activeIndex + 1) % tabs.length;
1801
+ }
1802
+ else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
1803
+ nextIndex = (activeIndex - 1 + tabs.length) % tabs.length;
1804
+ }
1805
+ else if (event.key === 'Home') {
1806
+ nextIndex = 0;
1807
+ }
1808
+ else if (event.key === 'End') {
1809
+ nextIndex = tabs.length - 1;
1810
+ }
1811
+ else {
1812
+ return;
1813
+ }
1814
+ event.preventDefault();
1815
+ const nextTab = tabs[nextIndex];
1816
+ this.tabChange.emit(nextTab.id);
1817
+ this.focusTab(nextTab.id);
1818
+ }
1819
+ focusTab(tabId) {
1820
+ const allTabs = this.tabs();
1821
+ const index = allTabs.findIndex(t => t.id === tabId);
1822
+ const buttons = this.tabButtons();
1823
+ buttons[index]?.nativeElement.focus();
1824
+ }
1825
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TabGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1826
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: TabGroupComponent, isStandalone: true, selector: "keepui-tab-group", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: true, transformFunction: null }, selectedTabId: { classPropertyName: "selectedTabId", publicName: "selectedTabId", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { tabChange: "tabChange" }, viewQueries: [{ propertyName: "tabButtons", predicate: ["tabButton"], descendants: true, isSignal: true }], ngImport: i0, template: `
1827
+ <div
1828
+ role="tablist"
1829
+ [attr.aria-label]="ariaLabel() || null"
1830
+ class="flex items-center p-1 rounded-full border transition-colors duration-200"
1831
+ [class]="containerClass()"
1832
+ (keydown)="onKeydown($event)"
1833
+ >
1834
+ @for (tab of tabs(); track tab.id; let i = $index) {
1835
+ <button
1836
+ #tabButton
1837
+ type="button"
1838
+ role="tab"
1839
+ [id]="'keepui-tab-' + tab.id"
1840
+ [attr.aria-selected]="selectedTabId() === tab.id"
1841
+ [attr.aria-disabled]="tab.disabled ? true : null"
1842
+ [disabled]="tab.disabled || false"
1843
+ [tabindex]="selectedTabId() === tab.id ? 0 : -1"
1844
+ class="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full
1845
+ transition-colors duration-200 text-sm font-medium cursor-pointer
1846
+ min-h-[2.75rem]
1847
+ focus-visible:outline-none focus-visible:ring-2
1848
+ focus-visible:ring-ku-action-primary focus-visible:ring-offset-2
1849
+ disabled:cursor-not-allowed disabled:opacity-40"
1850
+ [class]="buttonClass(tab.id)"
1851
+ (click)="onTabClick(tab)"
1852
+ >
1853
+ @if (tab.icon) {
1854
+ <keepui-icon
1855
+ [name]="tab.icon"
1856
+ [size]="16"
1857
+ [class]="iconClass(tab.id)"
1858
+ aria-hidden="true"
1859
+ />
1860
+ }
1861
+ <span>{{ tab.label }}</span>
1862
+ </button>
1863
+ }
1864
+ </div>
1865
+ `, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1866
+ }
1867
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TabGroupComponent, decorators: [{
1868
+ type: Component,
1869
+ args: [{
1870
+ selector: 'keepui-tab-group',
1871
+ standalone: true,
1872
+ changeDetection: ChangeDetectionStrategy.OnPush,
1873
+ imports: [IconComponent],
1874
+ template: `
1875
+ <div
1876
+ role="tablist"
1877
+ [attr.aria-label]="ariaLabel() || null"
1878
+ class="flex items-center p-1 rounded-full border transition-colors duration-200"
1879
+ [class]="containerClass()"
1880
+ (keydown)="onKeydown($event)"
1881
+ >
1882
+ @for (tab of tabs(); track tab.id; let i = $index) {
1883
+ <button
1884
+ #tabButton
1885
+ type="button"
1886
+ role="tab"
1887
+ [id]="'keepui-tab-' + tab.id"
1888
+ [attr.aria-selected]="selectedTabId() === tab.id"
1889
+ [attr.aria-disabled]="tab.disabled ? true : null"
1890
+ [disabled]="tab.disabled || false"
1891
+ [tabindex]="selectedTabId() === tab.id ? 0 : -1"
1892
+ class="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full
1893
+ transition-colors duration-200 text-sm font-medium cursor-pointer
1894
+ min-h-[2.75rem]
1895
+ focus-visible:outline-none focus-visible:ring-2
1896
+ focus-visible:ring-ku-action-primary focus-visible:ring-offset-2
1897
+ disabled:cursor-not-allowed disabled:opacity-40"
1898
+ [class]="buttonClass(tab.id)"
1899
+ (click)="onTabClick(tab)"
1900
+ >
1901
+ @if (tab.icon) {
1902
+ <keepui-icon
1903
+ [name]="tab.icon"
1904
+ [size]="16"
1905
+ [class]="iconClass(tab.id)"
1906
+ aria-hidden="true"
1907
+ />
1908
+ }
1909
+ <span>{{ tab.label }}</span>
1910
+ </button>
1911
+ }
1912
+ </div>
1913
+ `,
1914
+ }]
1915
+ }] });
1916
+
1917
+ /**
1918
+ * Visual step-progress indicator with optional navigation.
1919
+ *
1920
+ * Renders a sequence of numbered step circles connected by a progress bar.
1921
+ * Completed steps show a check-mark; the active step is highlighted; future
1922
+ * steps are muted. Steps with an `icon` replace the number with the given SVG symbol.
1923
+ *
1924
+ * Accessibility: uses `role="list"` / `role="listitem"` with `aria-current="step"` on
1925
+ * the active item. When steps are interactive (i.e. `stepChange` is listened to),
1926
+ * completed and current steps render as `<button>` elements; future steps remain
1927
+ * inert `<span>` elements.
1928
+ *
1929
+ * Works identically on **web** and **Angular + Capacitor**.
1930
+ *
1931
+ * ```html
1932
+ * <!-- Read-only progress indicator -->
1933
+ * <keepui-stepper [steps]="steps" [activeIndex]="1" />
1934
+ *
1935
+ * <!-- Interactive stepper (go back to a previous step) -->
1936
+ * <keepui-stepper
1937
+ * [steps]="steps"
1938
+ * [activeIndex]="currentStep"
1939
+ * (stepChange)="currentStep = $event"
1940
+ * />
1941
+ *
1942
+ * <!-- Small variant -->
1943
+ * <keepui-stepper [steps]="steps" [activeIndex]="0" size="sm" />
1944
+ * ```
1945
+ */
1946
+ class StepperComponent {
1947
+ constructor() {
1948
+ /** Steps to render. */
1949
+ this.steps = input.required();
1950
+ /** Zero-based index of the currently active step. @default 0 */
1951
+ this.activeIndex = input(0);
1952
+ /** Visual size of the step circles and connector bars. @default 'md' */
1953
+ this.size = input('md');
1954
+ /** Layout direction. @default 'horizontal' */
1955
+ this.orientation = input('horizontal');
1956
+ /**
1957
+ * Accessible label for the `<nav>` element.
1958
+ * @default ''
1959
+ */
1960
+ this.ariaLabel = input('');
1961
+ /**
1962
+ * Emits the zero-based index of the step the user clicked.
1963
+ * Only completed steps and the active step are clickable.
1964
+ * When no subscriber is attached, the stepper is purely visual.
1965
+ */
1966
+ this.stepChange = output();
1967
+ this.iconSize = computed(() => (this.size() === 'sm' ? 12 : 16));
1968
+ this.wrapperClass = computed(() => 'w-full');
1969
+ this.listClass = computed(() => {
1970
+ const base = 'list-none m-0 p-0 flex';
1971
+ return this.orientation() === 'vertical'
1972
+ ? `${base} flex-col gap-0`
1973
+ : `${base} flex-row items-start`;
1974
+ });
1975
+ this.itemClass = computed(() => {
1976
+ const base = 'flex';
1977
+ return this.orientation() === 'vertical'
1978
+ ? `${base} flex-row items-center flex-1`
1979
+ : `${base} flex-col items-center flex-1`;
1980
+ });
1981
+ this.stepColumnClass = computed(() => {
1982
+ return this.orientation() === 'vertical'
1983
+ ? 'flex flex-row items-center gap-3'
1984
+ : 'flex flex-col items-center gap-1.5';
1985
+ });
1986
+ }
1987
+ isCompleted(index) {
1988
+ return index < this.activeIndex();
1989
+ }
1990
+ isActive(index) {
1991
+ return index === this.activeIndex();
1992
+ }
1993
+ isClickable(index, step) {
1994
+ return !step.disabled && (this.isCompleted(index) || this.isActive(index));
1995
+ }
1996
+ onStepClick(index, step) {
1997
+ if (!step.disabled) {
1998
+ this.stepChange.emit(index);
1999
+ }
2000
+ }
2001
+ bubbleClass(index) {
2002
+ const sizeClass = this.size() === 'sm'
2003
+ ? 'w-6 h-6 text-xs'
2004
+ : 'w-9 h-9 text-sm';
2005
+ const base = [
2006
+ 'inline-flex items-center justify-center rounded-full shrink-0',
2007
+ 'transition-colors duration-200',
2008
+ 'focus-visible:outline-none focus-visible:ring-2',
2009
+ 'focus-visible:ring-ku-action-primary focus-visible:ring-offset-2',
2010
+ sizeClass,
2011
+ ].join(' ');
2012
+ if (this.isCompleted(index)) {
2013
+ return `${base} bg-ku-action-primary text-white cursor-pointer`;
2014
+ }
2015
+ if (this.isActive(index)) {
2016
+ return `${base} bg-ku-action-primary text-white ring-2 ring-ku-action-primary ring-offset-2`;
2017
+ }
2018
+ return `${base} bg-ku-secondary border-2 border-ku-secondary-border text-ku-gray-text cursor-default`;
2019
+ }
2020
+ labelClass(index) {
2021
+ const base = 'text-xs font-medium text-center transition-colors duration-200';
2022
+ if (this.isActive(index)) {
2023
+ return `${base} text-ku-primary-text`;
2024
+ }
2025
+ if (this.isCompleted(index)) {
2026
+ return `${base} text-ku-action-primary`;
2027
+ }
2028
+ return `${base} text-ku-gray-text`;
2029
+ }
2030
+ connectorClass(index) {
2031
+ const filled = this.isCompleted(index) || this.isActive(index);
2032
+ if (this.orientation() === 'vertical') {
2033
+ const height = this.size() === 'sm' ? 'h-8' : 'h-10';
2034
+ return [
2035
+ `${height} w-0.5 mx-auto my-1 rounded-full transition-colors duration-300`,
2036
+ filled ? 'bg-ku-action-primary' : 'bg-ku-secondary-border',
2037
+ ].join(' ');
2038
+ }
2039
+ return [
2040
+ 'w-full h-0.5 mt-4 mb-1 rounded-full transition-colors duration-300',
2041
+ filled ? 'bg-ku-action-primary' : 'bg-ku-secondary-border',
2042
+ ].join(' ');
2043
+ }
2044
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: StepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2045
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: StepperComponent, isStandalone: true, selector: "keepui-stepper", inputs: { steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stepChange: "stepChange" }, ngImport: i0, template: `
2046
+ <nav
2047
+ [attr.aria-label]="ariaLabel() || null"
2048
+ [class]="wrapperClass()"
2049
+ >
2050
+ <ol
2051
+ role="list"
2052
+ [class]="listClass()"
2053
+ >
2054
+ @for (step of steps(); track step.id; let i = $index; let last = $last) {
2055
+ <li
2056
+ role="listitem"
2057
+ [class]="itemClass()"
2058
+ [attr.aria-current]="isActive(i) ? 'step' : null"
2059
+ >
2060
+ <div [class]="stepColumnClass()">
2061
+ @if (isClickable(i, step)) {
2062
+ <button
2063
+ type="button"
2064
+ [class]="bubbleClass(i)"
2065
+ [attr.aria-label]="step.label"
2066
+ [attr.aria-disabled]="step.disabled ? true : null"
2067
+ [disabled]="step.disabled || false"
2068
+ (click)="onStepClick(i, step)"
2069
+ >
2070
+ <ng-container *ngTemplateOutlet="bubbleContent; context: { i, step }" />
2071
+ </button>
2072
+ } @else {
2073
+ <span [class]="bubbleClass(i)" aria-hidden="true">
2074
+ <ng-container *ngTemplateOutlet="bubbleContent; context: { i, step }" />
2075
+ </span>
2076
+ }
2077
+
2078
+ <span [class]="labelClass(i)">{{ step.label }}</span>
2079
+ </div>
2080
+
2081
+ @if (!last) {
2082
+ <div [class]="connectorClass(i)"></div>
2083
+ }
2084
+ </li>
2085
+ }
2086
+ </ol>
2087
+ </nav>
2088
+
2089
+ <ng-template #bubbleContent let-i="i" let-step="step">
2090
+ @if (isCompleted(i)) {
2091
+ <svg
2092
+ width="14"
2093
+ height="14"
2094
+ viewBox="0 0 24 24"
2095
+ fill="none"
2096
+ stroke="currentColor"
2097
+ stroke-width="3"
2098
+ stroke-linecap="round"
2099
+ stroke-linejoin="round"
2100
+ aria-hidden="true"
2101
+ >
2102
+ <polyline points="20 6 9 17 4 12" />
2103
+ </svg>
2104
+ } @else if (step.icon) {
2105
+ <keepui-icon [name]="step.icon" [size]="iconSize()" aria-hidden="true" />
2106
+ } @else {
2107
+ <span class="font-semibold leading-none">{{ i + 1 }}</span>
2108
+ }
2109
+ </ng-template>
2110
+ `, isInline: true, dependencies: [{ kind: "component", type: IconComponent, selector: "keepui-icon", inputs: ["name", "size", "viewBox", "ariaLabel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2111
+ }
2112
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: StepperComponent, decorators: [{
2113
+ type: Component,
2114
+ args: [{
2115
+ selector: 'keepui-stepper',
2116
+ standalone: true,
2117
+ changeDetection: ChangeDetectionStrategy.OnPush,
2118
+ imports: [IconComponent, NgTemplateOutlet],
2119
+ template: `
2120
+ <nav
2121
+ [attr.aria-label]="ariaLabel() || null"
2122
+ [class]="wrapperClass()"
2123
+ >
2124
+ <ol
2125
+ role="list"
2126
+ [class]="listClass()"
2127
+ >
2128
+ @for (step of steps(); track step.id; let i = $index; let last = $last) {
2129
+ <li
2130
+ role="listitem"
2131
+ [class]="itemClass()"
2132
+ [attr.aria-current]="isActive(i) ? 'step' : null"
2133
+ >
2134
+ <div [class]="stepColumnClass()">
2135
+ @if (isClickable(i, step)) {
2136
+ <button
2137
+ type="button"
2138
+ [class]="bubbleClass(i)"
2139
+ [attr.aria-label]="step.label"
2140
+ [attr.aria-disabled]="step.disabled ? true : null"
2141
+ [disabled]="step.disabled || false"
2142
+ (click)="onStepClick(i, step)"
2143
+ >
2144
+ <ng-container *ngTemplateOutlet="bubbleContent; context: { i, step }" />
2145
+ </button>
2146
+ } @else {
2147
+ <span [class]="bubbleClass(i)" aria-hidden="true">
2148
+ <ng-container *ngTemplateOutlet="bubbleContent; context: { i, step }" />
2149
+ </span>
2150
+ }
2151
+
2152
+ <span [class]="labelClass(i)">{{ step.label }}</span>
2153
+ </div>
2154
+
2155
+ @if (!last) {
2156
+ <div [class]="connectorClass(i)"></div>
2157
+ }
2158
+ </li>
2159
+ }
2160
+ </ol>
2161
+ </nav>
2162
+
2163
+ <ng-template #bubbleContent let-i="i" let-step="step">
2164
+ @if (isCompleted(i)) {
2165
+ <svg
2166
+ width="14"
2167
+ height="14"
2168
+ viewBox="0 0 24 24"
2169
+ fill="none"
2170
+ stroke="currentColor"
2171
+ stroke-width="3"
2172
+ stroke-linecap="round"
2173
+ stroke-linejoin="round"
2174
+ aria-hidden="true"
2175
+ >
2176
+ <polyline points="20 6 9 17 4 12" />
2177
+ </svg>
2178
+ } @else if (step.icon) {
2179
+ <keepui-icon [name]="step.icon" [size]="iconSize()" aria-hidden="true" />
2180
+ } @else {
2181
+ <span class="font-semibold leading-none">{{ i + 1 }}</span>
2182
+ }
2183
+ </ng-template>
2184
+ `,
2185
+ }]
2186
+ }] });
2187
+
693
2188
  /**
694
2189
  * Registers KeepUI core providers for a **web** Angular application.
695
2190
  *
@@ -725,6 +2220,10 @@ const EN = {
725
2220
  previewAlt: 'Selected image preview',
726
2221
  errorUnexpected: 'An unexpected error occurred',
727
2222
  },
2223
+ signalTextInput: {
2224
+ showPassword: 'Show password',
2225
+ hidePassword: 'Hide password',
2226
+ },
728
2227
  };
729
2228
  const ES = {
730
2229
  imagePreview: {
@@ -733,6 +2232,10 @@ const ES = {
733
2232
  previewAlt: 'Vista previa de imagen seleccionada',
734
2233
  errorUnexpected: 'Ha ocurrido un error inesperado',
735
2234
  },
2235
+ signalTextInput: {
2236
+ showPassword: 'Mostrar contraseña',
2237
+ hidePassword: 'Ocultar contraseña',
2238
+ },
736
2239
  };
737
2240
  const DE = {
738
2241
  imagePreview: {
@@ -741,6 +2244,10 @@ const DE = {
741
2244
  previewAlt: 'Vorschau des ausgewählten Bildes',
742
2245
  errorUnexpected: 'Ein unerwarteter Fehler ist aufgetreten',
743
2246
  },
2247
+ signalTextInput: {
2248
+ showPassword: 'Passwort anzeigen',
2249
+ hidePassword: 'Passwort verbergen',
2250
+ },
744
2251
  };
745
2252
  /** Map from locale code to its translation object. */
746
2253
  const KEEPUI_TRANSLATIONS = {
@@ -905,5 +2412,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
905
2412
  * Generated bundle index. Do not edit.
906
2413
  */
907
2414
 
908
- export { ButtonComponent, CardComponent, FILE_PORT, IconActionButtonComponent, IconComponent, ImagePreviewComponent, KEEPUI_AVAILABLE_LANGUAGES, KEEPUI_TRANSLATIONS, KEEPUI_TRANSLATION_KEYS, KeepUiLanguageService, MockFileService, WebFileService, provideKeepUi, provideKeepUiI18n };
2415
+ export { ButtonComponent, CardComponent, FILE_PORT, IconActionButtonComponent, IconComponent, ImagePreviewComponent, KEEPUI_AVAILABLE_LANGUAGES, KEEPUI_TRANSLATIONS, KEEPUI_TRANSLATION_KEYS, KeepUiLanguageService, MockFileService, SignalDropdownComponent, SignalTextInputComponent, SignalTextareaComponent, StepperComponent, TabGroupComponent, WebFileService, provideKeepUi, provideKeepUiI18n };
909
2416
  //# sourceMappingURL=keepui-ui.mjs.map