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,68 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .mf-table__wrapper {
6
+ overflow-x: auto;
7
+ border-radius: var(--mf-radius-md);
8
+ border: 1px solid var(--mf-color-border);
9
+ }
10
+
11
+ .mf-table .mat-mdc-table {
12
+ width: 100%;
13
+ font-family: var(--mf-font-base) !important;
14
+ }
15
+
16
+ .mf-table .mat-mdc-header-cell {
17
+ font-family: var(--mf-font-base) !important;
18
+ font-weight: var(--mf-weight-bold) !important;
19
+ font-size: var(--mf-text-sm) !important;
20
+ color: var(--mf-color-neutral-600) !important;
21
+ background-color: var(--mf-color-neutral-50) !important;
22
+ border-bottom-color: var(--mf-color-border) !important;
23
+ }
24
+
25
+ .mf-table .mat-mdc-cell {
26
+ font-family: var(--mf-font-base) !important;
27
+ font-size: var(--mf-text-sm) !important;
28
+ color: var(--mf-color-on-surface) !important;
29
+ border-bottom-color: var(--mf-color-border) !important;
30
+ }
31
+
32
+ .mf-table .mat-mdc-row:hover {
33
+ background-color: var(--mf-color-brand-light) !important;
34
+ }
35
+
36
+ /* ── Striped ──────────────────────────────────────────────────── */
37
+ .mf-table--striped .mat-mdc-row:nth-child(even) {
38
+ background-color: var(--mf-color-neutral-50);
39
+ }
40
+
41
+ /* ── Bordered ─────────────────────────────────────────────────── */
42
+ .mf-table--bordered .mat-mdc-cell,
43
+ .mf-table--bordered .mat-mdc-header-cell {
44
+ border-right: 1px solid var(--mf-color-border) !important;
45
+ }
46
+
47
+ .mf-table--bordered .mat-mdc-cell:last-child,
48
+ .mf-table--bordered .mat-mdc-header-cell:last-child {
49
+ border-right: none !important;
50
+ }
51
+
52
+ /* ── Sort header ──────────────────────────────────────────────── */
53
+ .mf-table .mat-sort-header-arrow {
54
+ color: var(--mf-color-brand) !important;
55
+ }
56
+
57
+ .mf-table__actions-header,
58
+ .mf-table__actions-cell {
59
+ white-space: nowrap;
60
+ width: 1%;
61
+ }
62
+
63
+ .mf-table__action {
64
+ min-width: 0;
65
+ font-family: var(--mf-font-base) !important;
66
+ font-size: var(--mf-text-sm) !important;
67
+ font-weight: var(--mf-weight-medium) !important;
68
+ }
@@ -0,0 +1,76 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MfTableComponent } from './mf-table.component';
3
+ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4
+
5
+ describe('MfTableComponent', () => {
6
+ let fixture: ComponentFixture<MfTableComponent>;
7
+ let component: MfTableComponent;
8
+
9
+ const testColumns = [
10
+ { key: 'name', header: 'Name' },
11
+ { key: 'email', header: 'Email' },
12
+ ];
13
+
14
+ const testData = [
15
+ { name: 'John', email: 'john@test.com' },
16
+ { name: 'Jane', email: 'jane@test.com' },
17
+ ];
18
+
19
+ beforeEach(async () => {
20
+ await TestBed.configureTestingModule({
21
+ imports: [MfTableComponent, NoopAnimationsModule],
22
+ }).compileComponents();
23
+
24
+ fixture = TestBed.createComponent(MfTableComponent);
25
+ component = fixture.componentInstance;
26
+ fixture.componentRef.setInput('columns', testColumns);
27
+ fixture.componentRef.setInput('data', testData);
28
+ fixture.detectChanges();
29
+ });
30
+
31
+ it('should create', () => {
32
+ expect(component).toBeTruthy();
33
+ });
34
+
35
+ it('should compute displayed columns', () => {
36
+ expect(component.displayedColumns()).toEqual(['name', 'email']);
37
+ });
38
+
39
+ it('should render table', () => {
40
+ const table = fixture.nativeElement.querySelector('table');
41
+ expect(table).toBeTruthy();
42
+ });
43
+
44
+ it('should render header cells', () => {
45
+ const headers = fixture.nativeElement.querySelectorAll('th');
46
+ expect(headers.length).toBe(2);
47
+ });
48
+
49
+ it('should render data rows', () => {
50
+ const rows = fixture.nativeElement.querySelectorAll('tr.mat-mdc-row');
51
+ expect(rows.length).toBe(2);
52
+ });
53
+
54
+ it('should apply default variant', () => {
55
+ expect(component.hostClasses()).toContain('mf-table--default');
56
+ });
57
+
58
+ it('should expose per-row aria labels for explicit actions', () => {
59
+ fixture.componentRef.setInput('rowActionLabel', 'Ver detalle');
60
+ fixture.componentRef.setInput(
61
+ 'rowActionAriaLabel',
62
+ (row: Record<string, unknown>) => `Ver detalle de ${row['name']}`,
63
+ );
64
+ fixture.detectChanges();
65
+
66
+ const actionButtons =
67
+ fixture.nativeElement.querySelectorAll('.mf-table__action');
68
+
69
+ expect(actionButtons[0].getAttribute('aria-label')).toBe(
70
+ 'Ver detalle de John',
71
+ );
72
+ expect(actionButtons[1].getAttribute('aria-label')).toBe(
73
+ 'Ver detalle de Jane',
74
+ );
75
+ });
76
+ });
@@ -0,0 +1,117 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ output,
7
+ } from '@angular/core';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import { MatIconModule } from '@angular/material/icon';
10
+ import { MatSortModule, Sort } from '@angular/material/sort';
11
+ import { MatTableModule } from '@angular/material/table';
12
+
13
+ export interface MfTableColumn {
14
+ key: string;
15
+ header: string;
16
+ sortable?: boolean;
17
+ }
18
+
19
+ export type MfTableVariant = 'default' | 'striped' | 'bordered';
20
+
21
+ /**
22
+ * Table de la librería ng-comps.
23
+ * Envuelve Angular Material `mat-table` y expone una API uniforme
24
+ * con look and feel de marca. Ideal para dashboards y paneles de datos.
25
+ */
26
+ @Component({
27
+ selector: 'mf-table',
28
+ imports: [MatTableModule, MatSortModule, MatButtonModule, MatIconModule],
29
+ template: `
30
+ <div class="mf-table__wrapper" [class]="hostClasses()">
31
+ <table
32
+ mat-table
33
+ [dataSource]="data()"
34
+ matSort
35
+ (matSortChange)="mfSortChange.emit($event)"
36
+ >
37
+ @for (col of columns(); track col.key) {
38
+ <ng-container [matColumnDef]="col.key">
39
+ @if (col.sortable) {
40
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>{{ col.header }}</th>
41
+ } @else {
42
+ <th mat-header-cell *matHeaderCellDef>{{ col.header }}</th>
43
+ }
44
+ <td mat-cell *matCellDef="let row">{{ row[col.key] }}</td>
45
+ </ng-container>
46
+ }
47
+
48
+ @if (rowActionLabel()) {
49
+ <ng-container [matColumnDef]="actionColumnKey">
50
+ <th mat-header-cell *matHeaderCellDef class="mf-table__actions-header">
51
+ {{ rowActionHeader() }}
52
+ </th>
53
+ <td mat-cell *matCellDef="let row" class="mf-table__actions-cell">
54
+ <button
55
+ mat-button
56
+ type="button"
57
+ class="mf-table__action"
58
+ [attr.aria-label]="getRowActionAriaLabel(row)"
59
+ (click)="emitRowAction(row)"
60
+ >
61
+ {{ rowActionLabel() }}
62
+ </button>
63
+ </td>
64
+ </ng-container>
65
+ }
66
+
67
+ <tr mat-header-row *matHeaderRowDef="displayedColumns()"></tr>
68
+ <tr mat-row *matRowDef="let row; columns: displayedColumns()"></tr>
69
+ </table>
70
+ </div>
71
+ `,
72
+ styleUrl: './mf-table.component.css',
73
+ changeDetection: ChangeDetectionStrategy.OnPush,
74
+ })
75
+ export class MfTableComponent {
76
+ protected readonly actionColumnKey = 'mf-row-action';
77
+
78
+ /** Columnas de la tabla */
79
+ readonly columns = input.required<MfTableColumn[]>();
80
+ /** Datos de la tabla */
81
+ readonly data = input.required<Record<string, unknown>[]>();
82
+ /** Variante visual */
83
+ readonly variant = input<MfTableVariant>('default');
84
+ /** Texto visible del botón de acción por fila */
85
+ readonly rowActionLabel = input<string | undefined>(undefined);
86
+ /** Cabecera visible de la columna de acción */
87
+ readonly rowActionHeader = input('Acciones');
88
+ readonly rowActionAriaLabel = input<
89
+ ((row: Record<string, unknown>) => string) | undefined
90
+ >(undefined);
91
+
92
+ readonly mfSortChange = output<Sort>();
93
+ readonly mfRowAction = output<Record<string, unknown>>();
94
+ /** @deprecated usa `mfRowAction` para interacciones explícitas por fila */
95
+ readonly mfRowClick = output<Record<string, unknown>>();
96
+
97
+ readonly displayedColumns = computed(() => {
98
+ const columns = this.columns().map((column) => column.key);
99
+ if (this.rowActionLabel()) {
100
+ columns.push(this.actionColumnKey);
101
+ }
102
+ return columns;
103
+ });
104
+
105
+ readonly hostClasses = computed(() => {
106
+ return ['mf-table', `mf-table--${this.variant()}`].join(' ');
107
+ });
108
+
109
+ emitRowAction(row: Record<string, unknown>): void {
110
+ this.mfRowAction.emit(row);
111
+ this.mfRowClick.emit(row);
112
+ }
113
+
114
+ getRowActionAriaLabel(row: Record<string, unknown>): string | null {
115
+ return this.rowActionAriaLabel()?.(row) ?? this.rowActionLabel() ?? null;
116
+ }
117
+ }
@@ -0,0 +1,2 @@
1
+ export { MfTabsComponent } from './mf-tabs.component';
2
+ export type { MfTab, MfTabsVariant } from './mf-tabs.component';
@@ -0,0 +1,31 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .mf-tabs .mat-mdc-tab-header {
6
+ border-bottom: 1px solid var(--mf-color-border) !important;
7
+ }
8
+
9
+ .mf-tabs .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab__text-label {
10
+ color: var(--mf-color-neutral-400) !important;
11
+ font-family: var(--mf-font-base) !important;
12
+ font-weight: var(--mf-weight-medium) !important;
13
+ }
14
+
15
+ .mf-tabs .mat-mdc-tab.mdc-tab--active .mdc-tab__text-label {
16
+ color: var(--mf-color-brand) !important;
17
+ font-family: var(--mf-font-base) !important;
18
+ font-weight: var(--mf-weight-bold) !important;
19
+ }
20
+
21
+ .mf-tabs .mat-mdc-tab-header .mdc-tab-indicator__content--underline {
22
+ border-color: var(--mf-color-brand) !important;
23
+ }
24
+
25
+ .mf-tabs__icon {
26
+ margin-right: var(--mf-space-2);
27
+ font-size: 1.1em;
28
+ height: 1.1em;
29
+ width: 1.1em;
30
+ vertical-align: middle;
31
+ }
@@ -0,0 +1,50 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MfTabsComponent } from './mf-tabs.component';
3
+ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4
+
5
+ describe('MfTabsComponent', () => {
6
+ let fixture: ComponentFixture<MfTabsComponent>;
7
+ let component: MfTabsComponent;
8
+
9
+ const testTabs = [
10
+ { label: 'Tab 1' },
11
+ { label: 'Tab 2' },
12
+ { label: 'Tab 3', disabled: true },
13
+ ];
14
+
15
+ beforeEach(async () => {
16
+ await TestBed.configureTestingModule({
17
+ imports: [MfTabsComponent, NoopAnimationsModule],
18
+ }).compileComponents();
19
+
20
+ fixture = TestBed.createComponent(MfTabsComponent);
21
+ component = fixture.componentInstance;
22
+ fixture.componentRef.setInput('tabs', testTabs);
23
+ fixture.detectChanges();
24
+ });
25
+
26
+ it('should create', () => {
27
+ expect(component).toBeTruthy();
28
+ });
29
+
30
+ it('should render tabs', () => {
31
+ const tabLabels = fixture.nativeElement.querySelectorAll('.mat-mdc-tab');
32
+ expect(tabLabels.length).toBe(3);
33
+ });
34
+
35
+ it('should apply default variant', () => {
36
+ expect(component.hostClasses()).toContain('mf-tabs--default');
37
+ });
38
+
39
+ it('should apply stretched variant', () => {
40
+ fixture.componentRef.setInput('variant', 'stretched');
41
+ expect(component.hostClasses()).toContain('mf-tabs--stretched');
42
+ });
43
+
44
+ it('should emit index change', () => {
45
+ const spy = vi.fn();
46
+ component.mfSelectedIndexChange.subscribe(spy);
47
+ component.mfSelectedIndexChange.emit(1);
48
+ expect(spy).toHaveBeenCalledWith(1);
49
+ });
50
+ });
@@ -0,0 +1,62 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ output,
7
+ } from '@angular/core';
8
+ import { MatTabsModule } from '@angular/material/tabs';
9
+ import { MatIconModule } from '@angular/material/icon';
10
+
11
+ export interface MfTab {
12
+ label: string;
13
+ icon?: string;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export type MfTabsVariant = 'default' | 'stretched';
18
+
19
+ /**
20
+ * Tabs de la librería ng-comps.
21
+ * Envuelve Angular Material `mat-tab-group` y expone una API uniforme
22
+ * con look and feel de marca.
23
+ */
24
+ @Component({
25
+ selector: 'mf-tabs',
26
+ imports: [MatTabsModule, MatIconModule],
27
+ template: `
28
+ <mat-tab-group
29
+ [selectedIndex]="selectedIndex()"
30
+ [class]="hostClasses()"
31
+ [mat-stretch-tabs]="variant() === 'stretched'"
32
+ (selectedIndexChange)="mfSelectedIndexChange.emit($event)"
33
+ >
34
+ @for (tab of tabs(); track tab.label) {
35
+ <mat-tab [disabled]="tab.disabled ?? false">
36
+ <ng-template mat-tab-label>
37
+ @if (tab.icon) {
38
+ <mat-icon class="mf-tabs__icon" aria-hidden="true">{{ tab.icon }}</mat-icon>
39
+ }
40
+ {{ tab.label }}
41
+ </ng-template>
42
+ </mat-tab>
43
+ }
44
+ </mat-tab-group>
45
+ `,
46
+ styleUrl: './mf-tabs.component.css',
47
+ changeDetection: ChangeDetectionStrategy.OnPush,
48
+ })
49
+ export class MfTabsComponent {
50
+ /** Pestañas */
51
+ readonly tabs = input.required<MfTab[]>();
52
+ /** Índice seleccionado */
53
+ readonly selectedIndex = input(0);
54
+ /** Variante visual */
55
+ readonly variant = input<MfTabsVariant>('default');
56
+
57
+ readonly mfSelectedIndexChange = output<number>();
58
+
59
+ readonly hostClasses = computed(() => {
60
+ return ['mf-tabs', `mf-tabs--${this.variant()}`].join(' ');
61
+ });
62
+ }
@@ -0,0 +1,2 @@
1
+ export { MfTextareaComponent } from './mf-textarea.component';
2
+ export type { MfTextareaSize, MfTextareaResize } from './mf-textarea.component';
@@ -0,0 +1,48 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .mf-textarea.mat-mdc-form-field {
6
+ font-family: var(--mf-font-base) !important;
7
+ width: 100%;
8
+ }
9
+
10
+ .mf-textarea textarea.mat-mdc-input-element {
11
+ font-family: var(--mf-font-base) !important;
12
+ line-height: var(--mf-leading-normal) !important;
13
+ }
14
+
15
+ .mf-textarea .mdc-notched-outline__leading,
16
+ .mf-textarea .mdc-notched-outline__notch,
17
+ .mf-textarea .mdc-notched-outline__trailing {
18
+ border-color: var(--mf-color-border) !important;
19
+ }
20
+
21
+ .mf-textarea.mat-mdc-form-field.mat-focused .mdc-notched-outline__leading,
22
+ .mf-textarea.mat-mdc-form-field.mat-focused .mdc-notched-outline__notch,
23
+ .mf-textarea.mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing {
24
+ border-color: var(--mf-color-brand) !important;
25
+ }
26
+
27
+ .mf-textarea .mdc-notched-outline {
28
+ border-radius: var(--mf-radius-md) !important;
29
+ }
30
+
31
+ /* ── Tamaños ───────────────────────────────────────────────────── */
32
+ .mf-textarea--sm textarea.mat-mdc-input-element {
33
+ font-size: var(--mf-text-sm) !important;
34
+ }
35
+
36
+ .mf-textarea--md textarea.mat-mdc-input-element {
37
+ font-size: var(--mf-text-base) !important;
38
+ }
39
+
40
+ .mf-textarea--lg textarea.mat-mdc-input-element {
41
+ font-size: var(--mf-text-lg) !important;
42
+ }
43
+
44
+ /* ── Resize ────────────────────────────────────────────────────── */
45
+ .mf-textarea--resize-none textarea { resize: none; }
46
+ .mf-textarea--resize-vertical textarea { resize: vertical; }
47
+ .mf-textarea--resize-horizontal textarea { resize: horizontal; }
48
+ .mf-textarea--resize-both textarea { resize: both; }
@@ -0,0 +1,55 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MfTextareaComponent } from './mf-textarea.component';
3
+ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4
+
5
+ describe('MfTextareaComponent', () => {
6
+ let fixture: ComponentFixture<MfTextareaComponent>;
7
+ let component: MfTextareaComponent;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [MfTextareaComponent, NoopAnimationsModule],
12
+ }).compileComponents();
13
+
14
+ fixture = TestBed.createComponent(MfTextareaComponent);
15
+ component = fixture.componentInstance;
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('should create', () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+
23
+ it('should render textarea', () => {
24
+ const textarea = fixture.nativeElement.querySelector('textarea');
25
+ expect(textarea).toBeTruthy();
26
+ });
27
+
28
+ it('should apply md size by default', () => {
29
+ expect(component.hostClasses()).toContain('mf-textarea--md');
30
+ });
31
+
32
+ it('should have default 4 rows', () => {
33
+ expect(component.rows()).toBe(4);
34
+ });
35
+
36
+ it('should apply vertical resize by default', () => {
37
+ expect(component.hostClasses()).toContain('mf-textarea--resize-vertical');
38
+ });
39
+
40
+ it('should render label when provided', () => {
41
+ fixture.componentRef.setInput('label', 'Description');
42
+ fixture.detectChanges();
43
+ const label = fixture.nativeElement.querySelector('mat-label');
44
+ expect(label?.textContent).toContain('Description');
45
+ });
46
+
47
+ it('should emit input event', () => {
48
+ const spy = vi.fn();
49
+ component.mfInput.subscribe(spy);
50
+ const textarea = fixture.nativeElement.querySelector('textarea');
51
+ textarea.value = 'Hello';
52
+ textarea.dispatchEvent(new Event('input'));
53
+ expect(spy).toHaveBeenCalledWith('Hello');
54
+ });
55
+ });