tailjng 0.0.61 → 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 (60) hide show
  1. package/README.md +186 -32
  2. package/cli/component-manager.js +71 -20
  3. package/cli/execute/init-app.js +251 -0
  4. package/cli/file-operations.js +45 -29
  5. package/cli/index.js +66 -15
  6. package/cli/settings/components-list.js +4 -147
  7. package/cli/settings/header-generator.js +9 -10
  8. package/cli/settings/lib-utils.js +89 -0
  9. package/cli/settings/overwrite-policy.js +18 -0
  10. package/cli/settings/path-utils.js +14 -29
  11. package/cli/settings/project-utils.js +220 -0
  12. package/cli/settings/prompt-utils.js +66 -5
  13. package/cli/templates/app.generator.js +382 -0
  14. package/fesm2022/tailjng.mjs +232 -66
  15. package/fesm2022/tailjng.mjs.map +1 -1
  16. package/lib/services/static/colors.service.d.ts +17 -0
  17. package/lib/services/transformer/transform.service.d.ts +3 -3
  18. package/package.json +1 -1
  19. package/public-api.d.ts +2 -0
  20. package/registry/components.json +164 -0
  21. package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +17 -0
  22. package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +83 -51
  23. package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +85 -53
  24. package/src/lib/components/alert/alert-toast/toast-alert.component.css +38 -4
  25. package/src/lib/components/alert/alert-toast/toast-alert.component.html +72 -40
  26. package/src/lib/components/alert/alert-toast/toast-alert.component.ts +84 -19
  27. package/src/lib/components/badge/badge.component.ts +1 -2
  28. package/src/lib/components/button/button.component.css +14 -0
  29. package/src/lib/components/button/button.component.html +17 -17
  30. package/src/lib/components/button/button.component.ts +139 -48
  31. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +5 -1
  32. package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +1 -1
  33. package/src/lib/components/filter/filter-complete/complete-filter.component.html +1 -1
  34. package/src/lib/components/form/form-sidebar/sidebar-form.component.html +130 -97
  35. package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +3 -2
  36. package/src/lib/components/input/input/input.component.css +35 -0
  37. package/src/lib/components/input/input/input.component.html +37 -27
  38. package/src/lib/components/input/input/input.component.ts +1 -0
  39. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +56 -0
  40. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +12 -0
  41. package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +41 -0
  42. package/src/lib/components/select/select-dropdown/dropdown-select.component.css +4 -0
  43. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +1 -1
  44. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +3 -3
  45. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +4 -0
  46. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +1 -1
  47. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +30 -20
  48. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +506 -172
  49. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.scss +92 -0
  50. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +141 -7
  51. package/src/lib/components/table/table-crud-complete/expand-grid/table-expand-grid.builder.ts +116 -0
  52. package/src/lib/components/table/table-crud-complete/expand-grid/table-expand-grid.helper.ts +43 -0
  53. package/src/lib/components/table/table-crud-complete/expand-grid/table-expand-grid.types.ts +39 -0
  54. package/src/lib/components/table/table-crud-complete/index.ts +3 -0
  55. package/src/lib/components/toggle-radio/toggle-radio.component.css +4 -0
  56. package/src/lib/components/toggle-radio/toggle-radio.component.html +4 -4
  57. package/src/lib/components/toggle-radio/toggle-radio.component.ts +15 -6
  58. package/src/lib/components/tooltip/tooltip.service.ts +0 -30
  59. package/tailjng-0.1.0.tgz +0 -0
  60. package/src/lib/components/color/colors.service.ts +0 -187
