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,18 +1,120 @@
1
- import { Component, Input } from '@angular/core';
2
- import { Gem, LucideAngularModule, LucideIconData } from 'lucide-angular';
1
+ import { NgClass } from '@angular/common';
2
+ import { Component, HostBinding, Input, numberAttribute } from '@angular/core';
3
+ import { LucideAngularModule, LucideIconData } from 'lucide-angular';
4
+ import { Icons } from '../.config/icons/icons.lucide';
3
5
 
6
+ /** Lucide reference accepted by {@link JIconComponent}. */
7
+ export type JIconData = LucideIconData;
8
+
9
+ /**
10
+ * Wrapper around `lucide-icon` with consistent defaults for tailjng.
11
+ *
12
+ * Install: `npx tailjng add icon`
13
+ *
14
+ * In the parent component:
15
+ * ```typescript
16
+ * import { Icons } from '../.config/icons/icons.lucide';
17
+ * readonly Icons = Icons;
18
+ * ```
19
+ *
20
+ * Template: `<JIcon [icon]="Icons.Loader2" [size]="18" />`
21
+ *
22
+ * Inside a colored parent (button, badge, …): `[inheritParentColor]="true"`.
23
+ */
4
24
  @Component({
5
25
  selector: 'JIcon',
6
- imports: [LucideAngularModule],
26
+ imports: [LucideAngularModule, NgClass],
7
27
  templateUrl: './icon.component.html',
8
- styleUrl: './icon.component.css'
28
+ styleUrl: './icon.component.css',
29
+ host: {
30
+ class: 'inline-flex shrink-0 items-center justify-center',
31
+ },
9
32
  })
