tailjng 0.1.5 → 0.1.7

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 (176) hide show
  1. package/README.md +12 -4
  2. package/cli/execute/init-app.js +5 -3
  3. package/cli/execute/sync-app.js +14 -2
  4. package/cli/settings/colors-config-utils.js +43 -8
  5. package/cli/settings/icons-config-utils.js +62 -0
  6. package/cli/settings/path-utils.js +32 -2
  7. package/cli/settings/project-utils.js +7 -1
  8. package/cli/templates/app.generator.js +2 -2
  9. package/fesm2022/tailjng.mjs +247 -80
  10. package/fesm2022/tailjng.mjs.map +1 -1
  11. package/lib/services/static/theme.service.d.ts +39 -1
  12. package/lib/utils/theme/theme-variables.util.d.ts +31 -0
  13. package/package.json +1 -1
  14. package/public-api.d.ts +2 -1
  15. package/registry/components.json +41 -18
  16. package/src/colors.safelist.css +2 -2
  17. package/src/lib/components/.config/README.md +11 -0
  18. package/src/lib/components/.config/colors/README.md +38 -0
  19. package/src/lib/components/{colors-config → .config/colors}/colors.config.ts +5 -5
  20. package/src/lib/components/{colors-config → .config/colors}/colors.safelist.css +2 -2
  21. package/src/lib/components/.config/icons/README.md +26 -0
  22. package/src/lib/components/.config/icons/icons.lucide.ts +134 -0
  23. package/src/lib/components/.config/input/README.md +24 -0
  24. package/src/lib/components/.config/input/input.classes.ts +119 -0
  25. package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +244 -2
  26. package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +25 -38
  27. package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +66 -56
  28. package/src/lib/components/alert/alert-dialog/dialog-alert.types.ts +19 -0
  29. package/src/lib/components/alert/alert-toast/toast-alert.component.css +630 -12
  30. package/src/lib/components/alert/alert-toast/toast-alert.component.html +103 -102
  31. package/src/lib/components/alert/alert-toast/toast-alert.component.ts +485 -128
  32. package/src/lib/components/alert/alert-toast/toast-alert.types.ts +25 -0
  33. package/src/lib/components/badge/badge.component.html +34 -21
  34. package/src/lib/components/badge/badge.component.ts +140 -31
  35. package/src/lib/components/button/button.component.html +16 -10
  36. package/src/lib/components/button/button.component.ts +162 -22
  37. package/src/lib/components/card/card-complete/complete-card.component.html +2 -2
  38. package/src/lib/components/card/card-complete/complete-card.component.ts +26 -16
  39. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +2 -2
  40. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +26 -16
  41. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.css +97 -0
  42. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +54 -46
  43. package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.ts +135 -64
  44. package/src/lib/components/checkbox/checkbox-input/input-checkbox.types.ts +3 -0
  45. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.css +112 -0
  46. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +28 -25
  47. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +67 -15
  48. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.types.ts +1 -0
  49. package/src/lib/components/coach-mark/coach-mark.component.html +4 -22
  50. package/src/lib/components/coach-mark/coach-mark.component.scss +1 -1
  51. package/src/lib/components/coach-mark/coach-mark.component.ts +51 -18
  52. package/src/lib/components/coach-mark/coach-mark.directive.ts +133 -78
  53. package/src/lib/components/coach-mark/coach-mark.types.ts +12 -0
  54. package/src/lib/components/dialog/dialog.component.css +103 -1
  55. package/src/lib/components/dialog/dialog.component.html +46 -66
  56. package/src/lib/components/dialog/dialog.component.ts +136 -110
  57. package/src/lib/components/dialog/dialog.types.ts +19 -0
  58. package/src/lib/components/filter/filter-complete/complete-filter.component.html +16 -19
  59. package/src/lib/components/filter/filter-complete/complete-filter.component.scss +35 -0
  60. package/src/lib/components/filter/filter-complete/complete-filter.component.ts +58 -34
  61. package/src/lib/components/filter/filter-complete/complete-filter.types.ts +7 -0
  62. package/src/lib/components/filter/filter-complete/complete-filter.util.ts +16 -0
  63. package/src/lib/components/form/form-container/container-form.component.css +4 -0
  64. package/src/lib/components/form/form-container/container-form.component.html +2 -2
  65. package/src/lib/components/form/form-container/container-form.component.ts +72 -16
  66. package/src/lib/components/form/form-container/container-form.types.ts +42 -0
  67. package/src/lib/components/form/form-container/form-col-span.directive.ts +25 -0
  68. package/src/lib/components/form/form-sidebar/sidebar-form.component.css +276 -0
  69. package/src/lib/components/form/form-sidebar/sidebar-form.component.html +117 -125
  70. package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +109 -34
  71. package/src/lib/components/form/form-sidebar/sidebar-form.types.ts +3 -0
  72. package/src/lib/components/{toggle-radio/toggle-radio.component.css → form/form-validation/validation-form.component.css} +0 -1
  73. package/src/lib/components/form/form-validation/validation-form.component.html +10 -6
  74. package/src/lib/components/form/form-validation/validation-form.component.ts +99 -12
  75. package/src/lib/components/form/form-validation/validation-form.types.ts +33 -0
  76. package/src/lib/components/icon/icon.component.html +8 -5
  77. package/src/lib/components/icon/icon.component.ts +111 -9
  78. package/src/lib/components/input/input/input.component.html +19 -16
  79. package/src/lib/components/input/input/input.component.ts +130 -53
  80. package/src/lib/components/input/input/input.types.ts +8 -0
  81. package/src/lib/components/input/input-file/file-input.component.html +65 -56
  82. package/src/lib/components/input/input-file/file-input.component.ts +276 -173
  83. package/src/lib/components/input/input-file/file-input.types.ts +2 -0
  84. package/src/lib/components/input/input-range/range-input.component.css +67 -0
  85. package/src/lib/components/input/input-range/range-input.component.html +50 -58
  86. package/src/lib/components/input/input-range/range-input.component.ts +148 -60
  87. package/src/lib/components/input/input-range/range-input.types.ts +7 -0
  88. package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
  89. package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
  90. package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
  91. package/src/lib/components/label/label.component.html +17 -16
  92. package/src/lib/components/label/label.component.ts +70 -16
  93. package/src/lib/components/label/label.types.ts +2 -0
  94. package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
  95. package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
  96. package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
  97. package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
  98. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
  99. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
  100. package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
  101. package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
  102. package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
  103. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
  104. package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
  105. package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
  106. package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
  107. package/src/lib/components/progress-bar/progress-bar.component.css +11 -0
  108. package/src/lib/components/progress-bar/progress-bar.component.html +41 -40
  109. package/src/lib/components/progress-bar/progress-bar.component.ts +95 -11
  110. package/src/lib/components/progress-bar/progress-bar.types.ts +2 -0
  111. package/src/lib/components/select/select-dropdown/dropdown-select.component.css +6 -0
  112. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +54 -44
  113. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
  114. package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
  115. package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
  116. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +6 -0
  117. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
  118. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
  119. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
  120. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
  121. package/src/lib/components/select/select-multi-table/multi-table-select.component.css +10 -0
  122. package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
  123. package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
  124. package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
  125. package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
  126. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +212 -0
  127. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +62 -53
  128. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
  129. package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
  130. package/src/lib/components/table/table-complete/complete-table.component.html +15 -17
  131. package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
  132. package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
  133. package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
  134. package/src/lib/components/table/table-complete/index.ts +2 -0
  135. package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
  136. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +73 -128
  137. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +542 -829
  138. package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
  139. package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
  140. package/src/lib/components/table/table-crud-complete/index.ts +3 -0
  141. package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
  142. package/src/lib/components/theme-generator/theme-generator.component.html +146 -116
  143. package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
  144. package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
  145. package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +44 -0
  146. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +135 -0
  147. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +52 -0
  148. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +198 -0
  149. package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
  150. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +108 -0
  151. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +37 -0
  152. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +193 -0
  153. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
  154. package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
  155. package/src/lib/components/tooltip/tooltip.service.ts +331 -133
  156. package/src/lib/components/tooltip/tooltip.types.ts +9 -0
  157. package/src/lib/components/viewer/viewer-image/image-viewer.component.css +90 -4
  158. package/src/lib/components/viewer/viewer-image/image-viewer.component.html +52 -103
  159. package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
  160. package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
  161. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +177 -0
  162. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +74 -24
  163. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
  164. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
  165. package/src/styles.css +2 -2
  166. package/lib/services/static/icons.service.d.ts +0 -65
  167. package/src/lib/components/colors-config/README.md +0 -38
  168. package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
  169. package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
  170. package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
  171. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
  172. package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
  173. package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
  174. package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
  175. package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
  176. package/tailjng-0.1.5.tgz +0 -0
