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
@@ -1,25 +1,26 @@
1
- <label
2
- [for]="for"
3
- [ngClass]="ngClass"
1
+ <label
2
+ [attr.for]="for || null"
3
+ [ngClass]="computedClasses"
4
4
  [class]="classes"
5
- class="flex gap-1 items-center text-black dark:text-white"
6
5
  >
7
6
  <ng-content></ng-content>
8
7
 
9
- @if (isRequired || isConditioned || isAutomated) {
10
- <div class="text-[8px]">
11
- <span [ngClass]="{
12
- 'text-red-600 dark:text-red-300': isRequired,
13
- 'text-blue-600 dark:text-blue-300': !isRequired && isConditioned,
14
- 'text-purple-600 dark:text-purple-300': !isRequired && !isConditioned && isAutomated
15
- }">✱</span>
16
- </div>
8
+ @if (showIndicator) {
9
+ <span class="text-[8px]" [ngClass]="indicatorClasses" aria-hidden="true">✱</span>
17
10
  }
18
11
 
19
12
  @if (tooltip) {
20
- <span [jTooltip]="tooltip" [jTooltipPosition]="tooltipPosition" >
21
- <lucide-icon [name]="iconsService.icons.info" [size]="15" />
13
+ <span
14
+ class="inline-flex shrink-0 items-center"
15
+ [jTooltip]="tooltip"
16
+ [jTooltipPosition]="tooltipPosition"
17
+ >
18
+ <JIcon
19
+ [icon]="Icons.Info"
20
+ [size]="15"
21
+ [inheritParentColor]="true"
22
+ [ariaHidden]="true"
23
+ />
22
24
  </span>
23
25
  }
24
-
25
- </label>
26
+ </label>
@@ -1,27 +1,81 @@
1
1
  import { NgClass } from '@angular/common';
2
2
  import { Component, Input } from '@angular/core';
3
+ import { Icons } from '../.config/icons/icons.lucide';
4
+ import { JIconComponent } from '../icon/icon.component';
3
5
  import { JTooltipDirective } from '../tooltip/tooltip.directive';
4
- import { JIconsService } from 'tailjng';
5
- import { LucideAngularModule } from 'lucide-angular';
6
+ import { TooltipPosition } from '../tooltip/tooltip.types';
7
+ import { LabelIndicator } from './label.types';
6
8
 
9
+ export type { LabelIndicator } from './label.types';
10
+ export type { TooltipPosition as JLabelTooltipPosition } from '../tooltip/tooltip.types';
11
+
12
+ /**
13
+ * Form field label with optional indicators and info tooltip.
14
+ *
15
+ * Install: `npx tailjng add label`
16
+ *
17
+ * ```html
18
+ * <JLabel indicator="required">Nombre</JLabel>
19
+ * <JLabel indicator="conditioned">Teléfono</JLabel>
20
+ * <JLabel indicator="automated" tooltip="Generado por el sistema">Código</JLabel>
21
+ * ```
22
+ */
7
23
  @Component({
8
24
  selector: 'JLabel',
9
- imports: [NgClass, JTooltipDirective, LucideAngularModule],
25
+ imports: [NgClass, JTooltipDirective, JIconComponent],
10
26
  templateUrl: './label.component.html',
11
- styleUrl: './label.component.css'
27
+ styleUrl: './label.component.css',
12
28
  })