10
33
  export class JIconComponent {
34
+ // Project Icons registry (same pattern as in parent components)
35
+ readonly Icons = Icons;
36
+
37
+ // Lucide reference; defaults to Icons.CircleHelp when [icon] is omitted
38
+ @Input() icon: JIconData = Icons.CircleHelp;
39
+
40
+ // Size in pixels (`size="18"` in templates is coerced via numberAttribute)
41
+ @Input({ transform: numberAttribute }) size = 18;
42
+
43
+ // Static CSS classes on the lucide element
44
+ @Input() iconClass = '';
45
+
46
+ // Conditional classes (equivalent to [ngClass])
47
+ @Input() ngClasses: Record<string, boolean> = {};
48
+
49
+ // Screen reader text when the icon conveys meaning
50
+ @Input() ariaLabel?: string;
51
+
52
+ // true = decorative icon; hidden from assistive technologies
53
+ @Input() ariaHidden = false;
54
+
55
+ // true = inherit color from parent (e.g. JButton with text-white)
56
+ @Input() inheritParentColor = false;
57
+
58
+ /** Theme default for standalone icons — light: black, dark: white. */
59
+ @HostBinding('class.text-black')
60
+ get themeLightDefault(): boolean {
61
+ return !this.inheritParentColor && !this.hasCustomColor();
62
+ }
63
+
64
+ @HostBinding('class.dark:text-white')
65
+ get themeDarkDefault(): boolean {
66
+ return !this.inheritParentColor && !this.hasCustomColor();
67
+ }
68
+
69
+ /** Pass-through when nested in a colored parent or when color is set on the SVG. */
70
+ @HostBinding('class.text-inherit')
71
+ get inheritHostColor(): boolean {
72
+ return this.inheritParentColor || this.hasCustomColor();
73
+ }
74
+
75
+ /**
76
+ * Final CSS classes on the lucide element.
77
+ * @returns `iconClass` merged with `text-current` when no explicit SVG color is set.
78
+ */
79
+ get resolvedIconClass(): string {
80
+ const custom = this.iconClass.trim();
81
+
82
+ if (this.hasCustomColor()) {
83
+ return custom;
84
+ }
85
+
86
+ const current = 'text-current';
87
+ return custom ? `${current} ${custom}` : current;
88
+ }
89
+
90
+ /**
91
+ * Whether the consumer set an explicit text/stroke/fill color on the SVG.
92
+ * @returns `true` when a color utility is present in `iconClass` or `ngClasses`.
93
+ */
94
+ private hasCustomColor(): boolean {
95
+ const tokens = [
96
+ ...this.iconClass.split(/\s+/).filter(Boolean),
97
+ ...Object.entries(this.ngClasses)
98
+ .filter(([, active]) => active)
99
+ .map(([className]) => className),
100
+ ];
101
+
102
+ return tokens.some((token) => /^!?((text|stroke|fill)(-|\[|$))/.test(token));
103
+ }
11
104
 
12
- @Input() icon: LucideIconData = Gem;
13
- @Input() size: number = 18;
14
- @Input() classes: string = "";
15
- @Input() ngClasses: { [key: string]: boolean } = {};
105
+ /**
106
+ * Final `aria-label` value on the SVG.
107
+ * @returns Accessible text or `null` to omit the attribute.
108
+ */
109
+ get resolvedAriaLabel(): string | null {
110
+ return this.ariaLabel ?? null;
111
+ }
16
112
 
17
- constructor() { }
113
+ /**
114
+ * Final `aria-hidden` value on the SVG.
115
+ * @returns `true` when decorative; `null` to omit the attribute.
116
+ */
117
+ get resolvedAriaHidden(): true | null {
118
+ return this.ariaHidden ? true : null;
119
+ }
18
120
  }
@@ -1,11 +1,11 @@
1
1
  <div class="relative w-full">
2
- @if (type === 'date' && !value && placeholder) {
3
- <span
4
- class="date-fake-placeholder pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[12px] text-muted-foreground/70 dark:text-dark-muted-foreground/70 z-[1]"
5
- >
6
- {{ placeholder }}
7
- </span>
8
- }
2
+ @if (showDatePlaceholder) {
3
+ <span
4
+ class="date-fake-placeholder pointer-events-none absolute left-3 top-1/2 z-[1] -translate-y-1/2 text-[12px] text-muted-foreground/70 dark:text-dark-muted-foreground/70"
5
+ >
6
+ {{ placeholder }}
7
+ </span>
8
+ }
9
9
 
10
10
  <input
11
11
  [type]="type"
@@ -17,23 +17,26 @@
17
17
  [required]="required"
18
18
  [disabled]="isDisabled"
19
19
  [readonly]="isReadonly"
20
- (blur)="onTouched()"
21
- [ngClass]="combinedNgClass"
20
+ (blur)="onBlur()"
21
+ [ngClass]="computedClasses"
22
22
  [class]="classes"
23
- class="input w-full h-[40px] max-[400px]:h-[35px] text-[12px] bg-background dark:bg-dark-background border border-border dark:border-dark-border text-black dark:text-white placeholder:text-muted-foreground/70 dark:placeholder:text-dark-muted-foreground/70 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-dark-primary transition duration-200 resize-y disabled:opacity-60 disabled:cursor-not-allowed"
24
23
  />
25
24
 
26
- @if (value && clearButton) {
25
+ @if (showClearButton) {
27
26
  <button
28
27
  type="button"
29
- class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 text-gray-400 hover:text-gray-500 pr-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer"
28
+ class="absolute right-2 top-1/2 flex -translate-y-1/2 cursor-pointer items-center justify-center p-0 focus:outline-none"
29
+ [ngClass]="clearButtonClasses"
30
30
  (click)="clearInput()"
31
31
  [disabled]="isDisabled"
32
- [ngClass]="{
33
- 'cursor-not-allowed opacity-50': isDisabled,
34
- }"
32
+ aria-label="Clear"
35
33
  >
36
- <lucide-icon [name]="iconsService.icons.close" class="w-4 h-4" />
34
+ <JIcon
35
+ [icon]="Icons.X"
36
+ [size]="16"
37
+ [inheritParentColor]="true"
38
+ [ariaHidden]="true"
39
+ />
37
40
  </button>
38
41
  }
39
42
  </div>
@@ -1,107 +1,184 @@
1
+ import { NgClass } from '@angular/common';
1
2
  import { Component, Input, forwardRef } from '@angular/core';
2
- import { FormsModule, ReactiveFormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3
- import { NgClass, CommonModule } from '@angular/common';
4
- import { LucideAngularModule } from 'lucide-angular';
5
- import { JIconsService } from 'tailjng';
6
-
3
+ import {
4
+ ControlValueAccessor,
5
+ FormsModule,
6
+ NG_VALUE_ACCESSOR,
7
+ ReactiveFormsModule,
8
+ } from '@angular/forms';
9
+ import { Icons } from '../../.config/icons/icons.lucide';
10
+ import {
11
+ INPUT_DISABLED_OVERLAY_CLASSES,
12
+ INPUT_FOCUS_CLASSES,
13
+ INPUT_PADDING_X_SINGLE,
14
+ INPUT_SINGLE_LINE_HEIGHT_CLASSES,
15
+ INPUT_SURFACE_CLASSES,
16
+ inputActionButtonClasses,
17
+ mergeInputClasses,
18
+ } from '../../.config/input/input.classes';
19
+ import { JIconComponent } from '../../icon/icon.component';
20
+ import { JInputType } from './input.types';
21
+
22
+ export type { JInputType } from './input.types';
23
+
24
+ /**
25
+ * Text input with ControlValueAccessor, clear button, and theme-aware styling.
26
+ *
27
+ * Install: `npx tailjng add input`
28
+ */
7
29
  @Component({
8
30
  selector: 'JInput',
9
- imports: [FormsModule, ReactiveFormsModule, NgClass, LucideAngularModule, CommonModule],
31
+ imports: [FormsModule, ReactiveFormsModule, NgClass, JIconComponent],
10
32
  templateUrl: './input.component.html',
11
- styleUrls: ['./input.component.css'],
33
+ styleUrl: './input.component.css',
12
34
  providers: [
13
35
  {
14
36
  provide: NG_VALUE_ACCESSOR,
15
37
  useExisting: forwardRef(() => JInputComponent),
16
- multi: true
17
- }
18
- ]
38
+ multi: true,
39
+ },
40
+ ],
19
41
  })
