tailjng 0.1.6 → 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 -2
  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.6.tgz +0 -0
@@ -0,0 +1,25 @@
1
+ import { LucideIconData } from 'lucide-angular';
2
+ import { Icons } from '../../.config/icons/icons.lucide';
3
+
4
+ export type AlertToastType =
5
+ | 'success'
6
+ | 'error'
7
+ | 'warning'
8
+ | 'info'
9
+ | 'question'
10
+ | 'loading';
11
+
12
+ export type AlertToastPosition =
13
+ | 'top-right'
14
+ | 'top-left'
15
+ | 'bottom-right'
16
+ | 'bottom-left';
17
+
18
+ export const ALERT_TOAST_TYPE_ICONS: Record<AlertToastType, LucideIconData> = {
19
+ success: Icons.CircleCheck,
20
+ error: Icons.CircleX,
21
+ warning: Icons.TriangleAlert,
22
+ info: Icons.Info,
23
+ question: Icons.CircleHelp,
24
+ loading: Icons.Loader2,
25
+ };
@@ -1,38 +1,51 @@
1
1
  <div
2
2
  [style]="{
3
- 'opacity': disabled ? 0.2 : 1,
4
- 'cursor': hasClickHandler ? 'pointer' : 'default'
3
+ opacity: disabled ? 0.2 : 1,
4
+ cursor: hasClickHandler && !disabled && !isLoading ? 'pointer' : 'default'
5
5
  }"
6
6
  [jTooltip]="tooltip"
7
7
  [jTooltipPosition]="tooltipPosition"
8
+ [attr.role]="hasClickHandler ? 'button' : null"
9
+ [attr.tabindex]="hasClickHandler && !disabled && !isLoading ? 0 : null"
10
+ [attr.aria-label]="resolvedAriaLabel"
11
+ [attr.aria-busy]="isLoading ? true : null"
12
+ [attr.aria-disabled]="disabled ? true : null"
8
13
  (click)="handleClick($event)"
9
- >
14
+ >
10
15
  <div
11
- class="flex justify-center items-center border-2 rounded-full"
16
+ class="flex justify-center items-center border-2 rounded-full select-none"
12
17
  [style]="{
13
- 'width': size + 'px',
14
- 'height': size + 'px',
15
- 'font-size': textSize + 'px',
16
- 'padding': '0',
17
- 'position': 'relative'
18
+ width: size + 'px',
19
+ height: size + 'px',
20
+ fontSize: resolvedTextSize + 'px',
21
+ padding: '0',
22
+ position: 'relative'
18
23
  }"
19
24
  [ngClass]="computedClasses"
20
25
  [class]="classes"
21
26
  >
22
- <span class="font-bold"
23
- [style]="{
24
- 'position': 'absolute',
25
- 'left': '50%',
26
- 'top': '50%',
27
- 'transform': 'translate(-50%, -50%)',
28
- 'transform-origin': 'center',
29
- 'margin-left': textOffsetX + 'px',
30
- 'margin-top': textOffsetY + 'px'
31
- }">
27
+ <span
28
+ class="font-bold inline-flex items-center justify-center"
29
+ [style]="{
30
+ position: 'absolute',
31
+ left: '50%',
32
+ top: '50%',
33
+ transform: 'translate(-50%, -50%)',
34
+ transformOrigin: 'center',
35
+ marginLeft: textOffsetX + 'px',
36
+ marginTop: textOffsetY + 'px'
37
+ }"
38
+ >
32
39
  @if (isLoading) {
33
- <lucide-icon [name]="iconsService.icons.loading" [size]="textSize + 'px'" class="animate-spin" />
40
+ <JIcon
41
+ [icon]="Icons.Loader2"
42
+ [size]="resolvedLoaderSize"
43
+ iconClass="animate-spin shrink-0"
44
+ [inheritParentColor]="true"
45
+ [ariaHidden]="true"
46
+ />
34
47
  } @else {
35
- {{value}}
48
+ {{ value }}
36
49
  }