13
29
  export class JLabelComponent {
30
+ // Project Icons registry — template usage: Icons.Info
31
+ readonly Icons = Icons;
32
+
33
+ // Tooltip position (jTooltip) on the info icon
34
+ @Input() tooltipPosition: TooltipPosition = 'top';
35
+
36
+ // Tooltip text; when set, shows an info icon
37
+ @Input() tooltip = '';
38
+
39
+ // Associates the label with a control id (`for` attribute)
40
+ @Input() for = '';
41
+
42
+ // Extra Tailwind classes on the `<label>`
43
+ @Input() classes = '';
44
+
45
+ // Extra conditional classes (same role as [ngClass] on the host)
46
+ @Input() ngClasses: Record<string, boolean> = {};
47
+
48
+ // required = red · conditioned = blue · automated = purple · null = none
49
+ @Input() indicator: LabelIndicator | null = null;
50
+
51
+ /**
52
+ * Whether the indicator asterisk should render.
53
+ * @returns `true` when `indicator` is set.
54
+ */
55
+ get showIndicator(): boolean {
56
+ return this.indicator != null;
57
+ }
58
+
59
+ /**
60
+ * Host class map for `[ngClass]`.
61
+ * @returns Base label layout and theme text colors merged with `ngClasses`.
62
+ */
63
+ get computedClasses(): Record<string, boolean> {
64
+ return {
65
+ 'flex gap-1 items-center text-black dark:text-white': true,
66
+ ...this.ngClasses,
67
+ };
68
+ }
14
69
 
15
- constructor(public iconsService: JIconsService) { }
16
-
17
- @Input() tooltipPosition: 'top' | 'right' | 'bottom' | 'left' = 'top';
18
- @Input() tooltip: string = '';
19
- @Input() for: string = '';
20
-
21
- @Input() classes: string = '';
22
- @Input() ngClass: { [key: string]: boolean } = {};
23
-
24
- @Input() isRequired: boolean = false;
25
- @Input() isConditioned: boolean = false;
26
- @Input() isAutomated: boolean = false;
70
+ /**
71
+ * Color classes for the indicator asterisk based on `indicator`.
72
+ * @returns Class map for the indicator `<span>`.
73
+ */
74
+ get indicatorClasses(): Record<string, boolean> {
75
+ return {
76
+ 'text-red-600 dark:text-red-300': this.indicator === 'required',
77
+ 'text-blue-600 dark:text-blue-300': this.indicator === 'conditioned',
78
+ 'text-purple-600 dark:text-purple-300': this.indicator === 'automated',
79
+ };
80
+ }
27
81
  }
@@ -0,0 +1,2 @@
1
+ /** Field indicator shown as a colored asterisk next to the label text. */
2
+ export type LabelIndicator = 'required' | 'conditioned' | 'automated';
@@ -0,0 +1,34 @@
1
+ import { LucideIconData } from 'lucide-angular';
2
+ import {
3
+ OptionsTableMenuClassesResolver,
4
+ OptionsTableMenuIconResolver,
5
+ } from './options-table-menu.types';
6
+
7
+ export const defaultMenuIconResolver: OptionsTableMenuIconResolver = (icon, group) => {
8
+ if (typeof icon === 'function') {
9
+ return icon(group) as LucideIconData | undefined;
10
+ }
11
+
12
+ return icon as LucideIconData | undefined;
13
+ };
14
+
15
+ export const defaultMenuNgClassesResolver: OptionsTableMenuClassesResolver = (
16
+ ngClass,
17
+ group,
18
+ ) => {
19
+ let resolved: unknown = ngClass;
20
+
21
+ if (typeof ngClass === 'function') {
22
+ resolved = ngClass(group);
23
+ }
24
+
25
+ if (typeof resolved === 'string' && resolved.length > 0) {
26
+ return { [resolved]: true };
27
+ }
28
+
29
+ if (typeof resolved === 'object' && resolved) {
30
+ return resolved as Record<string, boolean>;
31
+ }
32
+
33
+ return {};
34
+ };
@@ -1,37 +1,51 @@
1
-
2
- <div class="relative isolate">
3
- <JButton
4
- [icon]="iconsService.icons.ellipsisVertical"
5
- [iconSize]="20"
1
+ <div class="inline-flex relative">
2
+ <JButton
3
+ size="icon"
4
+ [icon]="Icons.EllipsisVertical"
5
+ [iconSize]="18"
6
+ [ariaLabel]="'Acciones de fila'"
7
+ classes="secondary rounded-full"
8
+ [ngClasses]="triggerClasses()"
6
9
  (clicked)="onClickMenuOption($event)"
