@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,34 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+
3
+ // Icono inline extraído de react-icons/tb (TbCopy), espejo de
4
+ // packages/vue/src/icons/TbCopy.vue (mismo `viewBox`/`d`, trazo en vez de
5
+ // relleno: `fill="none"` + `stroke="currentColor"`).
6
+ // Ver check-icon.component.ts para la explicación del enrutado de `class`.
7
+ @Component({
8
+ selector: 'ba-tb-copy',
9
+ standalone: true,
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ host: { style: 'display: contents' },
12
+ template: `
13
+ <svg
14
+ [class]="class()"
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ aria-hidden="true"
22
+ >
23
+ <path
24
+ d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z"
25
+ />
26
+ <path
27
+ d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1"
28
+ />
29
+ </svg>
30
+ `,
31
+ })
32
+ export class TbCopyComponent {
33
+ class = input('');
34
+ }
@@ -0,0 +1,21 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+
3
+ // Icono inline extraído de react-icons/ti (TiVendorMicrosoft), espejo de
4
+ // packages/vue/src/icons/TiVendorMicrosoft.vue (mismo `viewBox`/`d`).
5
+ // Ver check-icon.component.ts para la explicación del enrutado de `class`.
6
+ @Component({
7
+ selector: 'ba-ti-vendor-microsoft',
8
+ standalone: true,
9
+ changeDetection: ChangeDetectionStrategy.OnPush,
10
+ host: { style: 'display: contents' },
11
+ template: `
12
+ <svg [class]="class()" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
13
+ <path
14
+ d="M10 12.5c0-.3-.2-.5-.5-.5h-6c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.6l6 .7c.3 0 .5-.2.5-.4v-5.9zM11.5 12c-.3 0-.5.2-.5.5v5.9c0 .3.2.5.5.6l9 1c.3 0 .5-.2.5-.4v-7c0-.3-.2-.5-.5-.5l-9-.1zM10 4.7c0-.3-.2-.5-.5-.4l-6 .7c-.3 0-.5.2-.5.5v5c0 .3.2.5.5.5h6c.3 0 .5-.2.5-.5v-5.8zM11.5 4.1c-.3 0-.5.3-.5.6v5.9c0 .3.2.5.5.5h9c.3 0 .5-.2.5-.5v-7c0-.3-.2-.5-.5-.4l-9 .9z"
15
+ />
16
+ </svg>
17
+ `,
18
+ })
19
+ export class TiVendorMicrosoftComponent {
20
+ class = input('');
21
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen } from '@testing-library/angular';
4
+ import { atom } from 'nanostores';
5
+ import { injectStore } from './inject-store';
6
+
7
+ const $count = atom(0);
8
+
9
+ @Component({
10
+ selector: 'ba-probe',
11
+ standalone: true,
12
+ template: `<span data-testid="count">{{ count() }}</span>`,
13
+ })
14
+ class ProbeComponent {
15
+ count = injectStore($count);
16
+ }
17
+
18
+ describe('injectStore', () => {
19
+ it('refleja el valor inicial y reacciona a cambios del atom', async () => {
20
+ const { detectChanges } = await render(ProbeComponent);
21
+ expect(screen.getByTestId('count').textContent).toBe('0');
22
+ $count.set(5);
23
+ detectChanges();
24
+ expect(screen.getByTestId('count').textContent).toBe('5');
25
+ });
26
+ });
@@ -0,0 +1,16 @@
1
+ import { DestroyRef, inject, signal, type Signal } from '@angular/core';
2
+ import type { Store, StoreValue } from 'nanostores';
3
+
4
+ /**
5
+ * Puente nanostores → Angular Signal (equivalente a useStore de
6
+ * @nanostores/react and /vue). Debe llamarse en contexto de inyección
7
+ * (constructor / inicializador de campo), como cualquier inject().
8
+ * La suscripción se limpia con DestroyRef.
9
+ */
10
+ export function injectStore<S extends Store>(store: S): Signal<StoreValue<S>> {
11
+ const destroyRef = inject(DestroyRef);
12
+ const state = signal<StoreValue<S>>(store.get());
13
+ const unsubscribe = store.listen((value) => state.set(value));
14
+ destroyRef.onDestroy(unsubscribe);
15
+ return state.asReadonly();
16
+ }
@@ -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 { ArrowIconComponent } from './arrow-icon.component';
5
+ describe('ArrowIconComponent', () => {
6
+ it('renderiza la flecha con la clase segun la direccion', async () => {
7
+ @Component({ standalone: true, imports: [ArrowIconComponent], template: `<ba-arrow-icon direction="left" />` })
8
+ class Host {}
9
+ const { container } = await render(Host);
10
+ expect(container.querySelector('.bal-ba-arrow-left')).toBeTruthy();
11
+ });
12
+ });
@@ -0,0 +1,38 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+
3
+ // Port directo de ArrowIcon.tsx / ArrowIcon.vue (triple-chevron animado con
4
+ // las clases `bal-ba-arrow*` absorbidas en lib-styles.css).
5
+ @Component({
6
+ selector: 'ba-arrow-icon',
7
+ standalone: true,
8
+ changeDetection: ChangeDetectionStrategy.OnPush,
9
+ template: `
10
+ <div class="bas-flex bas-h-[30px] bas-w-[65px] bas-items-center bas-justify-center">
11
+ <div
12
+ [class]="
13
+ 'bal-ba-arrow bas-relative bas-flex bas-h-[65px] bas-w-[65px] bas-cursor-pointer bas-justify-center ' +
14
+ (direction() === 'right' ? 'bal-ba-arrow-right bas-items-start' : '') +
15
+ ' ' +
16
+ (direction() === 'left' ? 'bal-ba-arrow-left bas-items-center' : '')
17
+ "
18
+ >
19
+ <span [class]="spanClassNames()"></span>
20
+ <span [class]="spanClassNames()"></span>
21
+ <span [class]="spanClassNames()"></span>
22
+ </div>
23
+ </div>
24
+ `,
25
+ })
26
+ export class ArrowIconComponent {
27
+ direction = input<'right' | 'left'>('right');
28
+
29
+ spanClassNames = computed(
30
+ () =>
31
+ `bas-block bas-w-2.5 bas-h-2.5 bas-border-solid
32
+ bas-border-[var(--bal-primary-500)] bas-absolute -bas-m-2.5 ${
33
+ this.direction() === 'right'
34
+ ? 'bas-border-t-[2.5px] bas-border-l-[2.5px] bas-border-b-0 bas-border-r-0'
35
+ : 'bas-border-b-[2.5px] bas-border-r-[2.5px] bas-border-t-0 bas-border-l-0'
36
+ }`,
37
+ );
38
+ }
@@ -0,0 +1,22 @@
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 { BtnArrowDirComponent } from './btn-arrow-dir.component';
5
+ describe('BtnArrowDirComponent', () => {
6
+ it('emite clicked al pulsar si no esta disabled', async () => {
7
+ const onClicked = vi.fn();
8
+ @Component({ standalone: true, imports: [BtnArrowDirComponent], template: `<ba-btn-arrow-dir direction="forward" (clicked)="f()" />` })
9
+ class Host { f = onClicked; }
10
+ await render(Host);
11
+ await fireEvent.click(screen.getByTestId('btn-arrow-forward'));
12
+ expect(onClicked).toHaveBeenCalled();
13
+ });
14
+ it('no emite clicked cuando disabled', async () => {
15
+ const onClicked = vi.fn();
16
+ @Component({ standalone: true, imports: [BtnArrowDirComponent], template: `<ba-btn-arrow-dir [disabled]="true" (clicked)="f()" />` })
17
+ class Host { f = onClicked; }
18
+ await render(Host);
19
+ await fireEvent.click(screen.getByTestId('btn-arrow-back'));
20
+ expect(onClicked).not.toHaveBeenCalled();
21
+ });
22
+ });
@@ -0,0 +1,65 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+ import {
3
+ IoIosArrowBackComponent,
4
+ IoIosArrowForwardComponent,
5
+ IoMdArrowBackComponent,
6
+ IoMdArrowForwardComponent,
7
+ } from '../icons';
8
+
9
+ // Mapeo dirección+arrowType -> icono:
10
+ // chevron `IoIos*` para arrowType 'arrow', flecha Material `IoMd*` para
11
+ // 'arrow-round', según dirección. Los componentes vienen del módulo de
12
+ // iconos inline dedicado (`../icons`, Task A6).
13
+ // DISCLAIMER: se usa un <span> en vez de <button>
14
+ // porque el elemento button no posiciona bien el icono de flecha.
15
+ @Component({
16
+ selector: 'ba-btn-arrow-dir',
17
+ standalone: true,
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ imports: [IoIosArrowBackComponent, IoIosArrowForwardComponent, IoMdArrowBackComponent, IoMdArrowForwardComponent],
20
+ template: `
21
+ <span
22
+ [class]="
23
+ 'bal-ba-transition-2 bal-ba-outline-none bas-group bas-flex bas-items-center bas-justify-center bas-border-none ' +
24
+ bgtnSizeClassNames() + ' ' + (!disabled() ? btnClassNames() : btnDisabledClassNames())
25
+ "
26
+ [attr.data-testid]="'btn-arrow-' + direction()"
27
+ (click)="onBtnClicked()"
28
+ >
29
+ @if (arrowType() === 'arrow' && direction() === 'back') {
30
+ <ba-io-ios-arrow-back [class]="arrowClasses()" />
31
+ } @else if (arrowType() === 'arrow' && direction() === 'forward') {
32
+ <ba-io-ios-arrow-forward [class]="arrowClasses()" />
33
+ } @else if (arrowType() === 'arrow-round' && direction() === 'back') {
34
+ <ba-io-md-arrow-back [class]="arrowClasses()" />
35
+ } @else {
36
+ <ba-io-md-arrow-forward [class]="arrowClasses()" />
37
+ }
38
+ </span>
39
+ `,
40
+ })
41
+ export class BtnArrowDirComponent {
42
+ direction = input<'back' | 'forward'>('back');
43
+ disabled = input(false);
44
+ arrowType = input<'arrow' | 'arrow-round'>('arrow');
45
+ btnClassNames = input('bas-rounded-full bas-cursor-pointer bas-bg-[var(--bal-primary-10)] hover:bas-bg-[#e0edf5]');
46
+ btnDisabledClassNames = input('bas-rounded-full bas-cursor-not-allowed bas-bg-[var(--bal-primary-10)]');
47
+ bgtnSizeClassNames = input('bas-h-8 bas-w-8');
48
+ arrowClassNames = input('bas-text-[var(--bal-primary-600)] group-hover:bas-text-[var(--bal-primary-800)]');
49
+ arrowDisabledClassNames = input('bas-text-[var(--bal-primary-500)]');
50
+ arrowSizesClassNames = input('bas-h-6 bas-w-6');
51
+
52
+ clicked = output<void>();
53
+
54
+ arrowClasses = computed(
55
+ () =>
56
+ `bal-ba-transition-2 ${this.arrowSizesClassNames()} ${
57
+ !this.disabled() ? this.arrowClassNames() : this.arrowDisabledClassNames()
58
+ }`,
59
+ );
60
+
61
+ onBtnClicked(): void {
62
+ if (this.disabled()) return;
63
+ this.clicked.emit();
64
+ }
65
+ }
@@ -0,0 +1,38 @@
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 { BtnBaseComponent } from './btn-base.component';
5
+
6
+ // Los signal inputs se fijan con binding de plantilla desde un host component
7
+ // (forma inequívoca con `input()`), en vez de `componentInputs` (que no aplica
8
+ // signal inputs de forma fiable en @testing-library/angular 18).
9
+ describe('BtnBaseComponent', () => {
10
+ it('renderiza el texto', async () => {
11
+ @Component({ standalone: true, imports: [BtnBaseComponent], template: `<ba-btn-base text="Entrar" />` })
12
+ class HostComponent {}
13
+ await render(HostComponent);
14
+ expect(screen.getByRole('button').textContent).toContain('Entrar');
15
+ });
16
+
17
+ it('al hacer click emite action', async () => {
18
+ const onAction = vi.fn();
19
+ @Component({
20
+ standalone: true,
21
+ imports: [BtnBaseComponent],
22
+ template: `<ba-btn-base text="Entrar" (action)="onAction()" />`,
23
+ })
24
+ class HostComponent {
25
+ onAction = onAction;
26
+ }
27
+ await render(HostComponent);
28
+ await fireEvent.click(screen.getByRole('button'));
29
+ expect(onAction).toHaveBeenCalled();
30
+ });
31
+
32
+ it('en modo loader no muestra cursor-pointer y monta el loader', async () => {
33
+ @Component({ standalone: true, imports: [BtnBaseComponent], template: `<ba-btn-base [isLoader]="true" />` })
34
+ class HostComponent {}
35
+ await render(HostComponent);
36
+ expect(screen.getByRole('button').className).toContain('bas-cursor-default');
37
+ });
38
+ });
@@ -0,0 +1,140 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+ import { HorizontalLoaderComponent } from './horizontal-loader.component';
3
+ import { IconBaseComponent } from './icon-base.component';
4
+
5
+ // El JSX de React/Vue renderiza dos <button> distintos por `type`; aquí
6
+ // `btnClass()` computa la clase según `type()` con un único <button> — el
7
+ // DOM resultante es el mismo.
8
+ type BtnSize = 'xxs' | 'xs' | 'tiny' | 'small' | 'base' | 'large';
9
+
10
+ @Component({
11
+ selector: 'ba-btn-base',
12
+ standalone: true,
13
+ // El <button> interno lleva las clases de layout (p.ej. bas-w-full). El host
14
+ // debe ser transparente para que el <button> sea el hijo real del flex/grid
15
+ // del padre (como en React, donde BtnBase renderiza el <button> directamente).
16
+ // Sin esto, `bas-w-full` mide el 100% del host inline (que se encoge al
17
+ // contenido) y el botón "Sign Up" del flow no ocupa todo el ancho.
18
+ host: { style: 'display: contents' },
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ imports: [HorizontalLoaderComponent, IconBaseComponent],
21
+ template: `
22
+ <button
23
+ [id]="btnId()"
24
+ [type]="btnType()"
25
+ [disabled]="disabled()"
26
+ [class]="btnClass()"
27
+ (click)="onBtnClicked($event)"
28
+ >
29
+ @if (isLoader()) {
30
+ <ba-horizontal-loader
31
+ [loaderColor]="loaderColor()"
32
+ [loaderHeight]="loaderHeight()"
33
+ [loaderWidth]="loaderWidth()"
34
+ [classNames]="loaderContainerClassNames()"
35
+ />
36
+ }
37
+ @if (isIcon()) {
38
+ <ba-icon-base
39
+ [iconUrl]="iconUrl()"
40
+ [iconWidth]="iconWidth()"
41
+ [iconHeight]="iconHeight()"
42
+ [iconClassNames]="iconClassNames()"
43
+ ><ng-content select="[slot=icon]" /></ba-icon-base>
44
+ }
45
+ <span [class]="'bal-ba-transition-2 ' + realTextClassNames() + ' bas-flex bas-h-full bas-items-center'">
46
+ <ng-content />{{ text() }}
47
+ </span>
48
+ </button>
49
+ `,
50
+ })
51
+ export class BtnBaseComponent {
52
+ btnId = input('btn-base');
53
+ text = input('');
54
+ size = input<BtnSize>('base');
55
+ type = input<'button-outlined' | 'button-border'>('button-outlined');
56
+ btnType = input<'button' | 'submit' | 'reset'>('button');
57
+ ringClassNames = input('bal-ba-hover-ring-primary-200 bal-ba-focus-ring-primary-300');
58
+ offsetClassNames = input('bal-ba-focus-ring-offset-2 bal-ba-hover-ring-offset-2');
59
+ commonClassNames = input(`bal-ba-transition-2 bas-group bas-relative bas-box-border
60
+ bas-flex bas-flex-row bas-gap-2 bas-items-center bas-overflow-hidden`);
61
+ classNames = input('bas-justify-center');
62
+ bgClassNames = input<string | undefined>(undefined);
63
+ borderClassNames = input<string | undefined>(undefined);
64
+ paddingClassNames = input<string | undefined>(undefined);
65
+ textClassNames = input<string | undefined>(undefined);
66
+ isIcon = input(true);
67
+ iconUrl = input<string | undefined>(undefined);
68
+ iconWidth = input(24);
69
+ iconHeight = input(24);
70
+ iconClassNames = input('bas-rounded-full');
71
+ isLoader = input(false);
72
+ loaderColor = input('#003e79');
73
+ loaderWidth = input(80);
74
+ loaderContainerClassNames = input('bas-rounded-md');
75
+ isFocusOutlinedEnabled = input(true);
76
+ disabled = input(false);
77
+ applyDisabledStyles = input(true);
78
+
79
+ /** Equivalente al emit `click` de BtnBase.vue / onClick de BtnBase.tsx. */
80
+ action = output<void>();
81
+
82
+ onBtnClicked(e: MouseEvent): void {
83
+ e.preventDefault();
84
+ this.action.emit();
85
+ }
86
+
87
+ private realBorderClassNames = computed(
88
+ () =>
89
+ this.borderClassNames() ??
90
+ (this.type() === 'button-border'
91
+ ? `bas-rounded-md bas-border-2 bas-border-solid bas-border-[var(--bal-primary-100)]
92
+ hover:bas-border-[var(--bal-primary-300)]`
93
+ : 'bas-border-none bas-rounded-md'),
94
+ );
95
+ realTextClassNames = computed(() => {
96
+ if (this.textClassNames()) return this.textClassNames()!;
97
+ const textBase = 'bas-text-sm bas-font-semibold bas-leading-6';
98
+ return this.type() === 'button-border'
99
+ ? `${textBase} bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]`
100
+ : `${textBase} bas-text-white`;
101
+ });
102
+ private realBgClassNames = computed(
103
+ () =>
104
+ this.bgClassNames() ??
105
+ (this.type() === 'button-border'
106
+ ? 'bas-bg-transparent'
107
+ : 'bas-bg-[var(--bal-primary-500)] hover:bas-bg-[var(--bal-primary-600)]'),
108
+ );
109
+ private sizeStyles = computed(
110
+ () =>
111
+ ({
112
+ xxs: `bas-h-[34px] ${this.paddingClassNames() || 'bas-px-1 bas-py-0'}`,
113
+ xs: `bas-h-[36px] ${this.paddingClassNames() || 'bas-px-1.5 bas-py-0'}`,
114
+ tiny: `bas-h-[38px] ${this.paddingClassNames() || 'bas-px-2 bas-py-0'}`,
115
+ small: `bas-h-[42px] ${this.paddingClassNames() || 'bas-px-2.5 bas-py-0'}`,
116
+ base: `bas-h-[46px] ${this.paddingClassNames() || 'bas-px-3 bas-py-0'}`,
117
+ large: `bas-h-[54px] ${this.paddingClassNames() || 'bas-px-3.5 bas-py-0'}`,
118
+ })[this.size()],
119
+ );
120
+ loaderHeight = computed(
121
+ () =>
122
+ ({ xxs: 34, xs: 36, tiny: 38, small: 42, base: 46, large: 54 }[this.size()] -
123
+ (this.type() === 'button-border' ? 4 : 0)),
124
+ );
125
+
126
+ btnClass = computed(() => {
127
+ if (this.type() === 'button-outlined') {
128
+ return `bal-ba-ring-base bal-ba-ring ${this.commonClassNames()} ${this.realBgClassNames()}
129
+ ${this.ringClassNames()} ${this.sizeStyles()}
130
+ ${this.applyDisabledStyles() ? 'disabled:bas-cursor-not-allowed disabled:bas-opacity-50' : ''}
131
+ ${this.isLoader() ? 'bas-cursor-default' : 'bal-ba-hover-ring-2 bas-cursor-pointer'}
132
+ ${this.isFocusOutlinedEnabled() && !this.isLoader() ? 'bal-ba-focus-ring-2 focus:bas-opacity-90' : ''}
133
+ ${this.offsetClassNames()} ${this.classNames() || ''} ${this.realBorderClassNames()}`;
134
+ }
135
+ return `${this.commonClassNames()} ${this.sizeStyles()}
136
+ ${this.isLoader() ? 'bas-cursor-default' : 'bas-cursor-pointer'}
137
+ ${this.applyDisabledStyles() ? 'disabled:bas-cursor-not-allowed disabled:bas-opacity-50' : ''}
138
+ ${this.realBorderClassNames()} ${this.realBgClassNames()} ${this.classNames() || ''}`;
139
+ });
140
+ }
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, screen } from '@testing-library/angular';
4
+ import { DividerBaseComponent } from './divider-base.component';
5
+ describe('DividerBaseComponent', () => {
6
+ it('proyecta el contenido central cuando lo hay', async () => {
7
+ @Component({ standalone: true, imports: [DividerBaseComponent], template: `<ba-divider-base><span data-testid="mid">or</span></ba-divider-base>` })
8
+ class Host {}
9
+ await render(Host);
10
+ expect(screen.getByTestId('mid')).toBeTruthy();
11
+ });
12
+ });
@@ -0,0 +1,74 @@
1
+ import {
2
+ AfterViewChecked,
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ input,
7
+ signal,
8
+ viewChild,
9
+ } from '@angular/core';
10
+
11
+ // `children` (ReactNode / slot por
12
+ // defecto) pasa a ser `<ng-content />`; la presencia de contenido proyectado
13
+ // se detecta igual que en IconBaseComponent (ver comentario allí).
14
+ @Component({
15
+ selector: 'ba-divider-base',
16
+ standalone: true,
17
+ changeDetection: ChangeDetectionStrategy.OnPush,
18
+ template: `
19
+ <div
20
+ [class]="
21
+ 'bas-group bas-relative bas-box-border bas-grid bas-w-full bas-items-center ' +
22
+ classNames() +
23
+ ' ' +
24
+ zIndex() +
25
+ ' ' +
26
+ (hasChildren() ? 'bas-grid-cols-[1fr_auto_1fr]' : 'bas-grid-cols-[1fr_1fr]')
27
+ "
28
+ >
29
+ <span
30
+ [class]="
31
+ 'bas-border-solid bas-w-full bas-border-x-0 bas-border-b-0 ' +
32
+ borderSizeClassName() +
33
+ ' ' +
34
+ (dividerBorderColorClassNames() || 'bas-border-gray-300')
35
+ "
36
+ ></span>
37
+ <span
38
+ #childrenSlot
39
+ [style.display]="hasChildren() ? 'contents' : 'none'"
40
+ [class]="'bas-group ' + childrenContainerClassNames()"
41
+ >
42
+ <ng-content />
43
+ </span>
44
+ <span
45
+ [class]="
46
+ 'bas-border-solid bas-w-full bas-border-x-0 bas-border-b-0 ' +
47
+ borderSizeClassName() +
48
+ ' ' +
49
+ (dividerBorderColorClassNames() || 'bas-border-gray-300')
50
+ "
51
+ ></span>
52
+ </div>
53
+ `,
54
+ })
55
+ export class DividerBaseComponent implements AfterViewChecked {
56
+ classNames = input('');
57
+ dividerBorderColorClassNames = input<string | undefined>(undefined);
58
+ borderSizeClassName = input('bas-border-t');
59
+ childrenContainerClassNames = input(`bas-flex bas-flex-row bas-items-center bas-text-sm
60
+ bas-justify-center bas-gap-1 bas-bg-transparent bas-px-2 bas-text-gray-500`);
61
+ zIndex = input('bas-z-50');
62
+
63
+ // Signal query (no `@ViewChild` — los decoradores de campo clásicos no se
64
+ // soportan en el modo JIT del harness de test AnalogJS/Vitest).
65
+ private readonly childrenSlot = viewChild<ElementRef<HTMLElement>>('childrenSlot');
66
+
67
+ hasChildren = signal(false);
68
+
69
+ ngAfterViewChecked(): void {
70
+ const el = this.childrenSlot()?.nativeElement ?? null;
71
+ const has = !!el && el.childNodes.length > 0;
72
+ if (has !== this.hasChildren()) this.hasChildren.set(has);
73
+ }
74
+ }
@@ -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 { DropdownArrowComponent } from './dropdown-arrow.component';
5
+ describe('DropdownArrowComponent', () => {
6
+ it('aplica la clase abierta cuando isOpen', async () => {
7
+ @Component({ standalone: true, imports: [DropdownArrowComponent], template: `<ba-dropdown-arrow [isOpen]="true" />` })
8
+ class Host {}
9
+ const { container } = await render(Host);
10
+ expect(container.querySelector('.bas-dropdown-arrow-open')).toBeTruthy();
11
+ });
12
+ });
@@ -0,0 +1,46 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+
3
+ // La rotación animada de
4
+ // framer-motion (variants open/closed) pasa a `transition: transform` en los
5
+ // `styles` del componente con la clase `bas-dropdown-arrow-open` condicional.
6
+ @Component({
7
+ selector: 'ba-dropdown-arrow',
8
+ standalone: true,
9
+ changeDetection: ChangeDetectionStrategy.OnPush,
10
+ template: `
11
+ <span
12
+ [id]="id()"
13
+ class="bas-dropdown-arrow bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center"
14
+ [class.bas-dropdown-arrow-open]="isOpen()"
15
+ (click)="clicked.emit()"
16
+ >
17
+ <svg
18
+ [class]="(disabled() ? 'bas-cursor-not-allowed' : 'bas-cursor-pointer') + ' ' + sizeClassNames()"
19
+ viewBox="0 0 20 20"
20
+ >
21
+ <path d="M0 7 L 20 7 L 10 16" [attr.fill]="color()" />
22
+ </svg>
23
+ </span>
24
+ `,
25
+ styles: [
26
+ `
27
+ .bas-dropdown-arrow {
28
+ transform: rotate(0deg);
29
+ transition: transform 0.25s ease;
30
+ }
31
+ .bas-dropdown-arrow-open {
32
+ transform: rotate(180deg);
33
+ }
34
+ `,
35
+ ],
36
+ })
37
+ export class DropdownArrowComponent {
38
+ id = input('animated-dropdown-id');
39
+ isOpen = input(false);
40
+ sizeClassNames = input('bas-h-4 bas-w-4');
41
+ color = input('var(--bal-primary-700)');
42
+ disabled = input(false);
43
+
44
+ /** Equivalente al emit `click` de DropdownArrow.vue. */
45
+ clicked = output<void>();
46
+ }
@@ -0,0 +1,49 @@
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 { DropdownBaseComponent } from './dropdown-base.component';
5
+
6
+ describe('DropdownBaseComponent', () => {
7
+ it('al pulsar el boton emite isOpenChange con el valor invertido', async () => {
8
+ const onOpen = vi.fn();
9
+ @Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="false" (isOpenChange)="f($event)" />` })
10
+ class Host { f = onOpen; }
11
+ await render(Host);
12
+ await fireEvent.click(screen.getByRole('button'));
13
+ expect(onOpen).toHaveBeenCalledWith(true);
14
+ });
15
+ it('con isOpen muestra la lista y al elegir un item emite changeSelectedItem', async () => {
16
+ const onSel = vi.fn();
17
+ @Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="true" (changeSelectedItem)="f($event)" />` })
18
+ class Host { f = onSel; }
19
+ await render(Host);
20
+ await fireEvent.click(screen.getByText('Block-Auth'));
21
+ expect(onSel).toHaveBeenCalled();
22
+ expect(onSel.mock.calls[0][0]).toMatchObject({ value: 'blockauth' });
23
+ });
24
+ });
25
+
26
+ // Click-outside con container (semantica portada de DropdownBase.tsx).
27
+ describe('DropdownBaseComponent — click-outside', () => {
28
+ it('sin container ningun click emite clickOutside', async () => {
29
+ const onOut = vi.fn();
30
+ @Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="true" (clickOutside)="f()" />` })
31
+ class Host { f = onOut; }
32
+ await render(Host);
33
+ await fireEvent.click(document.body);
34
+ expect(onOut).not.toHaveBeenCalled();
35
+ });
36
+ it('click dentro de container pero fuera del dropdown emite clickOutside', async () => {
37
+ const onOut = vi.fn();
38
+ const outer = document.createElement('div');
39
+ document.body.appendChild(outer);
40
+ const sibling = document.createElement('div');
41
+ outer.appendChild(sibling);
42
+ @Component({ standalone: true, imports: [DropdownBaseComponent], template: `<ba-dropdown-base [isOpen]="true" [container]="c" (clickOutside)="f()" />` })
43
+ class Host { c = outer; f = onOut; }
44
+ await render(Host);
45
+ await fireEvent.click(sibling);
46
+ expect(onOut).toHaveBeenCalled();
47
+ document.body.removeChild(outer);
48
+ });
49
+ });