@block-auth.io/angular 0.1.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 (148) hide show
  1. package/.turbo/turbo-build.log +37 -0
  2. package/.turbo/turbo-lint.log +4 -0
  3. package/.turbo/turbo-test.log +82 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/CHANGELOG.md +10 -0
  6. package/ng-package.json +6 -0
  7. package/package.json +50 -0
  8. package/scripts/css-to-ts.mjs +14 -0
  9. package/src/lib/__tests__/integration.test.ts +45 -0
  10. package/src/lib/block-auth.service.test.ts +69 -0
  11. package/src/lib/block-auth.service.ts +81 -0
  12. package/src/lib/components/btns/btn-base-provider.component.test.ts +22 -0
  13. package/src/lib/components/btns/btn-base-provider.component.ts +55 -0
  14. package/src/lib/components/btns/btn-block-auth.component.test.ts +79 -0
  15. package/src/lib/components/btns/btn-block-auth.component.ts +104 -0
  16. package/src/lib/components/btns/btn-gmail.component.test.ts +16 -0
  17. package/src/lib/components/btns/btn-gmail.component.ts +41 -0
  18. package/src/lib/components/btns/btn-google-base.component.test.ts +16 -0
  19. package/src/lib/components/btns/btn-google-base.component.ts +73 -0
  20. package/src/lib/components/btns/btn-metamask.component.test.ts +20 -0
  21. package/src/lib/components/btns/btn-metamask.component.ts +98 -0
  22. package/src/lib/components/btns/btn-microsoft.component.test.ts +16 -0
  23. package/src/lib/components/btns/btn-microsoft.component.ts +77 -0
  24. package/src/lib/components/btns/btn-phantom.component.test.ts +20 -0
  25. package/src/lib/components/btns/btn-phantom.component.ts +93 -0
  26. package/src/lib/components/btns/btn-signin.component.test.ts +55 -0
  27. package/src/lib/components/btns/btn-signin.component.ts +108 -0
  28. package/src/lib/components/btns/btn-wallet-connect.component.test.ts +20 -0
  29. package/src/lib/components/btns/btn-wallet-connect.component.ts +109 -0
  30. package/src/lib/components/btns/index.ts +11 -0
  31. package/src/lib/components/contents/content-signin.component.test.ts +33 -0
  32. package/src/lib/components/contents/content-signin.component.ts +68 -0
  33. package/src/lib/components/contents/content-signup.component.test.ts +78 -0
  34. package/src/lib/components/contents/content-signup.component.ts +225 -0
  35. package/src/lib/components/dds/change-account.component.test.ts +66 -0
  36. package/src/lib/components/dds/change-account.component.ts +112 -0
  37. package/src/lib/components/dds/dropdown-profile-btn.component.test.ts +66 -0
  38. package/src/lib/components/dds/dropdown-profile-btn.component.ts +185 -0
  39. package/src/lib/components/dds/dropdown-profile-item.component.test.ts +75 -0
  40. package/src/lib/components/dds/dropdown-profile-item.component.ts +74 -0
  41. package/src/lib/components/dds/dropdown-profile.component.test.ts +98 -0
  42. package/src/lib/components/dds/dropdown-profile.component.ts +258 -0
  43. package/src/lib/components/flow-block-auth.component.test.ts +93 -0
  44. package/src/lib/components/flow-block-auth.component.ts +226 -0
  45. package/src/lib/components/modals/modal-overlay.component.test.ts +81 -0
  46. package/src/lib/components/modals/modal-overlay.component.ts +43 -0
  47. package/src/lib/components/modals/modal-restoration-expired.component.test.ts +71 -0
  48. package/src/lib/components/modals/modal-restoration-expired.component.ts +83 -0
  49. package/src/lib/components/modals/modal-restore.component.test.ts +74 -0
  50. package/src/lib/components/modals/modal-restore.component.ts +104 -0
  51. package/src/lib/components/modals/modal-signin.component.test.ts +66 -0
  52. package/src/lib/components/modals/modal-signin.component.ts +44 -0
  53. package/src/lib/components/modals/modal-signup.component.test.ts +56 -0
  54. package/src/lib/components/modals/modal-signup.component.ts +40 -0
  55. package/src/lib/components/sections/footer-section.component.test.ts +19 -0
  56. package/src/lib/components/sections/footer-section.component.ts +49 -0
  57. package/src/lib/components/sections/internals/providers-buttons.component.test.ts +27 -0
  58. package/src/lib/components/sections/internals/providers-buttons.component.ts +290 -0
  59. package/src/lib/components/sections/internals/providers-filters.component.test.ts +25 -0
  60. package/src/lib/components/sections/internals/providers-filters.component.ts +139 -0
  61. package/src/lib/components/sections/internals/selected-button.component.test.ts +36 -0
  62. package/src/lib/components/sections/internals/selected-button.component.ts +92 -0
  63. package/src/lib/components/sections/login-section.component.test.ts +49 -0
  64. package/src/lib/components/sections/login-section.component.ts +120 -0
  65. package/src/lib/components/sections/providers-section.component.test.ts +22 -0
  66. package/src/lib/components/sections/providers-section.component.ts +187 -0
  67. package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +14 -0
  68. package/src/lib/components/txns/internals/permissions-required-modal.component.ts +35 -0
  69. package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +72 -0
  70. package/src/lib/components/txns/internals/solicited-data-modal.component.ts +187 -0
  71. package/src/lib/components/txns/internals/txn-error.component.test.ts +100 -0
  72. package/src/lib/components/txns/internals/txn-error.component.ts +121 -0
  73. package/src/lib/components/txns/internals/txn-type.component.test.ts +52 -0
  74. package/src/lib/components/txns/internals/txn-type.component.ts +51 -0
  75. package/src/lib/components/txns/internals/txn.component.test.ts +188 -0
  76. package/src/lib/components/txns/internals/txn.component.ts +298 -0
  77. package/src/lib/components/txns/txns-overview.component.test.ts +107 -0
  78. package/src/lib/components/txns/txns-overview.component.ts +229 -0
  79. package/src/lib/icons/ai-outline-disconnect.component.ts +21 -0
  80. package/src/lib/icons/bi-logo-gmail.component.ts +21 -0
  81. package/src/lib/icons/cg-profile.component.ts +31 -0
  82. package/src/lib/icons/check-icon.component.ts +27 -0
  83. package/src/lib/icons/ci-clock2.component.ts +31 -0
  84. package/src/lib/icons/fa-arrow-right.component.ts +21 -0
  85. package/src/lib/icons/fa-times.component.ts +21 -0
  86. package/src/lib/icons/fa-undo.component.ts +21 -0
  87. package/src/lib/icons/icons.test.ts +25 -0
  88. package/src/lib/icons/index.ts +22 -0
  89. package/src/lib/icons/io-ios-arrow-back.component.ts +21 -0
  90. package/src/lib/icons/io-ios-arrow-forward.component.ts +21 -0
  91. package/src/lib/icons/io-md-arrow-back.component.ts +19 -0
  92. package/src/lib/icons/io-md-arrow-forward.component.ts +19 -0
  93. package/src/lib/icons/pi-warning-fill.component.ts +21 -0
  94. package/src/lib/icons/tb-arrows-exchange2.component.ts +30 -0
  95. package/src/lib/icons/tb-copy-check-filled.component.ts +21 -0
  96. package/src/lib/icons/tb-copy.component.ts +34 -0
  97. package/src/lib/icons/ti-vendor-microsoft.component.ts +21 -0
  98. package/src/lib/inject-store.test.ts +26 -0
  99. package/src/lib/inject-store.ts +16 -0
  100. package/src/lib/primitives/arrow-icon.component.test.ts +12 -0
  101. package/src/lib/primitives/arrow-icon.component.ts +38 -0
  102. package/src/lib/primitives/btn-arrow-dir.component.test.ts +22 -0
  103. package/src/lib/primitives/btn-arrow-dir.component.ts +65 -0
  104. package/src/lib/primitives/btn-base.component.test.ts +38 -0
  105. package/src/lib/primitives/btn-base.component.ts +140 -0
  106. package/src/lib/primitives/divider-base.component.test.ts +12 -0
  107. package/src/lib/primitives/divider-base.component.ts +74 -0
  108. package/src/lib/primitives/dropdown-arrow.component.test.ts +12 -0
  109. package/src/lib/primitives/dropdown-arrow.component.ts +46 -0
  110. package/src/lib/primitives/dropdown-base.component.test.ts +49 -0
  111. package/src/lib/primitives/dropdown-base.component.ts +329 -0
  112. package/src/lib/primitives/helpers.ts +151 -0
  113. package/src/lib/primitives/horizontal-loader.component.test.ts +19 -0
  114. package/src/lib/primitives/horizontal-loader.component.ts +70 -0
  115. package/src/lib/primitives/icon-base.component.test.ts +19 -0
  116. package/src/lib/primitives/icon-base.component.ts +58 -0
  117. package/src/lib/primitives/img-loader.component.test.ts +12 -0
  118. package/src/lib/primitives/img-loader.component.ts +118 -0
  119. package/src/lib/primitives/index.ts +29 -0
  120. package/src/lib/primitives/input-base.component.test.ts +20 -0
  121. package/src/lib/primitives/input-base.component.ts +210 -0
  122. package/src/lib/primitives/lifetime-slider-base.component.test.ts +88 -0
  123. package/src/lib/primitives/lifetime-slider-base.component.ts +75 -0
  124. package/src/lib/primitives/oval-spinner.component.test.ts +9 -0
  125. package/src/lib/primitives/oval-spinner.component.ts +158 -0
  126. package/src/lib/primitives/pickers/dropdown-picker-animated-base.component.ts +220 -0
  127. package/src/lib/primitives/pickers/picker-button.component.ts +74 -0
  128. package/src/lib/primitives/pickers/time-picker-dropdown-selector.component.ts +188 -0
  129. package/src/lib/primitives/pickers/time-picker.component.ts +124 -0
  130. package/src/lib/primitives/pickers/time-unit-selector.component.ts +82 -0
  131. package/src/lib/primitives/progressbar.component.test.ts +18 -0
  132. package/src/lib/primitives/progressbar.component.ts +93 -0
  133. package/src/lib/primitives/providersStyles.ts +268 -0
  134. package/src/lib/primitives/slider-base.component.ts +56 -0
  135. package/src/lib/provide-block-auth.test.ts +43 -0
  136. package/src/lib/provide-block-auth.ts +43 -0
  137. package/src/lib/runtime/block-auth-runtime.directive.test.ts +89 -0
  138. package/src/lib/runtime/block-auth-runtime.directive.ts +68 -0
  139. package/src/lib/teleport-body.directive.test.ts +29 -0
  140. package/src/lib/teleport-body.directive.ts +21 -0
  141. package/src/lib/testing.ts +15 -0
  142. package/src/lib/transitions.css +23 -0
  143. package/src/public-api.ts +85 -0
  144. package/src/vitest.setup.ts +8 -0
  145. package/tailwind.config.cjs +6 -0
  146. package/tsconfig.json +15 -0
  147. package/tsconfig.lib.json +9 -0
  148. package/vite.config.mts +44 -0