37
50
  </span>
38
51
  </div>
@@ -1,64 +1,173 @@
1
+ import { NgClass } from '@angular/common';
1
2
  import { Component, EventEmitter, Input, Output } from '@angular/core';
3
+ import { JColorsService } from 'tailjng';
4
+ import { Icons } from '../.config/icons/icons.lucide';
5
+ import { JIconComponent } from '../icon/icon.component';
2
6
  import { JTooltipDirective } from '../tooltip/tooltip.directive';
3
- import { JIconsService, JColorsService } from 'tailjng';
4
- import { LucideAngularModule } from 'lucide-angular';
5
- import { NgClass } from '@angular/common';
7
+ import { TooltipPosition } from '../tooltip/tooltip.types';
6
8
 
9
+ export type { TooltipPosition as JBadgeTooltipPosition } from '../tooltip/tooltip.types';
10
+
11
+ /**
12
+ * Circular badge with color variants, optional tooltip, and click support.
13
+ *
14
+ * Install: `npx tailjng add badge`
15
+ */
7
16
  @Component({
8
17
  selector: 'JBadge',
9
- imports: [JTooltipDirective, LucideAngularModule, NgClass],
18
+ imports: [JTooltipDirective, JIconComponent, NgClass],
10
19
  templateUrl: './badge.component.html',
11
- styleUrl: './badge.component.css'
20
+ styleUrl: './badge.component.css',
12
21
  })
