@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,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen } from '@testing-library/angular';
4
+ import { FooterSectionComponent } from './footer-section.component';
5
+
6
+ // Port de packages/react/src/components/sections/FooterSection.test.tsx.
7
+ describe('FooterSection', () => {
8
+ it('renderiza el footer con el enlace', async () => {
9
+ @Component({
10
+ standalone: true,
11
+ imports: [FooterSectionComponent],
12
+ template: `<ba-footer-section text1="Powered by" text2="Block-Auth" href="https://block-auth.io" />`,
13
+ })
14
+ class HostComponent {}
15
+ await render(HostComponent);
16
+ const link = screen.getByText('Block-Auth');
17
+ expect(link.getAttribute('href')).toBe('https://block-auth.io');
18
+ });
19
+ });
@@ -0,0 +1,49 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+
3
+ /**
4
+ * Port literal de packages/react/src/components/sections/FooterSection.tsx
5
+ * (mapa: packages/vue/.../sections/FooterSection.vue). Footer presentacional
6
+ * puro, sin core logic ni stores.
7
+ */
8
+ @Component({
9
+ selector: 'ba-footer-section',
10
+ standalone: true,
11
+ // Hijo directo del grid de content-signup: el host debe ser transparente al
12
+ // layout para que el <footer> interno sea el grid item real (ver
13
+ // providers-section para el detalle). Sin esto el layout del modal se solapa.
14
+ host: { style: 'display: contents' },
15
+ changeDetection: ChangeDetectionStrategy.OnPush,
16
+ template: `
17
+ <footer
18
+ data-testid="footer-section"
19
+ [class]="
20
+ 'bas-group bas-box-border bas-flex bas-h-12 bas-items-end bas-justify-start bas-py-1 bas-text-left bas-text-gray-500 ' +
21
+ getFooterClassNames(isEditable(), device())
22
+ "
23
+ >
24
+ <p class="bas-ba-transition-2 bas-m-0 bas-text-left bas-text-gray-500 group-hover:bas-text-slate-600">
25
+ {{ text1() }}
26
+ <a
27
+ [href]="href()"
28
+ target="_blank"
29
+ rel="noreferrer"
30
+ class="bal-ba-transition-2 bas-font-semibold bas-leading-6 bas-text-[var(--bal-primary-900)] group-hover:bas-text-[var(--bal-primary-600)]"
31
+ >{{ text2() }}</a
32
+ >
33
+ </p>
34
+ </footer>
35
+ `,
36
+ })
37
+ export class FooterSectionComponent {
38
+ isEditable = input(false);
39
+ device = input<'desktop' | 'phone'>('desktop');
40
+ text1 = input('');
41
+ text2 = input('');
42
+ href = input('');
43
+
44
+ getFooterClassNames(editable: boolean, dev: string): string {
45
+ if (!editable) return 'bas-text-xxs xsm:bas-text-xs xsm1:bas-text-sm sm1:bas-col-span-2';
46
+ if (dev === 'phone') return 'bas-text-xs';
47
+ return 'bas-text-sm bas-col-span-2';
48
+ }
49
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen, fireEvent } from '@testing-library/angular';
4
+ import { ProvidersButtonsComponent } from './providers-buttons.component';
5
+
6
+ // Port de packages/react/src/components/sections/internals/ProvidersButtons.test.tsx.
7
+ // ProvidersButtons no inyecta el client (renderiza <ba-btn-base-provider>, que
8
+ // tampoco lo hace), así que no requiere provideBlockAuthClientForTest.
9
+ describe('ProvidersButtons', () => {
10
+ it('renderiza un boton por proveedor y propaga la seleccion', async () => {
11
+ const onSelected = vi.fn();
12
+ @Component({
13
+ standalone: true,
14
+ imports: [ProvidersButtonsComponent],
15
+ template: `<ba-providers-buttons [showedProviders]="providers" (selectedServiceChange)="onSelected($event)" />`,
16
+ })
17
+ class HostComponent {
18
+ providers = ['metamask', 'phantom'];
19
+ onSelected = onSelected;
20
+ }
21
+ await render(HostComponent);
22
+ const buttons = screen.getAllByRole('button');
23
+ expect(buttons.length).toBe(2);
24
+ await fireEvent.click(buttons[0]);
25
+ expect(onSelected).toHaveBeenCalledWith('metamask');
26
+ });
27
+ });
@@ -0,0 +1,290 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ DestroyRef,
5
+ ElementRef,
6
+ PLATFORM_ID,
7
+ effect,
8
+ inject,
9
+ input,
10
+ output,
11
+ signal,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import { isPlatformBrowser } from '@angular/common';
15
+ import { BtnArrowDirComponent } from '../../../primitives/btn-arrow-dir.component';
16
+ import { providersStyles } from '../../../primitives/providersStyles';
17
+ import { BtnBaseProviderComponent } from '../../btns/btn-base-provider.component';
18
+
19
+ /**
20
+ * Grid de proveedores: tarjetas blancas bordeadas, cada una con el icono del
21
+ * provider sobre fondo tintado y el borde/hover de `providersStyles`.
22
+ *
23
+ * Diferencia de arquitectura respecto a react (idéntica al port Vue): allí
24
+ * `providersWrapperRef`/`providersContainerRef` los crea el padre
25
+ * (`ProvidersSection`) para medir `scrollWidth`/`clientWidth` y derivar
26
+ * `isProvidersScroll`. En Angular los template refs son locales al componente
27
+ * que los declara, así que ese efecto de medición se internaliza aquí (refs
28
+ * propios + estado `isProvidersScroll`). `setSelectedService` (callback) ->
29
+ * output `selectedServiceChange`. El shake de framer-motion -> clase `.ba-shake`.
30
+ */
31
+ @Component({
32
+ selector: 'ba-providers-buttons',
33
+ standalone: true,
34
+ changeDetection: ChangeDetectionStrategy.OnPush,
35
+ imports: [BtnArrowDirComponent, BtnBaseProviderComponent],
36
+ template: `
37
+ <div
38
+ #wrapper
39
+ data-testid="providers-buttons"
40
+ [class]="
41
+ 'bas-relative bas-z-30 bas-box-border bas-w-full ' +
42
+ getProvidersWrapperClassNames(providersFiltersEnabled(), isMobile(), isProvidersScroll(), isEditable(), device()) +
43
+ (isEditable() && isEditing() && !isOpenProvidersBtnsEditable ? ' ba-shake' : '')
44
+ "
45
+ >
46
+ @if (isMobile() && isProvidersScroll()) {
47
+ <div class="bas-box-border bas-flex bas-h-full bas-w-full bas-items-center bas-justify-start bas-pb-4">
48
+ <ba-btn-arrow-dir direction="back" [disabled]="false" arrowType="arrow" (clicked)="onScrollArrowClicked('left')" />
49
+ </div>
50
+ }
51
+
52
+ <div
53
+ #container
54
+ [attr.draggable]="isProvidersScroll()"
55
+ [class]="
56
+ 'bal-ba-scroll-base bas-box-border bas-w-full bas-items-start ' +
57
+ (isMobile() && isProvidersScroll() ? '' : 'bas-justify-center') + ' ' +
58
+ (isEditable() && device() === 'phone' ? 'bal-ba-scroll-horizontal' : 'bal-ba-max-659-scroll-horizontal') + ' ' +
59
+ providersContainerClassNames(
60
+ uiConfigProvidersInfo().providersColumns ?? 3,
61
+ providersFiltersEnabled(),
62
+ showedProviders(),
63
+ isMobile(),
64
+ isEditable(),
65
+ device()
66
+ )
67
+ "
68
+ >
69
+ @for (item of showedProviders(); track $index) {
70
+ <div [class]="getBtnBaseProviderContainerClassNames(isEditable(), device())">
71
+ <ba-btn-base-provider
72
+ [text]="getBtnProviderText(item, uiConfigProvidersInfo().providersColumns ?? 3, isMobile(), uiConfigProvidersInfo().showProviderNameOnMobile)"
73
+ [sizeClassNames]="getBtnProviderSize(isMobile(), uiConfigProvidersInfo().showProviderNameOnMobile, isEditable(), device())"
74
+ [borderClassNames]="getBtnProviderBorderClassNames(item, selectedService())"
75
+ [isEditable]="isEditable()"
76
+ [device]="device()"
77
+ (providerClicked)="selectedServiceChange.emit(item)"
78
+ >
79
+ @if (providersStyles[item]) {
80
+ <img
81
+ slot="icon"
82
+ [src]="providersStyles[item].imgSrc"
83
+ [alt]="providersStyles[item].name"
84
+ [class]="getIconSizeClassNames(item, isEditable(), device()) + ' bas-rounded-lg'"
85
+ />
86
+ }
87
+ </ba-btn-base-provider>
88
+ </div>
89
+ }
90
+ </div>
91
+
92
+ @if (isMobile() && isProvidersScroll()) {
93
+ <div class="bas-box-border bas-flex bas-h-full bas-w-full bas-items-center bas-justify-end bas-pb-4">
94
+ <ba-btn-arrow-dir direction="forward" [disabled]="false" arrowType="arrow" (clicked)="onScrollArrowClicked('right')" />
95
+ </div>
96
+ }
97
+ </div>
98
+ `,
99
+ styles: [
100
+ `
101
+ @keyframes ba-shake {
102
+ 0% { transform: rotate(0deg); }
103
+ 35% { transform: rotate(-0.4deg); }
104
+ 70% { transform: rotate(0.6deg); }
105
+ 100% { transform: rotate(0deg); }
106
+ }
107
+ .ba-shake { animation: ba-shake 0.35s infinite; }
108
+ `,
109
+ ],
110
+ })
111
+ export class ProvidersButtonsComponent {
112
+ providersFiltersEnabled = input(false);
113
+ isMobile = input(false);
114
+ uiConfigProvidersInfo = input<{ providersColumns?: number; showProviderNameOnMobile?: boolean }>({
115
+ providersColumns: 3,
116
+ showProviderNameOnMobile: true,
117
+ });
118
+ showedProviders = input<string[]>([]);
119
+ selectedService = input('');
120
+ isEditable = input(false);
121
+ isEditing = input(false);
122
+ device = input<'desktop' | 'phone'>('desktop');
123
+
124
+ selectedServiceChange = output<string>();
125
+
126
+ // Alias del mapa de estilos para poder referenciarlo desde el template.
127
+ protected readonly providersStyles = providersStyles;
128
+ // `isOpenProvidersBtnsEditable` (useState local nunca mutado) — conservado
129
+ // por paridad de API del shake.
130
+ protected readonly isOpenProvidersBtnsEditable = false;
131
+
132
+ private readonly wrapperRef = viewChild<ElementRef<HTMLDivElement>>('wrapper');
133
+ private readonly containerRef = viewChild<ElementRef<HTMLDivElement>>('container');
134
+ protected readonly isProvidersScroll = signal(false);
135
+
136
+ private readonly platformId = inject(PLATFORM_ID);
137
+ private readonly destroyRef = inject(DestroyRef);
138
+
139
+ constructor() {
140
+ // Port 1:1 del efecto de medición de ProvidersSection.tsx (líneas 85-103),
141
+ // internalizado aquí (ver comentario de cabecera).
142
+ effect((onCleanup) => {
143
+ // Dependencias reactivas del efecto original.
144
+ const mobile = this.isMobile();
145
+ this.showedProviders();
146
+ this.isEditable();
147
+ this.isEditing();
148
+ this.device();
149
+ if (!mobile) {
150
+ this.isProvidersScroll.set(false);
151
+ return;
152
+ }
153
+ if (!isPlatformBrowser(this.platformId)) return;
154
+ this.measureScroll();
155
+ const t = setTimeout(() => this.measureScroll(), 500);
156
+ const onResize = () => this.measureScroll();
157
+ window.addEventListener('resize', onResize);
158
+ onCleanup(() => {
159
+ clearTimeout(t);
160
+ window.removeEventListener('resize', onResize);
161
+ });
162
+ });
163
+ }
164
+
165
+ private measureScroll(): void {
166
+ const container = this.containerRef()?.nativeElement;
167
+ const wrapper = this.wrapperRef()?.nativeElement;
168
+ if (!container || !wrapper) return;
169
+ this.isProvidersScroll.set(container.scrollWidth > wrapper.clientWidth - 24);
170
+ }
171
+
172
+ onScrollArrowClicked(direction: 'left' | 'right'): void {
173
+ const el = this.containerRef()?.nativeElement;
174
+ if (!el) return;
175
+ if (direction === 'left') el.scrollLeft -= 128;
176
+ else el.scrollLeft += 128;
177
+ }
178
+
179
+ // ------ CLASSNAMES (port literal) ------
180
+ providersContainerClassNames(
181
+ providersCols: number,
182
+ filtersEnabled: boolean,
183
+ providers: string[],
184
+ mobile: boolean,
185
+ editable: boolean,
186
+ dev: string,
187
+ ): string {
188
+ let classNames = '';
189
+ if (!editable)
190
+ classNames += ' bas-flex bas-flex-row bas-overflow-x-auto sm1:bas-grid sm1:bas-overflow-y-auto ';
191
+ else {
192
+ if (dev === 'phone') classNames += ' bas-flex bas-flex-row bas-overflow-x-auto ';
193
+ else classNames += ' bas-grid bas-overflow-y-auto ';
194
+ }
195
+ const height = mobile ? 'bas-h-full' : 'bas-h-fit';
196
+ const hPrClassNames = `bas-h-full ${!editable ? 'sm1:bas-pr-1.5' : dev !== 'phone' ? 'bas-pr-1.5' : ''}`;
197
+ const getHPrClassNames = (length: number) => {
198
+ if (providers.length > length) classNames += ` ${hPrClassNames} `;
199
+ else classNames += ` ${height} `;
200
+ };
201
+ const gridClassNames = (classWithSm1: string, classWithoutSm1: string) => {
202
+ if (!editable) return classWithSm1;
203
+ if (dev !== 'phone') return classWithoutSm1;
204
+ return '';
205
+ };
206
+ if (providersCols === 1) {
207
+ classNames += ` bas-gap-2 ${gridClassNames('sm1:bas-grid-cols-1', 'bas-grid-cols-1')} `;
208
+ if (filtersEnabled) getHPrClassNames(2);
209
+ else getHPrClassNames(3);
210
+ } else if (providersCols === 2) {
211
+ classNames += ` bas-gap-2.5 ${gridClassNames('sm1:bas-grid-cols-2', 'bas-grid-cols-2')} `;
212
+ if (filtersEnabled) getHPrClassNames(4);
213
+ else getHPrClassNames(6);
214
+ } else if (providersCols === 3) {
215
+ classNames += ` bas-gap-x-2 bas-gap-y-2.5 ${gridClassNames('sm1:bas-grid-cols-3', 'bas-grid-cols-3')} `;
216
+ if (filtersEnabled) getHPrClassNames(6);
217
+ else getHPrClassNames(9);
218
+ }
219
+ if (!mobile && filtersEnabled) {
220
+ if (!editable) classNames += ' sm1:bas-mt-4 sm1:bas-max-h-[calc(100%-16px)] ';
221
+ else if (dev !== 'phone') classNames += ' bas-mt-4 bas-max-h-[calc(100%-16px)] ';
222
+ }
223
+ return classNames;
224
+ }
225
+
226
+ getBtnProviderText(item: string, providerCols: number, mobile: boolean, showOnMobile?: boolean): string {
227
+ if ((providerCols === 1 && !mobile) || (mobile && showOnMobile)) return item;
228
+ return '';
229
+ }
230
+
231
+ getBtnProviderSize(mobile: boolean, showOnMobile: boolean | undefined, editable: boolean, dev: string): string {
232
+ let classNames = '';
233
+ if (!editable) classNames += ' sm1:bas-h-16 sm1:bas-w-full ';
234
+ else if (dev !== 'phone') classNames += ' bas-h-16 bas-w-full ';
235
+ if (!mobile) return classNames;
236
+ if (showOnMobile) classNames += ' bas-min-w-[128px] bas-h-12 ';
237
+ classNames += ' bas-w-16 bas-h-12 ';
238
+ return classNames;
239
+ }
240
+
241
+ getBtnProviderBorderClassNames(item: string, selected: string): string {
242
+ const style = providersStyles[item];
243
+ let classNames = `${style ? style.hoverBorderStyles : 'hover:bas-border-slate-400'}`;
244
+ if (item === 'blockauth') classNames += ' bas-border-[3px] ';
245
+ else classNames += ' bas-border-2 ';
246
+ if (selected === item && style) classNames += ` ${style.borderStyles} `;
247
+ else classNames += ' bas-border-slate-300 ';
248
+ return classNames;
249
+ }
250
+
251
+ getBtnBaseProviderContainerClassNames(editable: boolean, dev: string): string {
252
+ if (!editable) return 'bas-h-fit bas-w-fit sm1:bas-h-16 sm1:bas-w-full';
253
+ if (dev === 'phone') return 'bas-h-fit bas-w-fit';
254
+ return 'bas-h-16 bas-w-full';
255
+ }
256
+
257
+ getIconSizeClassNames(item: string, editable: boolean, dev: string): string {
258
+ if (item === 'blockauth') {
259
+ if (!editable) return 'bas-h-9 bas-w-9 sm1:bas-h-12 sm1:bas-w-12';
260
+ if (dev === 'phone') return 'bas-h-9 bas-w-9';
261
+ return 'bas-h-12 bas-w-12';
262
+ }
263
+ if (!editable) return 'bas-h-8 bas-w-8 sm1:bas-h-10 sm1:bas-w-10';
264
+ if (dev === 'phone') return 'bas-h-8 bas-w-8';
265
+ return 'bas-h-10 bas-w-10';
266
+ }
267
+
268
+ getProvidersWrapperClassNames(
269
+ filtersEnabled: boolean,
270
+ mobile: boolean,
271
+ providersScroll: boolean,
272
+ editable: boolean,
273
+ dev: string,
274
+ ): string {
275
+ const isMobileIsProvidersScrollClassNames = `${
276
+ mobile && providersScroll ? 'bas-grid bas-grid-cols-[40px_1fr_40px] bas-px-2' : 'bas-px-3'
277
+ }`;
278
+ if (!editable) {
279
+ return `bas-py-1.5 bas-col-span-2 bas-h-[76px] sm1:bas-col-span-1
280
+ sm1:bas-px-2 sm1:bas-py-0 ${filtersEnabled ? 'sm1:bas-h-[188px]' : 'sm1:bas-h-[230px]'} ${isMobileIsProvidersScrollClassNames}`;
281
+ }
282
+ if (dev === 'phone') {
283
+ return `bas-py-1.5 bas-col-span-2 bas-h-[76px]
284
+ ${isMobileIsProvidersScrollClassNames}`;
285
+ }
286
+ return `bas-col-span-1 bas-px-2 bas-py-0 ${filtersEnabled ? 'bas-h-[188px]' : 'bas-h-[230px]'} ${
287
+ mobile && providersScroll ? 'bas-grid bas-grid-cols-[40px_1fr_40px]' : ''
288
+ }`;
289
+ }
290
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen, fireEvent } from '@testing-library/angular';
4
+ import { ProvidersFiltersComponent } from './providers-filters.component';
5
+
6
+ // Port de packages/react/src/components/sections/internals/ProvidersFilters.test.tsx.
7
+ // El i18n se lee del store `$locale` del core (singleton), así que no hace
8
+ // falta un client falso para este componente presentacional.
9
+ describe('ProvidersFilters', () => {
10
+ it('renderiza el buscador y propaga el cambio', async () => {
11
+ const onChangeProviderSearch = vi.fn();
12
+ @Component({
13
+ standalone: true,
14
+ imports: [ProvidersFiltersComponent],
15
+ template: `<ba-providers-filters (changeProviderSearch)="onChangeProviderSearch($event)" />`,
16
+ })
17
+ class HostComponent {
18
+ onChangeProviderSearch = onChangeProviderSearch;
19
+ }
20
+ await render(HostComponent);
21
+ const input = screen.getByTestId('provider-search');
22
+ await fireEvent.input(input, { target: { value: 'meta' } });
23
+ expect(onChangeProviderSearch).toHaveBeenCalledWith('meta');
24
+ });
25
+ });
@@ -0,0 +1,139 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+ import { $locale, t as translate, type TextKey } from '@block-auth.io/core';
3
+ import { injectStore } from '../../../inject-store';
4
+ import { InputBaseComponent } from '../../../primitives/input-base.component';
5
+ import { DropdownBaseComponent, type DropdownItem } from '../../../primitives/dropdown-base.component';
6
+
7
+ /**
8
+ * Fila de búsqueda de proveedores + filtro por web-type. El buscador es el
9
+ * `<ba-input-base>` absorbido; el filtro "Todos" es el `<ba-dropdown-base>`.
10
+ * El shake de framer-motion se traduce a la clase CSS `.ba-shake` (mismo
11
+ * patrón que el port Vue). Callback props -> outputs:
12
+ * `setOpenWebType` -> `openWebTypeChange`, `onChangeProviderSearch` ->
13
+ * `changeProviderSearch`, `onChangeWebType` -> `changeWebType`.
14
+ *
15
+ * `containerWithActiveClickOutsideRef` (React ref) -> input `container`
16
+ * reenviado a `<ba-dropdown-base>`. i18n vía el store `$locale` del core
17
+ * (mismo patrón que BtnBlockAuth), no vía BlockAuthService, para no exigir
18
+ * un client completo a este componente presentacional.
19
+ */
20
+ // Extiende DropdownItem (incluye su index signature `[key: string]: unknown`)
21
+ // para poder pasarse a `<ba-dropdown-base [items]/[selectedItem]>` bajo
22
+ // strictTemplates de ng-packagr; `name`/`value` quedan explícitos por claridad.
23
+ export interface WebTypeItem extends DropdownItem {
24
+ name: string;
25
+ value: string;
26
+ }
27
+
28
+ /** Ítems localizados del filtro de web-type ("Web 2"/"Web 3" iguales en ambos idiomas). */
29
+ export const makeWebTypeItems = (t: (key: TextKey) => string): WebTypeItem[] => [
30
+ { name: t('webTypeAll'), value: 'all' },
31
+ { name: 'Web 2', value: 'web2' },
32
+ { name: 'Web 3', value: 'web3' },
33
+ ];
34
+
35
+ @Component({
36
+ selector: 'ba-providers-filters',
37
+ standalone: true,
38
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
+ imports: [InputBaseComponent, DropdownBaseComponent],
40
+ template: `
41
+ <div
42
+ data-testid="providers-filters"
43
+ [class]="
44
+ 'bas-relative bas-z-50 bas-box-border bas-h-fit bas-w-full bas-items-end bas-justify-end bas-gap-2 bas-px-2 ' +
45
+ (bothEnabled() ? 'bas-grid bas-grid-cols-[1fr_100px]' : 'bas-flex') +
46
+ (isEditable() && isEditing() && !isOpenFiltersEditable ? ' ba-shake' : '')
47
+ "
48
+ >
49
+ @if (uiConfigProvidersInfo().enableSearch) {
50
+ <ba-input-base
51
+ size="small"
52
+ [labelText]="searchLabelText() ?? t('search')"
53
+ inputId="search-provider-label"
54
+ inputTestId="provider-search"
55
+ [enableError]="false"
56
+ [isInputValueManaged]="true"
57
+ [className]="bothEnabled() ? '' : 'bas-max-w-[254px]'"
58
+ [inputValue]="searchProviderValue()"
59
+ (changedInputValue)="changeProviderSearch.emit($event)"
60
+ borderRadiusClassNames="bas-rounded-xl"
61
+ [blueStyles]="true"
62
+ />
63
+ }
64
+ @if (uiConfigProvidersInfo().enableWebTypeFilter) {
65
+ <div class="bas-w-full bas-max-w-[254px]">
66
+ <ba-dropdown-base
67
+ btnId="btn-web-type-filter"
68
+ [isOpen]="openWebType()"
69
+ (isOpenChange)="openWebTypeChange.emit($event)"
70
+ [showTitle]="false"
71
+ size="small"
72
+ [items]="items()"
73
+ [selectedItem]="selected()"
74
+ keyName="value"
75
+ (changeSelectedItem)="onDropdownChangeSelectedItem($event)"
76
+ width="100%"
77
+ [dropdownHeight]="122"
78
+ borderRadiusClassNames="bas-rounded-xl"
79
+ [container]="container()"
80
+ (clickOutside)="openWebTypeChange.emit(false)"
81
+ [showStatusIndicator]="true"
82
+ [showImg]="false"
83
+ [showCheckWhenActive]="true"
84
+ [manageActiveWithIsOptionSelected]="false"
85
+ [blueStyles]="true"
86
+ [disabled]="isEditable() && isEditing()"
87
+ />
88
+ </div>
89
+ }
90
+ </div>
91
+ `,
92
+ styles: [
93
+ `
94
+ @keyframes ba-shake {
95
+ 0% { transform: rotate(0deg); }
96
+ 35% { transform: rotate(-0.4deg); }
97
+ 70% { transform: rotate(0.6deg); }
98
+ 100% { transform: rotate(0deg); }
99
+ }
100
+ .ba-shake { animation: ba-shake 0.35s infinite; }
101
+ `,
102
+ ],
103
+ })
104
+ export class ProvidersFiltersComponent {
105
+ container = input<HTMLElement | null>(null);
106
+ uiConfigProvidersInfo = input<{ enableSearch?: boolean; enableWebTypeFilter?: boolean }>({
107
+ enableSearch: true,
108
+ enableWebTypeFilter: true,
109
+ });
110
+ searchProviderValue = input('');
111
+ openWebType = input(false);
112
+ selectedWebType = input<WebTypeItem | undefined>(undefined);
113
+ webTypesItems = input<WebTypeItem[] | undefined>(undefined);
114
+ isEditable = input(false);
115
+ isEditing = input(false);
116
+ device = input<'desktop' | 'tablet' | 'phone'>('desktop');
117
+ searchLabelText = input<string | undefined>(undefined);
118
+
119
+ openWebTypeChange = output<boolean>();
120
+ changeProviderSearch = output<string>();
121
+ changeWebType = output<WebTypeItem>();
122
+
123
+ private readonly locale = injectStore($locale);
124
+ protected t = (key: TextKey): string => translate(key, this.locale());
125
+
126
+ // `isOpenFiltersEditable` (useState local nunca mutado en el JSX pegado):
127
+ // se conserva por paridad de API del cálculo del shake.
128
+ protected readonly isOpenFiltersEditable = false;
129
+
130
+ protected items = computed<WebTypeItem[]>(() => this.webTypesItems() ?? makeWebTypeItems(this.t));
131
+ protected selected = computed<WebTypeItem>(() => this.selectedWebType() ?? this.items()[0]);
132
+ protected bothEnabled = computed(
133
+ () => !!this.uiConfigProvidersInfo().enableSearch && !!this.uiConfigProvidersInfo().enableWebTypeFilter,
134
+ );
135
+
136
+ onDropdownChangeSelectedItem(item: DropdownItem): void {
137
+ this.changeWebType.emit({ name: item.name, value: item.value });
138
+ }
139
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen } from '@testing-library/angular';
4
+ import { map } from 'nanostores';
5
+ import { SelectedButtonComponent } from './selected-button.component';
6
+ import { provideBlockAuthClientForTest } from '../../../testing';
7
+
8
+ vi.mock('@wagmi/core', () => ({
9
+ createConfig: vi.fn(() => ({ connectors: [] })),
10
+ connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
11
+ signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
12
+ }));
13
+ vi.mock('@wagmi/connectors', () => ({
14
+ injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
15
+ }));
16
+
17
+ function makeClient() {
18
+ return {
19
+ web3: { signup: vi.fn() },
20
+ state: { $ui: map({}), $wallet: map({}) },
21
+ } as any;
22
+ }
23
+
24
+ // Port de packages/react/src/components/sections/internals/SelectedButton.test.tsx.
25
+ describe('SelectedButton', () => {
26
+ it('renderiza el boton del servicio seleccionado (metamask)', async () => {
27
+ @Component({
28
+ standalone: true,
29
+ imports: [SelectedButtonComponent],
30
+ template: `<ba-selected-button selectedService="metamask" />`,
31
+ })
32
+ class HostComponent {}
33
+ await render(HostComponent, { providers: [provideBlockAuthClientForTest(makeClient())] });
34
+ expect(screen.getByText('MetaMask')).toBeDefined();
35
+ });
36
+ });