@@ -0,0 +1,329 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ ElementRef,
5
+ PLATFORM_ID,
6
+ computed,
7
+ effect,
8
+ inject,
9
+ input,
10
+ output,
11
+ viewChild,
12
+ } from '@angular/core';
13
+ import { isPlatformBrowser } from '@angular/common';
14
+ import { DropdownArrowComponent } from './dropdown-arrow.component';
15
+ import { CheckIconComponent } from '../icons';
16
+
17
+ export interface DropdownItem {
18
+ name: string;
19
+ value: string;
20
+ icon_url?: string;
21
+ isOptionSelected?: boolean;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export const defaultItems: DropdownItem[] = [
26
+ { name: 'Select', value: 'service_default', icon_url: 'https://picsum.photos/200/300', isOptionSelected: true },
27
+ { name: 'Block-Auth', value: 'blockauth', icon_url: 'https://dashboard.block-auth.io/img/blockauth_logo.png', isOptionSelected: false },
28
+ { name: 'Auditech', value: 'auditech', icon_url: 'https://auditech.es/wp-content/uploads/2020/05/Auditech-Iso-Mobile.png', isOptionSelected: false },
29
+ { name: 'Microsoft', value: 'microsoft', icon_url: 'https://avatars.githubusercontent.com/u/6154722?s=200&v=4', isOptionSelected: false },
30
+ ];
31
+
32
+ // - `setIsOpen` (callback) -> emit `isOpenChange` (dos-vías idiomática Angular).
33
+ // - `onChangeSelectedItem` -> emit `changeSelectedItem`.
34
+ // - `callbackWhenClickOutside` -> emit `clickOutside`.
35
+ // - `containerWithActiveClickOutsideRef` (React ref) -> input `container?: HTMLElement | null`.
36
+ // Semántica 1:1 de DropdownBase.tsx (187-214): sin `container` no se añade
37
+ // listener (click-outside desactivado); con `container` el listener va sobre
38
+ // `container` (no `document`) solo cuando `isOpen`; "fuera" se calcula vs el
39
+ // contenedor propio del dropdown; al cerrar (o destruir) se quita, EXCEPTO si
40
+ // `isInsideModal` (leak deliberado del original). Se replica con `effect()`
41
+ // sobre `isOpen()` (equivalente al useEffect(...,[isOpen]) de react).
42
+ // - SSR: la lógica de click-outside solo corre en navegador (isPlatformBrowser).
43
+ // - Animación open/close: `bas-pop` (transitions.css). El CheckIcon del item
44
+ // seleccionado usa el icono compartido `ba-check-icon` (heroicons
45
+ // CheckIcon 20/solid, Task A6).
46
+ @Component({
47
+ selector: 'ba-dropdown-base',
48
+ standalone: true,
49
+ changeDetection: ChangeDetectionStrategy.OnPush,
50
+ imports: [DropdownArrowComponent, CheckIconComponent],
51
+ template: `
52
+ <div
53
+ [class]="
54
+ 'bas-box-border bas-group bas-relative bas-items-center ' + displayClassNames() + ' ' + classNames() + ' ' +
55
+ getHeight(showTitle(), size()) + ' ' + (showTitle() ? containerGridRows() : '')
56
+ "
57
+ [style.width]="width()"
58
+ [style.minWidth]="minWidth()"
59
+ [style.maxWidth]="maxWidth()"
60
+ >
61
+ @if (showTitle()) {
62
+ <p
63
+ [class]="
64
+ 'bal-ba-transition-2 bas-m-0 bas-h-[18px] bas-text-sm ' +
65
+ (blueStyles()
66
+ ? 'bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]'
67
+ : 'bas-text-gray-700 group-hover:bas-text-gray-900')
68
+ "
69
+ >{{ title() }}</p>
70
+ }
71
+ @if (showFloatingLabel()) {
72
+ <span
73
+ [class]="
74
+ 'bal-ba-transition-2 bas-group bas-absolute -bas-top-3 bas-left-2 bas-z-50 bas-flex bas-items-center bas-justify-center bas-px-1 bas-py-0.5 ' +
75
+ floatingLabelBgColor() + ' ' +
76
+ (blueStyles()
77
+ ? 'bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]'
78
+ : 'bas-text-gray-700 group-hover:bas-text-gray-900')
79
+ "
80
+ >
81
+ <p [class]="'bal-ba-transition-2 bas-m-0 ' + (size() !== 'xxs' ? 'bas-text-xs' : 'bas-text-xxs')">
82
+ {{ floatingLabelTxt() }}
83
+ </p>
84
+ </span>
85
+ }
86
+ <div #ddBaseContainer [class]="'bas-flex bas-w-full bas-flex-col bas-items-center bas-justify-start ' + getHeight(false, size())">
87
+ <button
88
+ [id]="btnId()"
89
+ [disabled]="disabled()"
90
+ [class]="
91
+ 'bal-ba-transition-2 bal-ba-outline-none bas-relative bas-box-border bas-flex bas-w-full bas-cursor-pointer bas-flex-row bas-justify-between bas-gap-2 bas-border-2 bas-border-solid bas-bg-transparent bas-px-2 disabled:bas-cursor-not-allowed ' +
92
+ getBtnHeight(size()) + ' ' + borderRadiusClassNames() + ' ' +
93
+ (blueStyles() || isOpen()
94
+ ? 'bas-border-[var(--bal-primary-100)] group-hover:bas-border-[var(--bal-primary-300)]'
95
+ : 'bas-border-gray-300 group-hover:bas-border-gray-400')
96
+ "
97
+ (click)="onBtnClicked($event)"
98
+ >
99
+ <div class="bas-group bas-flex bas-h-full bas-w-full bas-flex-row bas-items-center bas-gap-1.5">
100
+ @if (showStatusIndicator()) {
101
+ <span [class]="'bas-h-2 bas-w-2 bas-rounded-full ' + (!isSelectedDefault() ? 'bas-bg-green-500' : 'bas-bg-sky-300')"></span>
102
+ }
103
+ @if (showImg() && selectedItem().icon_url) {
104
+ <img [src]="selectedItem().icon_url" [alt]="'Image of ' + selectedItem()[titleKeyName()]" class="bas-h-5 bas-w-5 bas-flex-shrink-0" />
105
+ }
106
+ <p
107
+ [class]="
108
+ 'bal-ba-transition-2 bas-m-0 ' + (size() !== 'xxs' ? 'bas-text-base' : 'bas-text-sm') + ' ' +
109
+ (isOpen() || blueStyles()
110
+ ? (!isSelectedDefault()
111
+ ? 'bas-text-[var(--bal-primary-800)] group-hover:bas-text-[var(--bal-primary-900)]'
112
+ : 'bas-text-[var(--bal-primary-500)] group-hover:bas-text-[var(--bal-primary-600)]')
113
+ : (!isSelectedDefault()
114
+ ? 'bas-text-gray-800 group-hover:bas-text-gray-900'
115
+ : 'bas-text-gray-500 group-hover:bas-text-gray-600'))
116
+ "
117
+ >{{ selectedItem()[titleKeyName()] }}</p>
118
+ </div>
119
+ <div class="bas-h-full bas-w-fit bas-justify-center">
120
+ <ba-dropdown-arrow
121
+ id="web-type-arrow"
122
+ [isOpen]="isOpen()"
123
+ sizeClassNames="bas-h-3 bas-w-3"
124
+ [color]="blueStyles() || isOpen() ? 'var(--bal-primary-700)' : 'gray'"
125
+ />
126
+ </div>
127
+ </button>
128
+ @if (isOpen()) {
129
+ <div
130
+ class="ba-pop bal-ba-transition-2 bal-ba-shadow-base bal-ba-hover-shadow-md bas-absolute bas-top-full bas-z-50 bas-mt-1 bas-w-full bas-overflow-hidden bas-rounded-md bas-border bas-border-solid bas-border-[var(--bal-primary-100)] bas-bg-white hover:bas-border-[var(--bal-primary-300)]"
131
+ [style.minHeight.px]="listMinHeight()"
132
+ >
133
+ <ul
134
+ [class]="
135
+ 'bal-ba-transition-2 bal-ba-scroll-base bas-m-0 bas-box-border bas-flex bas-h-full bas-w-full bas-flex-col bas-items-center bas-justify-start bas-p-0 bas-text-base bas-text-[var(--bal-primary-700)] hover:bas-text-[var(--bal-primary-900)] bas-overflow-y-auto'
136
+ "
137
+ [style.maxHeight.px]="listMinHeight()"
138
+ >
139
+ @for (item of items(); track item[keyName()]; let index = $index) {
140
+ <li
141
+ [id]="'dropdown-base-item-' + item[keyName()]"
142
+ [class]="
143
+ 'bal-ba-outline-none bas-group bas-box-border bas-flex bas-w-full bas-flex-row bas-items-center bas-justify-between bas-border-solid bas-border-[var(--bal-primary-90)] bas-px-2 bas-py-1 ' +
144
+ getItemBorderStyles(index, items().length) + ' ' +
145
+ (isItemDefault(item) ? 'bas-cursor-default' : 'bas-cursor-pointer') + ' ' +
146
+ (isSelected(item) ? 'bas-bg-[#e7f2f7] hover:bas-bg-[#dbecf3]' : 'hover:bas-bg-[#f3f8fb]') + ' ' +
147
+ (size() !== 'xxs' ? 'bas-min-h-[40px]' : 'bas-min-h-[34px]')
148
+ "
149
+ (click)="onItemClicked(item)"
150
+ >
151
+ <div class="bas-group bas-flex bas-h-full bas-w-fit bas-flex-row bas-items-center bas-gap-1.5">
152
+ @if (showStatusIndicator()) {
153
+ <span
154
+ [class]="
155
+ 'bas-h-2 bas-w-2 bas-rounded-full ' +
156
+ (!isItemDefault(item) ? (isSelected(item) ? 'bas-bg-green-500' : 'bas-bg-slate-300') : 'bas-bg-sky-300')
157
+ "
158
+ ></span>
159
+ }
160
+ @if (showImg() && item.icon_url) {
161
+ <img [src]="item.icon_url" [alt]="'Image of ' + item[titleKeyName()]" class="bas-h-5 bas-w-5 bas-flex-shrink-0" />
162
+ }
163
+ <p [class]="getOptionStyles(item, size())">{{ item[titleKeyName()] }}</p>
164
+ </div>
165
+ @if (showCheckWhenActive() && isSelected(item) && !isItemDefault(item)) {
166
+ <span class="bal-ba-transition-2 bas-flex bas-items-center bas-text-[#004e98] group-hover:bas-text-[#00296b]">
167
+ <ba-check-icon class="bas-h-4 bas-w-4" />
168
+ </span>
169
+ }
170
+ </li>
171
+ }
172
+ </ul>
173
+ </div>
174
+ }
175
+ </div>
176
+ @if (showTitle()) {
177
+ <p class="bal-ba-error-text-invisible bas-m-0 bas-h-[18px]">white space</p>
178
+ }
179
+ </div>
180
+ `,
181
+ styles: [
182
+ `
183
+ .ba-pop {
184
+ animation: ba-pop-in 0.15s ease;
185
+ transform-origin: top;
186
+ }
187
+ @keyframes ba-pop-in {
188
+ from { opacity: 0; transform: scale(0.95); }
189
+ to { opacity: 1; transform: scale(1); }
190
+ }
191
+ `,
192
+ ],
193
+ })
194
+ export class DropdownBaseComponent {
195
+ private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
196
+ private readonly ddBaseContainer = viewChild<ElementRef<HTMLDivElement>>('ddBaseContainer');
197
+
198
+ btnId = input('dropdown-base-button');
199
+ isOpen = input(false);
200
+ title = input('Select an item');
201
+ showTitle = input(false);
202
+ showFloatingLabel = input(false);
203
+ floatingLabelTxt = input('Select an item');
204
+ floatingLabelBgColor = input('bas-bg-white');
205
+ size = input<'xxs' | 'xs' | 'tiny' | 'small' | 'base' | 'large'>('base');
206
+ items = input<DropdownItem[]>(defaultItems);
207
+ selectedItem = input<DropdownItem>(defaultItems[0]);
208
+ keyName = input('value');
209
+ titleKeyName = input('name');
210
+ width = input<number | string>('100%');
211
+ minWidth = input<number | string>(0);
212
+ maxWidth = input<number | string>('100%');
213
+ dropdownHeight = input<number | null>(null);
214
+ classNames = input('');
215
+ displayClassNames = input('bas-grid');
216
+ borderRadiusClassNames = input('bas-rounded-lg');
217
+ container = input<HTMLElement | null>(null);
218
+ isInsideModal = input(false);
219
+ showStatusIndicator = input(true);
220
+ showImg = input(false);
221
+ showCheckWhenActive = input(true);
222
+ manageActiveWithIsOptionSelected = input(false);
223
+ blueStyles = input(false);
224
+ disabled = input(false);
225
+
226
+ isOpenChange = output<boolean>();
227
+ changeSelectedItem = output<DropdownItem>();
228
+ clickOutside = output<void>();
229
+
230
+ private readonly handleClickOutside = (e: Event): void => {
231
+ const el = this.ddBaseContainer()?.nativeElement;
232
+ if (!el) return;
233
+ if (!el.contains(e.target as Node)) this.clickOutside.emit();
234
+ };
235
+
236
+ constructor() {
237
+ // Port 1:1 del useEffect(...,[isOpen]) de DropdownBase.tsx (187-214),
238
+ // solo en navegador (SSR-safe): añade el listener sobre `container` al
239
+ // abrir; lo quita al cerrar/destruir salvo `isInsideModal`.
240
+ effect((onCleanup) => {
241
+ if (!this.isBrowser) return;
242
+ const container = this.container();
243
+ if (!container) return;
244
+ const open = this.isOpen();
245
+ const removeEventListeners = () => {
246
+ if (this.isInsideModal()) return;
247
+ container.removeEventListener('click', this.handleClickOutside);
248
+ };
249
+ if (open) container.addEventListener('click', this.handleClickOutside);
250
+ else removeEventListeners();
251
+ onCleanup(removeEventListeners);
252
+ });
253
+ }
254
+
255
+ onBtnClicked(e: MouseEvent): void {
256
+ e.preventDefault();
257
+ this.isOpenChange.emit(!this.isOpen());
258
+ }
259
+
260
+ onItemClicked(item: DropdownItem): void {
261
+ this.changeSelectedItem.emit(item);
262
+ this.isOpenChange.emit(false);
263
+ }
264
+
265
+ isSelected = (item: DropdownItem): boolean =>
266
+ this.manageActiveWithIsOptionSelected()
267
+ ? !!item.isOptionSelected
268
+ : this.selectedItem()[this.keyName()] === item[this.keyName()];
269
+
270
+ isSelectedDefault = computed(() => `${this.selectedItem()[this.keyName()]}`.includes('default'));
271
+ isItemDefault = (item: DropdownItem): boolean => `${item[this.keyName()]}`.includes('default');
272
+
273
+ listMinHeight = computed(
274
+ () => this.dropdownHeight() || this.items().length * (this.size() !== 'xxs' ? 40 : 34) + 2,
275
+ );
276
+
277
+ containerGridRows = computed(
278
+ () =>
279
+ ({
280
+ xxs: 'bas-grid-rows-[18px_34px_18px]',
281
+ xs: 'bas-grid-rows-[18px_36px_18px]',
282
+ tiny: 'bas-grid-rows-[18px_38px_18px]',
283
+ small: 'bas-grid-rows-[18px_42px_18px]',
284
+ base: 'bas-grid-rows-[18px_46px_18px]',
285
+ large: 'bas-grid-rows-[18px_54px_18px]',
286
+ })[this.size()],
287
+ );
288
+
289
+ getHeight(showTitle: boolean, size: string): string {
290
+ return {
291
+ xxs: showTitle ? 'bas-h-[70px]' : 'bas-h-[34px]',
292
+ xs: showTitle ? 'bas-h-[72px]' : 'bas-h-[36px]',
293
+ tiny: showTitle ? 'bas-h-[74px]' : 'bas-h-[38px]',
294
+ small: showTitle ? 'bas-h-[78px]' : 'bas-h-[42px]',
295
+ base: showTitle ? 'bas-h-[82px]' : 'bas-h-[46px]',
296
+ large: showTitle ? 'bas-h-[90px]' : 'bas-h-[54px]',
297
+ }[size]!;
298
+ }
299
+
300
+ getBtnHeight(size: string): string {
301
+ return {
302
+ xxs: 'bas-min-h-[34px]',
303
+ xs: 'bas-min-h-[36px]',
304
+ tiny: 'bas-min-h-[38px]',
305
+ small: 'bas-min-h-[42px]',
306
+ base: 'bas-min-h-[46px]',
307
+ large: 'bas-min-h-[54px]',
308
+ }[size]!;
309
+ }
310
+
311
+ getOptionStyles(item: DropdownItem, size: string): string {
312
+ const classes = 'bas-m-0 bas-block bal-ba-transition-2';
313
+ if (`${item[this.keyName()]}`.includes('default')) return `${classes} bas-text-[#004e98] bas-text-sm`;
314
+ return `${classes} ${size !== 'xxs' ? 'bas-text-base' : 'bas-text-sm'} ${
315
+ this.isSelected(item)
316
+ ? 'bas-font-medium bas-text-[#00296b] group-hover:bas-text-[#002055]'
317
+ : 'bas-text-[#004e98] group-hover:bas-text-[#00296b]'
318
+ }`;
319
+ }
320
+
321
+ getItemBorderStyles(index: number, itemsLength: number): string {
322
+ let classNames = '';
323
+ if (index < itemsLength - 1) classNames += ' bas-border-x-0 bas-border-b bas-border-t-0 ';
324
+ else classNames += ' bas-border-0 ';
325
+ if (index === 0) classNames += ' bas-rounded-t-lg ';
326
+ else if (index === itemsLength - 1) classNames += ' bas-rounded-b-lg ';
327
+ return classNames;
328
+ }
329
+ }
@@ -0,0 +1,151 @@
1
+ // Port literal de packages/react/src/primitives/helpers.ts: helpers de UI
2
+ // puros en TS, sin dependencias de React ni de framer-motion, por lo que se
3
+ // copian tal cual (solo cambia el comentario de cabecera). Las variantes de
4
+ // framer-motion (`dropdownContainerVariantsFromTopRight`, `liItemVariants`,
5
+ // etc.) ya no las usa ningún primitivo Vue (DropdownBase anima con
6
+ // `<Transition>` + CSS), pero se conservan por si algún componente futuro
7
+ // las necesita como referencia/objeto de datos plano (no importan
8
+ // framer-motion, son solo objetos JS).
9
+ //
10
+ // `imgUrlByService` ya vive en `@block-auth.io/core` utils — se reexporta
11
+ // aquí para que los primitivos puedan importar todo desde un mismo sitio.
12
+ import { utils } from '@block-auth.io/core';
13
+
14
+ export const imgUrlByService = utils.imgUrlByService;
15
+
16
+ /**
17
+ * Variantes framer-motion usadas por el `DropdownBase` original (el dropdown
18
+ * se revelaba desde la esquina superior derecha vía un círculo de
19
+ * clip-path animado). Portado literalmente desde `helperUI.dropdownContainerVariantsFromTopRight`.
20
+ * No se usa en el `DropdownBase.vue` (que anima con `<Transition name="bas-pop">`),
21
+ * se conserva por paridad de API con el paquete react.
22
+ */
23
+ export function dropdownContainerVariantsFromTopRight(
24
+ height = 1000,
25
+ left: number | string = 'auto',
26
+ ) {
27
+ return {
28
+ open: {
29
+ clipPath: `circle(${height * 2 + 200}px at 100% 0%)`,
30
+ left,
31
+ transition: {
32
+ type: 'spring',
33
+ stiffness: 20,
34
+ restDelta: 2,
35
+ duration: 1,
36
+ delayChildren: 0.3,
37
+ staggerChildren: 0.05,
38
+ },
39
+ },
40
+ closed: {
41
+ clipPath: 'circle(0px at 100% 0%)',
42
+ left,
43
+ transition: {
44
+ delay: 0,
45
+ type: 'spring',
46
+ stiffness: 400,
47
+ damping: 40,
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Variantes de stagger por item de la lista del dropdown. Portado
55
+ * literalmente desde `helperUI.liItemVariants`. No usado en `DropdownBase.vue`.
56
+ */
57
+ export function liItemVariants(openOpacity = 1) {
58
+ return {
59
+ open: {
60
+ opacity: openOpacity,
61
+ y: 0,
62
+ transition: { duration: 0.2 },
63
+ },
64
+ closed: { opacity: 0, y: 20, transition: { duration: 0.2 } },
65
+ };
66
+ }
67
+
68
+ /* ------------------------------------------------------------------ *
69
+ * Helpers de tiempo + variantes de dropdown de pickers — absorbidos del
70
+ * `helperUI` original. Los usan `pickers/` y `LifetimeSliderBase`, ambos
71
+ * fuera de alcance en esta tarea (excluidos por el plan), pero se conservan
72
+ * (son TS puro, sin coste) por si un futuro port de esos componentes los
73
+ * necesita.
74
+ * ------------------------------------------------------------------ */
75
+
76
+ /** Descompone segundos totales en `{ hours, minutes, seconds }`. */
77
+ export function convertSecondsToTime(totalSeconds: number) {
78
+ const hours = Math.floor(totalSeconds / 3600);
79
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
80
+ const seconds = totalSeconds % 60;
81
+ return { hours, minutes, seconds };
82
+ }
83
+
84
+ /** Rellena con cero un número menor a 10 (`5` → `"05"`). */
85
+ export function formatNumberToString(num: number) {
86
+ return num < 10 ? `0${num}` : `${num}`;
87
+ }
88
+
89
+ /** Parsea un string `HH:MM:SS` a segundos totales. */
90
+ export function getSecFromTimeValue(timeValue: string) {
91
+ const [h, m, s] = timeValue.split(':');
92
+ return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s);
93
+ }
94
+
95
+ /** Segundos totales → `HH:MM:SS` (port de `getTimeBySec`). */
96
+ export function getTimeBySec(totalSeconds: number) {
97
+ const { hours, minutes, seconds } = convertSecondsToTime(totalSeconds);
98
+ return [hours, minutes, seconds].map(formatNumberToString).join(':');
99
+ }
100
+
101
+ /**
102
+ * Revelado del dropdown del picker (hacia abajo) — círculo de clip-path
103
+ * desde arriba al centro. Port literal de `dropdownContainerVariantsTopCenterCircle`.
104
+ */
105
+ export function dropdownContainerVariantsTopCenterCircle(height = 1000) {
106
+ return {
107
+ open: {
108
+ clipPath: `circle(${height * 2 + 200}px at 50% 0%)`,
109
+ top: 0,
110
+ transition: {
111
+ type: 'spring',
112
+ stiffness: 20,
113
+ restDelta: 2,
114
+ duration: 1,
115
+ delayChildren: 0.3,
116
+ staggerChildren: 0.05,
117
+ },
118
+ },
119
+ closed: {
120
+ clipPath: 'circle(0px at 50% 0%)',
121
+ top: '-100%',
122
+ transition: { delay: 0, type: 'spring', stiffness: 400, damping: 40 },
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Revelado del dropdown del picker (hacia arriba) — círculo de clip-path
129
+ * desde abajo al centro. Port literal de `dropdownContainerUpwardsVariants`.
130
+ */
131
+ export function dropdownContainerUpwardsVariants(top = -288, height = 1000) {
132
+ return {
133
+ open: {
134
+ clipPath: `circle(${height * 2 + 200}px at 50% 100%)`,
135
+ top,
136
+ transition: {
137
+ type: 'tween',
138
+ stiffness: 20,
139
+ restDelta: 2,
140
+ duration: 2,
141
+ delayChildren: 0.3,
142
+ staggerChildren: 0.05,
143
+ },
144
+ },
145
+ closed: {
146
+ clipPath: 'circle(0px at 50% 100%)',
147
+ top,
148
+ transition: { delay: 0, type: 'tween', stiffness: 400, damping: 40 },
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/angular';
3
+ import { HorizontalLoaderComponent } from './horizontal-loader.component';
4
+ describe('HorizontalLoaderComponent', () => {
5
+ it('renderiza un svg', async () => {
6
+ const { container } = await render(HorizontalLoaderComponent);
7
+ expect(container.querySelector('svg')).toBeTruthy();
8
+ });
9
+
10
+ // Regresión: el host debe ser `display: contents` para que el <div> absoluto
11
+ // sea hijo directo del flex del botón (como en React). Sin esto el loader
12
+ // aparece descentrado dentro del botón en estado de carga.
13
+ it('el host es transparente al layout (display: contents)', async () => {
14
+ const { container } = await render(HorizontalLoaderComponent);
15
+ // testing-library monta el componente sobre su propio host (el `container`),
16
+ // donde Angular aplica el binding `host` del @Component.
17
+ expect(container.style.display).toBe('contents');
18
+ });
19
+ });
@@ -0,0 +1,70 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+
3
+ // Sin react-loader-spinner:
4
+ // se dibujan 3 puntos SVG con una animación de opacidad escalonada
5
+ // (definida en `styles`), reproduciendo el look del `ThreeDots` original.
6
+ @Component({
7
+ selector: 'ba-horizontal-loader',
8
+ standalone: true,
9
+ // El host debe ser transparente al layout para que el <div> absoluto sea hijo
10
+ // directo del flex del botón (como en React, donde HorizontalLoader renderiza
11
+ // el <div> directamente). Sin esto el host `<ba-horizontal-loader>` es un
12
+ // flex-item de ancho ~0 colocado por `justify-center`, y la posición estática
13
+ // del <div> `bas-absolute` (sin left/top) se corre al centro; con `bas-w-full`
14
+ // se desborda a la derecha y el loader aparece descentrado.
15
+ host: { style: 'display: contents' },
16
+ changeDetection: ChangeDetectionStrategy.OnPush,
17
+ template: `
18
+ <div
19
+ [class]="
20
+ 'bal-ba-backdrop-blur-base bal-ba-backdrop-blur-sm bas-absolute bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center ' +
21
+ classNames()
22
+ "
23
+ >
24
+ <svg
25
+ class="bas-three-dots"
26
+ [attr.height]="loaderHeight()"
27
+ [attr.width]="loaderWidth()"
28
+ viewBox="0 0 80 40"
29
+ role="img"
30
+ aria-label="three-dots-loading"
31
+ >
32
+ <circle class="bas-dot bas-dot-1" cx="15" cy="20" r="9" [attr.fill]="loaderColor()" />
33
+ <circle class="bas-dot bas-dot-2" cx="40" cy="20" r="9" [attr.fill]="loaderColor()" />
34
+ <circle class="bas-dot bas-dot-3" cx="65" cy="20" r="9" [attr.fill]="loaderColor()" />
35
+ </svg>
36
+ </div>
37
+ `,
38
+ styles: [
39
+ `
40
+ @keyframes bas-three-dots-fade {
41
+ 0%,
42
+ 80%,
43
+ 100% {
44
+ opacity: 0.25;
45
+ }
46
+ 40% {
47
+ opacity: 1;
48
+ }
49
+ }
50
+ .bas-dot {
51
+ animation: bas-three-dots-fade 1.2s ease-in-out infinite;
52
+ }
53
+ .bas-dot-1 {
54
+ animation-delay: -0.32s;
55
+ }
56
+ .bas-dot-2 {
57
+ animation-delay: -0.16s;
58
+ }
59
+ .bas-dot-3 {
60
+ animation-delay: 0s;
61
+ }
62
+ `,
63
+ ],
64
+ })
65
+ export class HorizontalLoaderComponent {
66
+ loaderColor = input('#61a5c2');
67
+ loaderHeight = input(40);
68
+ loaderWidth = input(80);
69
+ classNames = input('bas-rounded-md');
70
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen } from '@testing-library/angular';
4
+ import { IconBaseComponent } from './icon-base.component';
5
+
6
+ describe('IconBaseComponent', () => {
7
+ it('renderiza el contenido proyectado cuando lo hay', async () => {
8
+ @Component({ standalone: true, imports: [IconBaseComponent], template: `<ba-icon-base><span data-testid="slot">x</span></ba-icon-base>` })
9
+ class Host {}
10
+ await render(Host);
11
+ expect(screen.getByTestId('slot')).toBeTruthy();
12
+ });
13
+ it('renderiza un <img> desde iconUrl cuando no hay contenido proyectado', async () => {
14
+ @Component({ standalone: true, imports: [IconBaseComponent], template: `<ba-icon-base iconUrl="https://x/y.png" />` })
15
+ class Host {}
16
+ const { container } = await render(Host);
17
+ expect(container.querySelector('img')?.getAttribute('src')).toBe('https://x/y.png');
18
+ });
19
+ });
@@ -0,0 +1,58 @@
1
+ import {
2
+ AfterViewChecked,
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ input,
7
+ signal,
8
+ viewChild,
9
+ } from '@angular/core';
10
+
11
+ // Renderiza el contenido proyectado
12
+ // (equivalente al slot `icon` de Vue / ReactNode `icon` de React) si hay
13
+ // contenido real, si no un <img> desde `iconUrl`, si no nada.
14
+ //
15
+ // Vue resuelve esto con `useSlots().default({}).length > 0` (comprobación en
16
+ // tiempo de render de si el slot forwardeado produjo vnodes reales). Angular
17
+ // no tiene un equivalente directo para <ng-content>, así que se envuelve la
18
+ // proyección en un <span #iconSlot> propio del template (no del content) y
19
+ // se comprueba en cada ciclo de content-check si ese wrapper terminó con
20
+ // nodos hijos reales — mismo resultado observable (fallback a `iconUrl` solo
21
+ // cuando no se proyectó nada).
22
+ @Component({
23
+ selector: 'ba-icon-base',
24
+ standalone: true,
25
+ changeDetection: ChangeDetectionStrategy.OnPush,
26
+ template: `
27
+ <span #iconSlot style="display: contents"><ng-content /></span>
28
+ @if (!hasIconContent() && iconUrl()) {
29
+ <img
30
+ [src]="iconUrl()"
31
+ [width]="iconWidth()"
32
+ [height]="iconHeight()"
33
+ [class]="'bas-flex-shrink-0 bas-object-contain ' + iconClassNames()"
34
+ [style.width.px]="iconWidth()"
35
+ [style.height.px]="iconHeight()"
36
+ alt=""
37
+ />
38
+ }
39
+ `,
40
+ })
41
+ export class IconBaseComponent implements AfterViewChecked {
42
+ iconUrl = input<string | undefined>(undefined);
43
+ iconWidth = input(24);
44
+ iconHeight = input(24);
45
+ iconClassNames = input('bas-rounded-full');
46
+
47
+ // Signal query (no `@ViewChild` — los decoradores de campo clásicos no se
48
+ // soportan en el modo JIT del harness de test AnalogJS/Vitest).
49
+ private readonly iconSlot = viewChild<ElementRef<HTMLElement>>('iconSlot');
50
+
51
+ hasIconContent = signal(false);
52
+
53
+ ngAfterViewChecked(): void {
54
+ const el = this.iconSlot()?.nativeElement ?? null;
55
+ const hasContent = !!el && el.childNodes.length > 0;
56
+ if (hasContent !== this.hasIconContent()) this.hasIconContent.set(hasContent);
57
+ }
58
+ }
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render } from '@testing-library/angular';
4
+ import { ImgLoaderComponent } from './img-loader.component';
5
+ describe('ImgLoaderComponent', () => {
6
+ it('renderiza la imagen con el imgSrc dado', async () => {
7
+ @Component({ standalone: true, imports: [ImgLoaderComponent], template: `<ba-img-loader imgSrc="https://x/y.png" imgAlt="a" />` })
8
+ class Host {}
9
+ const { container } = await render(Host);
10
+ expect(container.querySelector('img')?.getAttribute('src')).toBe('https://x/y.png');
11
+ });
12
+ });