@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,158 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+
3
+ // Sin react-loader-spinner (`<Oval>`)
4
+ // ni `animate.css`: el anillo animado se dibuja con un <svg> propio (pista +
5
+ // arco giratorio con la clase `bas-oval-spin` de transitions.css) y el
6
+ // fade-in de entrada se resuelve con una animación CSS local equivalente a
7
+ // `animate__fadeIn`. `icon` (ReactNode / slot `icon`) pasa a ser
8
+ // `<ng-content select="[slot=icon]" />`.
9
+ const BLOCKAUTH_LOGO = 'https://dashboard.block-auth.io/img/blockauth_logo.png';
10
+
11
+ type OvalSize = 'xs' | 'small' | 'medium' | 'large';
12
+ type OvalPosition =
13
+ | 'center'
14
+ | 'top'
15
+ | 'bottom'
16
+ | 'left'
17
+ | 'right'
18
+ | 'top-left'
19
+ | 'top-right'
20
+ | 'bottom-left'
21
+ | 'bottom-right';
22
+
23
+ @Component({
24
+ selector: 'ba-oval-spinner',
25
+ standalone: true,
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ template: `
28
+ <div
29
+ [class]="
30
+ 'bal-ba-backdrop-blur-base bal-ba-backdrop-blur-xl bas-absolute bas-flex bas-h-full bas-w-full ' +
31
+ (animateBg() ? 'bas-oval-fadein' : '') +
32
+ ' ' +
33
+ positionClasses() +
34
+ ' ' +
35
+ zIndex() +
36
+ ' ' +
37
+ classNames()
38
+ "
39
+ >
40
+ <div
41
+ [class]="'bas-relative bas-flex bas-items-center bas-justify-center ' + positionClassNames()"
42
+ >
43
+ <svg
44
+ class="bas-oval-fadein"
45
+ [attr.width]="ovalSize()"
46
+ [attr.height]="ovalSize()"
47
+ role="img"
48
+ aria-label="oval-loading"
49
+ [attr.viewBox]="'0 0 ' + ovalSize() + ' ' + ovalSize()"
50
+ >
51
+ <circle
52
+ [attr.cx]="ovalSize() / 2"
53
+ [attr.cy]="ovalSize() / 2"
54
+ [attr.r]="radius()"
55
+ fill="none"
56
+ stroke="#93C5FD"
57
+ [attr.stroke-width]="strokeWidth"
58
+ />
59
+ <circle
60
+ class="bas-oval-spin"
61
+ [attr.cx]="ovalSize() / 2"
62
+ [attr.cy]="ovalSize() / 2"
63
+ [attr.r]="radius()"
64
+ fill="none"
65
+ stroke="#00509A"
66
+ [attr.stroke-width]="strokeWidth"
67
+ [attr.stroke-dasharray]="circumference() * 0.25 + ' ' + circumference()"
68
+ stroke-linecap="round"
69
+ style="transform-origin: 50% 50%"
70
+ />
71
+ </svg>
72
+ <div [class]="showLogo() ? 'bas-absolute ' + (customLogoSize() || imageSize()) : 'bas-hidden'">
73
+ @if (!isIcon()) {
74
+ <img
75
+ id="loader-image"
76
+ [src]="imageLogoSrc()"
77
+ class="bas-aspect-square bas-h-full bas-w-full bas-rounded-full"
78
+ [alt]="imageAlt()"
79
+ />
80
+ } @else {
81
+ <ng-content select="[slot=icon]" />
82
+ }
83
+ </div>
84
+ </div>
85
+ </div>
86
+ `,
87
+ styles: [
88
+ `
89
+ @keyframes bas-oval-fadein-kf {
90
+ from {
91
+ opacity: 0;
92
+ }
93
+ to {
94
+ opacity: 1;
95
+ }
96
+ }
97
+ .bas-oval-fadein {
98
+ animation: bas-oval-fadein-kf 0.3s ease-in;
99
+ }
100
+ `,
101
+ ],
102
+ })
103
+ export class OvalSpinnerComponent {
104
+ size = input<OvalSize>('medium');
105
+ customSize = input<number | undefined>(undefined);
106
+ classNames = input('');
107
+ position = input<OvalPosition>('center');
108
+ positionClassNames = input('');
109
+ animateBg = input(true);
110
+ showLogo = input(false);
111
+ logoSize = input<OvalSize>('medium');
112
+ customLogoSize = input<string | undefined>(undefined);
113
+ isIcon = input(false);
114
+ imageLogoSrc = input(BLOCKAUTH_LOGO);
115
+ zIndex = input('bas-z-50');
116
+
117
+ readonly strokeWidth = 2;
118
+
119
+ ovalSize = computed(() => {
120
+ if (this.customSize()) return this.customSize()!;
121
+ if (this.size() === 'xs') return 48;
122
+ if (this.size() === 'small') return 64;
123
+ if (this.size() === 'medium') return 80;
124
+ return 112; // large
125
+ });
126
+
127
+ positionClasses = computed(
128
+ () =>
129
+ ({
130
+ center: 'bas-items-center bas-justify-center',
131
+ top: 'bas-items-start bas-justify-center',
132
+ bottom: 'bas-items-end bas-justify-center',
133
+ left: 'bas-items-center bas-justify-start',
134
+ right: 'bas-items-center bas-justify-end',
135
+ 'top-left': 'bas-items-start bas-justify-start',
136
+ 'top-right': 'bas-items-start bas-justify-end',
137
+ 'bottom-left': 'bas-items-end bas-justify-start',
138
+ 'bottom-right': 'bas-items-end bas-justify-end',
139
+ })[this.position()],
140
+ );
141
+
142
+ imageSize = computed(
143
+ () =>
144
+ ({
145
+ xs: 'bas-h-10 bas-w-10',
146
+ small: 'bas-h-12 bas-w-12',
147
+ medium: 'bas-h-16 bas-w-16',
148
+ large: 'bas-h-20 bas-w-20',
149
+ })[this.logoSize()],
150
+ );
151
+
152
+ imageAlt = computed(() =>
153
+ this.imageLogoSrc() === BLOCKAUTH_LOGO ? 'Block-Auth Logo' : 'Image inside loader',
154
+ );
155
+
156
+ radius = computed(() => this.ovalSize() / 2 - this.strokeWidth);
157
+ circumference = computed(() => 2 * Math.PI * this.radius());
158
+ }
@@ -0,0 +1,220 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ ElementRef,
5
+ PLATFORM_ID,
6
+ computed,
7
+ effect,
8
+ inject,
9
+ input,
10
+ output,
11
+ viewChild,
12
+ } from '@angular/core';
13
+ import { isPlatformBrowser } from '@angular/common';
14
+ import { PickerButtonComponent } from './picker-button.component';
15
+
16
+ // Shell animado de los pickers: el `<ba-picker-button>` más el revelado del
17
+ // cuerpo del dropdown (posicionado en absoluto, por lo que su orden en el DOM
18
+ // respecto del botón es irrelevante — se renderiza el botón UNA sola vez y el
19
+ // dropdown se coloca arriba/abajo según `dropdownDirection`).
20
+ //
21
+ // - La animación (framer-motion en react / `<Transition clip-path>` en vue)
22
+ // pasa a una animación CSS `.ba-clip-down`/`.ba-clip-up` (revelado circular).
23
+ // - `icon` (ReactNode / slot #icon) se reenvía por `[slot=icon]` a
24
+ // `<ba-picker-button>` (vía `ngProjectAs`); los hijos van al slot por defecto.
25
+ // - `setIsOpen`/`toggleDropdown`/`clickedOutside` -> outputs `isOpenChange`/
26
+ // `toggleDropdown`/`clickOutside`.
27
+ // - Click-outside anclado a `document` (o al elemento del modal cuando
28
+ // `isInsideModal`); NO se retira el listener del modal (al desmontar ya no
29
+ // existe — mismo leak deliberado que el original). Se replica con `effect()`
30
+ // sobre `isOpen()`, sólo en navegador (SSR-safe).
31
+ @Component({
32
+ selector: 'ba-dropdown-picker-animated-base',
33
+ standalone: true,
34
+ changeDetection: ChangeDetectionStrategy.OnPush,
35
+ host: { style: 'display: contents' },
36
+ imports: [PickerButtonComponent],
37
+ template: `
38
+ <div
39
+ [class]="
40
+ displayClassNames() + ' bas-w-full bas-items-center bas-gap-1.5 ' +
41
+ getHeightAndGridClassNames(size(), enableError(), enableWhiteSpaces()) + ' ' + classNames()
42
+ "
43
+ [style.width]="widthStyle()"
44
+ >
45
+ @if (enableError() && enableWhiteSpaces()) {
46
+ <p
47
+ [class]="
48
+ 'bas-m-0 bas-h-[18px] ' + (error() ? 'bal-ba-error-text' : 'bal-ba-error-text-invisible')
49
+ "
50
+ >{{ errorMessage() }}</p>
51
+ }
52
+ <div #internalContainer class="bas-relative bas-h-full bas-w-full">
53
+ <section
54
+ [class]="
55
+ 'bal-ba-transition-2 bas-relative bas-flex bas-w-full bas-flex-col bas-items-center ' +
56
+ 'bas-justify-start ' + getHeightAndGridClassNames(size(), false, false)
57
+ "
58
+ >
59
+ <ba-picker-button
60
+ [size]="size()"
61
+ [isOpen]="isOpen()"
62
+ [blueStyles]="blueStyles()"
63
+ [btnId]="btnId()"
64
+ [inputId]="inputId()"
65
+ [inputValue]="inputValue()"
66
+ [btnZIndexClassNames]="btnZIndexClassNames()"
67
+ (toggleDropdown)="toggleDropdown.emit()"
68
+ >
69
+ <ng-container ngProjectAs="[slot=icon]"><ng-content select="[slot=icon]" /></ng-container>
70
+ </ba-picker-button>
71
+
72
+ @if (activeDropdown() && isOpen()) {
73
+ <div [class]="dropdownClasses()">
74
+ <ng-content />
75
+ </div>
76
+ }
77
+ </section>
78
+ </div>
79
+ @if (enableError()) {
80
+ <p class="bal-ba-error-text-invisible bas-m-0 bas-h-[18px]">white space</p>
81
+ }
82
+ </div>
83
+ `,
84
+ styles: [
85
+ `
86
+ .ba-clip-down {
87
+ animation: ba-clip-down-in 0.2s ease;
88
+ transform-origin: top center;
89
+ }
90
+ .ba-clip-up {
91
+ animation: ba-clip-up-in 0.2s ease;
92
+ transform-origin: bottom center;
93
+ }
94
+ @keyframes ba-clip-down-in {
95
+ from { clip-path: circle(0% at 50% 0%); opacity: 0.4; }
96
+ to { clip-path: circle(150% at 50% 0%); opacity: 1; }
97
+ }
98
+ @keyframes ba-clip-up-in {
99
+ from { clip-path: circle(0% at 50% 100%); opacity: 0.4; }
100
+ to { clip-path: circle(150% at 50% 100%); opacity: 1; }
101
+ }
102
+ `,
103
+ ],
104
+ })
105
+ export class DropdownPickerAnimatedBaseComponent {
106
+ size = input<'xxs' | 'xs' | 'small' | 'base' | 'large'>('base');
107
+ isOpen = input(false);
108
+ closeWhenClickedOutside = input(true);
109
+ enableError = input(true);
110
+ enableWhiteSpaces = input(true);
111
+ error = input(false);
112
+ errorMessage = input('');
113
+ btnId = input('');
114
+ inputId = input('');
115
+ inputValue = input('');
116
+ container = input<HTMLElement | null>(null);
117
+ exceptions = input<(HTMLElement | null)[]>([]);
118
+ blueStyles = input(true);
119
+ activeDropdown = input(true);
120
+ width = input<number | string>('100%');
121
+ classNames = input('');
122
+ displayClassNames = input('bas-grid');
123
+ btnZIndexClassNames = input('bas-z-0');
124
+ dropdownZIndexClassNames = input('bas-z-50');
125
+ isInsideModal = input(false);
126
+ modalId = input('txns-overview');
127
+ dropdownDirection = input<'up' | 'down'>('down');
128
+
129
+ isOpenChange = output<boolean>();
130
+ toggleDropdown = output<void>();
131
+ clickOutside = output<EventTarget | null>();
132
+
133
+ private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
134
+ private readonly internalContainer =
135
+ viewChild<ElementRef<HTMLDivElement>>('internalContainer');
136
+
137
+ protected readonly widthStyle = computed(() => {
138
+ const w = this.width();
139
+ return typeof w === 'number' ? `${w}px` : w;
140
+ });
141
+
142
+ protected readonly dropdownClasses = computed(() => {
143
+ const down = this.dropdownDirection() === 'down';
144
+ return (
145
+ 'bal-ba-transition-2 bal-ba-shadow-base bas-w-full bas-min-w-[152px] ' +
146
+ 'bas-rounded-md bas-border bas-border-solid bas-border-[#a9d6e5] ' +
147
+ 'bas-bg-white hover:bas-border-[#98c0ce] ' +
148
+ this.dropdownZIndexClassNames() +
149
+ ' ' +
150
+ (down
151
+ ? 'ba-clip-down bal-ba-hover-shadow-md bas-absolute bas-top-full bas-mt-2'
152
+ : 'ba-clip-up bal-ba-hover-shadow-top-md bas-absolute bas-mb-2 bas-bottom-full')
153
+ );
154
+ });
155
+
156
+ private readonly handleClickOutsideTimer = (e: Event): void => {
157
+ if (!this.isOpen()) return;
158
+ const target = e.target as HTMLElement;
159
+ const activeException = this.exceptions().some((el) => el && el.id === target.id);
160
+ if (activeException) return;
161
+ const el = this.internalContainer()?.nativeElement;
162
+ if (el && !el.contains(target)) {
163
+ if (this.closeWhenClickedOutside()) this.isOpenChange.emit(false);
164
+ else this.clickOutside.emit(target);
165
+ }
166
+ };
167
+
168
+ constructor() {
169
+ // Port 1:1 del useEffect([isOpen]) de DropdownPickerAnimatedBase.tsx.
170
+ effect((onCleanup) => {
171
+ if (!this.isBrowser || !this.activeDropdown()) return;
172
+ const open = this.isOpen();
173
+ const insideModal = this.isInsideModal();
174
+ const removeListener = (): void => {
175
+ if (!insideModal) document.removeEventListener('click', this.handleClickOutsideTimer);
176
+ };
177
+ if (open) {
178
+ if (!insideModal) {
179
+ document.addEventListener('click', this.handleClickOutsideTimer);
180
+ } else {
181
+ const modal = document.getElementById(this.modalId());
182
+ if (modal) modal.addEventListener('click', this.handleClickOutsideTimer);
183
+ }
184
+ } else {
185
+ removeListener();
186
+ }
187
+ onCleanup(removeListener);
188
+ });
189
+ }
190
+
191
+ getHeightAndGridClassNames(size: string, enableError: boolean, enableWhiteSpaces: boolean): string {
192
+ return {
193
+ xxs: enableError
194
+ ? enableWhiteSpaces
195
+ ? 'bas-h-[82px] bas-grid-rows-[18px_34px_18px]'
196
+ : 'bas-h-[58px] bas-grid-rows-[34px_18px]'
197
+ : 'bas-h-[34px]',
198
+ xs: enableError
199
+ ? enableWhiteSpaces
200
+ ? 'bas-h-[86px] bas-grid-rows-[18px_38px_18px]'
201
+ : 'bas-h-[62px] bas-grid-rows-[38px_18px]'
202
+ : 'bas-h-[38px]',
203
+ small: enableError
204
+ ? enableWhiteSpaces
205
+ ? 'bas-h-[90px] bas-grid-rows-[18px_42px_18px]'
206
+ : 'bas-h-[66px] bas-grid-rows-[42px_18px]'
207
+ : 'bas-h-[42px]',
208
+ base: enableError
209
+ ? enableWhiteSpaces
210
+ ? 'bas-h-[94px] bas-grid-rows-[18px_46px_18px]'
211
+ : 'bas-h-[70px] bas-grid-rows-[46px_18px]'
212
+ : 'bas-h-[46px]',
213
+ large: enableError
214
+ ? enableWhiteSpaces
215
+ ? 'bas-h-[102px] bas-grid-rows-[18px_54px_18px]'
216
+ : 'bas-h-[78px] bas-grid-rows-[54px_18px]'
217
+ : 'bas-h-[54px]',
218
+ }[size]!;
219
+ }
220
+ }
@@ -0,0 +1,74 @@
1
+ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
2
+
3
+ // Input de solo-lectura + icono al final que alterna la apertura del dropdown
4
+ // del picker. El icono llega por el slot `[slot=icon]` (en react era la prop
5
+ // `icon?: ReactNode`). `toggleDropdown` (callback prop) -> output `toggleDropdown`.
6
+ // `:host { display: contents }`: el `<button>` es el hijo real del flex/grid del
7
+ // contenedor del picker (como en react/vue).
8
+ @Component({
9
+ selector: 'ba-picker-button',
10
+ standalone: true,
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ host: { style: 'display: contents' },
13
+ template: `
14
+ <button
15
+ type="button"
16
+ [id]="btnId()"
17
+ [class]="
18
+ 'bal-ba-outline-none bal-ba-transition-150 bas-grid bas-h-full ' +
19
+ 'bas-w-full bas-cursor-pointer bas-grid-cols-[1fr_28px] bas-items-center ' +
20
+ 'bas-rounded-lg bas-border-2 bas-border-solid ' + bgColor() + ' ' +
21
+ getHeightClassNames(size()) + ' ' + btnZIndexClassNames() + ' ' +
22
+ (blueStyles() || isOpen()
23
+ ? 'bas-border-[var(--bal-primary-100)] hover:bas-border-[var(--bal-primary-300)]'
24
+ : 'bas-border-gray-300 hover:bas-border-slate-400')
25
+ "
26
+ (click)="onBtnClicked($event)"
27
+ >
28
+ <input
29
+ [id]="inputId()"
30
+ [class]="
31
+ 'bal-ba-outline-none bal-ba-transition-150 bal-ba-shadow-base bal-ba-focus-shadow-none ' +
32
+ 'bas-box-border bas-h-full bas-w-full bas-cursor-pointer bas-border-none bas-px-0 ' +
33
+ 'bas-py-1 bas-text-start bas-font-normal xsm1:bas-pl-2 ' +
34
+ (size() !== 'xxs' ? 'bas-text-lg' : 'bas-text-sm') + ' ' +
35
+ (blueStyles() || isOpen()
36
+ ? 'bas-text-[#004e98] hover:bas-text-[#00296b]'
37
+ : 'bas-text-gray-500 hover:bas-text-slate-600') + ' ' + bgColor()
38
+ "
39
+ [value]="inputValue()"
40
+ readonly
41
+ />
42
+ <span class="bas-flex bas-h-full bas-w-full bas-items-center bas-justify-center bas-rounded-full">
43
+ <ng-content select="[slot=icon]" />
44
+ </span>
45
+ </button>
46
+ `,
47
+ })
48
+ export class PickerButtonComponent {
49
+ size = input<'xxs' | 'xs' | 'small' | 'base' | 'large'>('base');
50
+ isOpen = input(false);
51
+ blueStyles = input(false);
52
+ btnId = input('picker-btn-id');
53
+ inputId = input('picker-btn-id');
54
+ inputValue = input('Picker Value');
55
+ bgColor = input('bas-bg-transparent');
56
+ btnZIndexClassNames = input('bas-z-0');
57
+
58
+ toggleDropdown = output<void>();
59
+
60
+ onBtnClicked(e: MouseEvent): void {
61
+ e.preventDefault();
62
+ this.toggleDropdown.emit();
63
+ }
64
+
65
+ getHeightClassNames(size: string): string {
66
+ return {
67
+ xxs: 'bas-h-[34px] bas-px-1.5 bas-pb-0.5 bas-pt-0.5',
68
+ xs: 'bas-h-[38px] bas-px-1.5 bas-pb-0.5 bas-pt-1',
69
+ small: 'bas-h-[42px] bas-px-1.5 bas-pb-1 bas-pt-1.5',
70
+ base: 'bas-h-[46px] bas-px-1.5 bas-pb-1.5 bas-pt-2',
71
+ large: 'bas-h-[54px] bas-px-1.5 bas-pb-2.5 bas-pt-3',
72
+ }[size]!;
73
+ }
74
+ }
@@ -0,0 +1,188 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ effect,
5
+ input,
6
+ output,
7
+ signal,
8
+ untracked,
9
+ } from '@angular/core';
10
+ import { getSecFromTimeValue } from '../helpers';
11
+ import { TimeUnitSelectorComponent } from './time-unit-selector.component';
12
+
13
+ export interface TimeArrays {
14
+ minRange: string[];
15
+ defaultRange: string[];
16
+ maxRange: string[];
17
+ }
18
+
19
+ // Selector de tiempo de tres columnas (horas/minutos/segundos) dentro del
20
+ // dropdown del picker, más el botón Guardar. `onChangeTimeUnit` reconcilia los
21
+ // rangos min/max a medida que el usuario cambia cada unidad (máximo total
22
+ // 29100s → 08:05:00); Guardar confía el tiempo elegido al padre como segundos
23
+ // totales vía el output `timeChanged`. Los `useState` de react -> signals; el
24
+ // `useEffect([defaultValues])` (reset) -> effect() que sólo trackea
25
+ // `defaultValues`. `:host { display: contents }`: el grid interno es el hijo
26
+ // real del contenedor del dropdown.
27
+ const range = (length: number, offset = 0): string[] =>
28
+ Array.from({ length }, (_, i) => String(i + offset).padStart(2, '0'));
29
+
30
+ const initialStates = {
31
+ hoursArray: range(8, 1),
32
+ minutesArrays: { minRange: range(50, 10), defaultRange: range(60), maxRange: range(41) },
33
+ secondsArrays: { minRange: range(50, 10), defaultRange: range(60), maxRange: range(1) },
34
+ };
35
+
36
+ @Component({
37
+ selector: 'ba-time-picker-dropdown-selector',
38
+ standalone: true,
39
+ changeDetection: ChangeDetectionStrategy.OnPush,
40
+ host: { style: 'display: contents' },
41
+ imports: [TimeUnitSelectorComponent],
42
+ template: `
43
+ <div class="bas-grid bas-grid-rows-[232px_32px] bas-justify-center bas-gap-1 bas-py-1.5 bas-pl-2.5 bas-pr-1">
44
+ <div class="bas-grid bas-h-full bas-w-[136px] bas-grid-cols-3 bas-gap-0.5">
45
+ <ba-time-unit-selector
46
+ [id]="id()"
47
+ [timeUnitArray]="hoursArray()"
48
+ timeUnit="hours"
49
+ [selectedValue]="selectedValues()[0]"
50
+ [firstValue]="parseIntSafe(hoursArray()[0])"
51
+ (changeTimeUnit)="onChangeTimeUnit($event.timeUnit, $event.value)"
52
+ />
53
+ <ba-time-unit-selector
54
+ [id]="id()"
55
+ [timeUnitArray]="minutesArray()"
56
+ timeUnit="minutes"
57
+ [selectedValue]="selectedValues()[1]"
58
+ [firstValue]="parseIntSafe(minutesArray()[0])"
59
+ (changeTimeUnit)="onChangeTimeUnit($event.timeUnit, $event.value)"
60
+ />
61
+ <ba-time-unit-selector
62
+ [id]="id()"
63
+ [timeUnitArray]="secondsArray()"
64
+ timeUnit="seconds"
65
+ [selectedValue]="selectedValues()[2]"
66
+ [firstValue]="parseIntSafe(secondsArray()[0])"
67
+ (changeTimeUnit)="onChangeTimeUnit($event.timeUnit, $event.value)"
68
+ />
69
+ </div>
70
+ <div class="bas-flex bas-items-center bas-justify-end bas-px-1.5">
71
+ <button
72
+ type="button"
73
+ [id]="id() + '-btn-save'"
74
+ class="bal-ba-transition-2 bas-cursor-pointer bas-rounded-md
75
+ bas-border-none bas-bg-[var(--bal-primary-300)] bas-px-1.5 bas-py-1
76
+ bas-text-sm bas-text-white hover:bas-bg-[#4d849b]
77
+ disabled:bas-cursor-not-allowed disabled:bas-opacity-50"
78
+ [disabled]="isBtnSaveDisabled()"
79
+ (click)="onSaveTimeBtnClicked()"
80
+ >
81
+ {{ btnSaveText() }}
82
+ </button>
83
+ </div>
84
+ </div>
85
+ `,
86
+ })
87
+ export class TimePickerDropdownSelectorComponent {
88
+ id = input('time-picker');
89
+ hoursArray = input<string[]>(initialStates.hoursArray);
90
+ minutesArrays = input<TimeArrays>(initialStates.minutesArrays);
91
+ secondsArrays = input<TimeArrays>(initialStates.secondsArrays);
92
+ defaultValues = input('08:05:00');
93
+ btnSaveText = input('Save');
94
+
95
+ timeChanged = output<number>();
96
+ toggleTimerDropdown = output<void>();
97
+
98
+ protected readonly minutesArray = signal<string[]>(initialStates.minutesArrays.defaultRange);
99
+ protected readonly secondsArray = signal<string[]>(initialStates.secondsArrays.defaultRange);
100
+ protected readonly selectedValues = signal<string[]>('08:05:00'.split(':'));
101
+ protected readonly isBtnSaveDisabled = signal(true);
102
+ private initialValues: string[] = '08:05:00'.split(':');
103
+
104
+ constructor() {
105
+ // Reset: sólo trackea defaultValues;
106
+ // el resto se lee sin tracking para no re-disparar en cascada.
107
+ effect(() => {
108
+ const defaultValues = this.defaultValues();
109
+ untracked(() => {
110
+ const splitted = defaultValues.split(':');
111
+ this.initialValues = [...splitted];
112
+ this.selectedValues.set([...splitted]);
113
+ this.isBtnSaveDisabled.set(true);
114
+ this.onChangeTimeUnit('hours', splitted[0], [...splitted], true);
115
+ });
116
+ });
117
+ }
118
+
119
+ parseIntSafe(v: string | undefined): number {
120
+ return parseInt(v ?? '0', 10) || 0;
121
+ }
122
+
123
+ onChangeTimeUnit(
124
+ timeUnit: string,
125
+ newValue: string,
126
+ timeArrayValues: string[] = this.selectedValues(),
127
+ isDefaultTime = false,
128
+ ): void {
129
+ const timeArray = [...timeArrayValues];
130
+ const hoursArray = this.hoursArray();
131
+ if (timeUnit === 'hours' || timeUnit === 'minutes') {
132
+ const minMinutesArray = this.minutesArrays().minRange;
133
+ const defaultMinutesArray = this.minutesArrays().defaultRange;
134
+ const maxMinutesArray = this.minutesArrays().maxRange;
135
+ const minSecondsArray = this.secondsArrays().minRange;
136
+ const defaultSecondsArray = this.secondsArrays().defaultRange;
137
+ const maxSecondsArray = this.secondsArrays().maxRange;
138
+ let newMinutesArray = defaultMinutesArray;
139
+ let newSecondsArray = defaultSecondsArray;
140
+ if (timeUnit === 'hours') timeArray[0] = newValue;
141
+ else timeArray[1] = newValue;
142
+ if (timeArray[0] === hoursArray[hoursArray.length - 1]) {
143
+ newMinutesArray = maxMinutesArray;
144
+ if (
145
+ !maxMinutesArray.includes(timeArray[1]) ||
146
+ timeArray[1] === maxMinutesArray[maxMinutesArray.length - 1]
147
+ ) {
148
+ if (!maxMinutesArray.includes(timeArray[1])) {
149
+ timeArray[1] = maxMinutesArray[maxMinutesArray.length - 1];
150
+ }
151
+ newSecondsArray = maxSecondsArray;
152
+ if (!maxSecondsArray.includes(timeArray[2])) {
153
+ timeArray[2] = maxSecondsArray[maxSecondsArray.length - 1];
154
+ }
155
+ }
156
+ } else if (timeArray[0] === hoursArray[0]) {
157
+ newMinutesArray = minMinutesArray;
158
+ if (!minMinutesArray.includes(timeArray[1]) || timeArray[1] === minMinutesArray[0]) {
159
+ if (!minMinutesArray.includes(timeArray[1])) {
160
+ timeArray[1] = minMinutesArray[0];
161
+ }
162
+ newSecondsArray = minSecondsArray;
163
+ if (!minSecondsArray.includes(timeArray[2])) {
164
+ timeArray[2] = minSecondsArray[0];
165
+ }
166
+ }
167
+ }
168
+ this.minutesArray.set([...newMinutesArray]);
169
+ this.secondsArray.set([...newSecondsArray]);
170
+ } else timeArray[2] = newValue;
171
+
172
+ this.selectedValues.set([...timeArray]);
173
+
174
+ if (isDefaultTime) this.isBtnSaveDisabled.set(true);
175
+ else {
176
+ this.isBtnSaveDisabled.set(
177
+ timeArray.every((timeValue, index) => timeValue === this.initialValues[index]),
178
+ );
179
+ }
180
+ }
181
+
182
+ onSaveTimeBtnClicked(): void {
183
+ this.initialValues = this.selectedValues();
184
+ const newTimeValue = this.selectedValues().join(':');
185
+ this.timeChanged.emit(getSecFromTimeValue(newTimeValue));
186
+ this.toggleTimerDropdown.emit();
187
+ }
188
+ }