7
- classes="secondary w-[35px] h-[35px] rounded-full"
8
- [ngClasses]="{ 'bg-primary dark:bg-dark-primary/70 text-white hover:text-black dark:hover:text-white' : isActiveMenuOption}"
9
10
  />
10
-
11
+
11
12
  @if (isActiveMenuOption) {
12
- <div #menuContainer class="absolute right-0 top-full mt-1 flex flex-col border border-border dark:border-dark-border w-[150px] bg-white dark:bg-dark-background rounded-lg shadow-lg overflow-hidden p-2 gap-2">
13
+ <div
14
+ #menuContainer
15
+ class="absolute right-0 top-[calc(100%+0.375rem)] z-[10000] flex flex-col gap-2 w-[10.625rem] p-1.5 overflow-hidden rounded-[0.625rem] border border-border dark:border-dark-border bg-background dark:bg-dark-background shadow-[0_10px_24px_rgb(0_0_0/0.12),0_2px_6px_rgb(0_0_0/0.06)] dark:shadow-[0_12px_28px_rgb(0_0_0/0.35),0_2px_8px_rgb(0_0_0/0.2)]"
16
+ [class.fixed]="isMenuPortaled"
17
+ role="menu"
18
+ [attr.aria-label]="'Acciones'"
19
+ >
13
20
  @if (optionsTable.length > 0) {
14
- @for (option of optionsTable; track $index) {
21
+ @for (option of optionsTable; track option.type ?? $index) {
15
22
  @if (getIsVisible(option, group)) {
16
- <div class="w-full hover:bg-primary/10 dark:hover:bg-dark-primary/10 text-[10px]">
17
- <JButton onKeyPress
23
+ <div
24
+ class="flex w-full min-w-0 rounded-lg transition-colors hover:bg-primary/10 dark:hover:bg-dark-primary/10"
25
+ role="none"
26
+ >
27
+ <JButton
28
+ size="sm"
18
29
  [icon]="getIcon(option.icon, group)"
19
- [iconSize]="20"
20
- (clicked)="$event.stopPropagation(); handleClick(option, group)"
30
+ [iconSize]="14"
21
31
  [tooltip]="getTooltip(option.tooltip ?? '', group)"
22
32
  [tooltipPosition]="option.tooltipPosition ?? 'top'"
23
33
  [disabled]="getDisabled(option, group)"
24
- [isLoading]="false"
25
- [classes]="'w-full text-left justify-start ' + (option.classes ?? '')"
26
- [ngClasses]="mergeNgClasses(option.ngClass, group)"
34
+ [isLoading]="isOptionLoading(option)"
35
+ [classes]="optionButtonClasses(option)"
36
+ [ngClasses]="optionColorClasses(option, group)"
37
+ (clicked)="handleClick(option, group, $event)"
27
38
  >
28
- {{ getTooltip(option.text ?? '', group) }}
39
+ {{ resolveOptionText(option, group) }}
29
40
  </JButton>
30
41
  </div>
31
42
  }
32
43
  }
33
- }@else {
34
- <div class="text-center text-gray-500 dark:text-dark-text-secondary text-xs">
44
+ } @else {
45
+ <div
46
+ class="py-2 px-1.5 text-center text-xs leading-5 text-muted-foreground dark:text-dark-muted-foreground"
47
+ role="status"
48
+ >
35
49
  No hay opciones disponibles
36
50
  </div>
37
51
  }
@@ -1,44 +1,96 @@
1
- import { Component, Input, Output, EventEmitter, ElementRef, OnDestroy, ViewChild, AfterViewInit, Renderer2 } from '@angular/core';
2
- import { LucideAngularModule } from 'lucide-angular';
1
+ import {
2
+ AfterViewInit,
3
+ Component,
4
+ ElementRef,
5
+ EventEmitter,
6
+ HostBinding,
7
+ HostListener,
8
+ Input,
9
+ OnDestroy,
10
+ Output,
11
+ Renderer2,
12
+ ViewChild} from '@angular/core';
13
+ import { OptionsTable } from 'tailjng';
14
+ import { Icons } from '../../.config/icons/icons.lucide';
3
15
  import { JButtonComponent } from '../../button/button.component';
4
- import { JIconsService } from 'tailjng';
16
+ import {
17
+ defaultMenuIconResolver,
18
+ defaultMenuNgClassesResolver} from './menu-options-defaults';
19
+ import {
20
+ OptionsTableMenuAlign,
21
+ OptionsTableMenuClassesResolver,
22
+ OptionsTableMenuClickEvent,
23
+ OptionsTableMenuDisabledResolver,
24
+ OptionsTableMenuIconResolver,
25
+ OptionsTableMenuLoadingResolver,
26
+ OptionsTableMenuTextResolver,
27
+ OptionsTableMenuVisibleResolver} from './options-table-menu.types';
5
28
 
29
+ export type {
30
+ OptionsTableMenuAlign,
31
+ OptionsTableMenuClickEvent} from './options-table-menu.types';
32
+
33
+ /**
34
+ * Row actions dropdown for tables. Portals the menu to `document.body` to avoid
35
+ * clipping inside scrollable table cells.
36
+ *
37
+ * Install: `npx tailjng add menu-options-table`
38
+ */
6
39
  @Component({
7
40
  selector: 'JOptionsTableMenu',
8
41
  standalone: true,
9
- imports: [LucideAngularModule, JButtonComponent],
42
+ imports: [JButtonComponent],
10
43
  templateUrl: './options-table-menu.component.html',
11
- styleUrls: ['./options-table-menu.component.scss']
12
- })
44
+ host: { class: 'inline-flex relative' }})
13
45
  export class JOptionsTableMenuComponent implements OnDestroy, AfterViewInit {
46
+ readonly Icons = Icons;
47
+
48
+ private static readonly MENU_Z_INDEX = 9999;
49
+ private static readonly BLOCKED_NG_CLASSES = new Set([
50
+ 'min-w-auto',
51
+ 'p-[1px]!',
52
+ 'pl-[5px]!',
53
+ 'pr-[5px]!',
54
+ ]);
14
55
 
15
- @Input() optionsTable: any[] = [];
16
- @Input() group: any;
56
+ @Input() optionsTable: OptionsTable[] = [];
57
+ @Input() group: unknown;
17
58
  @Input() rowId: string | number | null = null;
18
59
 
19
- @Input() getIcon: (icon: any, group: any) => any = () => null;
20
- @Input() getTooltip: (tooltip: string, group: any) => string = () => '';
21
- @Input() getDisabled: (option: any, group: any) => boolean = () => false;
22
- @Input() getIsVisible: (option: any, group: any) => boolean = () => true;
23
- @Input() mergeNgClasses: (ngClass: any, group: any) => any = () => '';
24
- @Input() isAditionalButtonLoading: (type: string, id: any) => boolean = () => false;
60
+ @Input() getIcon: OptionsTableMenuIconResolver = defaultMenuIconResolver;
61
+ @Input() getTooltip: OptionsTableMenuTextResolver = () => '';
62
+ @Input() getDisabled: OptionsTableMenuDisabledResolver = () => false;
63
+ @Input() getIsVisible: OptionsTableMenuVisibleResolver = () => true;
64
+ @Input() mergeNgClasses: OptionsTableMenuClassesResolver = defaultMenuNgClassesResolver;
65
+ @Input() isAditionalButtonLoading: OptionsTableMenuLoadingResolver = () => false;
66
+
67
+ /** Horizontal alignment of the panel relative to the trigger. */
68
+ @Input() align: OptionsTableMenuAlign = 'end';
25
69
 
26
- @Output() clicked = new EventEmitter<{ option: any; group: any }>();
70
+ @Input() closeOnEscape = true;
27
71
 
28
- @ViewChild('menuContainer', { static: false }) menuContainer!: ElementRef;
72
+ @Output() clicked = new EventEmitter<OptionsTableMenuClickEvent>();
73
+
74
+ @ViewChild('menuContainer', { static: false }) menuContainer?: ElementRef<HTMLElement>;
29
75
 
30
76
  isActiveMenuOption = false;
31
- menuPosition = { top: 0, left: 0 };
32
- buttonElement: HTMLElement | null = null;
33
77
 
34
- private static activeInstance: JOptionsTableMenuComponent | null = null;
78
+ @HostBinding('class.z-[10000]')
79
+ get hostElevated(): boolean {
80
+ return this.isActiveMenuOption;
81
+ }
82
+
83
+ get isMenuPortaled(): boolean {
84
+ return this.movedToBody;
85
+ }
35
86
 
87
+ private buttonElement: HTMLElement | null = null;
88
+ private static activeInstance: JOptionsTableMenuComponent | null = null;
36
89
  private movedToBody = false;
37
90
  private originalParent: Node | null = null;
38
91
 
39
92
  constructor(
40
- public readonly iconsService: JIconsService,
41
- private readonly elementRef: ElementRef,
93
+ private readonly elementRef: ElementRef<HTMLElement>,
42
94
  private readonly renderer: Renderer2,
43
95
  ) {
44
96
  this.handleDocumentClick = this.handleDocumentClick.bind(this);
@@ -57,56 +109,133 @@ export class JOptionsTableMenuComponent implements OnDestroy, AfterViewInit {
57
109
  }
58
110
  }
59
111
 
60
- private handleDocumentClick(event: Event): void {
61
- const target = event.target as HTMLElement;
62
- if (
63
- !this.elementRef.nativeElement.contains(target) &&
64
- this.menuContainer &&
65
- !this.menuContainer.nativeElement.contains(target) &&
66
- this.isActiveMenuOption
67
- ) {
68
- this.closeDropdown();
112
+ triggerClasses(): Record<string, boolean> {
113
+ return {
114
+ 'bg-primary': this.isActiveMenuOption,
115
+ 'dark:bg-dark-primary/70': this.isActiveMenuOption,
116
+ 'text-white': this.isActiveMenuOption,
117
+ 'hover:text-black': this.isActiveMenuOption,
118
+ 'dark:hover:text-white': this.isActiveMenuOption};
119
+ }
120
+
121
+ optionButtonClasses(option: OptionsTable): string {
122
+ return [
123
+ 'w-full',
124
+ 'min-w-0',
125
+ 'h-[30px]',
126
+ 'min-h-[30px]',
127
+ 'text-left',
128
+ 'justify-start',
129
+ 'text-[14px]',
130
+ 'leading-tight',
131
+ 'font-medium',
132
+ option.classes ?? '',
133
+ ]
134
+ .filter(Boolean)
135
+ .join(' ');
136
+ }
137
+
138
+ optionColorClasses(option: OptionsTable, group: unknown): Record<string, boolean> {
139
+ const merged = this.mergeNgClasses(option.ngClass, group);
140
+ const normalized: Record<string, boolean> = {
141
+ 'w-full': true,
142
+ 'min-w-0': true};
143
+
144
+ for (const [className, active] of Object.entries(merged)) {
145
+ if (
146
+ !active ||
147
+ className === 'text-destructive' ||
148
+ JOptionsTableMenuComponent.BLOCKED_NG_CLASSES.has(className)
149
+ ) {
150
+ continue;
151
+ }
152
+ normalized[className] = true;
153
+ }
154
+
155
+ if (merged['text-destructive']) {
156
+ normalized['text-red-600'] = true;
157
+ normalized['dark:text-red-300'] = true;
69
158
  }
159
+
160
+ return normalized;
70
161
  }
71
162
 
72
- onClickMenuOption(event?: Event): void {
73
- if (event) {
74
- event.stopPropagation();
163
+ resolveOptionText(option: OptionsTable, group: unknown): string {
164
+ const { text, tooltip } = option;
165
+
166
+ if (typeof text === 'function') {
167
+ return text(group);
168
+ }
169
+
170
+ if (typeof text === 'string' && text.length > 0) {
171
+ return text;
75
172
  }
76
173
 
174
+ return this.getTooltip(tooltip ?? '', group);
175
+ }
176
+
177
+ isOptionLoading(option: OptionsTable): boolean {
178
+ if (this.rowId == null) {
179
+ return this.isAditionalButtonLoading(String(option.type ?? ''));
180
+ }
181
+
182
+ return this.isAditionalButtonLoading(String(option.type ?? ''), this.rowId);
183
+ }
184
+
185
+ onClickMenuOption(event?: Event): void {
186
+ event?.stopPropagation();
187
+
77
188
  if (this.isActiveMenuOption) {
78
189
  this.closeDropdown();
79
190
  return;
80
191
  }
81
192
 
82
- if (JOptionsTableMenuComponent.activeInstance && JOptionsTableMenuComponent.activeInstance !== this) {
193
+ if (
194
+ JOptionsTableMenuComponent.activeInstance &&
195
+ JOptionsTableMenuComponent.activeInstance !== this
196
+ ) {
83
197
  JOptionsTableMenuComponent.activeInstance.closeDropdown();
84
198
  }
85
199
 
86
- this.calculateMenuPosition();
87
-
88
200
  this.isActiveMenuOption = true;
89
201
  JOptionsTableMenuComponent.activeInstance = this;
90
202
 
91
- setTimeout(() => {
203
+ queueMicrotask(() => {
92
204
  this.moveMenuToBody();
93
205
  this.applyMenuPosition();
94
- }, 0);
206
+ });
207
+ }
208
+
209
+ handleClick(option: OptionsTable, group: unknown, event?: Event): void {
210
+ event?.stopPropagation();
211
+ this.clicked.emit({ option, group });
212
+ this.closeDropdown();
95
213
  }
96
214
 
97
- private calculateMenuPosition(): void {
98
- if (!this.buttonElement) return;
99
- const rect = this.buttonElement.getBoundingClientRect();
215
+ @HostListener('document:keydown.escape')
216
+ handleEscape(): void {
217
+ if (this.isActiveMenuOption && this.closeOnEscape) {
218
+ this.closeDropdown();
219
+ }
220
+ }
100
221
 
101
- this.menuPosition = {
102
- top: rect.bottom + window.scrollY + 5,
103
- left: rect.right - 150 + window.scrollX,
104
- };
222
+ private handleDocumentClick(event: Event): void {
223
+ const target = event.target as Node;
224
+
225
+ if (
226
+ !this.elementRef.nativeElement.contains(target) &&
227
+ this.menuContainer &&
228
+ !this.menuContainer.nativeElement.contains(target) &&
229
+ this.isActiveMenuOption
230
+ ) {
231
+ this.closeDropdown();
232
+ }
105
233
  }
106
234
 
107
235
  private moveMenuToBody(): void {
108
- if (this.movedToBody) return;
109
- if (!this.menuContainer) return;
236
+ if (this.movedToBody || !this.menuContainer) {
237
+ return;
238
+ }
110
239
 
111
240
  const menuEl = this.menuContainer.nativeElement;
112
241
  this.originalParent = menuEl.parentNode;
@@ -115,7 +244,9 @@ export class JOptionsTableMenuComponent implements OnDestroy, AfterViewInit {
115
244
  }
116
245
 
117
246
  private restoreMenuToOriginalParent(): void {
118
- if (!this.movedToBody || !this.menuContainer || !this.originalParent) return;
247
+ if (!this.movedToBody || !this.menuContainer || !this.originalParent) {
248
+ return;
249
+ }
119
250
 
120
251
  const menuEl = this.menuContainer.nativeElement;
121
252
  this.originalParent.appendChild(menuEl);
@@ -123,25 +254,47 @@ export class JOptionsTableMenuComponent implements OnDestroy, AfterViewInit {
123
254
  }
124
255
 
125
256
  private applyMenuPosition(): void {
126
- if (!this.menuContainer) return;
257
+ if (!this.menuContainer || !this.buttonElement) {
258
+ return;
259
+ }
127
260
 
128
261
  const menuEl = this.menuContainer.nativeElement;
129
- this.renderer.setStyle(menuEl, 'position', 'absolute');
130
- this.renderer.setStyle(menuEl, 'top', `${this.menuPosition.top}px`);
131
- this.renderer.setStyle(menuEl, 'left', `${this.menuPosition.left}px`);
132
- this.renderer.setStyle(menuEl, 'z-index', '500');
262
+ const triggerRect = this.buttonElement.getBoundingClientRect();
263
+ const menuWidth = menuEl.offsetWidth || menuEl.getBoundingClientRect().width || 170;
264
+ const menuHeight = menuEl.offsetHeight || menuEl.getBoundingClientRect().height || 120;
265
+ const gap = 6;
266
+ const viewportPadding = 8;
267
+
268
+ let left =
269
+ this.align === 'end'
270
+ ? triggerRect.right - menuWidth
271
+ : triggerRect.left;
272
+
273
+ let top = triggerRect.bottom + gap;
274
+
275
+ left = Math.max(
276
+ viewportPadding,
277
+ Math.min(left, window.innerWidth - menuWidth - viewportPadding),
278
+ );
279
+
280
+ if (top + menuHeight > window.innerHeight - viewportPadding) {
281
+ top = triggerRect.top - menuHeight - gap;
282
+ }
283
+
284
+ top = Math.max(viewportPadding, top);
285
+
286
+ this.renderer.setStyle(menuEl, 'position', 'fixed');
287
+ this.renderer.setStyle(menuEl, 'top', `${top}px`);
288
+ this.renderer.setStyle(menuEl, 'left', `${left}px`);
289
+ this.renderer.setStyle(menuEl, 'z-index', `${JOptionsTableMenuComponent.MENU_Z_INDEX}`);
133
290
  }
134
291
 
135
292
  private closeDropdown(): void {
136
293
  this.isActiveMenuOption = false;
137
294
  this.restoreMenuToOriginalParent();
295
+
138
296
  if (JOptionsTableMenuComponent.activeInstance === this) {
139
297
  JOptionsTableMenuComponent.activeInstance = null;
140
298
  }
141
299
  }
142
-
143
- handleClick(option: any, group: any): void {
144
- this.clicked.emit({ option, group });
145
- this.closeDropdown();
146
- }
147
300
  }
@@ -0,0 +1,38 @@
1
+ import { LucideIconData } from 'lucide-angular';
2
+ import { OptionsTable } from 'tailjng';
3
+ export type OptionsTableMenuClickEvent = {
4
+ option: OptionsTable;
5
+ group: unknown;
6
+ };
7
+
8
+ export type OptionsTableMenuIconResolver = (
9
+ icon: OptionsTable['icon'],
10
+ group: unknown,
11
+ ) => LucideIconData | undefined;
12
+
13
+ export type OptionsTableMenuTextResolver = (
14
+ value: string | ((data?: unknown) => string),
15
+ group: unknown,
16
+ ) => string;
17
+
18
+ export type OptionsTableMenuDisabledResolver = (
19
+ option: OptionsTable,
20
+ group: unknown,
21
+ ) => boolean;
22
+
23
+ export type OptionsTableMenuVisibleResolver = (
24
+ option: OptionsTable,
25
+ group: unknown,
26
+ ) => boolean;
27
+
28
+ export type OptionsTableMenuClassesResolver = (
29
+ ngClass: OptionsTable['ngClass'],
30
+ group: unknown,
31
+ ) => Record<string, boolean>;
32
+
33
+ export type OptionsTableMenuLoadingResolver = (
34
+ type: string,
35
+ rowId?: string | number,
36
+ ) => boolean;
37
+
38
+ export type OptionsTableMenuAlign = 'start' | 'end';