ng-comps 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.editorconfig +17 -0
  2. package/.github/copilot-instructions.md +55 -0
  3. package/.github/workflows/ci.yml +29 -0
  4. package/.prettierrc +12 -0
  5. package/.storybook/main.ts +21 -0
  6. package/.storybook/preview.ts +27 -0
  7. package/.storybook/tsconfig.doc.json +10 -0
  8. package/.storybook/tsconfig.json +15 -0
  9. package/.storybook/typings.d.ts +4 -0
  10. package/.vscode/extensions.json +4 -0
  11. package/.vscode/launch.json +20 -0
  12. package/.vscode/mcp.json +9 -0
  13. package/.vscode/tasks.json +42 -0
  14. package/ACCESSIBILITY.md +127 -0
  15. package/README.md +79 -62
  16. package/angular.json +105 -0
  17. package/documentation.json +13394 -0
  18. package/ng-package.json +27 -0
  19. package/package.json +58 -45
  20. package/public/favicon.ico +0 -0
  21. package/scripts/prepare-package.mjs +61 -0
  22. package/src/app/a11y/accessibility.utils.ts +35 -0
  23. package/src/app/a11y/index.ts +6 -0
  24. package/src/app/accessibility/ng-comps.a11y.spec.ts +108 -0
  25. package/src/app/app.config.ts +11 -0
  26. package/src/app/app.css +107 -0
  27. package/src/app/app.html +48 -0
  28. package/src/app/app.routes.ts +3 -0
  29. package/src/app/app.spec.ts +23 -0
  30. package/src/app/app.ts +10 -0
  31. package/src/app/components/accordion/index.ts +2 -0
  32. package/src/app/components/accordion/mf-accordion.component.css +38 -0
  33. package/src/app/components/accordion/mf-accordion.component.spec.ts +48 -0
  34. package/src/app/components/accordion/mf-accordion.component.ts +53 -0
  35. package/src/app/components/alert/index.ts +2 -0
  36. package/src/app/components/alert/mf-alert.component.css +100 -0
  37. package/src/app/components/alert/mf-alert.component.spec.ts +59 -0
  38. package/src/app/components/alert/mf-alert.component.ts +68 -0
  39. package/src/app/components/autocomplete/index.ts +5 -0
  40. package/src/app/components/autocomplete/mf-autocomplete.component.css +105 -0
  41. package/src/app/components/autocomplete/mf-autocomplete.component.spec.ts +116 -0
  42. package/src/app/components/autocomplete/mf-autocomplete.component.ts +307 -0
  43. package/src/app/components/avatar/index.ts +2 -0
  44. package/src/app/components/avatar/mf-avatar.component.css +27 -0
  45. package/src/app/components/avatar/mf-avatar.component.spec.ts +49 -0
  46. package/src/app/components/avatar/mf-avatar.component.ts +99 -0
  47. package/src/app/components/badge/index.ts +2 -0
  48. package/src/app/components/badge/mf-badge.component.css +32 -0
  49. package/src/app/components/badge/mf-badge.component.spec.ts +40 -0
  50. package/src/app/components/badge/mf-badge.component.ts +105 -0
  51. package/src/app/components/breadcrumb/index.ts +2 -0
  52. package/src/app/components/breadcrumb/mf-breadcrumb.component.css +61 -0
  53. package/src/app/components/breadcrumb/mf-breadcrumb.component.spec.ts +61 -0
  54. package/src/app/components/breadcrumb/mf-breadcrumb.component.ts +75 -0
  55. package/src/app/components/button/index.ts +2 -0
  56. package/src/app/components/button/mf-button.component.css +136 -0
  57. package/src/app/components/button/mf-button.component.ts +174 -0
  58. package/src/app/components/card/index.ts +2 -0
  59. package/src/app/components/card/mf-card.component.css +82 -0
  60. package/src/app/components/card/mf-card.component.ts +59 -0
  61. package/src/app/components/checkbox/index.ts +1 -0
  62. package/src/app/components/checkbox/mf-checkbox.component.css +75 -0
  63. package/src/app/components/checkbox/mf-checkbox.component.ts +187 -0
  64. package/src/app/components/chip/index.ts +2 -0
  65. package/src/app/components/chip/mf-chip.component.css +69 -0
  66. package/src/app/components/chip/mf-chip.component.spec.ts +47 -0
  67. package/src/app/components/chip/mf-chip.component.ts +77 -0
  68. package/src/app/components/datepicker/index.ts +2 -0
  69. package/src/app/components/datepicker/mf-datepicker.component.css +102 -0
  70. package/src/app/components/datepicker/mf-datepicker.component.spec.ts +69 -0
  71. package/src/app/components/datepicker/mf-datepicker.component.ts +233 -0
  72. package/src/app/components/dialog/index.ts +3 -0
  73. package/src/app/components/dialog/mf-dialog.component.css +73 -0
  74. package/src/app/components/dialog/mf-dialog.component.ts +160 -0
  75. package/src/app/components/dialog/mf-dialog.service.spec.ts +61 -0
  76. package/src/app/components/dialog/mf-dialog.service.ts +52 -0
  77. package/src/app/components/divider/index.ts +2 -0
  78. package/src/app/components/divider/mf-divider.component.css +38 -0
  79. package/src/app/components/divider/mf-divider.component.spec.ts +40 -0
  80. package/src/app/components/divider/mf-divider.component.ts +44 -0
  81. package/src/app/components/form-field/index.ts +1 -0
  82. package/src/app/components/form-field/mf-form-field.component.css +51 -0
  83. package/src/app/components/form-field/mf-form-field.component.ts +74 -0
  84. package/src/app/components/grid-list/index.ts +2 -0
  85. package/src/app/components/grid-list/mf-grid-list.component.css +47 -0
  86. package/src/app/components/grid-list/mf-grid-list.component.spec.ts +57 -0
  87. package/src/app/components/grid-list/mf-grid-list.component.ts +68 -0
  88. package/src/app/components/icon/index.ts +2 -0
  89. package/src/app/components/icon/mf-icon.component.css +56 -0
  90. package/src/app/components/icon/mf-icon.component.ts +41 -0
  91. package/src/app/components/input/index.ts +2 -0
  92. package/src/app/components/input/mf-input.component.css +105 -0
  93. package/src/app/components/input/mf-input.component.ts +217 -0
  94. package/src/app/components/menu/index.ts +2 -0
  95. package/src/app/components/menu/mf-menu.component.css +31 -0
  96. package/src/app/components/menu/mf-menu.component.spec.ts +49 -0
  97. package/src/app/components/menu/mf-menu.component.ts +66 -0
  98. package/src/app/components/paginator/index.ts +1 -0
  99. package/src/app/components/paginator/mf-paginator.component.css +32 -0
  100. package/src/app/components/paginator/mf-paginator.component.spec.ts +44 -0
  101. package/src/app/components/paginator/mf-paginator.component.ts +52 -0
  102. package/src/app/components/progress-bar/index.ts +2 -0
  103. package/src/app/components/progress-bar/mf-progress-bar.component.css +53 -0
  104. package/src/app/components/progress-bar/mf-progress-bar.component.spec.ts +65 -0
  105. package/src/app/components/progress-bar/mf-progress-bar.component.ts +79 -0
  106. package/src/app/components/progress-spinner/index.ts +2 -0
  107. package/src/app/components/progress-spinner/mf-progress-spinner.component.css +38 -0
  108. package/src/app/components/progress-spinner/mf-progress-spinner.component.spec.ts +59 -0
  109. package/src/app/components/progress-spinner/mf-progress-spinner.component.ts +81 -0
  110. package/src/app/components/radio-button/index.ts +2 -0
  111. package/src/app/components/radio-button/mf-radio-button.component.css +86 -0
  112. package/src/app/components/radio-button/mf-radio-button.component.spec.ts +55 -0
  113. package/src/app/components/radio-button/mf-radio-button.component.ts +219 -0
  114. package/src/app/components/select/index.ts +2 -0
  115. package/src/app/components/select/mf-select.component.css +121 -0
  116. package/src/app/components/select/mf-select.component.spec.ts +108 -0
  117. package/src/app/components/select/mf-select.component.ts +252 -0
  118. package/src/app/components/sidenav/index.ts +2 -0
  119. package/src/app/components/sidenav/mf-sidenav.component.css +168 -0
  120. package/src/app/components/sidenav/mf-sidenav.component.spec.ts +57 -0
  121. package/src/app/components/sidenav/mf-sidenav.component.ts +126 -0
  122. package/src/app/components/slide-toggle/index.ts +1 -0
  123. package/src/app/components/slide-toggle/mf-slide-toggle.component.css +42 -0
  124. package/src/app/components/slide-toggle/mf-slide-toggle.component.spec.ts +43 -0
  125. package/src/app/components/slide-toggle/mf-slide-toggle.component.ts +188 -0
  126. package/src/app/components/snackbar/index.ts +2 -0
  127. package/src/app/components/snackbar/mf-snackbar.service.css +31 -0
  128. package/src/app/components/snackbar/mf-snackbar.service.spec.ts +81 -0
  129. package/src/app/components/snackbar/mf-snackbar.service.ts +77 -0
  130. package/src/app/components/table/index.ts +2 -0
  131. package/src/app/components/table/mf-table.component.css +68 -0
  132. package/src/app/components/table/mf-table.component.spec.ts +76 -0
  133. package/src/app/components/table/mf-table.component.ts +117 -0
  134. package/src/app/components/tabs/index.ts +2 -0
  135. package/src/app/components/tabs/mf-tabs.component.css +31 -0
  136. package/src/app/components/tabs/mf-tabs.component.spec.ts +50 -0
  137. package/src/app/components/tabs/mf-tabs.component.ts +62 -0
  138. package/src/app/components/textarea/index.ts +2 -0
  139. package/src/app/components/textarea/mf-textarea.component.css +48 -0
  140. package/src/app/components/textarea/mf-textarea.component.spec.ts +55 -0
  141. package/src/app/components/textarea/mf-textarea.component.ts +227 -0
  142. package/src/app/components/toolbar/index.ts +2 -0
  143. package/src/app/components/toolbar/mf-toolbar.component.css +77 -0
  144. package/src/app/components/toolbar/mf-toolbar.component.ts +56 -0
  145. package/src/app/components/tooltip/index.ts +3 -0
  146. package/src/app/components/tooltip/mf-tooltip.component.css +7 -0
  147. package/src/app/components/tooltip/mf-tooltip.component.spec.ts +37 -0
  148. package/src/app/components/tooltip/mf-tooltip.component.ts +47 -0
  149. package/src/app/components/tooltip/mf-tooltip.directive.ts +22 -0
  150. package/src/index.html +18 -0
  151. package/src/main.ts +6 -0
  152. package/src/public-api.ts +31 -0
  153. package/src/stories/About.mdx +72 -0
  154. package/src/stories/Accessibility.mdx +59 -0
  155. package/src/stories/Welcome.mdx +27 -0
  156. package/src/stories/assets/accessibility.png +0 -0
  157. package/src/stories/assets/accessibility.svg +1 -0
  158. package/src/stories/assets/addon-library.png +0 -0
  159. package/src/stories/assets/assets.png +0 -0
  160. package/src/stories/assets/avif-test-image.avif +0 -0
  161. package/src/stories/assets/context.png +0 -0
  162. package/src/stories/assets/discord.svg +1 -0
  163. package/src/stories/assets/docs.png +0 -0
  164. package/src/stories/assets/figma-plugin.png +0 -0
  165. package/src/stories/assets/github.svg +1 -0
  166. package/src/stories/assets/share.png +0 -0
  167. package/src/stories/assets/styling.png +0 -0
  168. package/src/stories/assets/testing.png +0 -0
  169. package/src/stories/assets/theming.png +0 -0
  170. package/src/stories/assets/tutorials.svg +1 -0
  171. package/src/stories/assets/youtube.svg +1 -0
  172. package/src/stories/mf-a11y-contracts.stories.ts +472 -0
  173. package/src/stories/mf-autocomplete.stories.ts +188 -0
  174. package/src/stories/mf-button.stories.ts +156 -0
  175. package/src/stories/mf-card.stories.ts +148 -0
  176. package/src/stories/mf-checkbox.stories.ts +88 -0
  177. package/src/stories/mf-datepicker.stories.ts +118 -0
  178. package/src/stories/mf-dialog.stories.ts +167 -0
  179. package/src/stories/mf-form-field.stories.ts +108 -0
  180. package/src/stories/mf-grid-list.stories.ts +105 -0
  181. package/src/stories/mf-icon.stories.ts +130 -0
  182. package/src/stories/mf-input.stories.ts +158 -0
  183. package/src/stories/mf-menu.stories.ts +71 -0
  184. package/src/stories/mf-progress-bar.stories.ts +119 -0
  185. package/src/stories/mf-progress-spinner.stories.ts +124 -0
  186. package/src/stories/mf-radio-button.stories.ts +111 -0
  187. package/src/stories/mf-select.stories.ts +178 -0
  188. package/src/stories/mf-sidenav.stories.ts +334 -0
  189. package/src/stories/mf-table.stories.ts +80 -0
  190. package/src/stories/mf-toolbar.stories.ts +112 -0
  191. package/src/stories/user.ts +3 -0
  192. package/src/styles.css +58 -21
  193. package/src/theme/tokens.css +7 -4
  194. package/tsconfig.app.json +15 -0
  195. package/tsconfig.json +33 -0
  196. package/tsconfig.spec.json +15 -0
  197. package/vercel.json +6 -0
  198. package/fesm2022/ng-comps.mjs +0 -2479
  199. package/fesm2022/ng-comps.mjs.map +0 -1
  200. package/types/ng-comps.d.ts +0 -917