20
42
  export class JInputComponent implements ControlValueAccessor {
43
+ // Project Icons registry — template usage: Icons.X
44
+ readonly Icons = Icons;
21
45
 
22
- @Input() type: 'text' | 'password' | 'number' | 'date' | 'datetime-local' | 'email' = 'text';
46
+ // Native input type
47
+ @Input() type: JInputType = 'text';
23
48
 
24
49
  @Input() id?: string;
25
50
  @Input() name?: string;
26
- @Input() placeholder: string = '';
51
+ @Input() placeholder = '';
52
+
53
+ @Input() isDisabled = false;
54
+ @Input() isReadonly = false;
55
+ @Input() required = false;
56
+
57
+ // Show clear (X) button when the field has a value
58
+ @Input() clearButton = false;
59
+
60
+ // Extra Tailwind classes on the `<input>`
61
+ @Input() classes = '';
27
62
 
28
- @Input() isDisabled: boolean = false;
29
- @Input() isReadonly: boolean = false;
30
- @Input() required: boolean = false;
31
- @Input() clearButton: boolean = false;
63
+ // Extra conditional classes (same role as [ngClass] on the input)
64
+ @Input() ngClasses: Record<string, boolean> = {};
32
65
 
33
- @Input() classes: string = '';
34
- @Input() ngClass: { [key: string]: boolean } = {};
66
+ innerValue = '';
35
67
 
36
- innerValue: any = '';
68
+ private onChange: (value: string) => void = () => {};
69
+ private onTouched: () => void = () => {};
37
70
 
38
- get value(): any {
71
+ /**
72
+ * Current value bound to the native input.
73
+ * @returns The internal string value.
74
+ */
75
+ get value(): string {
39
76
  return this.innerValue;
40
77
  }
41
78
 
42
- set value(val: any) {
43
- if (val !== this.innerValue) {
44
- this.innerValue = val;
45
- this.onChange(val);
46
- }
79
+ /**
80
+ * Whether the clear button should be visible.
81
+ * @returns `true` when `clearButton` is enabled and the field is not empty.
82
+ */
83
+ get showClearButton(): boolean {
84
+ return this.clearButton && this.hasValue;
47
85
  }
48
86
 
49
- get combinedNgClass() {
50
- return {
51
- ...(this.ngClass || {}),
52
- 'opacity-50': this.isDisabled
53
- };
87
+ /**
88
+ * Whether the field has a non-empty value.
89
+ * @returns `true` when `innerValue` is not an empty string.
90
+ */
91
+ get hasValue(): boolean {
92
+ return this.innerValue !== '';
54
93
  }
55
94
 
56
- // ControlValueAccessor methods
57
- onChange: any = () => { };
58
- onTouched: any = () => { };
95
+ /**
96
+ * Whether to show the fake date placeholder overlay (mobile / coarse pointer).
97
+ * @returns `true` for date inputs with placeholder and no value yet.
98
+ */
99
+ get showDatePlaceholder(): boolean {
100
+ return this.type === 'date' && !this.hasValue && !!this.placeholder;
101
+ }
59
102
 
60
- constructor(public readonly iconsService: JIconsService) { }
103
+ /**
104
+ * Class map for the native `<input>` element.
105
+ * @returns Base input styles merged with `ngClasses` and disabled opacity.
106
+ */
107
+ get computedClasses(): Record<string, boolean> {
108
+ return mergeInputClasses(
109
+ INPUT_SURFACE_CLASSES,
110
+ INPUT_SINGLE_LINE_HEIGHT_CLASSES,
111
+ INPUT_FOCUS_CLASSES,
112
+ INPUT_PADDING_X_SINGLE,
113
+ this.ngClasses,
114
+ { 'pr-3': !this.clearButton, 'pr-7': this.clearButton },
115
+ this.isDisabled ? INPUT_DISABLED_OVERLAY_CLASSES : {},
116
+ );
117
+ }
61
118
 
62
119
  /**
63
- * Writes a value to the component.
64
- * @param value - The value to write.
120
+ * Class map for the clear button.
121
+ * @returns Hover and disabled utilities for the clear control.
65
122
  */
66
- writeValue(value: any): void {
67
- if (value !== undefined) {
68
- this.value = value;
69
- }
123
+ get clearButtonClasses(): Record<string, boolean> {
124
+ return inputActionButtonClasses(this.isDisabled);
70
125
  }
71
126
 
72
127
  /**
73
- * Registers a callback function to be called when the input value changes.
74
- * @param fn - The callback function to register.
128
+ * Writes the value from the form model into the view.
129
+ * @param value Value from Angular forms.
75
130
  */
76
- registerOnChange(fn: (value: any) => void): void {
131
+ writeValue(value: unknown): void {
132
+ this.innerValue = value != null ? String(value) : '';
133
+ }
134
+
135
+ /**
136
+ * Registers the change callback for Angular forms.
137
+ * @param fn Callback invoked when the value changes.
138
+ */
139
+ registerOnChange(fn: (value: string) => void): void {
77
140
  this.onChange = fn;
78
141
  }
79
142
 
80
143
  /**
81
- * Registers a callback function to be called when the input is touched.
82
- * @param fn - The callback function to register.
144
+ * Registers the touched callback for Angular forms.
145
+ * @param fn Callback invoked when the control is touched.
83
146
  */
84
147
  registerOnTouched(fn: () => void): void {
85
148
  this.onTouched = fn;
86
149
  }
87
150
 
88
151
  /**
89
- * Writes a value to the component.
90
- * @param event
152
+ * Syncs disabled state from Angular forms.
153
+ * @param isDisabled Whether the control is disabled.
154
+ */
155
+ setDisabledState(isDisabled: boolean): void {
156
+ this.isDisabled = isDisabled;
157
+ }
158
+
159
+ /**
160
+ * Handles native input events and propagates the value to forms.
161
+ * @param event Native input event.
91
162
  */
92
163
  onInput(event: Event): void {
93
- const target = event.target as HTMLInputElement | HTMLTextAreaElement;
94
- this.value = target.value;
95
- this.onChange(this.value);
164
+ const target = event.target as HTMLInputElement;
165
+ this.innerValue = target.value;
166
+ this.onChange(this.innerValue);
167
+ this.onTouched();
168
+ }
169
+
170
+ /**
171
+ * Marks the control as touched on blur.
172
+ */
173
+ onBlur(): void {
96
174
  this.onTouched();
97
175
  }
98
176
 
99
177
  /**
100
- * Clears the input value and resets the component state.
101
- * This method is typically used when a clear button is clicked.
178
+ * Clears the field value and notifies Angular forms.
102
179
  */
103
180
  clearInput(): void {
104
- this.value = '';
181
+ this.innerValue = '';
105
182
  this.onChange('');
106
183
  this.onTouched();
107
184
  }
@@ -0,0 +1,8 @@
1
+ /** Supported native input types for {@link JInputComponent}. */
2
+ export type JInputType =
3
+ | 'text'
4
+ | 'password'
5
+ | 'number'
6
+ | 'date'
7
+ | 'datetime-local'
8
+ | 'email';
@@ -1,66 +1,75 @@
1
- <div class="relative w-full select-none">
2
- <input
3
- #fileInput
4
- type="file"
5
- [id]="id"
6
- [name]="name ?? ''"
7
- [accept]="accept"
8
- [multiple]="multiple"
9
- (change)="onFileSelected($event)"
10
- [required]="required"
11
- [disabled]="isDisabled"
12
- class="hidden"
13
- (blur)="onTouched()"
1
+ <div class="relative w-full">
2
+ <input
3
+ #fileInput
4
+ type="file"
5
+ [id]="id"
6
+ [name]="name ?? ''"
7
+ [accept]="accept"
8
+ [multiple]="multiple"
9
+ (change)="onFileSelected($event)"
10
+ [required]="required"
11
+ [disabled]="isDisabled"
12
+ class="hidden"
13
+ (blur)="onBlur()"
14
14
  />
15
-
16
- <label
17
- [for]="id"
18
- class="input block text-sm w-full h-[40px] max-[400px]:h-[35px] text-[12px] pr-1 bg-background dark:bg-dark-background border border-border dark:border-dark-border text-black dark:text-white placeholder:text-muted-foreground dark:placeholder:text-dark-muted-foreground rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-dark-primary transition duration-200 resize-y disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-between transition duration-200 cursor-pointer"
19
- >
20
- <div class="truncate" [ngClass]="{ 'opacity-80' : !innerValue?.name }">
21
- @if (innerValue?.name) {
22
- <span>{{ innerValue?.name }}</span>
23
- } @else {
24
- <span class="flex items-center gap-2 max-[400px]:h-[35px] text-muted-foreground dark:text-dark-muted-foreground">
25
- <lucide-icon [name]="iconsService.icons.upload" [size]="15" />
26
- Seleccionar archivo...
27
- </span>
28
- }
29
- </div>
30
15
 
31
- <div class="flex gap-1">
32
- @if (value && clearButton) {
33
- <button
34
- type="button"
35
- class="text-gray-400 hover:text-gray-500 pr-1 mr-1 hover:text-gray-600 focus:outline-none cursor-pointer"
36
- (click)="clearFile()"
37
- >
38
- <lucide-icon [name]="iconsService.icons.close" class="w-4 h-4" />
39
- </button>
16
+ <label [attr.for]="id" [ngClass]="computedClasses" [class]="classes">
17
+ <div class="min-w-0 flex-1 truncate" [class.opacity-80]="!hasValue">
18
+ @if (displayFileName) {
19
+ <span>{{ displayFileName }}</span>
20
+ } @else {
21
+ <span class="flex items-center gap-2 text-muted-foreground dark:text-dark-muted-foreground">
22
+ <JIcon [icon]="Icons.Upload" [size]="15" [inheritParentColor]="true" [ariaHidden]="true" />
23
+ {{ placeholder }}
24
+ </span>
40
25
  }
41
-
42
- <button
43
- type="button"
44
- class="text-gray-400 hover:text-gray-500 pr-1 mr-1 hover:text-gray-600 focus:outline-none cursor-pointer"
45
- (click)="getImageFromClipboard()"
46
- >
47
- <lucide-icon [name]="iconsService.icons.clipboard" class="w-4 h-4" />
48
- </button>
49
26
  </div>
50
27
 
28
+ <div class="flex shrink-0 items-center gap-1">
29
+ @if (showClearButton) {
30
+ <button
31
+ type="button"
32
+ class="inline-flex cursor-pointer items-center justify-center p-0 focus:outline-none"
33
+ [ngClass]="actionButtonClasses"
34
+ (click)="onClearClick($event)"
35
+ [disabled]="isDisabled"
36
+ aria-label="Clear"
37
+ >
38
+ <JIcon
39
+ [icon]="Icons.X"
40
+ [size]="16"
41
+ [inheritParentColor]="true"
42
+ [ariaHidden]="true"
43
+ />
44
+ </button>
45
+ }
46
+
47
+ <button
48
+ type="button"
49
+ class="inline-flex cursor-pointer items-center justify-center p-0 focus:outline-none"
50
+ [ngClass]="actionButtonClasses"
51
+ (click)="onClipboardClick($event)"
52
+ [disabled]="isDisabled"
53
+ aria-label="Paste from clipboard"
54
+ >
55
+ <JIcon
56
+ [icon]="Icons.Clipboard"
57
+ [size]="16"
58
+ [inheritParentColor]="true"
59
+ [ariaHidden]="true"
60
+ />
61
+ </button>
62
+ </div>
51
63
  </label>
52
64
 
53
- @if (previewUrl && showImage) {
54
- <div class="mt-2 w-full align-center justify-center flex">
55
- <img
56
- [src]="previewUrl"
57
- alt="Vista previa"
58
- [ngStyle]="{
59
- width: widthImgFile ? widthImgFile + 'px' : '100%',
60
- height: heightImgFile ? heightImgFile + 'px' : 'auto'
61
- }"
62
- class="rounded border border-border dark:border-dark-border shadow-sm object-cover"
63
- />
65
+ @if (showPreview) {
66
+ <div class="mt-2 flex w-full items-center justify-center">
67
+ <img
68
+ [src]="previewImageUrl"
69
+ alt="Preview"
70
+ [ngStyle]="previewStyles"
71
+ class="rounded border border-border object-cover shadow-sm dark:border-dark-border"
72
+ />
64
73
  </div>
65
74
  }
66
75
  </div>