@@ -1,201 +1,558 @@
1
- import { Component, computed, inject, Input } from '@angular/core';
2
- import { trigger, transition, style, animate } from "@angular/animations";
3
- import { NgClass } from '@angular/common';
4
- import { LucideAngularModule } from 'lucide-angular';
1
+ import {
2
+ Component,
3
+ computed,
4
+ effect,
5
+ ElementRef,
6
+ inject,
7
+ Input,
8
+ ChangeDetectorRef,
9
+ ViewChild,
10
+ } from '@angular/core';
11
+ import { trigger, transition, style, animate } from '@angular/animations';
12
+ import { NgClass, NgStyle } from '@angular/common';
13
+ import { JAlertToastService, JColorsService, Toast } from 'tailjng';
5
14
  import { JButtonComponent } from '../../button/button.component';
6
- import { JIconsService, JAlertToastService, JColorsService } from 'tailjng';
7
-
15
+ import { JIconComponent } from '../../icon/icon.component';
16
+ import { Icons } from '../../.config/icons/icons.lucide';
17
+ import {
18
+ AlertToastPosition,
19
+ AlertToastType,
20
+ ALERT_TOAST_TYPE_ICONS,
21
+ } from './toast-alert.types';
22
+
23
+ export type { AlertToastPosition, AlertToastType } from './toast-alert.types';
24
+
25
+ /**
26
+ * Global toast notification host — mount once (e.g. app root) and call `JAlertToastService.AlertToast()`.
27
+ *
28
+ * Install: `npx tailjng add alert-toast`
29
+ */
8
30
  @Component({
9
31
  selector: 'JAlertToast',
10
- imports: [LucideAngularModule, NgClass, JButtonComponent],
32
+ imports: [NgClass, NgStyle, JButtonComponent, JIconComponent],
11
33
  templateUrl: './toast-alert.component.html',
12
34
  styleUrl: './toast-alert.component.css',
13
35
  animations: [
14
36
  trigger('toastTransition', [
15
37
  transition(':enter', [
16
38
  style({ opacity: 0, transform: 'translateX(1.25rem) scale(0.98)' }),
17
- animate('320ms cubic-bezier(0.16, 1, 0.3, 1)', style({ opacity: 1, transform: 'translateX(0) scale(1)' })),
39
+ animate(
40
+ '320ms cubic-bezier(0.16, 1, 0.3, 1)',
41
+ style({ opacity: 1, transform: 'translateX(0) scale(1)' }),
42
+ ),
18
43
  ]),
19
44
  transition(':leave', [
20
- animate('180ms cubic-bezier(0.4, 0, 1, 1)', style({ opacity: 0, transform: 'translateX(0.75rem) scale(0.98)' })),
45
+ animate(
46
+ '180ms cubic-bezier(0.4, 0, 1, 1)',
47
+ style({ opacity: 0, transform: 'translateX(0.75rem) scale(0.98)' }),
48
+ ),
21
49
  ]),
22
50
  ]),
23
51
  ],
24
52
  })
