@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,118 @@
1
+ import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
2
+
3
+ // El anillo giratorio de framer-motion
4
+ // (`motion.span` con animate={{rotate:360}} infinito) pasa a ser una
5
+ // animación CSS local. La <img> hace fade-in cuando termina de cargar
6
+ // (`(load)`) aplicando las clases `bas-fade-enter-active`/`bas-fade-enter-from`
7
+ // de transitions.css (equivalente al `<Transition name="bas-fade">` +
8
+ // `v-show` del port Vue): se mantiene siempre montada (para que el navegador
9
+ // pueda disparar `load`) y se alterna la clase `-enter-from` según `loaded()`.
10
+ type ImgLoaderSize = 'small' | 'medium' | 'large' | 'big';
11
+
12
+ const SIZES_CONTAINER: Record<ImgLoaderSize, string> = {
13
+ small: 'bas-h-[82px] bas-w-[82px]',
14
+ medium: 'bas-h-[90px] bas-w-[90px]',
15
+ large: 'bas-h-[120px] bas-w-[120px]',
16
+ big: 'bas-h-[200px] bas-w-[200px]',
17
+ };
18
+ const SIZES_ICON_CONTAINER: Record<ImgLoaderSize, string> = {
19
+ small: 'bas-h-[72px] bas-w-[72px]',
20
+ medium: 'bas-h-20 bas-w-20',
21
+ large: 'bas-h-[108px] bas-w-[108px]',
22
+ big: 'bas-h-[180px] bas-w-[180px]',
23
+ };
24
+ const SIZES_ICON: Record<ImgLoaderSize, string> = {
25
+ small: 'bas-h-16 bas-w-16',
26
+ medium: 'bas-h-[80px] bas-w-[80px]',
27
+ large: 'bas-h-[100px] bas-w-[100px]',
28
+ big: 'bas-h-[166px] bas-w-[166px]',
29
+ };
30
+ const MOTION_SIZES: Record<ImgLoaderSize, string> = {
31
+ small: 'bas-h-[112px] bas-w-[112px]',
32
+ medium: 'bas-h-[124px] bas-w-[124px]',
33
+ large: 'bas-h-[164px] bas-w-[164px]',
34
+ big: 'bas-h-[272px] bas-w-[272px]',
35
+ };
36
+ const MOTION_SPAN_SIZES: Record<ImgLoaderSize, string> = {
37
+ small: 'bas-border-[22px]',
38
+ medium: 'bas-border-[24px]',
39
+ large: 'bas-border-[28px]',
40
+ big: 'bas-border-[46px]',
41
+ };
42
+
43
+ @Component({
44
+ selector: 'ba-img-loader',
45
+ standalone: true,
46
+ changeDetection: ChangeDetectionStrategy.OnPush,
47
+ template: `
48
+ <div
49
+ [class]="
50
+ 'bas-relative bas-flex ' +
51
+ SIZES_CONTAINER[size()] +
52
+ ' bas-items-center bas-justify-center bas-overflow-hidden bas-rounded-lg'
53
+ "
54
+ >
55
+ <div
56
+ [class]="
57
+ 'bas-z-10 bas-flex ' +
58
+ SIZES_ICON_CONTAINER[size()] +
59
+ ' bas-items-center bas-justify-center bas-rounded-md bas-bg-white'
60
+ "
61
+ >
62
+ <img
63
+ [src]="imgSrc()"
64
+ [alt]="imgAlt()"
65
+ [class]="
66
+ 'bal-ba-transform-base bal-ba-transition-2 bas-rounded-md ' +
67
+ SIZES_ICON[size()] +
68
+ ' ' +
69
+ (scaleImgWhenHovered() ? 'bal-ba-hover-scale-110' : '') +
70
+ ' bas-fade-enter-active ' +
71
+ (loaded() ? '' : 'bas-fade-enter-from')
72
+ "
73
+ [style.background-color]="imgBgColor()"
74
+ (load)="loaded.set(true)"
75
+ />
76
+ </div>
77
+ <div [class]="'bas-absolute ' + MOTION_SIZES[size()]">
78
+ <span
79
+ [id]="loaderId()"
80
+ [class]="
81
+ 'bas-img-loader-ring bas-left-0 bas-top-0 bas-box-border bas-block bas-h-full bas-w-full bas-rounded-full bas-border-solid bas-border-transparent ' +
82
+ MOTION_SPAN_SIZES[size()]
83
+ "
84
+ [style.border-top-color]="loaderColor()"
85
+ ></span>
86
+ </div>
87
+ </div>
88
+ `,
89
+ styles: [
90
+ `
91
+ @keyframes bas-img-loader-spin {
92
+ to {
93
+ transform: rotate(360deg);
94
+ }
95
+ }
96
+ .bas-img-loader-ring {
97
+ animation: bas-img-loader-spin 1.25s linear infinite;
98
+ }
99
+ `,
100
+ ],
101
+ })
102
+ export class ImgLoaderComponent {
103
+ readonly SIZES_CONTAINER = SIZES_CONTAINER;
104
+ readonly SIZES_ICON_CONTAINER = SIZES_ICON_CONTAINER;
105
+ readonly SIZES_ICON = SIZES_ICON;
106
+ readonly MOTION_SIZES = MOTION_SIZES;
107
+ readonly MOTION_SPAN_SIZES = MOTION_SPAN_SIZES;
108
+
109
+ loaderId = input('img-fm-loader');
110
+ imgSrc = input('');
111
+ imgAlt = input('');
112
+ imgBgColor = input('');
113
+ loaderColor = input('');
114
+ scaleImgWhenHovered = input(false);
115
+ size = input<ImgLoaderSize>('small');
116
+
117
+ loaded = signal(false);
118
+ }
@@ -0,0 +1,29 @@
1
+ // Barrel de las primitivas de UI de @block-auth.io/angular.
2
+ // Excluidas (fuera de scope, igual que en el port Vue): Switcher.
3
+ export { ArrowIconComponent } from './arrow-icon.component';
4
+ export { BtnArrowDirComponent } from './btn-arrow-dir.component';
5
+ export { BtnBaseComponent } from './btn-base.component';
6
+ export { DividerBaseComponent } from './divider-base.component';
7
+ export { DropdownArrowComponent } from './dropdown-arrow.component';
8
+ export { DropdownBaseComponent, type DropdownItem, defaultItems } from './dropdown-base.component';
9
+ export { HorizontalLoaderComponent } from './horizontal-loader.component';
10
+ export { IconBaseComponent } from './icon-base.component';
11
+ export { ImgLoaderComponent } from './img-loader.component';
12
+ export { InputBaseComponent } from './input-base.component';
13
+ export { LifetimeSliderBaseComponent } from './lifetime-slider-base.component';
14
+ export { OvalSpinnerComponent } from './oval-spinner.component';
15
+ export { ProgressbarComponent } from './progressbar.component';
16
+ export { SliderBaseComponent } from './slider-base.component';
17
+
18
+ // pickers/* — port de packages/react/src/primitives/pickers.
19
+ export { DropdownPickerAnimatedBaseComponent } from './pickers/dropdown-picker-animated-base.component';
20
+ export { PickerButtonComponent } from './pickers/picker-button.component';
21
+ export { TimePickerComponent } from './pickers/time-picker.component';
22
+ export {
23
+ TimePickerDropdownSelectorComponent,
24
+ type TimeArrays,
25
+ } from './pickers/time-picker-dropdown-selector.component';
26
+ export { TimeUnitSelectorComponent } from './pickers/time-unit-selector.component';
27
+
28
+ export { providersStyles, type ProviderStyle } from './providersStyles';
29
+ export * from './helpers';
@@ -0,0 +1,20 @@
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 { InputBaseComponent } from './input-base.component';
5
+ describe('InputBaseComponent', () => {
6
+ it('renderiza el label', async () => {
7
+ @Component({ standalone: true, imports: [InputBaseComponent], template: `<ba-input-base labelText="Correo" inputId="e" />` })
8
+ class Host {}
9
+ await render(Host);
10
+ expect(screen.getByText('Correo')).toBeTruthy();
11
+ });
12
+ it('emite changedInputValue en input cuando isInputValueManaged', async () => {
13
+ const onChange = vi.fn();
14
+ @Component({ standalone: true, imports: [InputBaseComponent], template: `<ba-input-base [isInputValueManaged]="true" inputTestId="in" (changedInputValue)="f($event)" />` })
15
+ class Host { f = onChange; }
16
+ await render(Host);
17
+ await fireEvent.input(screen.getByTestId('in'), { target: { value: 'hola' } });
18
+ expect(onChange).toHaveBeenCalledWith('hola');
19
+ });
20
+ });
@@ -0,0 +1,210 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
2
+
3
+ // `register` (react-hook-form) no tiene
4
+ // equivalente y se elimina; `onChangedInputValue` -> emit `changedInputValue`.
5
+ // `min`/`max`/`step` se dejan sin default (undefined) para no introducir un
6
+ // clamp que react nunca tuvo por defecto (misma decisión que InputBase.vue).
7
+ type InputSize = 'xxs' | 'xs' | 'small' | 'base' | 'medium';
8
+
9
+ @Component({
10
+ selector: 'ba-input-base',
11
+ standalone: true,
12
+ changeDetection: ChangeDetectionStrategy.OnPush,
13
+ template: `
14
+ <div
15
+ [class]="
16
+ (enableError() ? displayClassNames() + ' ' + widthClassNames() + ' bas-items-center bas-gap-1.5' : '') +
17
+ ' ' + outerHeightGrid() + ' ' + className()
18
+ "
19
+ >
20
+ @if ((enableError() || info()) && enableWhiteSpaces()) {
21
+ <p class="bal-ba-error-text-invisible bas-m-0 bas-h-[18px]">white space</p>
22
+ }
23
+ <div [class]="'bas-relative bas-min-w-0 ' + innerHeightGrid()">
24
+ @if (isTextArea()) {
25
+ <textarea
26
+ [id]="inputId()"
27
+ [class]="inputClassNames()"
28
+ placeholder=""
29
+ [disabled]="isDisabled()"
30
+ [readonly]="isReadOnly()"
31
+ autocomplete="off"
32
+ [value]="isInputValueManaged() ? inputValue() : ''"
33
+ (input)="isInputValueManaged() && onInput($event)"
34
+ ></textarea>
35
+ } @else {
36
+ <input
37
+ [type]="type()"
38
+ [id]="inputId()"
39
+ [attr.data-testid]="inputTestId()"
40
+ [class]="inputClassNames()"
41
+ placeholder=""
42
+ [disabled]="isDisabled()"
43
+ [readonly]="isReadOnly()"
44
+ autocomplete="off"
45
+ [attr.min]="min()"
46
+ [attr.max]="max()"
47
+ [attr.step]="step()"
48
+ [value]="isInputValueManaged() ? inputValue() : ''"
49
+ (input)="isInputValueManaged() && onInput($event)"
50
+ />
51
+ }
52
+ <label [for]="inputId()" [class]="floatingLabelClassNames()">
53
+ <p [class]="'bas-m-0 ' + (isDisabled() ? 'bas-cursor-not-allowed' : 'bas-cursor-text')">
54
+ {{ labelText() }}
55
+ </p>
56
+ </label>
57
+ </div>
58
+ @if (enableError() || info()) {
59
+ <p
60
+ [class]="
61
+ 'bas-m-0 bas-h-[18px] ' +
62
+ (error() || info() ? 'bal-ba-error-text' : 'bal-ba-error-text-invisible') + ' ' +
63
+ (info() ? (blueStyles() ? 'bal-ba-info-text-blue-styles' : 'bal-ba-info-text') : '')
64
+ "
65
+ >
66
+ {{ error() ? errorMessage() : '' }}{{ info() ? infoMessage() : '' }}
67
+ </p>
68
+ }
69
+ </div>
70
+ `,
71
+ })
72
+ export class InputBaseComponent {
73
+ type = input('text');
74
+ inputId = input('floating_outlined');
75
+ isTextArea = input(false);
76
+ labelText = input('Floating outlined');
77
+ size = input<InputSize>('base');
78
+ isDisabled = input(false);
79
+ isReadOnly = input(false);
80
+ enableError = input(true);
81
+ enableWhiteSpaces = input(true);
82
+ error = input(false);
83
+ errorMessage = input('Error message');
84
+ info = input(false);
85
+ infoMessage = input('Info message');
86
+ isBgRedWhenError = input(false);
87
+ bgColor = input('bas-bg-transparent');
88
+ bgColorWhenError = input('bas-bg-red-50');
89
+ isInputValueManaged = input(false);
90
+ inputValue = input('');
91
+ floatingLabelTextStyles = input('bas-text-sm');
92
+ floatingLabelBgColor = input('bas-bg-white');
93
+ borderRadiusClassNames = input('bas-rounded-lg');
94
+ displayClassNames = input('bas-grid');
95
+ className = input('');
96
+ widthClassNames = input('bas-w-full');
97
+ blueStyles = input(false);
98
+ min = input<number | undefined>(undefined);
99
+ max = input<number | undefined>(undefined);
100
+ step = input<number | undefined>(undefined);
101
+ inputTestId = input<string | undefined>(undefined);
102
+
103
+ /** Equivalente al emit `changedInputValue` de InputBase.vue. */
104
+ changedInputValue = output<string>();
105
+
106
+ onInput(e: Event): void {
107
+ this.changedInputValue.emit((e.target as HTMLInputElement | HTMLTextAreaElement).value);
108
+ }
109
+
110
+ private heightGrid(enableError: boolean, enableWhiteSpaces: boolean): string {
111
+ if (!this.isTextArea()) {
112
+ return {
113
+ xxs: enableError
114
+ ? enableWhiteSpaces ? 'bas-h-[82px] bas-grid-rows-[18px_34px_18px]' : 'bas-h-[58px] bas-grid-rows-[34px_18px]'
115
+ : 'bas-h-[34px]',
116
+ xs: enableError
117
+ ? enableWhiteSpaces ? 'bas-h-[86px] bas-grid-rows-[18px_38px_18px]' : 'bas-h-[62px] bas-grid-rows-[38px_18px]'
118
+ : 'bas-h-[38px]',
119
+ small: enableError
120
+ ? enableWhiteSpaces ? 'bas-h-[90px] bas-grid-rows-[18px_42px_18px]' : 'bas-h-[66px] bas-grid-rows-[42px_18px]'
121
+ : 'bas-h-[42px]',
122
+ base: enableError
123
+ ? enableWhiteSpaces ? 'bas-h-[94px] bas-grid-rows-[18px_46px_18px]' : 'bas-h-[70px] bas-grid-rows-[46px_18px]'
124
+ : 'bas-h-[46px]',
125
+ medium: enableError
126
+ ? enableWhiteSpaces ? 'bas-h-[102px] bas-grid-rows-[18px_54px_18px]' : 'bas-h-[78px] bas-grid-rows-[54px_18px]'
127
+ : 'bas-h-[54px]',
128
+ }[this.size()];
129
+ }
130
+ return {
131
+ xxs: enableError
132
+ ? enableWhiteSpaces ? 'bas-h-[96px] bas-grid-rows-[18px_48px_18px]' : 'bas-h-[72px] bas-grid-rows-[48px_18px]'
133
+ : 'bas-h-[48px]',
134
+ xs: enableError
135
+ ? enableWhiteSpaces ? 'bas-h-[112px] bas-grid-rows-[18px_64px_18px]' : 'bas-h-[88px] bas-grid-rows-[64px_18px]'
136
+ : 'bas-h-[64px]',
137
+ small: enableError
138
+ ? enableWhiteSpaces ? 'bas-h-[128px] bas-grid-rows-[18px_80px_18px]' : 'bas-h-[104px] bas-grid-rows-[80px_18px]'
139
+ : 'bas-h-[80px]',
140
+ base: enableError
141
+ ? enableWhiteSpaces ? 'bas-h-[144px] bas-grid-rows-[18px_96px_18px]' : 'bas-h-[120px] bas-grid-rows-[96px_18px]'
142
+ : 'bas-h-[96px]',
143
+ medium: enableError
144
+ ? enableWhiteSpaces ? 'bas-h-[160px] bas-grid-rows-[18px_112px_18px]' : 'bas-h-[136px] bas-grid-rows-[112px_18px]'
145
+ : 'bas-h-[112px]',
146
+ }[this.size()];
147
+ }
148
+
149
+ outerHeightGrid = computed(() => this.heightGrid(this.enableError(), this.enableWhiteSpaces()));
150
+ innerHeightGrid = computed(() => this.heightGrid(false, false));
151
+
152
+ private inputHeightClassNames = computed(() => {
153
+ if (!this.isTextArea()) {
154
+ return {
155
+ xxs: 'bas-h-[34px] bas-pb-0.5 bas-pt-1.5',
156
+ xs: 'bas-h-[38px] bas-pb-0.5 bas-pt-2',
157
+ small: 'bas-h-[42px] bas-pb-1 bas-pt-2.5',
158
+ base: 'bas-h-[46px] bas-pb-1.5 bas-pt-3',
159
+ medium: 'bas-h-[54px] bas-pb-2.5 bas-pt-4',
160
+ }[this.size()];
161
+ }
162
+ return {
163
+ xxs: 'bas-h-[48px] bas-pb-0.5 bas-pt-1.5',
164
+ xs: 'bas-h-[64px] bas-pb-0.5 bas-pt-2',
165
+ small: 'bas-h-[80px] bas-pb-1 bas-pt-2.5',
166
+ base: 'bas-h-[96px] bas-pb-1.5 bas-pt-3',
167
+ medium: 'bas-h-[112px] bas-pb-2.5 bas-pt-4',
168
+ }[this.size()];
169
+ });
170
+
171
+ inputClassNames = computed(
172
+ () => `bal-ba-floating-input-base bal-ba-floating-input-outlined
173
+ bas-peer bas-box-border bas-block bas-w-full bas-appearance-none bas-border-2
174
+ bal-ba-transition-150 bas-border-solid bas-px-2.5 bas-text-sm bal-ba-outline-none
175
+ bal-ba-shadow-base bal-ba-focus-shadow-none disabled:bas-cursor-not-allowed
176
+ disabled:bas-border-gray-200 disabled:bas-text-gray-500
177
+ ${this.size() !== 'xxs' ? 'xsm1:bas-text-base' : ''} ${this.isTextArea() ? 'bal-ba-scroll-base bas-resize-none' : ''}
178
+ ${this.borderRadiusClassNames()} ${this.inputHeightClassNames()} ${
179
+ !this.error()
180
+ ? `focus:bas-border-[var(--bal-primary-100)] focus:hover:bas-border-[var(--bal-primary-300)] ${
181
+ this.blueStyles()
182
+ ? 'bas-text-[#004e98] hover:bas-text-[#00296b] bas-border-[var(--bal-primary-100)] hover:bas-border-[var(--bal-primary-300)]'
183
+ : `bas-text-gray-800 hover:bas-text-gray-900 bas-border-gray-300 hover:bas-border-gray-400
184
+ focus:bas-text-[#004e98] focus:hover:bas-text-[#00296b]`
185
+ }`
186
+ : 'bas-border-red-500 bas-text-red-500 focus:bas-border-red-600'
187
+ } ${this.isBgRedWhenError() && this.error() ? this.bgColorWhenError() : this.bgColor()}`,
188
+ );
189
+
190
+ floatingLabelClassNames = computed(
191
+ () => `bal-ba-floating-label-base bal-ba-floating-label-outlined
192
+ bal-ba-transform-base bal-ba-transition-2 bas-absolute
193
+ bas-start-1 bas-z-10 bas-origin-[0] bas-cursor-text bas-px-2
194
+ peer-placeholder-shown:bas-top-1/2 peer-focus:bas-px-2
195
+ rtl:peer-focus:bas-left-auto ${
196
+ this.size() === 'small'
197
+ ? 'bal-ba-floating-label-small-outlined bas-top-1 peer-focus:bas-top-1'
198
+ : 'bal-ba-floating-label-base-medium-outlined bas-top-2 peer-focus:bas-top-2'
199
+ } ${
200
+ this.isDisabled()
201
+ ? 'bas-cursor-not-allowed bas-text-gray-400'
202
+ : !this.error()
203
+ ? this.blueStyles()
204
+ ? 'bas-text-[#004e98] peer-hover:bas-text-[#00296b]'
205
+ : `bas-text-gray-500 peer-hover:bas-text-gray-600
206
+ peer-focus:bas-text-[#004e98] peer-focus:hover:bas-text-[#00296b]`
207
+ : 'bas-text-red-500 peer-focus:bas-text-red-500'
208
+ } ${this.isBgRedWhenError() && this.error() ? this.bgColorWhenError() : this.floatingLabelBgColor()} ${this.floatingLabelTextStyles()}`,
209
+ );
210
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Component } from '@angular/core';
3
+ import { render, fireEvent } from '@testing-library/angular';
4
+ import { LifetimeSliderBaseComponent } from './lifetime-slider-base.component';
5
+
6
+ // Espejo de packages/vue/src/primitives/LifetimeSliderBase.test.ts. Cubre el
7
+ // árbol completo del port (LifetimeSliderBase -> TimePicker ->
8
+ // DropdownPickerAnimatedBase + TimePickerDropdownSelector + TimeUnitSelector +
9
+ // SliderBase), en particular la reconciliación de rangos y el commit en
10
+ // segundos de TimePickerDropdownSelector, que es la parte con más estado.
11
+ describe('LifetimeSliderBaseComponent', () => {
12
+ const TEMPLATE = `
13
+ <ba-lifetime-slider-base
14
+ id="lt"
15
+ [sliderValue]="sliderValue"
16
+ [min]="10"
17
+ [max]="29100"
18
+ [timeInputValue]="timeInputValue"
19
+ [isOpen]="isOpen"
20
+ (sliderValueChanged)="onSlider($event)"
21
+ (timeChanged)="onTime($event)"
22
+ (toggleTimerDropdown)="onToggle()"
23
+ />
24
+ `;
25
+
26
+ function makeHost(overrides: Partial<Record<string, unknown>> = {}) {
27
+ @Component({ standalone: true, imports: [LifetimeSliderBaseComponent], template: TEMPLATE })
28
+ class Host {
29
+ sliderValue = 3600;
30
+ timeInputValue = '01:00:00';
31
+ isOpen = false;
32
+ onSlider = vi.fn();
33
+ onTime = vi.fn();
34
+ onToggle = vi.fn();
35
+ constructor() {
36
+ Object.assign(this, overrides);
37
+ }
38
+ }
39
+ return Host;
40
+ }
41
+
42
+ it('renderiza el slider (SliderBase) y el input del TimePicker con el valor HH:MM:SS', async () => {
43
+ const { container } = await render(makeHost());
44
+ const slider = container.querySelector('#lt-slider') as HTMLInputElement;
45
+ expect(slider).not.toBeNull();
46
+ expect(slider.type).toBe('range');
47
+ expect(slider.value).toBe('3600');
48
+ const timeInput = container.querySelector('#lt-time-picker-time-input') as HTMLInputElement;
49
+ expect(timeInput.value).toBe('01:00:00');
50
+ });
51
+
52
+ it('emite toggleTimerDropdown al pulsar el botón del picker', async () => {
53
+ const onToggle = vi.fn();
54
+ const { container } = await render(makeHost({ onToggle }));
55
+ await fireEvent.click(container.querySelector('#lt-time-picker-btn') as HTMLButtonElement);
56
+ expect(onToggle).toHaveBeenCalledOnce();
57
+ });
58
+
59
+ it('abierto: muestra las 3 columnas del picker (horas/minutos/segundos) y el botón Save', async () => {
60
+ const { container } = await render(makeHost({ isOpen: true }));
61
+ expect(container.querySelector('#lt-time-picker-hours-selector-container')).not.toBeNull();
62
+ expect(container.querySelector('#lt-time-picker-minutes-selector-container')).not.toBeNull();
63
+ expect(container.querySelector('#lt-time-picker-seconds-selector-container')).not.toBeNull();
64
+ expect(container.querySelector('#lt-time-picker-btn-save')).not.toBeNull();
65
+ });
66
+
67
+ it('elegir una hora y guardar emite timeChanged en segundos', async () => {
68
+ const onTime = vi.fn();
69
+ const onToggle = vi.fn();
70
+ const { container } = await render(makeHost({ isOpen: true, onTime, onToggle }));
71
+ // Selecciona 02 horas (id `${id}-time-picker-hours-02`).
72
+ await fireEvent.click(container.querySelector('#lt-time-picker-hours-02') as HTMLElement);
73
+ const save = container.querySelector('#lt-time-picker-btn-save') as HTMLButtonElement;
74
+ expect(save.disabled).toBe(false); // cambió respecto del valor inicial
75
+ await fireEvent.click(save);
76
+ // 02:00:00 -> 7200s.
77
+ expect(onTime).toHaveBeenCalledWith(7200);
78
+ expect(onToggle).toHaveBeenCalledOnce();
79
+ });
80
+
81
+ it('mover el slider emite sliderValueChanged en segundos', async () => {
82
+ const onSlider = vi.fn();
83
+ const { container } = await render(makeHost({ onSlider }));
84
+ const slider = container.querySelector('#lt-slider') as HTMLInputElement;
85
+ await fireEvent.input(slider, { target: { value: '7200' } });
86
+ expect(onSlider).toHaveBeenCalledWith(7200);
87
+ });
88
+ });
@@ -0,0 +1,75 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+ import { TimePickerComponent } from './pickers/time-picker.component';
3
+ import { SliderBaseComponent } from './slider-base.component';
4
+
5
+ // Control de lifetime de sesión: un `<ba-time-picker>` `HH:MM:SS` junto a un
6
+ // `<ba-slider-base>`, ambos manejando el mismo valor. El padre guarda el valor
7
+ // en segundos y lo pasa como `sliderValue` (número) y `timeInputValue` (el mismo
8
+ // valor formateado `HH:MM:SS`); tanto `sliderValueChanged` como `timeChanged`
9
+ // reportan de vuelta en segundos, así el padre mantiene ambas vistas en sync.
10
+ // `:host { display: contents }`: el grid raíz es el hijo real del contenedor.
11
+ @Component({
12
+ selector: 'ba-lifetime-slider-base',
13
+ standalone: true,
14
+ changeDetection: ChangeDetectionStrategy.OnPush,
15
+ host: { style: 'display: contents' },
16
+ imports: [TimePickerComponent, SliderBaseComponent],
17
+ template: `
18
+ <div [class]="classNames()">
19
+ <div [class]="timePickerContainerClassNames()">
20
+ <ba-time-picker
21
+ [id]="id() + '-time-picker'"
22
+ size="small"
23
+ [error]="timeInputError()"
24
+ [errorMessage]="timeInputErrorMessage()"
25
+ [isOpen]="isOpen()"
26
+ [min]="min()"
27
+ [max]="max()"
28
+ [btnSaveText]="btnSaveText()"
29
+ [timeInputValue]="timeInputValue()"
30
+ [isInsideModal]="isTimePickerInsideModal()"
31
+ [modalId]="modalId()"
32
+ [dropdownDirection]="timePickerDropdownDirection()"
33
+ (isOpenChange)="isOpenChange.emit($event)"
34
+ (toggleTimerDropdown)="toggleTimerDropdown.emit()"
35
+ (timeChanged)="timeChanged.emit($event)"
36
+ />
37
+ </div>
38
+ <ba-slider-base
39
+ [id]="id() + '-slider'"
40
+ [sliderValue]="sliderValue()"
41
+ [step]="step()"
42
+ [min]="min()"
43
+ [max]="max()"
44
+ [classNames]="sliderContainerClassNames()"
45
+ (sliderValueChanged)="sliderValueChanged.emit($event)"
46
+ />
47
+ </div>
48
+ `,
49
+ })
50
+ export class LifetimeSliderBaseComponent {
51
+ id = input('lifetime-slider');
52
+ sliderValue = input(150);
53
+ step = input(1);
54
+ min = input(0);
55
+ max = input(100);
56
+ btnSaveText = input('Save');
57
+ timeInputError = input(false);
58
+ timeInputErrorMessage = input('');
59
+ timeInputValue = input('08:05:00');
60
+ isOpen = input(false);
61
+ isTimePickerInsideModal = input(false);
62
+ modalId = input('txns-overview');
63
+ timePickerDropdownDirection = input<'up' | 'down'>('down');
64
+ classNames = input(
65
+ 'bas-grid bas-h-auto bas-w-full bas-grid-rows-[auto_auto] bas-items-center bas-gap-2 ' +
66
+ '2xsm1:bas-grid-cols-[152px_1fr] 2xsm1:bas-grid-rows-1 1xl:bas-gap-4 2xl:bas-gap-8 bas-mb-6 bas-mt-2',
67
+ );
68
+ timePickerContainerClassNames = input('bas-flex bas-w-full bas-items-center bas-justify-center');
69
+ sliderContainerClassNames = input('');
70
+
71
+ sliderValueChanged = output<number>();
72
+ timeChanged = output<number>();
73
+ isOpenChange = output<boolean>();
74
+ toggleTimerDropdown = output<void>();
75
+ }
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/angular';
3
+ import { OvalSpinnerComponent } from './oval-spinner.component';
4
+ describe('OvalSpinnerComponent', () => {
5
+ it('renderiza un svg con la clase de spin', async () => {
6
+ const { container } = await render(OvalSpinnerComponent);
7
+ expect(container.querySelector('.bas-oval-spin')).toBeTruthy();
8
+ });
9
+ });