@@ -2,86 +2,118 @@
2
2
  @for (toast of toasts(); track toast.id) {
3
3
  <div
4
4
  @toastTransition
5
- class="j-dialog-toas relative ml-auto w-100 border border-border border-l-4 p-4 rounded-lg shadow-lg overflow-hidden flex gap-3"
6
- [ngClass]="getToastClass(toast.config.type)"
5
+ class="j-toast group relative ml-auto w-full overflow-hidden rounded-xl border border-border/40 dark:border-dark-border/40 backdrop-blur-xl shadow-lg shadow-black/5 dark:shadow-black/40 pointer-events-auto"
6
+ [ngClass]="getToastSurfaceClass(toast.config.type)"
7
+ role="alert"
8
+ aria-live="polite"
7
9
  >
8
- <!-- Close button -->
9
- @if (toast.btnClose) {
10
- <button
11
- class="absolute p-1 top-2 right-2 cursor-pointer rounded transition duration-300 hover:bg-background dark:hover:bg-dark-background"
12
- [ngClass]="getIconClass(toast.config.type)"
13
- (click)="closeToast(toast.id)"
14
- >
15
- <lucide-icon
16
- [name]="iconsService.icons.close"
17
- [size]="16"
18
- ></lucide-icon>
19
- </button>
20
- }
10
+ <div
11
+ class="absolute left-0 top-0 w-1 z-[1]"
12
+ [ngClass]="getAccentBarClass(toast.config.type)"
13
+ [class.bottom-0]="!hasAutoClose(toast)"
14
+ [class.bottom-1]="hasAutoClose(toast)"
15
+ ></div>
21
16
 
22
- <!-- Big icon in the bottom left corner -->
17
+ <!-- Icono decorativo grande -->
23
18
  <div
19
+ class="j-toast-watermark j-toast-watermark--lg absolute -bottom-8 -left-5 z-0 pointer-events-none select-none"
24
20
  [ngClass]="{ 'animate-spin': toast.config.type === 'loading' }"
25
- class="absolute -bottom-5 opacity-15 dark:opacity-25 -left-5 text-black dark:text-white z-2 pointer-events-none"
26
21
  >
27
22
  <lucide-icon
28
23
  [name]="getIcon(toast.config.type)"
29
- [size]="100"
30
- [ngClass]="getIconClass(toast.config.type)"
24
+ [size]="104"
25
+ [ngClass]="getWatermarkIconClass(toast.config.type)"
31
26
  ></lucide-icon>
32
27
  </div>
33
28
 
34
- <!-- Small icon in the top right corner -->
29
+ <!-- Icono decorativo pequeño -->
35
30
  <div
31
+ class="j-toast-watermark j-toast-watermark--sm absolute top-2 z-0 pointer-events-none select-none"
32
+ [class.right-9]="toast.btnClose"
33
+ [class.right-3]="!toast.btnClose"
36
34
  [ngClass]="{ 'animate-spin': toast.config.type === 'loading' }"
37
- class="absolute top-5 opacity-15 dark:opacity-25 right-5 text-black dark:text-white z-2 pointer-events-none"
38
35
  >
39
36
  <lucide-icon
40
37
  [name]="getIcon(toast.config.type)"
41
- [size]="30"
42
- [ngClass]="getIconClass(toast.config.type)"
38
+ [size]="32"
39
+ [ngClass]="getWatermarkIconClass(toast.config.type, true)"
43
40
  ></lucide-icon>
44
41
  </div>
45
42
 
46
- <!-- Content -->
47
- <div class="flex-1">
48
- <h3 class="text-sm font-semibold text-black dark:text-white pb-2">
43
+ <!-- Texto -->
44
+ <div
45
+ class="relative z-10 pl-5 pr-4 pt-3.5"
46
+ [class.pr-10]="toast.btnClose"
47
+ [class.pb-2]="hasActions(toast)"
48
+ [class.pb-3.5]="!hasActions(toast) && !hasAutoClose(toast)"
49
+ [class.pb-4]="!hasActions(toast) && hasAutoClose(toast)"
50
+ >
51
+ <h3 class="m-0 text-sm font-semibold leading-tight text-foreground dark:text-white">
49
52
  {{ toast.config.title }}
50
53
  </h3>
51
- <p
52
- class="text-sm text-black/80 dark:text-dark-muted-foreground"
53
- [innerHTML]="toast.config.description"
54
- ></p>
55
54
 
56
- <!-- Action buttons -->
57
- <div class="flex justify-end gap-2 mt-2">
58
- <!-- Button Cancel -->
59
- @if (toast.config.type !== "success" && toast.onCancelCallback) {
55
+ @if (toast.config.description) {
56
+ <p
57
+ class="m-0 mt-1 text-xs leading-relaxed text-muted-foreground dark:text-dark-muted-foreground [&_b]:font-semibold [&_b]:text-foreground dark:[&_b]:text-white"
58
+ [innerHTML]="toast.config.description"
59
+ ></p>
60
+ }
61
+
62
+ @if (toast.btnClose) {
63
+ <button
64
+ type="button"
65
+ class="absolute top-2.5 right-2.5 z-20 flex h-7 w-7 items-center justify-center rounded-lg transition-all duration-200 hover:bg-black/5 dark:hover:bg-white/10 cursor-pointer"
66
+ [ngClass]="getIconClass(toast.config.type)"
67
+ [attr.aria-label]="'Cerrar notificación'"
68
+ (click)="closeToast(toast.id)"
69
+ >
70
+ <lucide-icon [name]="iconsService.icons.close" [size]="15"></lucide-icon>
71
+ </button>
72
+ }
73
+ </div>
74
+
75
+ <!-- Acciones: alineadas a la card, no al bloque de texto -->
76
+ @if (hasActions(toast)) {
77
+ <div
78
+ class="relative z-10 flex flex-wrap justify-end gap-1.5 pl-5 pr-4"
79
+ [class.pb-3.5]="!hasAutoClose(toast)"
80
+ [class.pb-4]="hasAutoClose(toast)"
81
+ >
82
+ @if (toast.config.type !== 'success' && toast.onCancelCallback) {
60
83
  <JButton
84
+ size="xs"
61
85
  (clicked)="handleAction(toast.id, 'cancel')"
62
86
  [disabled]="toast.isCancelLoading || toast.isActionLoading"
63
87
  [isLoading]="toast.isCancelLoading"
64
- classes="text-[10px] min-w-auto"
65
88
  [ngClasses]="getButtonSecondaryClass(toast.config.type)"
66
89
  >
67
- {{ toast.config.titleBtnCancel || "Cancelar" }}
90
+ {{ toast.config.titleBtnCancel || 'Cancelar' }}
68
91
  </JButton>
69
92
  }
70
93
 
71
- <!-- Button Action -->
72
- @if (toast.config.type !== "loading" && toast.onActionCallback) {
94
+ @if (toast.config.type !== 'loading' && toast.onActionCallback) {
73
95
  <JButton
96
+ size="xs"
74
97
  (clicked)="handleAction(toast.id, 'action')"
75
98
  [disabled]="toast.isCancelLoading || toast.isActionLoading"
76
99
  [isLoading]="toast.isActionLoading"
77
- classes="text-[10px] min-w-auto"
78
100
  [ngClasses]="getButtonClass(toast.config.type)"
79
101
  >
80
102
  {{ toast.actionNameButton }}
81
103
  </JButton>
82
104
  }
83
105
  </div>
84
- </div>
106
+ }
107
+
108
+ @if (hasAutoClose(toast)) {
109
+ <div class="j-toast-timeline absolute inset-x-0 bottom-0 z-20 h-1 bg-black/5 dark:bg-white/10">
110
+ <div
111
+ class="j-toast-progress h-full origin-left"
112
+ [ngClass]="getAccentBarClass(toast.config.type)"
113
+ [style.animation-duration.ms]="getAutoCloseDelay(toast)"
114
+ ></div>
115
+ </div>
116
+ }
85
117
  </div>
86
118
  }
87
119
  </div>
@@ -3,8 +3,7 @@ import { trigger, transition, style, animate } from "@angular/animations";
3
3
  import { NgClass } from '@angular/common';
4
4
  import { LucideAngularModule } from 'lucide-angular';
5
5
  import { JButtonComponent } from '../../button/button.component';
6
- import { JColorsService } from '../../color/colors.service';
7
- import { JIconsService, JAlertToastService } from 'tailjng';
6
+ import { JIconsService, JAlertToastService, JColorsService } from 'tailjng';
8
7
 
9
8
  @Component({
10
9
  selector: 'JAlertToast',
@@ -12,16 +11,16 @@ import { JIconsService, JAlertToastService } from 'tailjng';
12
11
  templateUrl: './toast-alert.component.html',
13
12
  styleUrl: './toast-alert.component.css',
14
13
  animations: [
15
- trigger("toastTransition", [
16
- transition(":enter", [
17
- style({ opacity: 0, transform: "translateY(10px)" }),
18
- animate("300ms ease-out", style({ opacity: 1, transform: "translateY(0)" }))
14
+ trigger('toastTransition', [
15
+ transition(':enter', [
16
+ 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)' })),
19
18
  ]),
20
- transition(":leave", [
21
- animate("150ms ease-in", style({ opacity: 0, transform: "translateY(10px)" }))
22
- ])
23
- ])
24
- ]
19
+ transition(':leave', [
20
+ animate('180ms cubic-bezier(0.4, 0, 1, 1)', style({ opacity: 0, transform: 'translateX(0.75rem) scale(0.98)' })),
21
+ ]),
22
+ ]),
23
+ ],
25
24
  })
26
25
  export class JAlertToastComponent {
27
26
 
@@ -71,15 +70,81 @@ export class JAlertToastComponent {
71
70
 
72
71
 
73
72
  /**
74
- * Get the class of the toast.
75
- * @param type The type of the toast.
76
- * @returns The class of the toast.
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';
78
+ }
79
+
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',
87
+ };
88
+
89
+ return surfaces[type] ?? 'bg-white/95 dark:bg-foreground/95';
90
+ }
91
+
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]`;
98
+ }
99
+ return `${this.getIconClass(type)} opacity-[0.2] dark:opacity-[0.3]`;
100
+ }
101
+
102
+ /**
103
+ * Barra lateral de acento según el tipo de toast.
77
104
  */
78
- getToastClass(type: string) {
79
- return this.colorsService.getAlertClass(type, this.monocromatic);
105
+ getAccentBarClass(type: string): string {
106
+ if (this.monocromatic) return 'bg-primary';
107
+
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',
115
+ };
116
+
117
+ return accents[type] ?? 'bg-primary';
80
118
  }
81
119
 
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 {
128
+ return (
129
+ (toast.config.type !== 'success' && Boolean(toast.onCancelCallback)) ||
130
+ (toast.config.type !== 'loading' && Boolean(toast.onActionCallback))
131
+ );
132
+ }
82
133
 
134
+ /**
135
+ * Indica si el toast se cierra automáticamente.
136
+ */
137
+ hasAutoClose(toast: { config: { type?: string; autoClose?: boolean } }): boolean {
138
+ const { config } = toast;
139
+ return config.autoClose === true || (config.type === 'success' && config.autoClose !== false);
140
+ }
141
+
142
+ /**
143
+ * Duración del auto-cierre en milisegundos.
144
+ */
145
+ getAutoCloseDelay(toast: { config: { autoCloseDelay?: number } }): number {
146
+ return toast.config.autoCloseDelay ?? 5000;
147
+ }
83
148
 
84
149
  /**
85
150
  * Get the class of the icon.
@@ -119,12 +184,12 @@ export class JAlertToastComponent {
119
184
  * @returns The class of the toast.
120
185
  */
121
186
  getPositionClass(): string {
122
- const base = 'w-full fixed z-1000 flex flex-col gap-2 max-w-md';
187
+ const base = 'w-full fixed z-1000 flex flex-col gap-3 max-w-[22rem] pointer-events-none p-1';
123
188
  switch (this.position) {
124
189
  case 'top-left':
125
- return `${base} top-4 left-4`;
190
+ return `${base} top-18 left-3`;
126
191
  case 'top-right':
127
- return `${base} top-20 right-4`;
192
+ return `${base} top-18 right-3`;
128
193
  case 'bottom-left':
129
194
  return `${base} bottom-4 left-4`;
130
195
  case 'bottom-right':
@@ -1,9 +1,8 @@
1
1
  import { Component, EventEmitter, Input, Output } from '@angular/core';
2
2
  import { JTooltipDirective } from '../tooltip/tooltip.directive';
3
- import { JIconsService } from 'tailjng';
3
+ import { JIconsService, JColorsService } from 'tailjng';
4
4
  import { LucideAngularModule } from 'lucide-angular';
5
5
  import { NgClass } from '@angular/common';
6
- import { JColorsService } from '../color/colors.service';
7
6
 
8
7
  @Component({
9
8
  selector: 'JBadge',
@@ -0,0 +1,14 @@
1
+ :host {
2
+ display: inline-flex;
3
+ }
4
+
5
+ :host:has(button.w-full),
6
+ :host:has(button[class*="w-full"]) {
7
+ display: flex;
8
+ width: 100%;
9
+ min-width: 0;
10
+ }
11
+
12
+ button {
13
+ line-height: 1.2;
14
+ }
@@ -7,30 +7,30 @@
7
7
  [class]="classes"
8
8
  (click)="handleClick($event)"
9
9
  >
10
- <div class="flex items-center justify-center gap-2">
11
-
10
+ <span
11
+ class="inline-flex items-center justify-center"
12
+ [ngClass]="innerContentClasses"
13
+ >
12
14
  @if (isLoading) {
13
- <div [ngClass]="{ 'pt-1 pb-1': icon }">
14
- <lucide-icon [name]="iconsService.icons.loading" [size]="iconSize || 15" class="animate-spin" />
15
- </div>
15
+ <lucide-icon
16
+ [name]="iconsService.icons.loading"
17
+ [size]="resolvedIconSize"
18
+ class="animate-spin shrink-0"
19
+ />
16
20
  }
17
21
 
18
22
  @if (!isLoading && icon) {
19
- <div class="pt-1 pb-1">
20
-
21
- @if (!isChangeIcon) {
22
- <lucide-icon [name]="icon" [size]="iconSize" />
23
- } @else {
24
- <lucide-icon [name]="iconChange" [size]="iconSize" />
25
- }
26
-
27
- </div>
23
+ @if (!isChangeIcon) {
24
+ <lucide-icon [name]="icon" [size]="resolvedIconSize" class="shrink-0" />
25
+ } @else {
26
+ <lucide-icon [name]="iconChange" [size]="resolvedIconSize" class="shrink-0" />
27
+ }
28
28
  }
29
29
 
30
30
  @if (text && (!isLoading || isLoadingText)) {
31
- <span class="text-[15px] max-[400px]:text-[12px]">{{ text }}</span>
31
+ <span>{{ text }}</span>
32
32
  }
33
33
 
34
34
  <ng-content></ng-content>
35
- </div>
36
- </button>
35
+ </span>
36
+ </button>
@@ -1,96 +1,187 @@
1
- import { NgClass } from "@angular/common"
2
- import { Component, Input, Output, EventEmitter } from "@angular/core"
3
- import { LucideAngularModule } from "lucide-angular"
4
- import { JIconsService } from "tailjng";
5
- import { JColorsService } from '../color/colors.service';
6
- import { JTooltipDirective } from "../tooltip/tooltip.directive"
1
+ import { NgClass } from '@angular/common';
2
+ import { Component, Input, Output, EventEmitter } from '@angular/core';
3
+ import { LucideAngularModule } from 'lucide-angular';
4
+ import { JIconsService, JColorsService } from 'tailjng';
5
+ import { JTooltipDirective } from '../tooltip/tooltip.directive';
6
+
7
+ export type JButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon';
7
8
 
8
9
  @Component({
9
10
  selector: 'JButton',
10
11
  imports: [NgClass, LucideAngularModule, JTooltipDirective],
11
12
  templateUrl: './button.component.html',
12
- styleUrl: './button.component.css'
13
+ styleUrl: './button.component.css',
13
14
  })
14
15
  export class JButtonComponent {
15
16
 
16
- @Input() type: "button" | "submit" | "reset" = "button";
17
- @Input() tooltipPosition: "top" | "right" | "bottom" | "left" = "top";
17
+ @Input() type: 'button' | 'submit' | 'reset' = 'button';
18
+ @Input() tooltipPosition: 'top' | 'right' | 'bottom' | 'left' = 'top';
18
19
 
19
20
  @Input() text!: string | number;
20
- @Input() tooltip: string = "";
21
+ @Input() tooltip: string = '';
21
22
 
22
23
  @Input() icon!: any;
23
- @Input() iconSize: number = 15;
24
+ @Input() iconSize: number = 0;
24
25
  @Input() iconChange!: any;
25
26
  @Input() isChangeIcon: boolean = false;
26
27
 
28
+ /** xs/sm: alertas y cards · md: formularios · lg: acciones destacadas · icon: solo icono */
29
+ @Input() size: JButtonSize = 'md';
30
+
27
31
  @Output() clicked = new EventEmitter<Event>();
28
32
 
29
33
  @Input() disabled = false;
30
34
  @Input() isLoading = false;
31
35
  @Input() isLoadingText = true;
32
36
 
33
- @Input() classes: string = "";
37
+ @Input() classes: string = '';
34
38
  @Input() ngClasses: { [key: string]: boolean } = {};
35
39
 
40
+ constructor(
41
+ public readonly iconsService: JIconsService,
42
+ private readonly colorsService: JColorsService,
43
+ ) { }
36
44
 
37
- // Define classes based on button type (switch)
38
45
  get variantClasses(): string {
39
- return this.colorsService.variants[this.getActiveVariant()] || "min-w-[100px] text-black dark:text-white shadow-md"
46
+ return this.colorsService.variants[this.getActiveVariant()]
47
+ ?? 'text-black dark:text-white shadow-md border border-border dark:border-dark-border';
48
+ }
49
+
50
+ get resolvedIconSize(): number {
51
+ if (this.iconSize > 0) return this.iconSize;
52
+
53
+ switch (this.size) {
54
+ case 'xs': return 14;
55
+ case 'sm': return 14;
56
+ case 'icon': return 16;
57
+ case 'lg': return 18;
58
+ default: return 15;
59
+ }
40
60
  }
41
61
 
42
- // Combine base classes with variants
43
62
  get computedClasses() {
63
+ const result: Record<string, boolean> = {
64
+ 'inline-flex items-center justify-center font-semibold border border-border dark:border-dark-border transition duration-300 select-none': true,
65
+ [this.dimensionClasses]: !this.hasCustomDimensions(),
66
+ [this.paddingClasses]: !this.hasCustomPadding(),
67
+ [this.radiusClasses]: !this.hasCustomRadius(),
68
+ [this.variantClasses]: true,
69
+ 'cursor-pointer': !this.disabled && !this.isLoading,
70
+ 'cursor-default opacity-50 pointer-events-none': this.disabled || this.isLoading,
71
+ ...this.ngClasses,
72
+ };
73
+
74
+ if (!this.hasCustomTextSize() && this.textSizeClasses) {
75
+ result[this.textSizeClasses] = true;
76
+ }
77
+
78
+ return result;
79
+ }
80
+
81
+ get innerContentClasses(): Record<string, boolean> {
44
82
  return {
45
- "flex gap-3 items-center justify-center font-semibold border border-border dark:border-dark-border px-3 max-[400px]:px-2! py-2 max-[400px]:py-1 rounded transition duration-300 select-none": true,
46
- [this.variantClasses]: true, // Apply variant classes based on switch
47
- "cursor-pointer": !this.disabled && !this.isLoading, // Default cursor when active
48
- "cursor-default opacity-50 pointer-events-none": this.disabled || this.isLoading, // Disabled cursor
49
- ...this.ngClasses, // Allows using dynamic validations with [ngClass]
83
+ 'gap-1': this.size === 'xs' || this.size === 'icon',
84
+ 'gap-1.5': this.size === 'sm',
85
+ 'gap-2': this.size === 'md' || this.size === 'lg',
86
+ };
87
+ }
88
+
89
+ get dimensionClasses(): string {
90
+ switch (this.size) {
91
+ case 'xs':
92
+ return 'h-7 min-w-0';
93
+ case 'sm':
94
+ return 'h-8 min-w-0';
95
+ case 'icon':
96
+ return 'h-8 w-8 min-w-0 shrink-0';
97
+ case 'lg':
98
+ return 'min-h-10 min-w-[7.5rem]';
99
+ case 'md':
100
+ default:
101
+ return this.icon && !this.text
102
+ ? 'min-h-9 min-w-9'
103
+ : 'min-h-9 min-w-[6.25rem]';
50
104
  }
51
105
  }
52
106
 
107
+ get paddingClasses(): string {
108
+ switch (this.size) {
109
+ case 'xs':
110
+ return 'px-2.5 py-0';
111
+ case 'sm':
112
+ return 'px-3 py-0';
113
+ case 'icon':
114
+ return 'p-0';
115
+ case 'lg':
116
+ return 'px-4 py-2.5';
117
+ case 'md':
118
+ default:
119
+ return 'px-3 py-2 max-[400px]:px-2 max-[400px]:py-1.5';
120
+ }
121
+ }
53
122
 
54
- constructor(
55
- public readonly iconsService: JIconsService,
56
- private readonly colorsService: JColorsService,
57
- ) { }
123
+ get textSizeClasses(): string {
124
+ switch (this.size) {
125
+ case 'xs':
126
+ return 'text-[11px] leading-tight font-medium';
127
+ case 'sm':
128
+ return 'text-xs leading-tight font-medium';
129
+ case 'lg':
130
+ return 'text-base leading-snug';
131
+ case 'icon':
132
+ return '';
133
+ case 'md':
134
+ default:
135
+ return 'text-sm leading-snug max-[400px]:text-xs';
136
+ }
137
+ }
58
138
 
139
+ get radiusClasses(): string {
140
+ return this.size === 'icon' || this.size === 'xs' || this.size === 'sm'
141
+ ? 'rounded-lg'
142
+ : 'rounded-md';
143
+ }
59
144
 
60
- /**
61
- * Verify if a class is present in `classes` or `ngClasses`
62
- * Split the class string by spaces to check each class individually
63
- * @param className Name of the class to check
64
- * @returns True if the class is present, false otherwise
65
- */
66
- private hasClass(className: string): boolean {
67
- const classArray = this.classes.split(" ")
68
- return classArray.includes(className) || this.ngClasses[className]
145
+ private getClassTokens(): string[] {
146
+ const fromClasses = this.classes.split(/\s+/).filter(Boolean);
147
+ const fromNgClasses = Object.entries(this.ngClasses)
148
+ .filter(([, active]) => active)
149
+ .map(([className]) => className);
150
+ return [...fromClasses, ...fromNgClasses];
69
151
  }
70
152
 
153
+ private matchesTokenPattern(pattern: RegExp): boolean {
154
+ return this.getClassTokens().some((token) => pattern.test(token));
155
+ }
71
156
 
157
+ hasCustomDimensions(): boolean {
158
+ return this.matchesTokenPattern(/^!?((min|max)-)?[wh](-|\[|$)|^!?size-/);
159
+ }
72
160
 
73
- /**
74
- * Get the active variant based on the provided classes.
75
- * It checks if the class exists in the `variants` object of `JColorsService
76
- * @returns The active variant based on the provided classes.
77
- */
78
- private getActiveVariant(): string {
79
- const variant = Object.keys(this.colorsService.variants).find((variant) => this.hasClass(variant))
80
- return variant ?? "default"
161
+ hasCustomPadding(): boolean {
162
+ return this.matchesTokenPattern(/^!?p[xytblr]?(-|\[|$)|^!?p$/);
81
163
  }
82
164
 
165
+ hasCustomTextSize(): boolean {
166
+ return this.matchesTokenPattern(/^!?text(-|\[|$)/);
167
+ }
168
+
169
+ hasCustomRadius(): boolean {
170
+ return this.matchesTokenPattern(/^!?rounded(-|\[|$)/);
171
+ }
83
172
 
173
+ private hasClass(className: string): boolean {
174
+ return this.getClassTokens().includes(className);
175
+ }
176
+
177
+ private getActiveVariant(): string {
178
+ const variants = Object.keys(this.colorsService.variants).sort((a, b) => b.length - a.length);
179
+ return variants.find((variant) => this.hasClass(variant)) ?? 'default';
180
+ }
84
181
 
85
- /**
86
- * Handle click event on the button.
87
- * Emits the clicked event if the button is not disabled and not loading.
88
- * @param event The click event
89
- */
90
182
  handleClick(event: Event) {
91
183
  if (!this.disabled && !this.isLoading) {
92
- this.clicked.emit(event)
184
+ this.clicked.emit(event);
93
185
  }
94
186
  }
95
-
96
187
  }
@@ -48,6 +48,8 @@ export class JCompleteCrudCardComponent implements OnInit {
48
48
  };
49
49
 
50
50
  @Input() endpoint!: string;
51
+ // Clave alternativa de response.data cuando difiere del mainEndpoint (ej. reminders en reminder/mine)
52
+ @Input() responseKey?: string;
51
53
  mainEndpoint!: string;
52
54
  @Input() columns: TableColumn<any>[] = [];
53
55
  @Input() defaultFilters: { [key: string]: any } = {};
@@ -177,7 +179,9 @@ export class JCompleteCrudCardComponent implements OnInit {
177
179
  // setTimeout(() => {
178
180
  this.genericService.findAll<any>({ endpoint: this.endpoint, params }).subscribe({
179
181
  next: (response) => {
180
- this.data = response.data[this.mainEndpoint] ?? [];
182
+ // Usa responseKey si la API devuelve una colección con nombre distinto al endpoint
183
+ const dataKey = this.responseKey ?? this.mainEndpoint;
184
+ this.data = response.data[dataKey] ?? [];
181
185
 
182
186
  if (response.meta?.page) {
183
187
  this.totalItems = response.meta.page.totalRecords;
@@ -10,7 +10,7 @@
10
10
  >
11
11
  <!-- Slider background -->
12
12
  <div class="absolute inset-0 rounded-full transition-all duration-300"
13
- [ngClass]="isChecked ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-500'">
13
+ [ngClass]="isChecked ? 'bg-dark-primary dark:bg-primary' : 'bg-gray-300 dark:bg-gray-500'">
14
14
  </div>
15
15
 
16
16
  <!-- Circle -->
@@ -95,7 +95,7 @@
95
95
  [tooltipPosition]="button.tooltipPosition ?? 'top'"
96
96
  (clicked)="filterBottomClick(button)"
97
97
  [classes]="button.classes ?? ''"
98
- [ngClasses]="{ 'min-w-auto p-1! pl-2! pr-2!': true }"
98
+ [ngClasses]="{ 'min-w-auto p-1! pl-2! pr-2! w-[40px]! h-[40px]!': true }"
99
99
  />
100
100
 
101
101
  @if (button.type === "upload") {