25
53
  export class JAlertToastComponent {
26
-
27
- @Input() monocromatic: boolean = false;
28
- @Input() position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'bottom-right';
54
+ readonly Icons = Icons;
55
+
56
+ @ViewChild('stackInner') stackInner?: ElementRef<HTMLElement>;
29
57
 
30
58
  private readonly alertToastService = inject(JAlertToastService);
59
+ private readonly colorsService = inject(JColorsService);
60
+ private readonly cdr = inject(ChangeDetectorRef);
61
+ private readonly defaultScrollGutterPx = 2;
62
+
63
+ @Input() monocromatic = false;
64
+ @Input() position: AlertToastPosition = 'bottom-right';
65
+ /** Collapse multiple toasts into a peeking stack; hover expands with scroll. */
66
+ @Input() stackable = true;
67
+ /** How many recent toasts peek in the collapsed pile (rest hidden until expand). */
68
+ @Input() collapsedStackLimit = 4;
69
+
70
+ expanded = false;
71
+ scrollReady = false;
72
+ scrollActive = false;
73
+ hasScrollOverflow = false;
74
+ scrollGutterPx = 0;
75
+ scrollEdgeFade = { top: false, bottom: false };
76
+ private progressLayoutKey = 0;
77
+ private readonly progressStyleCache = new Map<string, Record<string, string>>();
78
+ private expandMeasureTimer?: ReturnType<typeof setTimeout>;
79
+ private scrollActiveTimer?: ReturnType<typeof setTimeout>;
80
+ private scrollRefreshTimer?: ReturnType<typeof setTimeout>;
81
+ private scrollGutterRafId?: number;
82
+
83
+ readonly toasts = computed(() => this.alertToastService.toasts());
84
+
85
+ constructor() {
86
+ effect(() => {
87
+ this.toasts();
88
+ if (this.expanded && this.scrollReady) {
89
+ this.scheduleScrollStateRefresh();
90
+ }
91
+ });
92
+ }
31
93
 
32
- constructor(
33
- private readonly colorsService: JColorsService,
34
- public readonly iconsService: JIconsService
35
- ) { }
94
+ handleAction(toastId: string, action: 'action' | 'cancel'): void {
95
+ this.alertToastService.executeToastAction(toastId, action);
96
+ }
36
97
 
37
- toasts = computed(() => this.alertToastService.toasts());
98
+ closeToast(toastId: string): void {
99
+ this.alertToastService.closeToastById(toastId);
100
+ }
38
101
 
102
+ onStackEnter(): void {
103
+ if (!this.stackable || this.toasts().length <= 1) {
104
+ return;
105
+ }
39
106
 
40
- /**
41
- * Get the icon for a specific toast type.
42
- * @param type The type of the toast.
43
- * @returns The icon name for the toast type.
44
- */
45
- getIcon(type: string) {
46
- return this.iconsService.icons[type as keyof typeof this.iconsService.icons] || this.iconsService.icons.info;
107
+ this.expanded = true;
108
+ this.scrollReady = false;
109
+ this.scrollActive = false;
110
+ this.hasScrollOverflow = false;
111
+ this.scrollGutterPx = 0;
112
+ this.clearScrollActiveTimer();
113
+ this.scrollEdgeFade = { top: false, bottom: false };
114
+ this.syncProgressStyles();
115
+ this.scheduleExpandMeasure();
47
116
  }
48
117
 
118
+ onStackLeave(): void {
119
+ this.clearExpandMeasureTimer();
120
+ this.clearScrollRefreshTimer();
121
+ this.cancelScrollGutterMeasure();
122
+ this.expanded = false;
123
+ this.scrollReady = false;
124
+ this.scrollActive = false;
125
+ this.hasScrollOverflow = false;
126
+ this.scrollGutterPx = 0;
127
+ this.clearScrollActiveTimer();
128
+ this.scrollEdgeFade = { top: false, bottom: false };
129
+ this.syncProgressStyles();
130
+ }
49
131
 
132
+ onStackScroll(): void {
133
+ if (!this.scrollReady) {
134
+ return;
135
+ }
50
136
 
51
- /**
52
- * Handle toast actions.
53
- * @param toastId The ID of the toast.
54
- * @param action The action to perform.
55
- */
56
- handleAction(toastId: string, action: "action" | "cancel") {
57
- this.alertToastService.executeToastAction(toastId, action);
137
+ this.scrollActive = true;
138
+ this.clearScrollActiveTimer();
139
+ this.scrollActiveTimer = setTimeout(() => {
140
+ this.scrollActive = false;
141
+ this.scrollActiveTimer = undefined;
142
+ }, 900);
143
+ this.updateScrollState();
58
144
  }
59
145
 
146
+ onInnerTransitionEnd(event: TransitionEvent): void {
147
+ if (!this.expanded || event.propertyName !== 'max-height') {
148
+ return;
149
+ }
60
150
 
151
+ this.markExpandReady();
152
+ }
61
153
 
62
- /**
63
- * Close a toast by its ID.
64
- * @param toastId The ID of the toast.
65
- */
66
- closeToast(toastId: string) {
67
- this.alertToastService.closeToastById(toastId);
154
+ typeIcon(type: string) {
155
+ return ALERT_TOAST_TYPE_ICONS[type as AlertToastType] ?? ALERT_TOAST_TYPE_ICONS.info;
68
156
  }
69
157
 
158
+ stackClasses(): Record<string, boolean> {
159
+ const count = this.toasts().length;
160
+ const isStackable = this.stackable && count > 1;
161
+
162
+ return {
163
+ 'j-alert-toast-stack': true,
164
+ [`j-alert-toast-stack--${this.position}`]: true,
165
+ 'j-alert-toast-stack--stackable': isStackable,
166
+ 'j-alert-toast-stack--expanded': isStackable && this.expanded,
167
+ 'j-alert-toast-stack--has-scroll':
168
+ isStackable && this.expanded && this.scrollReady && this.hasScrollOverflow,
169
+ 'j-alert-toast-stack--single': count <= 1,
170
+ };
171
+ }
70
172
 
173
+ stackStyle(): Record<string, string> {
174
+ const style: Record<string, string> = {
175
+ '--j-toast-stack-visible': String(this.collapsedStackLimit),
176
+ };
71
177
 
72
- /**
73
- * Fondo suave del toast según el tipo (tinte del diseño anterior + glass actual).
74
- */
75
- getToastSurfaceClass(type: string): string {
76
- if (this.monocromatic) {
77
- return 'bg-white/95 dark:bg-foreground/95';
178
+ if (this.hasScrollOverflow && this.expanded && this.scrollReady) {
179
+ style['--j-toast-stack-scrollbar-gutter'] = `${this.scrollGutterPx}px`;
78
180
  }
79
181
 
80
- const surfaces: Record<string, string> = {
81
- success: 'bg-green-50/95 dark:bg-[#15241f]/95',
82
- error: 'bg-red-50/95 dark:bg-[#21181c]/95',
83
- warning: 'bg-yellow-50/95 dark:bg-[#1f1c1a]/95',
84
- info: 'bg-blue-50/95 dark:bg-[#1a1a24]/95',
85
- question: 'bg-purple-50/95 dark:bg-[#241732]/95',
86
- loading: 'bg-gray-50/95 dark:bg-[#15181e]/95',
182
+ return style;
183
+ }
184
+
185
+ innerClasses(): Record<string, boolean> {
186
+ const count = this.toasts().length;
187
+ const isStackable = this.stackable && count > 1;
188
+
189
+ return {
190
+ 'j-alert-toast-stack-inner': true,
191
+ 'j-alert-toast-stack-inner--stackable': isStackable,
192
+ 'j-alert-toast-stack-inner--expanded': isStackable && this.expanded,
193
+ 'j-alert-toast-stack-inner--scroll-ready': isStackable && this.expanded && this.scrollReady,
194
+ 'j-alert-toast-stack-inner--scrollable': isStackable && this.expanded && this.scrollReady && this.hasScrollOverflow,
195
+ 'j-alert-toast-stack-inner--scroll-active': isStackable && this.expanded && this.scrollReady && this.scrollActive,
87
196
  };
197
+ }
198
+
199
+ scrollFadeStyle(): Record<string, string> {
200
+ if (!this.expanded || !this.scrollReady || !this.hasScrollOverflow) {
201
+ return {};
202
+ }
203
+
204
+ const fade = 'var(--j-toast-stack-fade)';
205
+ const { top, bottom } = this.scrollEdgeFade;
206
+
207
+ if (top && bottom) {
208
+ const gradient = `linear-gradient(to bottom, transparent 0, #000 ${fade}, #000 calc(100% - ${fade}), transparent 100%)`;
209
+ return {
210
+ '-webkit-mask-image': gradient,
211
+ 'mask-image': gradient,
212
+ };
213
+ }
88
214
 
89
- return surfaces[type] ?? 'bg-white/95 dark:bg-foreground/95';
215
+ if (top) {
216
+ const gradient = `linear-gradient(to bottom, transparent 0, #000 ${fade}, #000 100%)`;
217
+ return {
218
+ '-webkit-mask-image': gradient,
219
+ 'mask-image': gradient,
220
+ };
221
+ }
222
+
223
+ if (bottom) {
224
+ const gradient = `linear-gradient(to bottom, #000 0, #000 calc(100% - ${fade}), transparent 100%)`;
225
+ return {
226
+ '-webkit-mask-image': gradient,
227
+ 'mask-image': gradient,
228
+ };
229
+ }
230
+
231
+ return {};
90
232
  }
91
233
 
92
- /**
93
- * Opacidad del icono decorativo de fondo.
94
- */
95
- getWatermarkIconClass(type: string, subtle = false): string {
96
- if (subtle) {
97
- return `${this.getIconClass(type)} opacity-[0.12] dark:opacity-[0.2]`;
234
+ cardAnchorStyle(index: number, total: number): Record<string, string> {
235
+ if (!this.stackable || total <= 1 || this.expanded) {
236
+ return {};
98
237
  }
99
- return `${this.getIconClass(type)} opacity-[0.2] dark:opacity-[0.3]`;
238
+
239
+ const pile = this.getPileMeta(index, total);
240
+ if (!pile.inPile) {
241
+ return {};
242
+ }
243
+
244
+ const offset = `${(pile.pileCount - 1 - pile.pileIndex) * 10}px`;
245
+
246
+ if (this.isBottomAnchor()) {
247
+ return { bottom: offset, top: 'auto' };
248
+ }
249
+
250
+ return { top: offset, bottom: 'auto' };
251
+ }
252
+
253
+ cardZIndex(index: number, total: number): number {
254
+ if (this.expanded) {
255
+ return index + 1;
256
+ }
257
+
258
+ const pile = this.getPileMeta(index, total);
259
+ if (!pile.inPile) {
260
+ return 0;
261
+ }
262
+
263
+ return pile.pileIndex + 1;
100
264
  }
101
265
 
102
- /**
103
- * Barra lateral de acento según el tipo de toast.
104
- */
105
- getAccentBarClass(type: string): string {
106
- if (this.monocromatic) return 'bg-primary';
266
+ cardShellClasses(index: number, total: number): Record<string, boolean> {
267
+ if (!this.stackable || total <= 1 || this.expanded) {
268
+ return {};
269
+ }
270
+
271
+ const pile = this.getPileMeta(index, total);
272
+ return {
273
+ 'j-alert-toast-card--in-pile': pile.inPile,
274
+ 'j-alert-toast-card--pile-hidden': !pile.inPile,
275
+ 'j-alert-toast-card--pile-top': pile.inPile && pile.pileIndex === pile.pileCount - 1,
276
+ };
277
+ }
107
278
 
108
- const accents: Record<string, string> = {
109
- success: 'bg-green-500',
110
- error: 'bg-red-500',
111
- warning: 'bg-yellow-500',
112
- info: 'bg-blue-500',
113
- question: 'bg-purple-500',
114
- loading: 'bg-gray-500',
279
+ private getPileMeta(index: number, total: number): {
280
+ inPile: boolean;
281
+ pileIndex: number;
282
+ pileCount: number;
283
+ } {
284
+ const pileCount = Math.min(total, this.collapsedStackLimit);
285
+ const startIndex = total - pileCount;
286
+ const inPile = index >= startIndex;
287
+
288
+ return {
289
+ inPile,
290
+ pileIndex: inPile ? index - startIndex : -1,
291
+ pileCount,
115
292
  };
293
+ }
294
+
295
+ displayToasts(): Toast[] {
296
+ const list = this.toasts();
297
+ if (this.expanded && !this.isBottomAnchor()) {
298
+ return [...list].reverse();
299
+ }
300
+ return list;
301
+ }
302
+
303
+ toastIndex(toast: Toast): number {
304
+ return this.toasts().findIndex((item) => item.id === toast.id);
305
+ }
306
+
307
+ private scheduleExpandMeasure(): void {
308
+ this.clearExpandMeasureTimer();
309
+ this.expandMeasureTimer = setTimeout(() => this.markExpandReady(), 340);
310
+ }
311
+
312
+ private markExpandReady(): void {
313
+ if (!this.expanded || this.scrollReady) {
314
+ return;
315
+ }
316
+
317
+ this.clearExpandMeasureTimer();
318
+ this.scrollReady = true;
319
+ this.updateScrollState();
320
+ this.cdr.detectChanges();
321
+ this.scrollToLatest();
322
+ this.scheduleScrollGutterMeasure();
323
+ }
324
+
325
+ private clearExpandMeasureTimer(): void {
326
+ if (this.expandMeasureTimer) {
327
+ clearTimeout(this.expandMeasureTimer);
328
+ this.expandMeasureTimer = undefined;
329
+ }
330
+ }
331
+
332
+ private clearScrollActiveTimer(): void {
333
+ if (this.scrollActiveTimer) {
334
+ clearTimeout(this.scrollActiveTimer);
335
+ this.scrollActiveTimer = undefined;
336
+ }
337
+ }
338
+
339
+ private readLiveScrollGutter(el: HTMLElement): number {
340
+ return Math.max(0, el.offsetWidth - el.clientWidth);
341
+ }
116
342
 
117
- return accents[type] ?? 'bg-primary';
343
+ private scheduleScrollGutterMeasure(): void {
344
+ this.cancelScrollGutterMeasure();
345
+ this.scrollGutterRafId = requestAnimationFrame(() => {
346
+ requestAnimationFrame(() => {
347
+ this.scrollGutterRafId = undefined;
348
+ this.applyMeasuredScrollGutter();
349
+ });
350
+ });
118
351
  }
119
352
 
120
- /**
121
- * Indica si el toast muestra botones de acción o cancelar.
122
- */
123
- hasActions(toast: {
124
- config: { type?: string };
125
- onCancelCallback?: unknown;
126
- onActionCallback?: unknown;
127
- }): boolean {
353
+ private cancelScrollGutterMeasure(): void {
354
+ if (this.scrollGutterRafId !== undefined) {
355
+ cancelAnimationFrame(this.scrollGutterRafId);
356
+ this.scrollGutterRafId = undefined;
357
+ }
358
+ }
359
+
360
+ private scheduleScrollStateRefresh(): void {
361
+ queueMicrotask(() => this.refreshScrollState());
362
+ requestAnimationFrame(() => this.refreshScrollState());
363
+
364
+ this.clearScrollRefreshTimer();
365
+ this.scrollRefreshTimer = setTimeout(() => this.refreshScrollState(), 220);
366
+ }
367
+
368
+ private refreshScrollState(): void {
369
+ this.updateScrollState();
370
+ this.cdr.detectChanges();
371
+ }
372
+
373
+ private clearScrollRefreshTimer(): void {
374
+ if (this.scrollRefreshTimer) {
375
+ clearTimeout(this.scrollRefreshTimer);
376
+ this.scrollRefreshTimer = undefined;
377
+ }
378
+ }
379
+
380
+ private applyMeasuredScrollGutter(): void {
381
+ const el = this.stackInner?.nativeElement;
382
+ if (!el || !this.expanded || !this.hasScrollOverflow) {
383
+ return;
384
+ }
385
+
386
+ const gutter = this.readLiveScrollGutter(el);
387
+ const next = gutter > 0 ? gutter : this.defaultScrollGutterPx;
388
+ if (next === this.scrollGutterPx) {
389
+ return;
390
+ }
391
+
392
+ this.scrollGutterPx = next;
393
+ this.cdr.detectChanges();
394
+ }
395
+
396
+ private updateScrollState(): void {
397
+ const el = this.stackInner?.nativeElement;
398
+ if (!el || !this.expanded) {
399
+ this.hasScrollOverflow = false;
400
+ this.scrollGutterPx = 0;
401
+ this.scrollEdgeFade = { top: false, bottom: false };
402
+ return;
403
+ }
404
+
405
+ const threshold = 4;
406
+ const hasOverflow = el.scrollHeight > el.clientHeight + 1;
407
+ this.hasScrollOverflow = hasOverflow;
408
+ this.scrollGutterPx = hasOverflow
409
+ ? Math.max(this.readLiveScrollGutter(el), this.defaultScrollGutterPx)
410
+ : 0;
411
+
412
+ if (!hasOverflow) {
413
+ this.cancelScrollGutterMeasure();
414
+ this.scrollEdgeFade = { top: false, bottom: false };
415
+ return;
416
+ }
417
+
418
+ this.scheduleScrollGutterMeasure();
419
+
420
+ this.scrollEdgeFade = {
421
+ top: el.scrollTop > threshold,
422
+ bottom: el.scrollTop + el.clientHeight < el.scrollHeight - threshold,
423
+ };
424
+ }
425
+
426
+ private scrollToLatest(): void {
427
+ const el = this.stackInner?.nativeElement;
428
+ if (!el) {
429
+ return;
430
+ }
431
+
432
+ if (this.isBottomAnchor()) {
433
+ el.scrollTop = el.scrollHeight;
434
+ } else {
435
+ el.scrollTop = 0;
436
+ }
437
+ }
438
+
439
+ private isBottomAnchor(): boolean {
440
+ return this.position === 'bottom-left' || this.position === 'bottom-right';
441
+ }
442
+
443
+ cardClasses(type: string, index: number, total: number): Record<string, boolean> {
444
+ const toastType = type as AlertToastType;
445
+ return {
446
+ [`j-alert-toast-card--${toastType}`]: !this.monocromatic,
447
+ 'j-alert-toast-card--mono': this.monocromatic,
448
+ ...this.cardShellClasses(index, total),
449
+ };
450
+ }
451
+
452
+ accentClasses(type: string): Record<string, boolean> {
453
+ const toastType = type as AlertToastType;
454
+ return {
455
+ 'j-alert-toast-accent': true,
456
+ [`j-alert-toast-accent--${toastType}`]: !this.monocromatic,
457
+ 'j-alert-toast-accent--mono': this.monocromatic,
458
+ };
459
+ }
460
+
461
+ watermarkClasses(type: string, subtle = false): Record<string, boolean> {
462
+ const toastType = type as AlertToastType;
463
+ return {
464
+ 'j-alert-toast-watermark': true,
465
+ 'j-alert-toast-watermark--lg': !subtle,
466
+ 'j-alert-toast-watermark--sm': subtle,
467
+ 'j-alert-toast-watermark--subtle': subtle,
468
+ [`j-alert-toast-watermark--${toastType}`]: !this.monocromatic,
469
+ 'j-alert-toast-watermark--mono': this.monocromatic,
470
+ 'j-alert-toast-watermark--spin': type === 'loading',
471
+ };
472
+ }
473
+
474
+ bodyClasses(toast: Toast): Record<string, boolean> {
475
+ return {
476
+ 'j-alert-toast-body': true,
477
+ 'j-alert-toast-body--closable': toast.btnClose,
478
+ 'j-alert-toast-body--with-actions': this.hasActions(toast),
479
+ 'j-alert-toast-body--with-timeline': this.hasAutoClose(toast) && !this.hasActions(toast),
480
+ 'j-alert-toast-body--with-actions-timeline':
481
+ this.hasAutoClose(toast) && this.hasActions(toast),
482
+ };
483
+ }
484
+
485
+ accentBarClasses(toast: Toast): Record<string, boolean> {
486
+ return {
487
+ ...this.accentClasses(toast.config.type),
488
+ 'j-alert-toast-accent--with-timeline': this.hasAutoClose(toast),
489
+ };
490
+ }
491
+
492
+ watermarkSmClasses(toast: Toast): Record<string, boolean> {
493
+ return {
494
+ ...this.watermarkClasses(toast.config.type, true),
495
+ 'j-alert-toast-watermark--with-close': toast.btnClose,
496
+ };
497
+ }
498
+
499
+ progressClasses(type: string): Record<string, boolean> {
500
+ const toastType = type as AlertToastType;
501
+ return {
502
+ [`j-alert-toast-progress--${toastType}`]: !this.monocromatic,
503
+ 'j-alert-toast-progress--mono': this.monocromatic,
504
+ };
505
+ }
506
+
507
+ hasActions(toast: Toast): boolean {
128
508
  return (
129
509
  (toast.config.type !== 'success' && Boolean(toast.onCancelCallback)) ||
130
510
  (toast.config.type !== 'loading' && Boolean(toast.onActionCallback))
131
511
  );
132
512
  }
133
513
 
134
- /**
135
- * Indica si el toast se cierra automáticamente.
136
- */
137
- hasAutoClose(toast: { config: { type?: string; autoClose?: boolean } }): boolean {
514
+ hasAutoClose(toast: Toast): boolean {
138
515
  const { config } = toast;
139
516
  return config.autoClose === true || (config.type === 'success' && config.autoClose !== false);
140
517
  }
141
518
 
142
- /**
143
- * Duración del auto-cierre en milisegundos.
144
- */
145
- getAutoCloseDelay(toast: { config: { autoCloseDelay?: number } }): number {
519
+ getAutoCloseDelay(toast: Toast): number {
146
520
  return toast.config.autoCloseDelay ?? 5000;
147
521
  }
148
522
 
149
- /**
150
- * Get the class of the icon.
151
- * @param type The type of the toast.
152
- * @returns The class of the icon.
153
- */
154
- getIconClass(type: string) {
155
- return this.colorsService.getIconClass(type, this.monocromatic);
156
- }
523
+ progressStyle(toast: Toast): Record<string, string> {
524
+ void this.progressLayoutKey;
157
525
 
526
+ const cached = this.progressStyleCache.get(toast.id);
527
+ if (cached) {
528
+ return cached;
529
+ }
158
530
 
531
+ const duration = this.getAutoCloseDelay(toast);
532
+ const elapsed = Math.min(Math.max(0, Date.now() - toast.createdAt), duration);
533
+ const style = {
534
+ 'animation-duration': `${duration}ms`,
535
+ 'animation-delay': `-${elapsed}ms`,
536
+ };
159
537
 
160
- /**
161
- * Get the class of the button.
162
- * @param type The type of the toast.
163
- * @returns The class of the button.
164
- */
165
- getButtonClass(type: string): { [key: string]: boolean } {
166
- return this.colorsService.getButtonClass(type, this.monocromatic);
538
+ this.progressStyleCache.set(toast.id, style);
539
+ return style;
167
540
  }
168
541
 
542
+ private syncProgressStyles(): void {
543
+ this.progressLayoutKey++;
544
+ this.progressStyleCache.clear();
545
+ }
169
546
 
547
+ getButtonClass(type: string): Record<string, boolean> {
548
+ return this.colorsService.getButtonClass(type, this.monocromatic);
549
+ }
170
550
 
171
- /**
172
- * Get the class of the secondary button.
173
- * @param type The type of the toast.
174
- * @returns The class of the secondary button.
175
- */
176
- getButtonSecondaryClass(type: string): { [key: string]: boolean } {
551
+ getButtonSecondaryClass(type: string): Record<string, boolean> {
177
552
  return this.colorsService.getButtonSecondaryClass(type, this.monocromatic);
178
553
  }
179
554
 
180
-
181
-
182
- /**
183
- * Get the class of the toast.
184
- * @returns The class of the toast.
185
- */
186
- getPositionClass(): string {
187
- const base = 'w-full fixed z-1000 flex flex-col gap-3 max-w-[22rem] pointer-events-none p-1';
188
- switch (this.position) {
189
- case 'top-left':
190
- return `${base} top-18 left-3`;
191
- case 'top-right':
192
- return `${base} top-18 right-3`;
193
- case 'bottom-left':
194
- return `${base} bottom-4 left-4`;
195
- case 'bottom-right':
196
- default:
197
- return `${base} bottom-4 right-4`;
198
- }
555
+ isActionDisabled(toast: Toast): boolean {
556
+ return toast.isCancelLoading || toast.isActionLoading;
199
557
  }
200
-
201
558
  }