13
22
  export class JBadgeComponent {
14
- @Input() value: string = 'J';
15
- @Input() size: number = 20;
16
- @Input() textSize: number = 12;
17
- @Input() tooltipPosition: 'top' | 'right' | 'bottom' | 'left' = 'top';
18
- @Input() tooltip: string = '';
19
- @Input() textOffsetX: number = 0;
20
- @Input() textOffsetY: number = 0;
21
- @Input() disabled: boolean = false;
22
- @Input() isLoading: boolean = false;
23
- @Input() classes: string = "";
24
- @Input() ngClasses: { [key: string]: boolean } = {};
23
+ // Project Icons registry — template usage: Icons.Loader2
24
+ readonly Icons = Icons;
25
+
26
+ // Label shown inside the badge (text or short counter)
27
+ @Input() value = 'J';
28
+
29
+ // Badge diameter in px
30
+ @Input() size = 20;
31
+
32
+ // Label font size in px; 0 = derived from `size`
33
+ @Input() textSize = 0;
34
+
35
+ // Tooltip position (jTooltip)
36
+ @Input() tooltipPosition: TooltipPosition = 'top';
37
+
38
+ // Tooltip text on hover / long-press
39
+ @Input() tooltip = '';
40
+
41
+ // Fine-tune label position inside the circle (px)
42
+ @Input() textOffsetX = 0;
43
+ @Input() textOffsetY = 0;
44
+
45
+ @Input() disabled = false;
46
+ @Input() isLoading = false;
47
+
48
+ // Color variant (primary, success_soft, …) + extra Tailwind classes
49
+ @Input() classes = '';
50
+
51
+ // Extra conditional classes (same role as [ngClass] on the inner circle)
52
+ @Input() ngClasses: Record<string, boolean> = {};
53
+
54
+ // Optional explicit accessible name when clickable
55
+ @Input() ariaLabel?: string;
25
56
 
26
57
  @Output() clicked = new EventEmitter<Event>();
27
58
 
59
+ constructor(private readonly colorsService: JColorsService) {}
60
+
61
+ /**
62
+ * Whether a `(clicked)` listener is attached.
63
+ * @returns `true` when the badge is interactive.
64
+ */
28
65
  get hasClickHandler(): boolean {
29
66
  return this.clicked.observers.length > 0;
30
67
  }
31
68
 
32
- constructor(
33
- public readonly iconsService: JIconsService,
34
- private readonly colorsService: JColorsService,
35
- ) { }
36
-
69
+ /**
70
+ * Tailwind classes for the active color variant without shadow utilities.
71
+ * @returns Variant string from `JColorsService`.
72
+ */
37
73
  get variantClasses(): string {
38
- return this.colorsService.getVariantClass(this.getActiveVariant(), false, true);
74
+ const base =
75
+ this.colorsService.variants[this.getActiveVariant()] ??
76
+ this.colorsService.variants['default'] ??
77
+ 'text-black dark:text-white border border-dark-border dark:border-border';
78
+
79
+ return base.replace(/\bshadow-\S+/g, '').replace(/\s{2,}/g, ' ').trim();
39
80
  }
40
81
 
41
- get computedClasses() {
82
+ /**
83
+ * Class map for the inner circle `[ngClass]`.
84
+ * @returns Object ready for `ngClass`.
85
+ */
86
+ get computedClasses(): Record<string, boolean> {
42
87
  return {
43
- "flex flex-col gap-1": true,
44
88
  ...this.ngClasses,
45
89
  [this.variantClasses]: true,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Final label font size in px.
95
+ * @returns `textSize` when greater than 0; otherwise derived from `size`.
96
+ */
97
+ get resolvedTextSize(): number {
98
+ if (this.textSize > 0) {
99
+ return this.textSize;
46
100
  }
101
+ return Math.max(10, Math.round(this.size * 0.55));
47
102
  }
48
103
 
49
- private hasClass(className: string): boolean {
50
- const classArray = this.classes.split(" ");
51
- return classArray.includes(className) || this.ngClasses[className];
104
+ /**
105
+ * Spinner size while loading.
106
+ * @returns Same as `resolvedTextSize`.
107
+ */
108
+ get resolvedLoaderSize(): number {
109
+ return this.resolvedTextSize;
52
110
  }
53
111
 
54
- private getActiveVariant(): string {
55
- const variant = Object.keys(this.colorsService.variants).find((variant) => this.hasClass(variant));
56
- return variant ?? "default";
112
+ /**
113
+ * Resolves `aria-label` for interactive badges.
114
+ * Priority: `ariaLabel` → `tooltip` → `value` (when clickable).
115
+ * @returns Accessible text or `null` when not applicable.
116
+ */
117
+ get resolvedAriaLabel(): string | null {
118
+ if (this.ariaLabel) {
119
+ return this.ariaLabel;
120
+ }
121
+ if (this.tooltip) {
122
+ return this.tooltip;
123
+ }
124
+ if (this.hasClickHandler && this.value) {
125
+ return this.value;
126
+ }
127
+ return null;
57
128
  }
58
129
 
130
+ /**
131
+ * Handles badge click.
132
+ * @param event Native click event.
133
+ * @returns Emits `clicked` when not `disabled`, not loading, and a listener exists.
134
+ */
59
135
  handleClick(event: Event) {
60
- if (!this.isLoading && this.hasClickHandler) {
136
+ if (!this.disabled && !this.isLoading && this.hasClickHandler) {
61
137
  this.clicked.emit(event);
62
138
  }
63
139
  }
140
+
141
+ /**
142
+ * Combines tokens from `classes` and active keys from `ngClasses`.
143
+ * @returns Flat list of class names.
144
+ */
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];
151
+ }
152
+
153
+ /**
154
+ * Whether an exact token is present in `classes` or `ngClasses`.
155
+ * @param className Class name to look for (e.g. `primary_soft`).
156
+ * @returns `true` when the token exists.
157
+ */
158
+ private hasClass(className: string): boolean {
159
+ return this.getClassTokens().includes(className);
160
+ }
161
+
162
+ /**
163
+ * Resolves the color variant used by the badge.
164
+ * Finds the longest matching variant name in `classes`.
165
+ * @returns Variant key (e.g. `primary`, `success_soft`) or `default`.
166
+ */
167
+ private getActiveVariant(): string {
168
+ const variants = Object.keys(this.colorsService.variants).sort(
169
+ (a, b) => b.length - a.length,
170
+ );
171
+ return variants.find((variant) => this.hasClass(variant)) ?? 'default';
172
+ }
64
173
  }
@@ -3,6 +3,8 @@
3
3
  [jTooltip]="tooltip"
4
4
  [jTooltipPosition]="tooltipPosition"
5
5
  [disabled]="disabled || isLoading"
6
+ [attr.aria-label]="resolvedAriaLabel"
7
+ [attr.aria-busy]="isLoading ? true : null"
6
8
  [ngClass]="computedClasses"
7
9
  [class]="classes"
8
10
  (click)="handleClick($event)"
@@ -12,22 +14,26 @@
12
14
  [ngClass]="innerContentClasses"
13
15
  >
14
16
  @if (isLoading) {
15
- <lucide-icon
16
- [name]="iconsService.icons.loading"
17
+ <JIcon
18
+ [icon]="Icons.Loader2"
17
19
  [size]="resolvedIconSize"
18
- class="animate-spin shrink-0"
20
+ iconClass="animate-spin shrink-0"
21
+ [inheritParentColor]="true"
22
+ [ariaHidden]="true"
19
23
  />
20
24
  }
21
25
 
22
- @if (!isLoading && icon) {
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
- }
26
+ @if (!isLoading && displayIcon) {
27
+ <JIcon
28
+ [icon]="displayIcon"
29
+ [size]="resolvedIconSize"
30
+ iconClass="shrink-0"
31
+ [inheritParentColor]="true"
32
+ [ariaHidden]="true"
33
+ />
28
34
  }
29
35
 
30
- @if (text && (!isLoading || isLoadingText)) {
36
+ @if (text !== '' && text !== null && text !== undefined && (!isLoading || isLoadingText)) {
31
37
  <span>{{ text }}</span>
32
38
  }
33
39
 
@@ -1,64 +1,144 @@
1
1
  import { NgClass } from '@angular/common';
2
2
  import { Component, Input, Output, EventEmitter } from '@angular/core';
3
- import { LucideAngularModule } from 'lucide-angular';
4
- import { JIconsService, JColorsService } from 'tailjng';
3
+ import { LucideIconData } from 'lucide-angular';
4
+ import { JColorsService } from 'tailjng';
5
+ import { Icons } from '../.config/icons/icons.lucide';
6
+ import { JIconComponent } from '../icon/icon.component';
5
7
  import { JTooltipDirective } from '../tooltip/tooltip.directive';
6
8
 
7
9
  export type JButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon';
10
+ export type JButtonIcon = LucideIconData;
8
11
 
9
12
  @Component({
10
13
  selector: 'JButton',
11
- imports: [NgClass, LucideAngularModule, JTooltipDirective],
14
+ imports: [NgClass, JIconComponent, JTooltipDirective],
12
15
  templateUrl: './button.component.html',
13
16
  styleUrl: './button.component.css',
14
17
  })
15
18
  export class JButtonComponent {
19
+ // Project Icons registry — template usage: Icons.Loader2, Icons.Save, …
20
+ readonly Icons = Icons;
16
21
 
22
+ // button | submit | reset
17
23
  @Input() type: 'button' | 'submit' | 'reset' = 'button';
24
+
25
+ // Tooltip position (jTooltip)
18
26
  @Input() tooltipPosition: 'top' | 'right' | 'bottom' | 'left' = 'top';
19
27
 
20
- @Input() text!: string | number;
21
- @Input() tooltip: string = '';
28
+ // Visible label; empty = icon-only
29
+ @Input() text: string | number = '';
30
+
31
+ // Tooltip text; on icon-only buttons also used as aria-label fallback
32
+ @Input() tooltip = '';
33
+
34
+ // Primary icon (Icons.* from .config/icons)
35
+ @Input() icon?: JButtonIcon;
22
36
 
23
- @Input() icon!: any;
24
- @Input() iconSize: number = 0;
25
- @Input() iconChange!: any;
26
- @Input() isChangeIcon: boolean = false;
37
+ // Alternate icon when isChangeIcon is true (e.g. copy → check)
38
+ @Input() iconChange?: JButtonIcon;
27
39
 
28
- /** xs/sm: alertas y cards · md: formularios · lg: acciones destacadas · icon: solo icono */
40
+ // true = render iconChange instead of icon
41
+ @Input() isChangeIcon = false;
42
+
43
+ // Icon size in px; 0 = automatic based on size
44
+ @Input() iconSize = 0;
45
+
46
+ // xs/sm: alerts · md: forms · lg: actions · icon: square icon-only
29
47
  @Input() size: JButtonSize = 'md';
30
48
 
31
49
  @Output() clicked = new EventEmitter<Event>();
32
50
 
33
51
  @Input() disabled = false;
34
52
  @Input() isLoading = false;
53
+
54
+ // false = hide label while loading (spinner only)
35
55
  @Input() isLoadingText = true;
36
56
 
37
- @Input() classes: string = '';
38
- @Input() ngClasses: { [key: string]: boolean } = {};
57
+ // Color variant (primary, success_soft, …) + extra Tailwind classes
58
+ @Input() classes = '';
59
+
60
+ // Extra conditional classes (same role as [ngClass] on the host)
61
+ @Input() ngClasses: Record<string, boolean> = {};
62
+
63
+ // Icon-only: explicit aria-label; falls back to tooltip when omitted
64
+ @Input() ariaLabel?: string;
65
+
66
+ constructor(private readonly colorsService: JColorsService) {}
67
+
68
+ /**
69
+ * Icon rendered in the template.
70
+ * Uses `iconChange` when `isChangeIcon` is true; otherwise `icon`.
71
+ * @returns Active Lucide reference or `undefined` when no icon is set.
72
+ */
73
+ get displayIcon(): JButtonIcon | undefined {
74
+ if (this.isChangeIcon && this.iconChange) {
75
+ return this.iconChange;
76
+ }
77
+ return this.icon;
78
+ }
79
+
80
+ /**
81
+ * Whether the button shows no visible text.
82
+ * @returns `true` with `size="icon"` or when `icon` is set and `text` is empty.
83
+ */
84
+ get isIconOnly(): boolean {
85
+ return this.size === 'icon' || (!!this.icon && this.text === '');
86
+ }
39
87
 
40
- constructor(
41
- public readonly iconsService: JIconsService,
42
- private readonly colorsService: JColorsService,
43
- ) { }
88
+ /**
89
+ * Resolves the `<button>` element `aria-label`.
90
+ * Priority: `ariaLabel` → `tooltip` (icon-only) → `text` (icon-only).
91
+ * @returns Accessible text or `null` when not applicable.
92
+ */
93
+ get resolvedAriaLabel(): string | null {
94
+ if (this.ariaLabel) {
95
+ return this.ariaLabel;
96
+ }
97
+ if (this.isIconOnly && this.tooltip) {
98
+ return this.tooltip;
99
+ }
100
+ if (this.isIconOnly && this.text !== '') {
101
+ return String(this.text);
102
+ }
103
+ return null;
104
+ }
44
105
 
106
+ /**
107
+ * Tailwind classes for the active color variant (`JColorsService`).
108
+ * @returns Tailwind utility string for background, text, and border.
109
+ */
45
110
  get variantClasses(): string {
46
111
  return this.colorsService.variants[this.getActiveVariant()]
47
112
  ?? 'text-black dark:text-white shadow-md border border-border dark:border-dark-border';
48
113
  }
49
114
 
115
+ /**
116
+ * Final icon size in pixels.
117
+ * @returns `iconSize` when greater than 0; otherwise the default for `size`.
118
+ */
50
119
  get resolvedIconSize(): number {
51
- if (this.iconSize > 0) return this.iconSize;
120
+ if (this.iconSize > 0) {
121
+ return this.iconSize;
122
+ }
52
123
 
53
124
  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;
125
+ case 'xs':
126
+ case 'sm':
127
+ return 14;
128
+ case 'icon':
129
+ return 16;
130
+ case 'lg':
131
+ return 18;
132
+ default:
133
+ return 15;
59
134
  }
60
135
  }
61
136
 
137
+ /**
138
+ * Host class map for `[ngClass]`.
139
+ * Skips dimensions, padding, radius, and text when already set in `classes` / `ngClasses`.
140
+ * @returns Class → boolean object ready for `ngClass`.
141
+ */
62
142
  get computedClasses() {
63
143
  const result: Record<string, boolean> = {
64
144
  'inline-flex items-center justify-center font-semibold border border-border dark:border-dark-border transition duration-300 select-none': true,
@@ -78,6 +158,10 @@ export class JButtonComponent {
78
158
  return result;
79
159
  }
80
160
 
161
+ /**
162
+ * Gap between icon and label inside the button.
163
+ * @returns `gap-*` class map based on `size`.
164
+ */
81
165
  get innerContentClasses(): Record<string, boolean> {
82
166
  return {
83
167
  'gap-1': this.size === 'xs' || this.size === 'icon',
@@ -86,6 +170,10 @@ export class JButtonComponent {
86
170
  };
87
171
  }
88
172
 
173
+ /**
174
+ * Button height and minimum width classes.
175
+ * @returns Tailwind utilities such as `h-*`, `min-h-*`, `min-w-*`, etc.
176
+ */
89
177
  get dimensionClasses(): string {
90
178
  switch (this.size) {
91
179
  case 'xs':
@@ -104,6 +192,10 @@ export class JButtonComponent {
104
192
  }
105
193
  }
106
194
 
195
+ /**
196
+ * Button padding classes.
197
+ * @returns Tailwind `px-*`, `py-*`, or `p-0` for `size="icon"`.
198
+ */
107
199
  get paddingClasses(): string {
108
200
  switch (this.size) {
109
201
  case 'xs':
@@ -120,6 +212,10 @@ export class JButtonComponent {
120
212
  }
121
213
  }
122
214
 
215
+ /**
216
+ * Label font-size classes.
217
+ * @returns `text-*` / `leading-*` utilities; empty string for `size="icon"`.
218
+ */
123
219
  get textSizeClasses(): string {
124
220
  switch (this.size) {
125
221
  case 'xs':
@@ -136,12 +232,20 @@ export class JButtonComponent {
136
232
  }
137
233
  }
138
234
 
235
+ /**
236
+ * Border-radius classes.
237
+ * @returns `rounded-lg` for xs/sm/icon; `rounded-md` for md/lg.
238
+ */
139
239
  get radiusClasses(): string {
140
240
  return this.size === 'icon' || this.size === 'xs' || this.size === 'sm'
141
241
  ? 'rounded-lg'
142
242
  : 'rounded-md';
143
243
  }
144
244
 
245
+ /**
246
+ * Merges tokens from `classes` and active keys from `ngClasses`.
247
+ * @returns Flat list of Tailwind class names.
248
+ */
145
249
  private getClassTokens(): string[] {
146
250
  const fromClasses = this.classes.split(/\s+/).filter(Boolean);
147
251
  const fromNgClasses = Object.entries(this.ngClasses)
@@ -150,35 +254,71 @@ export class JButtonComponent {
150
254
  return [...fromClasses, ...fromNgClasses];
151
255
  }
152
256
 
257
+ /**
258
+ * Checks whether any token matches a Tailwind pattern.
259
+ * @param pattern Regular expression over class names.
260
+ * @returns `true` when at least one token matches.
261
+ */
153
262
  private matchesTokenPattern(pattern: RegExp): boolean {
154
263
  return this.getClassTokens().some((token) => pattern.test(token));
155
264
  }
156
265
 
266
+ /**
267
+ * Whether the consumer already defined dimensions in `classes` / `ngClasses`.
268
+ * @returns `true` when tokens such as `h-*`, `w-*`, `min-h-*`, `size-*` exist.
269
+ */
157
270
  hasCustomDimensions(): boolean {
158
271
  return this.matchesTokenPattern(/^!?((min|max)-)?[wh](-|\[|$)|^!?size-/);
159
272
  }
160
273
 
274
+ /**
275
+ * Whether the consumer already defined padding in `classes` / `ngClasses`.
276
+ * @returns `true` when tokens such as `p-*`, `px-*`, `py-*` exist.
277
+ */
161
278
  hasCustomPadding(): boolean {
162
279
  return this.matchesTokenPattern(/^!?p[xytblr]?(-|\[|$)|^!?p$/);
163
280
  }
164
281
 
282
+ /**
283
+ * Whether the consumer already defined text size in `classes` / `ngClasses`.
284
+ * @returns `true` when `text-*` tokens exist.
285
+ */
165
286
  hasCustomTextSize(): boolean {
166
287
  return this.matchesTokenPattern(/^!?text(-|\[|$)/);
167
288
  }
168
289
 
290
+ /**
291
+ * Whether the consumer already defined border-radius in `classes` / `ngClasses`.
292
+ * @returns `true` when `rounded-*` tokens exist.
293
+ */
169
294
  hasCustomRadius(): boolean {
170
295
  return this.matchesTokenPattern(/^!?rounded(-|\[|$)/);
171
296
  }
172
297
 
298
+ /**
299
+ * Whether an exact token is present in `classes` or `ngClasses`.
300
+ * @param className Class name to look for (e.g. `primary_soft`).
301
+ * @returns `true` when the token exists.
302
+ */
173
303
  private hasClass(className: string): boolean {
174
304
  return this.getClassTokens().includes(className);
175
305
  }
176
306
 
307
+ /**
308
+ * Resolves the color variant used by the button.
309
+ * Finds the longest matching variant name in `classes`.
310
+ * @returns Variant key (e.g. `primary`, `success_soft`) or `default`.
311
+ */
177
312
  private getActiveVariant(): string {
178
313
  const variants = Object.keys(this.colorsService.variants).sort((a, b) => b.length - a.length);
179
314
  return variants.find((variant) => this.hasClass(variant)) ?? 'default';
180
315
  }
181
316
 
317
+ /**
318
+ * Handles the button click.
319
+ * @param event Native click event.
320
+ * @returns Emits `clicked` only when not `disabled` or `isLoading`.
321
+ */
182
322
  handleClick(event: Event) {
183
323
  if (!this.disabled && !this.isLoading) {
184
324
  this.clicked.emit(event);
@@ -46,7 +46,7 @@
46
46
  @if (isLoading('initialLoad') && displayData.length === 0) {
47
47
  <div class="w-full flex justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border">
48
48
  <div class="flex flex-col gap-3 items-center justify-center py-4">
49
- <lucide-icon [name]="iconsService.icons.loading" size="30" class="text-primary animate-spin"></lucide-icon>
49
+ <JIcon [icon]="Icons.Loader2" size="30" iconClass="text-primary animate-spin" [ariaHidden]="true" />
50
50
  <p>Cargando datos...</p>
51
51
  </div>
52
52
  </div>
@@ -83,7 +83,7 @@
83
83
 
84
84
  } @else if (!isLoading('pagination')) {
85
85
  <div class="w-full flex flex-col gap-3 justify-center items-center h-100 bg-white dark:bg-foreground rounded-[20px] border border-border dark:border-dark-border hover:border-primary/50 hover:dark:border-primary">
86
- <lucide-icon [name]="iconsService.icons.info" size="35" class="text-primary"></lucide-icon>
86
+ <JIcon [icon]="Icons.Info" size="35" iconClass="text-primary" [ariaHidden]="true" />
87
87
  <p class="text-black dark:text-white">No hay datos disponibles</p>
88
88
  </div>
89
89
  }