@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,66 @@
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 { map } from 'nanostores';
5
+ import { ChangeAccountComponent } from './change-account.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
+ // Port de packages/react/src/components/dds/ChangeAccountComponent.test.tsx
18
+ // (mapa: packages/vue/.../ChangeAccountComponent.test.ts).
19
+ describe('ChangeAccountComponent', () => {
20
+ function makeClient() {
21
+ return {
22
+ web3: { signup: vi.fn() },
23
+ oauth: { signupGoogle: vi.fn(), signupMicrosoft: vi.fn() },
24
+ connectBlockAuth: vi.fn(),
25
+ state: { $ui: map({}), $ws: map({}), $wallet: map({}) },
26
+ } as any;
27
+ }
28
+
29
+ const renderFor = (serviceName: string) =>
30
+ render(ChangeAccountComponent, {
31
+ inputs: { serviceName, text: 'Change account' },
32
+ providers: [provideBlockAuthClientForTest(makeClient())],
33
+ });
34
+
35
+ it('renderiza el texto "Change account" para el serviceName blockauth', async () => {
36
+ await renderFor('blockauth');
37
+ expect(screen.getByText('Change account')).toBeDefined();
38
+ });
39
+
40
+ for (const svc of ['blockauth', 'google', 'microsoft', 'metamask', 'phantom']) {
41
+ it(`renderiza para el provider soportado "${svc}"`, async () => {
42
+ await renderFor(svc);
43
+ expect(screen.getByText('Change account')).toBeDefined();
44
+ });
45
+ }
46
+
47
+ it('no renderiza ningun boton para un serviceName desconocido', async () => {
48
+ await renderFor('desconocido');
49
+ expect(screen.queryByText('Change account')).toBeNull();
50
+ });
51
+
52
+ it('emite changeAccountClicked al pulsar la fila', async () => {
53
+ const onChange = vi.fn();
54
+ @Component({
55
+ standalone: true,
56
+ imports: [ChangeAccountComponent],
57
+ template: `<ba-change-account serviceName="blockauth" text="Change account" (changeAccountClicked)="f()" />`,
58
+ })
59
+ class Host {
60
+ f = onChange;
61
+ }
62
+ await render(Host, { providers: [provideBlockAuthClientForTest(makeClient())] });
63
+ await fireEvent.click(screen.getByText('Change account'));
64
+ expect(onChange).toHaveBeenCalled();
65
+ });
66
+ });
@@ -0,0 +1,112 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+ import { TbArrowsExchange2Component } from '../../icons';
3
+ import { BtnBlockAuthComponent } from '../btns/btn-block-auth.component';
4
+ import { BtnGmailComponent } from '../btns/btn-gmail.component';
5
+ import { BtnMicrosoftComponent } from '../btns/btn-microsoft.component';
6
+ import { BtnMetamaskComponent } from '../btns/btn-metamask.component';
7
+ import { BtnPhantomComponent } from '../btns/btn-phantom.component';
8
+ import { DropdownProfileItemComponent } from './dropdown-profile-item.component';
9
+
10
+ // Rama de 5 vías sobre `serviceName` (blockauth/google/microsoft/metamask/
11
+ // phantom): cada una envuelve un `<ba-dropdown-profile-item>` (con el icono de
12
+ // intercambio) dentro del botón del provider en modo `isDropdownProfile`, que
13
+ // re-dispara el flujo de conexión de ese provider.
14
+ //
15
+ // El click real de "cambiar cuenta" no lo dispara este output por sí solo: el
16
+ // `<button>` nativo de cada `Btn*` en modo `isDropdownProfile` tiene su propio
17
+ // `(click)` (que ejecuta el signin/connect real); el `dropdown-profile-item`
18
+ // anidado es un `<div>` hijo de ese botón, y al clicarlo el evento burbujea al
19
+ // botón exterior, disparando AMBOS handlers.
20
+ @Component({
21
+ selector: 'ba-change-account',
22
+ standalone: true,
23
+ // Igual que `ba-dropdown-profile-item`: host transparente al layout para que
24
+ // el <div bas-w-full> sea hijo directo del flex de la <ul> y llene el ancho
25
+ // (con `bas-items-center` en la ul, un host inline se encogería y centraría).
26
+ host: { style: 'display: contents' },
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ imports: [
29
+ BtnBlockAuthComponent,
30
+ BtnGmailComponent,
31
+ BtnMicrosoftComponent,
32
+ BtnMetamaskComponent,
33
+ BtnPhantomComponent,
34
+ DropdownProfileItemComponent,
35
+ ],
36
+ template: `
37
+ <div class="bas-h-9 bas-w-full">
38
+ @switch (serviceName()) {
39
+ @case ('blockauth') {
40
+ <ba-btn-block-auth [text]="text()" [isDropdownProfile]="true">
41
+ <ba-dropdown-profile-item
42
+ id="ba-ddi-change-account-1"
43
+ [text]="text()"
44
+ [firstIcon]="exchangeIcon"
45
+ [isTopBorder]="isTopBorder()"
46
+ [disabled]="disabled()"
47
+ (clicked)="changeAccountClicked.emit()"
48
+ />
49
+ </ba-btn-block-auth>
50
+ }
51
+ @case ('google') {
52
+ <ba-btn-gmail [text]="text()" [isDropdownProfile]="true">
53
+ <ba-dropdown-profile-item
54
+ id="ba-ddi-change-account-1"
55
+ [text]="text()"
56
+ [firstIcon]="exchangeIcon"
57
+ [isTopBorder]="isTopBorder()"
58
+ [disabled]="disabled()"
59
+ (clicked)="changeAccountClicked.emit()"
60
+ />
61
+ </ba-btn-gmail>
62
+ }
63
+ @case ('microsoft') {
64
+ <ba-btn-microsoft [text]="text()" [isDropdownProfile]="true">
65
+ <ba-dropdown-profile-item
66
+ id="ba-ddi-change-account-1"
67
+ [text]="text()"
68
+ [firstIcon]="exchangeIcon"
69
+ [isTopBorder]="isTopBorder()"
70
+ [disabled]="disabled()"
71
+ (clicked)="changeAccountClicked.emit()"
72
+ />
73
+ </ba-btn-microsoft>
74
+ }
75
+ @case ('metamask') {
76
+ <ba-btn-metamask [text]="text()" [isDropdownProfile]="true">
77
+ <ba-dropdown-profile-item
78
+ id="ba-ddi-change-account-1"
79
+ [text]="text()"
80
+ [firstIcon]="exchangeIcon"
81
+ [isTopBorder]="isTopBorder()"
82
+ [disabled]="disabled()"
83
+ (clicked)="changeAccountClicked.emit()"
84
+ />
85
+ </ba-btn-metamask>
86
+ }
87
+ @case ('phantom') {
88
+ <ba-btn-phantom [text]="text()" [isDropdownProfile]="true">
89
+ <ba-dropdown-profile-item
90
+ id="ba-ddi-change-account-1"
91
+ [text]="text()"
92
+ [firstIcon]="exchangeIcon"
93
+ [isTopBorder]="isTopBorder()"
94
+ [disabled]="disabled()"
95
+ (clicked)="changeAccountClicked.emit()"
96
+ />
97
+ </ba-btn-phantom>
98
+ }
99
+ }
100
+ </div>
101
+ `,
102
+ })
103
+ export class ChangeAccountComponent {
104
+ serviceName = input('');
105
+ text = input('');
106
+ disabled = input(false);
107
+ isTopBorder = input(false);
108
+
109
+ changeAccountClicked = output<void>();
110
+
111
+ protected readonly exchangeIcon = TbArrowsExchange2Component;
112
+ }
@@ -0,0 +1,66 @@
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 { map } from 'nanostores';
5
+ import { DropdownProfileBtnComponent } from './dropdown-profile-btn.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
+ // Port de packages/react/src/components/dds/DropdownProfileBtn.test.tsx (mapa:
18
+ // packages/vue/.../DropdownProfileBtn.test.ts).
19
+ describe('DropdownProfileBtn', () => {
20
+ function makeClient(ws: Record<string, any> = {}) {
21
+ return { state: { $ui: map({}), $ws: map({ ...ws }) } } as any;
22
+ }
23
+
24
+ it('renderiza el data-service del serviceName y emite clicked al pulsar', async () => {
25
+ const onClick = vi.fn();
26
+ @Component({
27
+ standalone: true,
28
+ imports: [DropdownProfileBtnComponent],
29
+ template: `<ba-dropdown-profile-btn serviceName="blockauth" (clicked)="f()" />`,
30
+ })
31
+ class Host {
32
+ f = onClick;
33
+ }
34
+ await render(Host, { providers: [provideBlockAuthClientForTest(makeClient())] });
35
+
36
+ const btn = screen.getByTestId('dropdown-profile-btn');
37
+ expect(btn.getAttribute('data-service')).toBe('blockauth');
38
+ await fireEvent.click(btn);
39
+ expect(onClick).toHaveBeenCalled();
40
+ });
41
+
42
+ it('copia el DID al clicar la fila del DID sin propagar el click al boton', async () => {
43
+ const writeText = vi.fn();
44
+ Object.assign(navigator, { clipboard: { writeText } });
45
+ const onClick = vi.fn();
46
+ @Component({
47
+ standalone: true,
48
+ imports: [DropdownProfileBtnComponent],
49
+ template: `<ba-dropdown-profile-btn serviceName="blockauth" (clicked)="f()" />`,
50
+ })
51
+ class Host {
52
+ f = onClick;
53
+ }
54
+ // Forzar un DID legible desde $ws (fallback cuando storage está vacío).
55
+ await render(Host, {
56
+ providers: [provideBlockAuthClientForTest(makeClient({ did: '0xabcdef1234567890' }))],
57
+ });
58
+
59
+ const didRow = screen.getByText(/^did:/);
60
+ await fireEvent.click(didRow);
61
+ // Copia el DID completo, no la etiqueta acortada.
62
+ expect(writeText).toHaveBeenCalledWith('0xabcdef1234567890');
63
+ // stopPropagation: el click de copiar no debe emitir el click del botón.
64
+ expect(onClick).not.toHaveBeenCalled();
65
+ });
66
+ });
@@ -0,0 +1,185 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output, signal } from '@angular/core';
2
+ import { utils } from '@block-auth.io/core';
3
+ import { injectBlockAuthClient } from '../../provide-block-auth';
4
+ import { injectStore } from '../../inject-store';
5
+ import { providersStyles } from '../../primitives/providersStyles';
6
+ import { DropdownArrowComponent } from '../../primitives/dropdown-arrow.component';
7
+ import { TbCopyComponent, TbCopyCheckFilledComponent } from '../../icons';
8
+
9
+ // El click-outside lo maneja el PADRE, que envuelve este componente en su
10
+ // propio `<div>` con una template ref; por eso aquí no hay prop de contenedor.
11
+ // Los `classNames` por tamaño salen de mapas indexados por `dropdownBtnSize`.
12
+ //
13
+ // `rawDid`/`rawAccount` se leen dentro de `computed()`s que dependen de `ui`/`ws`
14
+ // (relee storage vía `utils.browserBase` en cada recompute).
15
+ @Component({
16
+ selector: 'ba-dropdown-profile-btn',
17
+ standalone: true,
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ imports: [DropdownArrowComponent, TbCopyComponent, TbCopyCheckFilledComponent],
20
+ template: `
21
+ <div
22
+ data-testid="dropdown-profile-btn"
23
+ [attr.data-service]="serviceNameResolved()"
24
+ [class]="
25
+ 'bal-ba-transition-2 bas-group bas-relative bas-box-border bas-grid bas-h-full bas-w-full bas-cursor-pointer bas-flex-row bas-items-center bas-justify-between bas-gap-1 bas-overflow-hidden bas-rounded-lg bas-mx-auto ' +
26
+ dropdownBtnBgClassNames() + ' bas-border-2 bas-border-solid ' +
27
+ gridClassNames() + ' ' +
28
+ (dropdownBtnBorderClassNames() || style().borderStyles + ' ' + style().hoverBorderStyles)
29
+ "
30
+ [style.minHeight]="height() != null ? height() + 'px' : null"
31
+ [style.width]="widthStyle()"
32
+ (click)="clicked.emit()"
33
+ >
34
+ <span
35
+ [class]="
36
+ 'bas-absolute bas-right-2.5 bas-top-1.5 bas-h-2 bas-w-2 bas-rounded-full ' +
37
+ (!isSignedFirstCheck() ? 'bas-bg-gray-400' : !isSigned() ? 'bas-bg-red-400' : 'bas-bg-green-400')
38
+ "
39
+ ></span>
40
+ <div class="bas-z-10 bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center bas-rounded-md">
41
+ <img
42
+ [src]="style().imgSrc"
43
+ [alt]="serviceNameResolved() + ' icon'"
44
+ [class]="'bal-ba-transition-2 bal-ba-transform-base bal-ba-hover-scale-110 bas-rounded-md ' + imgSizesClassNames()"
45
+ [style.backgroundColor]="style().imgBgColor"
46
+ />
47
+ </div>
48
+ <div
49
+ [class]="'bal-ba-transition-2 bas-flex bas-h-full bas-w-full bas-flex-col bas-items-center bas-justify-center bas-font-semibold bas-min-w-0 ' + dropdownBtnTextColorClassNames()"
50
+ >
51
+ <div
52
+ [class]="
53
+ 'bal-ba-transition-2 bas-flex bas-max-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-1 bas-cursor-pointer ' +
54
+ (isDidCopied() ? 'bas-text-[#4d849b]' : 'hover:bas-opacity-50')
55
+ "
56
+ (click)="onCopyDid($event)"
57
+ >
58
+ <p class="bas-m-0 bas-truncate bas-text-sm">{{ did() }}</p>
59
+ @if (isDidCopied()) {
60
+ <ba-tb-copy-check-filled class="bal-ba-transition-2 bas-h-[18px] bas-w-[18px] bas-shrink-0" />
61
+ } @else {
62
+ <ba-tb-copy class="bal-ba-transition-2 bas-h-[18px] bas-w-[18px] bas-shrink-0" />
63
+ }
64
+ </div>
65
+ @if (showAccountRow()) {
66
+ <div
67
+ [class]="
68
+ 'bal-ba-transition-2 bas-flex bas-max-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-1 bas-cursor-pointer ' +
69
+ (isAccountCopied() ? 'bas-text-[#4d849b]' : 'hover:bas-opacity-50')
70
+ "
71
+ (click)="onCopyAccount($event)"
72
+ >
73
+ <p class="bas-m-0 bas-truncate bas-text-xs">{{ accountSubtitle() }}</p>
74
+ @if (isAccountCopied()) {
75
+ <ba-tb-copy-check-filled class="bal-ba-transition-2 bas-h-4 bas-w-4 bas-shrink-0" />
76
+ } @else {
77
+ <ba-tb-copy class="bal-ba-transition-2 bas-h-4 bas-w-4 bas-shrink-0" />
78
+ }
79
+ </div>
80
+ }
81
+ </div>
82
+ <div [class]="arrowContainerClassNames()">
83
+ <ba-dropdown-arrow
84
+ id="ba-ddp-btn-arrow"
85
+ [isOpen]="isProfileOpen()"
86
+ [color]="style().colorBase"
87
+ [sizeClassNames]="arrowSizesClassNames()"
88
+ />
89
+ </div>
90
+ </div>
91
+ `,
92
+ })
93
+ export class DropdownProfileBtnComponent {
94
+ serviceName = input<string | null>(null);
95
+ dropdownBtnBgClassNames = input('bas-bg-transparent');
96
+ dropdownBtnSize = input<'xs' | 'small' | 'base' | 'large'>('xs');
97
+ dropdownBtnBorderClassNames = input<string | undefined>(undefined);
98
+ dropdownBtnTextColorClassNames = input(
99
+ 'bas-text-[var(--bal-primary-700)] group-hover:bas-text-[var(--bal-primary-900)]',
100
+ );
101
+ height = input<number | null>(null);
102
+ isProfileOpen = input(false);
103
+
104
+ clicked = output<void>();
105
+
106
+ private readonly client = injectBlockAuthClient();
107
+ private readonly ui = injectStore(this.client.state.$ui);
108
+ private readonly ws = injectStore(this.client.state.$ws);
109
+
110
+ private readonly gridClassNamesBySize: Record<string, string> = {
111
+ xs: 'bas-grid-cols-[46px_1fr_28px]',
112
+ small: 'bas-grid-cols-[46px_1fr_28px]',
113
+ base: 'bas-grid-cols-[56px_1fr_28px]',
114
+ large: 'bas-grid-cols-[56px_1fr_28px]',
115
+ };
116
+ private readonly widthBySize: Record<string, number> = { xs: 224, small: 224, base: 244, large: 244 };
117
+ private readonly imgSizesClassNamesBySize: Record<string, string> = {
118
+ xs: 'bas-h-9 bas-w-9',
119
+ small: 'bas-h-10 bas-w-10',
120
+ base: 'bas-h-12 bas-w-12',
121
+ large: 'bas-h-12 bas-w-12',
122
+ };
123
+ private readonly arrowSizesClassNamesBySize: Record<string, string> = {
124
+ xs: 'bas-h-3.5 bas-w-3.5',
125
+ small: 'bas-h-3.5 bas-w-3.5',
126
+ base: 'bas-h-4 bas-w-4',
127
+ large: 'bas-h-4 bas-w-4',
128
+ };
129
+
130
+ protected readonly serviceNameResolved = computed(
131
+ () => this.serviceName() ?? this.ui().serviceName ?? 'blockauth',
132
+ );
133
+ protected readonly style = computed(
134
+ () => providersStyles[this.serviceNameResolved()] ?? providersStyles['blockauth'],
135
+ );
136
+ protected readonly isSignedFirstCheck = computed(() => this.ui().isSignedFirstCheck);
137
+ protected readonly isSigned = computed(() => this.ui().isSigned);
138
+
139
+ private short = (value: string, start: number, end: number): string =>
140
+ value.length > start + end ? `${value.slice(0, start)}…${value.slice(-end)}` : value;
141
+
142
+ private readonly rawDid = computed(() => {
143
+ const session = utils.browserBase.getSession();
144
+ const external = utils.browserBase.getExternalService();
145
+ return (this.serviceNameResolved() === 'blockauth' ? session.did : external.did) || this.ws().did || '';
146
+ });
147
+ protected readonly did = computed(() => this.short(this.rawDid(), 6, 4).replace('0x', 'did:'));
148
+ private readonly rawAccount = computed(() => utils.browserBase.getExternalService().account || '');
149
+ protected readonly accountSubtitle = computed(() =>
150
+ this.serviceNameResolved() === 'blockauth' ? '' : this.short(this.rawAccount(), 5, 8),
151
+ );
152
+ protected readonly showAccountRow = computed(
153
+ () => this.serviceNameResolved() !== 'blockauth' && !!this.rawAccount(),
154
+ );
155
+
156
+ protected readonly isDidCopied = signal(false);
157
+ protected readonly isAccountCopied = signal(false);
158
+
159
+ protected readonly gridClassNames = computed(() => this.gridClassNamesBySize[this.dropdownBtnSize()]);
160
+ protected readonly imgSizesClassNames = computed(() => this.imgSizesClassNamesBySize[this.dropdownBtnSize()]);
161
+ protected readonly arrowSizesClassNames = computed(() => this.arrowSizesClassNamesBySize[this.dropdownBtnSize()]);
162
+ protected readonly widthStyle = computed(() => `min(100%, ${this.widthBySize[this.dropdownBtnSize()]}px)`);
163
+ protected readonly arrowContainerClassNames = computed(() => {
164
+ let classNames = 'bas-h-full bas-w-full bas-justify-center';
165
+ const size = this.dropdownBtnSize();
166
+ if (size === 'xs' || size === 'small') classNames += ' bas-pt-1.5';
167
+ return classNames;
168
+ });
169
+
170
+ onCopyDid(e: MouseEvent): void {
171
+ this.copy(this.rawDid(), this.isDidCopied, e);
172
+ }
173
+ onCopyAccount(e: MouseEvent): void {
174
+ this.copy(this.rawAccount(), this.isAccountCopied, e);
175
+ }
176
+
177
+ private copy(value: string, copiedSignal: { set: (v: boolean) => void }, e: MouseEvent): void {
178
+ // Don't toggle the dropdown open/closed when the user only wants to copy.
179
+ e.stopPropagation();
180
+ if (!value) return;
181
+ void navigator.clipboard?.writeText(value);
182
+ copiedSignal.set(true);
183
+ setTimeout(() => copiedSignal.set(false), 3000);
184
+ }
185
+ }
@@ -0,0 +1,75 @@
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 { DropdownProfileItemComponent } from './dropdown-profile-item.component';
5
+
6
+ // Port de packages/react/src/components/dds/DropdownProfileItem.test.tsx (mapa:
7
+ // packages/vue/.../DropdownProfileItem.test.ts). Puramente presentacional.
8
+ describe('DropdownProfileItem', () => {
9
+ it('renderiza el texto y emite clicked al pulsar', async () => {
10
+ const onClick = vi.fn();
11
+ @Component({
12
+ standalone: true,
13
+ imports: [DropdownProfileItemComponent],
14
+ template: `<ba-dropdown-profile-item text="Disconnect" (clicked)="f()" />`,
15
+ })
16
+ class Host {
17
+ f = onClick;
18
+ }
19
+ await render(Host);
20
+ await fireEvent.click(screen.getByText('Disconnect'));
21
+ expect(onClick).toHaveBeenCalled();
22
+ });
23
+
24
+ it('no emite clicked cuando disabled=true', async () => {
25
+ const onClick = vi.fn();
26
+ @Component({
27
+ standalone: true,
28
+ imports: [DropdownProfileItemComponent],
29
+ template: `<ba-dropdown-profile-item text="Disconnect" [disabled]="true" (clicked)="f()" />`,
30
+ })
31
+ class Host {
32
+ f = onClick;
33
+ }
34
+ await render(Host);
35
+ await fireEvent.click(screen.getByText('Disconnect'));
36
+ expect(onClick).not.toHaveBeenCalled();
37
+ });
38
+
39
+ it('renderiza el contenido proyectado de los slots firstIcon/secondIcon', async () => {
40
+ @Component({
41
+ standalone: true,
42
+ imports: [DropdownProfileItemComponent],
43
+ template: `<ba-dropdown-profile-item text="Item">
44
+ <span slot="firstIcon" data-testid="first-icon"></span>
45
+ <span slot="secondIcon" data-testid="second-icon"></span>
46
+ </ba-dropdown-profile-item>`,
47
+ })
48
+ class Host {}
49
+ await render(Host);
50
+ expect(screen.getByTestId('first-icon')).toBeDefined();
51
+ expect(screen.getByTestId('second-icon')).toBeDefined();
52
+ });
53
+
54
+ it('aplica el estilo de borde superior cuando isTopBorder=true', async () => {
55
+ const { container } = await render(DropdownProfileItemComponent, {
56
+ inputs: { text: 'Item', isTopBorder: true },
57
+ });
58
+ expect(container.querySelector('li')?.className).toContain('bas-border-t');
59
+ });
60
+
61
+ // Regresión: el host es display:contents para que el <li> (bas-w-full) sea hijo
62
+ // directo del flex de la <ul> y los items queden alineados a la izquierda (no
63
+ // centrados por el `bas-items-center` de la ul encogiendo el host).
64
+ it('el host es transparente al layout (display: contents)', async () => {
65
+ @Component({
66
+ standalone: true,
67
+ imports: [DropdownProfileItemComponent],
68
+ template: `<ba-dropdown-profile-item text="Item" />`,
69
+ })
70
+ class Host {}
71
+ const { container } = await render(Host);
72
+ const host = container.querySelector('ba-dropdown-profile-item') as HTMLElement;
73
+ expect(host.style.display).toBe('contents');
74
+ });
75
+ });
@@ -0,0 +1,74 @@
1
+ import { ChangeDetectionStrategy, Component, Type, input, output } from '@angular/core';
2
+ import { NgComponentOutlet } from '@angular/common';
3
+
4
+ // `firstIcon`/`secondIcon` (ReactNode) -> proyección de contenido con
5
+ // `[slot=firstIcon]`/`[slot=secondIcon]` (misma convención `[slot=icon]` ya
6
+ // usada por `ba-btn-base-provider`). `onClick` -> output `clicked`.
7
+ //
8
+ // Además, para el caso de la lista de items adicionales de `DropdownProfile`
9
+ // (iconos dinámicos por-item que no se pueden proyectar estáticamente dentro de
10
+ // un `@for`), se acepta un `firstIcon` opcional como *tipo de componente*
11
+ // (`Type<unknown>`) renderizado vía `NgComponentOutlet` — equivalente Angular
12
+ // idiomático del `VNode | (() => VNode)` del port Vue.
13
+ //
14
+ // La animación framer-motion (`motion.li` + `liItemVariants`) se elimina: el
15
+ // panel completo se anima a nivel de `DropdownProfile` (mismo patrón que
16
+ // `DropdownBase`). `onAnimationComplete` también se elimina aquí; la señal
17
+ // equivalente ("panel abierto") la gestiona `DropdownProfile`.
18
+ @Component({
19
+ selector: 'ba-dropdown-profile-item',
20
+ standalone: true,
21
+ // El host debe ser transparente al layout para que el <li> (bas-w-full) sea
22
+ // hijo directo del flex de la <ul> (como en React, donde <motion.li> se
23
+ // renderiza directamente). Sin esto el host `<ba-dropdown-profile-item>` es un
24
+ // flex-item que, con `bas-items-center` en la ul, se encoge a su contenido y
25
+ // queda CENTRADO en vez de llenar el ancho y alinear el texto a la izquierda.
26
+ host: { style: 'display: contents' },
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ imports: [NgComponentOutlet],
29
+ template: `
30
+ <li
31
+ [id]="id()"
32
+ [class]="
33
+ 'bas-box-border bas-max-h-[36px] bas-min-h-[36px] bas-w-full ' +
34
+ (isTopBorder()
35
+ ? 'bas-border-t bas-border-[var(--bal-primary-90)] bas-border-solid bas-border-x-0 bas-border-b-0 bas-box-border hover:bas-border-[var(--bal-primary-100)] bal-ba-transition-2'
36
+ : '')
37
+ "
38
+ >
39
+ <div
40
+ [class]="
41
+ 'bal-ba-transition-2 bas-box-border bas-flex bas-w-full bas-flex-row bas-items-center bas-justify-between bas-gap-2 bas-px-2 bas-py-1 bas-text-[var(--bal-primary-700)] hover:bas-bg-[var(--bal-primary-10)] hover:bas-text-[var(--bal-primary-900)] ' +
42
+ classNames() + ' ' +
43
+ (isTopBorder() ? 'bas-h-[35px]' : 'bas-h-9') + ' ' +
44
+ (disabled() ? 'bas-cursor-not-allowed bas-opacity-50' : 'bas-cursor-pointer')
45
+ "
46
+ (click)="onItemClicked()"
47
+ >
48
+ <div class="bas-flex bas-h-full bas-w-fit bas-flex-row bas-items-center bas-gap-3">
49
+ @if (firstIcon()) {
50
+ <ng-container [ngComponentOutlet]="firstIcon()" [ngComponentOutletInputs]="{ class: 'bas-h-5 bas-w-5' }" />
51
+ }
52
+ <ng-content select="[slot=firstIcon]" />
53
+ <p class="bas-m-0 bas-text-base">{{ text() }}</p>
54
+ </div>
55
+ <ng-content select="[slot=secondIcon]" />
56
+ </div>
57
+ </li>
58
+ `,
59
+ })
60
+ export class DropdownProfileItemComponent {
61
+ id = input('dd-item-container');
62
+ text = input('');
63
+ isTopBorder = input(false);
64
+ classNames = input('');
65
+ disabled = input(false);
66
+ /** Icono dinámico opcional (tipo de componente) para items en bucle. */
67
+ firstIcon = input<Type<unknown> | null>(null);
68
+
69
+ clicked = output<void>();
70
+
71
+ onItemClicked(): void {
72
+ if (!this.disabled()) this.clicked.emit();
73
+ }
74
+ }