@@ -0,0 +1,233 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ ChangeDetectorRef,
4
+ Component,
5
+ computed,
6
+ effect,
7
+ forwardRef,
8
+ inject,
9
+ input,
10
+ output,
11
+ signal,
12
+ } from '@angular/core';
13
+ import {
14
+ ControlValueAccessor,
15
+ NG_VALUE_ACCESSOR,
16
+ NgControl,
17
+ } from '@angular/forms';
18
+ import { ErrorStateMatcher } from '@angular/material/core';
19
+ import { MatNativeDateModule } from '@angular/material/core';
20
+ import { MatDatepickerModule } from '@angular/material/datepicker';
21
+ import { MatFormFieldModule } from '@angular/material/form-field';
22
+ import { MatIconModule } from '@angular/material/icon';
23
+ import { MatInputModule } from '@angular/material/input';
24
+ import {
25
+ createUniqueId,
26
+ hasAccessibleName,
27
+ mergeAriaIds,
28
+ warnInDev,
29
+ } from '../../a11y';
30
+
31
+ export type MfDatepickerSize = 'sm' | 'md' | 'lg';
32
+
33
+ /**
34
+ * Selector de fecha de la librería ng-comps.
35
+ * Envuelve Angular Material `mat-datepicker` y expone una API uniforme
36
+ * con look and feel de marca.
37
+ */
38
+ @Component({
39
+ selector: 'mf-datepicker',
40
+ imports: [
41
+ MatDatepickerModule,
42
+ MatFormFieldModule,
43
+ MatInputModule,
44
+ MatNativeDateModule,
45
+ MatIconModule,
46
+ ],
47
+ providers: [
48
+ {
49
+ provide: NG_VALUE_ACCESSOR,
50
+ useExisting: forwardRef(() => MfDatepickerComponent),
51
+ multi: true,
52
+ },
53
+ ],
54
+ template: `
55
+ <mat-form-field [appearance]="'outline'" [class]="hostClasses()">
56
+ @if (label()) {
57
+ <mat-label>{{ label() }}</mat-label>
58
+ }
59
+ <input
60
+ matInput
61
+ [id]="controlId()"
62
+ [matDatepicker]="picker"
63
+ [placeholder]="placeholder()"
64
+ [disabled]="isDisabled()"
65
+ [required]="required()"
66
+ [min]="min()"
67
+ [max]="max()"
68
+ [value]="internalValue()"
69
+ [errorStateMatcher]="errorStateMatcher"
70
+ [attr.aria-label]="resolvedAriaLabel()"
71
+ [attr.aria-labelledby]="ariaLabelledby() || null"
72
+ [attr.aria-describedby]="describedBy()"
73
+ [attr.aria-invalid]="isInvalid() ? 'true' : null"
74
+ [attr.aria-required]="required() ? 'true' : null"
75
+ (dateChange)="onDateChange($event)"
76
+ (blur)="onBlur()"
77
+ />
78
+ <mat-datepicker-toggle
79
+ matIconSuffix
80
+ [for]="picker"
81
+ [disabled]="isDisabled()"
82
+ [attr.aria-label]="toggleAriaLabel()"
83
+ >
84
+ <mat-icon matDatepickerToggleIcon aria-hidden="true">
85
+ calendar_month
86
+ </mat-icon>
87
+ </mat-datepicker-toggle>
88
+ <mat-datepicker #picker />
89
+ @if (hint()) {
90
+ <mat-hint [attr.id]="hintId()">{{ hint() }}</mat-hint>
91
+ }
92
+ </mat-form-field>
93
+ @if (error()) {
94
+ <p class="mf-datepicker__error" [attr.id]="errorId()" role="alert">
95
+ {{ error() }}
96
+ </p>
97
+ }
98
+ `,
99
+ styleUrl: './mf-datepicker.component.css',
100
+ changeDetection: ChangeDetectionStrategy.OnPush,
101
+ })
102
+ export class MfDatepickerComponent implements ControlValueAccessor {
103
+ private readonly cdr = inject(ChangeDetectorRef);
104
+ private readonly ngControl = inject(NgControl, { self: true, optional: true });
105
+ private readonly generatedId = createUniqueId('mf-datepicker');
106
+ private readonly disabledFromForm = signal(false);
107
+ protected readonly internalValue = signal<Date | null>(null);
108
+ private onControlChange: (value: Date | null) => void = () => undefined;
109
+ private onControlTouched: () => void = () => undefined;
110
+ readonly errorStateMatcher: ErrorStateMatcher = {
111
+ isErrorState: (control) =>
112
+ Boolean(
113
+ this.error() || (control?.invalid && (control.touched || control.dirty)),
114
+ ),
115
+ };
116
+
117
+ /** ID del control */
118
+ readonly id = input<string | undefined>(undefined);
119
+ /** Etiqueta flotante del campo */
120
+ readonly label = input<string | undefined>(undefined);
121
+ /** Etiqueta accesible alternativa cuando no existe label visible */
122
+ readonly ariaLabel = input<string | undefined>(undefined);
123
+ /** Referencia externa a elementos que etiquetan el control */
124
+ readonly ariaLabelledby = input<string | undefined>(undefined);
125
+ /** Referencia externa a elementos descriptivos adicionales */
126
+ readonly ariaDescribedby = input<string | undefined>(undefined);
127
+ /** Placeholder del input */
128
+ readonly placeholder = input('DD/MM/YYYY');
129
+ /** Tamaño del campo */
130
+ readonly size = input<MfDatepickerSize>('md');
131
+ /** Deshabilitado */
132
+ readonly disabled = input(false);
133
+ /** Requerido */
134
+ readonly required = input(false);
135
+ /** Valor inicial del datepicker */
136
+ readonly value = input<Date | null>(null);
137
+ /** Texto de ayuda debajo del campo */
138
+ readonly hint = input<string | undefined>(undefined);
139
+ /** Mensaje de error */
140
+ readonly error = input<string | undefined>(undefined);
141
+ /** Fecha mínima seleccionable */
142
+ readonly min = input<Date | null>(null);
143
+ /** Fecha máxima seleccionable */
144
+ readonly max = input<Date | null>(null);
145
+ /** Ancho completo */
146
+ readonly fullWidth = input(false);
147
+ /** Etiqueta accesible del botón para abrir el calendario */
148
+ readonly toggleAriaLabel = input('Abrir calendario');
149
+
150
+ readonly mfChange = output<Date | null>();
151
+ readonly mfBlur = output<void>();
152
+
153
+ constructor() {
154
+ effect(() => {
155
+ this.internalValue.set(this.value());
156
+ });
157
+
158
+ effect(() => {
159
+ if (
160
+ !hasAccessibleName(
161
+ this.label(),
162
+ this.ariaLabel(),
163
+ this.ariaLabelledby(),
164
+ )
165
+ ) {
166
+ warnInDev(
167
+ 'mf-datepicker requiere `label`, `ariaLabel` o `ariaLabelledby` para exponer un nombre accesible.',
168
+ );
169
+ }
170
+ });
171
+ }
172
+
173
+ readonly controlId = computed(() => this.id() ?? this.generatedId);
174
+ readonly hintId = computed(() =>
175
+ this.hint() ? `${this.controlId()}-hint` : null,
176
+ );
177
+ readonly errorId = computed(() =>
178
+ this.error() ? `${this.controlId()}-error` : null,
179
+ );
180
+ readonly describedBy = computed(() =>
181
+ mergeAriaIds(this.ariaDescribedby(), this.hintId(), this.errorId()),
182
+ );
183
+ readonly resolvedAriaLabel = computed(() =>
184
+ this.label() ? null : this.ariaLabel() ?? null,
185
+ );
186
+ readonly isDisabled = computed(
187
+ () => this.disabled() || this.disabledFromForm(),
188
+ );
189
+
190
+ readonly hostClasses = computed(() => {
191
+ const classes = ['mf-datepicker', `mf-datepicker--${this.size()}`];
192
+ if (this.fullWidth()) classes.push('mf-datepicker--full');
193
+ if (this.error()) classes.push('mf-datepicker--error');
194
+ return classes.join(' ');
195
+ });
196
+
197
+ writeValue(value: Date | null): void {
198
+ this.internalValue.set(value);
199
+ this.cdr.markForCheck();
200
+ }
201
+
202
+ registerOnChange(fn: (value: Date | null) => void): void {
203
+ this.onControlChange = fn;
204
+ }
205
+
206
+ registerOnTouched(fn: () => void): void {
207
+ this.onControlTouched = fn;
208
+ }
209
+
210
+ setDisabledState(isDisabled: boolean): void {
211
+ this.disabledFromForm.set(isDisabled);
212
+ this.cdr.markForCheck();
213
+ }
214
+
215
+ isInvalid(): boolean {
216
+ const control = this.ngControl?.control;
217
+ return Boolean(
218
+ this.error() || (control?.invalid && (control.touched || control.dirty)),
219
+ );
220
+ }
221
+
222
+ onDateChange(event: { value: Date | null }): void {
223
+ this.internalValue.set(event.value);
224
+ this.onControlChange(event.value);
225
+ this.onControlTouched();
226
+ this.mfChange.emit(event.value);
227
+ }
228
+
229
+ onBlur(): void {
230
+ this.onControlTouched();
231
+ this.mfBlur.emit();
232
+ }
233
+ }
@@ -0,0 +1,3 @@
1
+ export { MfDialogComponent } from './mf-dialog.component';
2
+ export { MfDialogService } from './mf-dialog.service';
3
+ export type { MfDialogOpenConfig } from './mf-dialog.service';
@@ -0,0 +1,73 @@
1
+ /* ── Base ──────────────────────────────────────────────────────── */
2
+ .mf-dialog {
3
+ font-family: var(--mf-font-base);
4
+ min-width: 360px;
5
+ max-width: 560px;
6
+ }
7
+
8
+ /* ── Header ────────────────────────────────────────────────────── */
9
+ .mf-dialog__header {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: var(--mf-space-3);
14
+ padding: var(--mf-space-6) var(--mf-space-6) var(--mf-space-3);
15
+ }
16
+
17
+ .mf-dialog__header--compact {
18
+ padding-bottom: 0;
19
+ }
20
+
21
+ .mf-dialog__title {
22
+ font-family: var(--mf-font-display);
23
+ font-size: var(--mf-text-xl);
24
+ font-weight: var(--mf-weight-bold);
25
+ color: var(--mf-color-on-surface);
26
+ margin: 0;
27
+ line-height: var(--mf-leading-tight);
28
+ }
29
+
30
+ /* ── Close button ─────────────────────────────────────────────── */
31
+ .mf-dialog__close {
32
+ flex-shrink: 0;
33
+ color: var(--mf-color-neutral-400);
34
+ transition:
35
+ background-color var(--mf-duration-fast) var(--mf-ease-standard),
36
+ color var(--mf-duration-fast) var(--mf-ease-standard);
37
+ }
38
+
39
+ .mf-dialog__close:hover {
40
+ background-color: var(--mf-color-neutral-100);
41
+ color: var(--mf-color-on-surface);
42
+ }
43
+
44
+ .mf-dialog__close .mat-icon {
45
+ font-size: 20px;
46
+ width: 20px;
47
+ height: 20px;
48
+ }
49
+
50
+ /* ── Body ──────────────────────────────────────────────────────── */
51
+ .mf-dialog__body {
52
+ padding: 0 var(--mf-space-6);
53
+ }
54
+
55
+ .mf-dialog__message {
56
+ font-size: var(--mf-text-sm);
57
+ color: var(--mf-color-neutral-600);
58
+ line-height: var(--mf-leading-normal);
59
+ margin: 0;
60
+ }
61
+
62
+ /* ── Content ───────────────────────────────────────────────────── */
63
+ .mf-dialog__content {
64
+ padding: var(--mf-space-3) var(--mf-space-6);
65
+ }
66
+
67
+ .mf-dialog__content:empty {
68
+ display: none;
69
+ }
70
+
71
+ /* ── Actions (footer) ─────────────────────────────────────────── */
72
+ /* La regla de gap está en styles.css (global) para que aplique también
73
+ al contenido proyectado, que escapa al scope de ViewEncapsulation. */
@@ -0,0 +1,160 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ inject,
7
+ input,
8
+ output,
9
+ } from '@angular/core';
10
+ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
11
+ import { MatIconModule } from '@angular/material/icon';
12
+ import {
13
+ createUniqueId,
14
+ hasAccessibleName,
15
+ mergeAriaIds,
16
+ warnInDev,
17
+ } from '../../a11y';
18
+
19
+ /**
20
+ * Contenido de diálogo de la librería ng-comps.
21
+ * Envuelve las directivas de Angular Material `mat-dialog-*` y expone
22
+ * una API uniforme con look and feel de marca.
23
+ *
24
+ * Uso:
25
+ * ```
26
+ * dialog.open(MfDialogComponent, {
27
+ * data: { title: 'Confirmar', message: '¿Deseas continuar?' }
28
+ * });
29
+ * ```
30
+ */
31
+ @Component({
32
+ selector: 'mf-dialog',
33
+ imports: [MatDialogModule, MatIconModule],
34
+ template: `
35
+ <div
36
+ [class]="hostClasses()"
37
+ [attr.role]="role()"
38
+ aria-modal="true"
39
+ [attr.aria-label]="resolvedAriaLabel()"
40
+ [attr.aria-labelledby]="computedLabelledby()"
41
+ [attr.aria-describedby]="computedDescribedby()"
42
+ >
43
+ @if (title()) {
44
+ <h2 mat-dialog-title class="mf-dialog__header" [id]="titleId()">
45
+ <span class="mf-dialog__title">{{ title() }}</span>
46
+ @if (showClose()) {
47
+ <button
48
+ mat-icon-button
49
+ class="mf-dialog__close"
50
+ (click)="onClose()"
51
+ [attr.aria-label]="closeButtonLabel()"
52
+ type="button"
53
+ >
54
+ <mat-icon aria-hidden="true">close</mat-icon>
55
+ </button>
56
+ }
57
+ </h2>
58
+ } @else if (showClose()) {
59
+ <div class="mf-dialog__header mf-dialog__header--compact">
60
+ <span></span>
61
+ <button
62
+ mat-icon-button
63
+ class="mf-dialog__close"
64
+ (click)="onClose()"
65
+ [attr.aria-label]="closeButtonLabel()"
66
+ type="button"
67
+ >
68
+ <mat-icon aria-hidden="true">close</mat-icon>
69
+ </button>
70
+ </div>
71
+ }
72
+
73
+ <div mat-dialog-content class="mf-dialog__content">
74
+ @if (message()) {
75
+ <p class="mf-dialog__message" [id]="descriptionId()">{{ message() }}</p>
76
+ }
77
+
78
+ <ng-content />
79
+ </div>
80
+
81
+ @if (showActions()) {
82
+ <div mat-dialog-actions class="mf-dialog__actions">
83
+ <ng-content select="[mfDialogActions]" />
84
+ </div>
85
+ }
86
+ </div>
87
+ `,
88
+ styleUrl: './mf-dialog.component.css',
89
+ changeDetection: ChangeDetectionStrategy.OnPush,
90
+ })
91
+ export class MfDialogComponent {
92
+ private readonly dialogRef = inject(MatDialogRef<MfDialogComponent>, {
93
+ optional: true,
94
+ });
95
+ private readonly generatedId = createUniqueId('mf-dialog');
96
+
97
+ /** ID base del diálogo */
98
+ readonly id = input<string | undefined>(undefined);
99
+ /** Título del diálogo */
100
+ readonly title = input<string | undefined>(undefined);
101
+ /** Etiqueta accesible alternativa cuando no existe título visible */
102
+ readonly ariaLabel = input<string | undefined>(undefined);
103
+ /** Referencia externa a elementos que etiquetan el diálogo */
104
+ readonly ariaLabelledby = input<string | undefined>(undefined);
105
+ /** Referencia externa a elementos descriptivos adicionales */
106
+ readonly ariaDescribedby = input<string | undefined>(undefined);
107
+ /** Mensaje descriptivo */
108
+ readonly message = input<string | undefined>(undefined);
109
+ /** Mostrar botón de cerrar */
110
+ readonly showClose = input(true);
111
+ /** Mostrar área de acciones (footer) */
112
+ readonly showActions = input(true);
113
+ /** Rol del diálogo */
114
+ readonly role = input<'dialog' | 'alertdialog'>('dialog');
115
+ /** Etiqueta accesible del botón de cierre */
116
+ readonly closeButtonLabel = input('Cerrar diálogo');
117
+
118
+ readonly mfClose = output<void>();
119
+
120
+ constructor() {
121
+ effect(() => {
122
+ if (
123
+ !hasAccessibleName(
124
+ this.title(),
125
+ this.ariaLabel(),
126
+ this.ariaLabelledby(),
127
+ )
128
+ ) {
129
+ warnInDev(
130
+ 'mf-dialog requiere `title`, `ariaLabel` o `ariaLabelledby` para exponer un nombre accesible.',
131
+ );
132
+ }
133
+ });
134
+ }
135
+
136
+ readonly dialogId = computed(() => this.id() ?? this.generatedId);
137
+ readonly titleId = computed(() => `${this.dialogId()}-title`);
138
+ readonly descriptionId = computed(() => `${this.dialogId()}-description`);
139
+ readonly computedLabelledby = computed(() =>
140
+ mergeAriaIds(
141
+ this.ariaLabelledby(),
142
+ this.title() ? this.titleId() : null,
143
+ ),
144
+ );
145
+ readonly computedDescribedby = computed(() =>
146
+ mergeAriaIds(
147
+ this.ariaDescribedby(),
148
+ this.message() ? this.descriptionId() : null,
149
+ ),
150
+ );
151
+ readonly resolvedAriaLabel = computed(() =>
152
+ this.title() ? null : this.ariaLabel() ?? null,
153
+ );
154
+ readonly hostClasses = computed(() => 'mf-dialog');
155
+
156
+ onClose(): void {
157
+ this.mfClose.emit();
158
+ this.dialogRef?.close();
159
+ }
160
+ }
@@ -0,0 +1,61 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { MatDialog } from '@angular/material/dialog';
3
+ import { MfDialogService } from './mf-dialog.service';
4
+
5
+ describe('MfDialogService', () => {
6
+ let service: MfDialogService;
7
+ let dialogSpy: { open: ReturnType<typeof vi.fn> };
8
+
9
+ beforeEach(() => {
10
+ dialogSpy = {
11
+ open: vi.fn().mockReturnValue({}),
12
+ };
13
+
14
+ TestBed.configureTestingModule({
15
+ providers: [
16
+ MfDialogService,
17
+ { provide: MatDialog, useValue: dialogSpy },
18
+ ],
19
+ });
20
+
21
+ service = TestBed.inject(MfDialogService);
22
+ });
23
+
24
+ it('should be created', () => {
25
+ expect(service).toBeTruthy();
26
+ });
27
+
28
+ it('should apply safe defaults for focus, role and panel class', () => {
29
+ class DialogContentComponent {}
30
+
31
+ service.open(DialogContentComponent, {
32
+ ariaLabel: 'Eliminar proyecto',
33
+ });
34
+
35
+ expect(dialogSpy.open).toHaveBeenCalledWith(
36
+ DialogContentComponent,
37
+ expect.objectContaining({
38
+ role: 'dialog',
39
+ autoFocus: 'first-tabbable',
40
+ restoreFocus: true,
41
+ ariaLabel: 'Eliminar proyecto',
42
+ panelClass: ['mf-dialog-panel'],
43
+ }),
44
+ );
45
+ });
46
+
47
+ it('should merge custom panel classes with the base dialog class', () => {
48
+ class DialogContentComponent {}
49
+
50
+ service.open(DialogContentComponent, {
51
+ panelClass: ['custom-panel', 'danger-panel'],
52
+ });
53
+
54
+ expect(dialogSpy.open).toHaveBeenCalledWith(
55
+ DialogContentComponent,
56
+ expect.objectContaining({
57
+ panelClass: ['mf-dialog-panel', 'custom-panel', 'danger-panel'],
58
+ }),
59
+ );
60
+ });
61
+ });
@@ -0,0 +1,52 @@
1
+ import { Injectable, TemplateRef, Type, inject } from '@angular/core';
2
+ import {
3
+ DialogRole,
4
+ MatDialog,
5
+ MatDialogConfig,
6
+ MatDialogRef,
7
+ } from '@angular/material/dialog';
8
+
9
+ export interface MfDialogOpenConfig<D = unknown>
10
+ extends Omit<
11
+ MatDialogConfig<D>,
12
+ | 'ariaDescribedBy'
13
+ | 'ariaLabel'
14
+ | 'ariaLabelledBy'
15
+ | 'autoFocus'
16
+ | 'panelClass'
17
+ | 'restoreFocus'
18
+ | 'role'
19
+ > {
20
+ ariaLabel?: string;
21
+ ariaLabelledby?: string;
22
+ ariaDescribedby?: string;
23
+ autoFocus?: MatDialogConfig<D>['autoFocus'];
24
+ panelClass?: string | string[];
25
+ restoreFocus?: boolean;
26
+ role?: DialogRole;
27
+ }
28
+
29
+ @Injectable({ providedIn: 'root' })
30
+ export class MfDialogService {
31
+ private readonly dialog = inject(MatDialog);
32
+
33
+ open<T, D = unknown, R = unknown>(
34
+ component: Type<T> | TemplateRef<T>,
35
+ config: MfDialogOpenConfig<D> = {},
36
+ ): MatDialogRef<T, R> {
37
+ const panelClass = Array.isArray(config.panelClass)
38
+ ? ['mf-dialog-panel', ...config.panelClass]
39
+ : ['mf-dialog-panel', ...(config.panelClass ? [config.panelClass] : [])];
40
+
41
+ return this.dialog.open(component, {
42
+ ...config,
43
+ role: config.role ?? 'dialog',
44
+ autoFocus: config.autoFocus ?? 'first-tabbable',
45
+ restoreFocus: config.restoreFocus ?? true,
46
+ ariaLabel: config.ariaLabel,
47
+ ariaLabelledBy: config.ariaLabelledby,
48
+ ariaDescribedBy: config.ariaDescribedby,
49
+ panelClass,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ export { MfDividerComponent } from './mf-divider.component';
2
+ export type { MfDividerVariant } from './mf-divider.component';
@@ -0,0 +1,38 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ :host:has(.mf-divider--vertical) {
6
+ display: inline-block;
7
+ height: 100%;
8
+ }
9
+
10
+ .mf-divider .mat-divider {
11
+ border-top-color: var(--mf-color-border) !important;
12
+ }
13
+
14
+ .mf-divider--vertical .mat-divider {
15
+ border-left-color: var(--mf-color-border) !important;
16
+ }
17
+
18
+ /* ── Middle inset ─────────────────────────────────────────────── */
19
+ .mf-divider--middle .mat-divider {
20
+ margin-left: var(--mf-space-4) !important;
21
+ margin-right: var(--mf-space-4) !important;
22
+ }
23
+
24
+ /* ── Spacing ──────────────────────────────────────────────────── */
25
+ .mf-divider--spacing-sm {
26
+ padding-top: var(--mf-space-2);
27
+ padding-bottom: var(--mf-space-2);
28
+ }
29
+
30
+ .mf-divider--spacing-md {
31
+ padding-top: var(--mf-space-4);
32
+ padding-bottom: var(--mf-space-4);
33
+ }
34
+
35
+ .mf-divider--spacing-lg {
36
+ padding-top: var(--mf-space-6);
37
+ padding-bottom: var(--mf-space-6);
38
+ }
@@ -0,0 +1,40 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MfDividerComponent } from './mf-divider.component';
3
+
4
+ describe('MfDividerComponent', () => {
5
+ let fixture: ComponentFixture<MfDividerComponent>;
6
+ let component: MfDividerComponent;
7
+
8
+ beforeEach(async () => {
9
+ await TestBed.configureTestingModule({
10
+ imports: [MfDividerComponent],
11
+ }).compileComponents();
12
+
13
+ fixture = TestBed.createComponent(MfDividerComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+
22
+ it('should render mat-divider', () => {
23
+ const divider = fixture.nativeElement.querySelector('mat-divider');
24
+ expect(divider).toBeTruthy();
25
+ });
26
+
27
+ it('should apply full variant by default', () => {
28
+ expect(component.hostClasses()).toContain('mf-divider--full');
29
+ });
30
+
31
+ it('should compute inset value for inset variant', () => {
32
+ fixture.componentRef.setInput('variant', 'inset');
33
+ expect(component.insetValue()).toBe(true);
34
+ });
35
+
36
+ it('should apply vertical class when vertical', () => {
37
+ fixture.componentRef.setInput('vertical', true);
38
+ expect(component.hostClasses()).toContain('mf-divider--vertical');
39
+ });
40
+ });