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,53 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ } from '@angular/core';
7
+ import { MatExpansionModule } from '@angular/material/expansion';
8
+
9
+ export interface MfAccordionPanel {
10
+ title: string;
11
+ description?: string;
12
+ content: string;
13
+ expanded?: boolean;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Accordion de la librería ng-comps.
19
+ * Envuelve Angular Material `mat-accordion` / `mat-expansion-panel` y expone
20
+ * una API uniforme con look and feel de marca.
21
+ */
22
+ @Component({
23
+ selector: 'mf-accordion',
24
+ imports: [MatExpansionModule],
25
+ template: `
26
+ <mat-accordion [multi]="multi()" [class]="hostClasses()">
27
+ @for (panel of panels(); track panel.title) {
28
+ <mat-expansion-panel
29
+ [expanded]="panel.expanded ?? false"
30
+ [disabled]="panel.disabled ?? false"
31
+ >
32
+ <mat-expansion-panel-header>
33
+ <mat-panel-title>{{ panel.title }}</mat-panel-title>
34
+ @if (panel.description) {
35
+ <mat-panel-description>{{ panel.description }}</mat-panel-description>
36
+ }
37
+ </mat-expansion-panel-header>
38
+ <p>{{ panel.content }}</p>
39
+ </mat-expansion-panel>
40
+ }
41
+ </mat-accordion>
42
+ `,
43
+ styleUrl: './mf-accordion.component.css',
44
+ changeDetection: ChangeDetectionStrategy.OnPush,
45
+ })
46
+ export class MfAccordionComponent {
47
+ /** Paneles del accordion */
48
+ readonly panels = input.required<MfAccordionPanel[]>();
49
+ /** Permite múltiples paneles abiertos */
50
+ readonly multi = input(false);
51
+
52
+ readonly hostClasses = computed(() => 'mf-accordion');
53
+ }
@@ -0,0 +1,2 @@
1
+ export { MfAlertComponent } from './mf-alert.component';
2
+ export type { MfAlertSeverity } from './mf-alert.component';
@@ -0,0 +1,100 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .mf-alert {
6
+ display: flex;
7
+ align-items: flex-start;
8
+ gap: var(--mf-space-3);
9
+ padding: var(--mf-space-3) var(--mf-space-4);
10
+ border-radius: var(--mf-radius-md);
11
+ font-family: var(--mf-font-base);
12
+ font-size: var(--mf-text-sm);
13
+ line-height: var(--mf-leading-normal);
14
+ }
15
+
16
+ /* ── Info ──────────────────────────────────────────────────────── */
17
+ .mf-alert--info {
18
+ background-color: var(--mf-color-secondary-50);
19
+ border-left: 4px solid var(--mf-color-secondary-500);
20
+ color: var(--mf-color-secondary-900);
21
+ }
22
+
23
+ .mf-alert--info .mf-alert__icon {
24
+ color: var(--mf-color-secondary-500);
25
+ }
26
+
27
+ /* ── Success ──────────────────────────────────────────────────── */
28
+ .mf-alert--success {
29
+ background-color: var(--mf-color-primary-50);
30
+ border-left: 4px solid var(--mf-color-primary-500);
31
+ color: var(--mf-color-primary-900);
32
+ }
33
+
34
+ .mf-alert--success .mf-alert__icon {
35
+ color: var(--mf-color-primary-500);
36
+ }
37
+
38
+ /* ── Warning ──────────────────────────────────────────────────── */
39
+ .mf-alert--warning {
40
+ background-color: #fffbeb;
41
+ border-left: 4px solid var(--mf-color-accent-500);
42
+ color: var(--mf-color-accent-700);
43
+ }
44
+
45
+ .mf-alert--warning .mf-alert__icon {
46
+ color: var(--mf-color-accent-500);
47
+ }
48
+
49
+ /* ── Error ─────────────────────────────────────────────────────── */
50
+ .mf-alert--error {
51
+ background-color: #fef2f2;
52
+ border-left: 4px solid var(--mf-color-error-500);
53
+ color: var(--mf-color-error-700);
54
+ }
55
+
56
+ .mf-alert--error .mf-alert__icon {
57
+ color: var(--mf-color-error-500);
58
+ }
59
+
60
+ /* ── Content ──────────────────────────────────────────────────── */
61
+ .mf-alert__content {
62
+ flex: 1;
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: var(--mf-space-1);
66
+ }
67
+
68
+ .mf-alert__title {
69
+ font-weight: var(--mf-weight-bold);
70
+ }
71
+
72
+ .mf-alert__icon {
73
+ flex-shrink: 0;
74
+ margin-top: 1px;
75
+ }
76
+
77
+ /* ── Close button ─────────────────────────────────────────────── */
78
+ .mf-alert__close {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ border: none;
83
+ background: none;
84
+ cursor: pointer;
85
+ padding: var(--mf-space-1);
86
+ border-radius: var(--mf-radius-sm);
87
+ color: inherit;
88
+ opacity: 0.6;
89
+ transition: opacity var(--mf-duration-fast) var(--mf-ease-standard);
90
+ }
91
+
92
+ .mf-alert__close:hover {
93
+ opacity: 1;
94
+ }
95
+
96
+ .mf-alert__close .mat-icon {
97
+ font-size: 18px;
98
+ width: 18px;
99
+ height: 18px;
100
+ }
@@ -0,0 +1,59 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MfAlertComponent } from './mf-alert.component';
3
+
4
+ describe('MfAlertComponent', () => {
5
+ let fixture: ComponentFixture<MfAlertComponent>;
6
+ let component: MfAlertComponent;
7
+
8
+ beforeEach(async () => {
9
+ await TestBed.configureTestingModule({
10
+ imports: [MfAlertComponent],
11
+ }).compileComponents();
12
+
13
+ fixture = TestBed.createComponent(MfAlertComponent);
14
+ component = fixture.componentInstance;
15
+ fixture.componentRef.setInput('message', 'Test alert');
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('should create', () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+
23
+ it('should render message', () => {
24
+ const msg = fixture.nativeElement.querySelector('.mf-alert__message');
25
+ expect(msg?.textContent).toContain('Test alert');
26
+ });
27
+
28
+ it('should apply info severity by default', () => {
29
+ expect(component.hostClasses()).toContain('mf-alert--info');
30
+ });
31
+
32
+ it('should show info icon by default', () => {
33
+ expect(component.iconName()).toBe('info');
34
+ });
35
+
36
+ it('should show check_circle icon for success', () => {
37
+ fixture.componentRef.setInput('severity', 'success');
38
+ expect(component.iconName()).toBe('check_circle');
39
+ });
40
+
41
+ it('should show title when provided', () => {
42
+ fixture.componentRef.setInput('title', 'Atención');
43
+ fixture.detectChanges();
44
+ const title = fixture.nativeElement.querySelector('.mf-alert__title');
45
+ expect(title?.textContent).toContain('Atención');
46
+ });
47
+
48
+ it('should show close button when dismissible', () => {
49
+ fixture.componentRef.setInput('dismissible', true);
50
+ fixture.detectChanges();
51
+ const close = fixture.nativeElement.querySelector('.mf-alert__close');
52
+ expect(close).toBeTruthy();
53
+ });
54
+
55
+ it('should have role alert', () => {
56
+ const alert = fixture.nativeElement.querySelector('[role="alert"]');
57
+ expect(alert).toBeTruthy();
58
+ });
59
+ });
@@ -0,0 +1,68 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ output,
7
+ } from '@angular/core';
8
+ import { MatIconModule } from '@angular/material/icon';
9
+
10
+ export type MfAlertSeverity = 'info' | 'success' | 'warning' | 'error';
11
+
12
+ /**
13
+ * Alerta de la librería ng-comps.
14
+ * Componente de banner para mensajes de sistema, alertas y notificaciones.
15
+ * Ideal para dashboards y paneles administrativos.
16
+ */
17
+ @Component({
18
+ selector: 'mf-alert',
19
+ imports: [MatIconModule],
20
+ template: `
21
+ <div [class]="hostClasses()" role="alert">
22
+ <mat-icon class="mf-alert__icon" aria-hidden="true">{{ iconName() }}</mat-icon>
23
+ <div class="mf-alert__content">
24
+ @if (title()) {
25
+ <strong class="mf-alert__title">{{ title() }}</strong>
26
+ }
27
+ <span class="mf-alert__message">{{ message() }}</span>
28
+ </div>
29
+ @if (dismissible()) {
30
+ <button
31
+ class="mf-alert__close"
32
+ (click)="mfDismiss.emit()"
33
+ [attr.aria-label]="'Cerrar alerta'"
34
+ >
35
+ <mat-icon aria-hidden="true">close</mat-icon>
36
+ </button>
37
+ }
38
+ </div>
39
+ `,
40
+ styleUrl: './mf-alert.component.css',
41
+ changeDetection: ChangeDetectionStrategy.OnPush,
42
+ })
43
+ export class MfAlertComponent {
44
+ /** Mensaje principal */
45
+ readonly message = input.required<string>();
46
+ /** Título opcional */
47
+ readonly title = input<string | undefined>(undefined);
48
+ /** Severidad de la alerta */
49
+ readonly severity = input<MfAlertSeverity>('info');
50
+ /** Se puede cerrar */
51
+ readonly dismissible = input(false);
52
+
53
+ readonly mfDismiss = output<void>();
54
+
55
+ readonly iconName = computed(() => {
56
+ const map: Record<MfAlertSeverity, string> = {
57
+ info: 'info',
58
+ success: 'check_circle',
59
+ warning: 'warning',
60
+ error: 'error',
61
+ };
62
+ return map[this.severity()];
63
+ });
64
+
65
+ readonly hostClasses = computed(() => {
66
+ return ['mf-alert', `mf-alert--${this.severity()}`].join(' ');
67
+ });
68
+ }
@@ -0,0 +1,5 @@
1
+ export { MfAutocompleteComponent } from './mf-autocomplete.component';
2
+ export type {
3
+ MfAutocompleteOption,
4
+ MfAutocompleteSize,
5
+ } from './mf-autocomplete.component';
@@ -0,0 +1,105 @@
1
+ :host {
2
+ display: inline-block;
3
+ }
4
+
5
+ /* ── Base ──────────────────────────────────────────────────────── */
6
+ .mf-autocomplete {
7
+ font-family: var(--mf-font-base) !important;
8
+ width: 100%;
9
+ }
10
+
11
+ .mf-autocomplete--full {
12
+ width: 100%;
13
+ }
14
+
15
+ /* ── Mat-form-field overrides ─────────────────────────────────── */
16
+ .mf-autocomplete .mat-mdc-text-field-wrapper {
17
+ border-radius: var(--mf-radius-md) !important;
18
+ }
19
+
20
+ .mf-autocomplete .mdc-notched-outline__leading,
21
+ .mf-autocomplete .mdc-notched-outline__notch,
22
+ .mf-autocomplete .mdc-notched-outline__trailing {
23
+ border-color: var(--mf-color-border) !important;
24
+ transition: border-color var(--mf-duration-base) var(--mf-ease-standard);
25
+ }
26
+
27
+ .mf-autocomplete .mat-mdc-form-field-focus-overlay {
28
+ background-color: transparent !important;
29
+ }
30
+
31
+ /* ── Focus state ──────────────────────────────────────────────── */
32
+ .mf-autocomplete.mat-focused .mdc-notched-outline__leading,
33
+ .mf-autocomplete.mat-focused .mdc-notched-outline__notch,
34
+ .mf-autocomplete.mat-focused .mdc-notched-outline__trailing {
35
+ border-color: var(--mf-color-brand) !important;
36
+ border-width: 1.5px !important;
37
+ }
38
+
39
+ /* ── Error state ──────────────────────────────────────────────── */
40
+ .mf-autocomplete--error .mdc-notched-outline__leading,
41
+ .mf-autocomplete--error .mdc-notched-outline__notch,
42
+ .mf-autocomplete--error .mdc-notched-outline__trailing {
43
+ border-color: var(--mf-color-error-500) !important;
44
+ }
45
+
46
+ /* ── Label ────────────────────────────────────────────────────── */
47
+ .mf-autocomplete .mat-mdc-floating-label {
48
+ font-family: var(--mf-font-base) !important;
49
+ font-weight: var(--mf-weight-medium) !important;
50
+ color: var(--mf-color-neutral-400) !important;
51
+ }
52
+
53
+ .mf-autocomplete.mat-focused .mat-mdc-floating-label {
54
+ color: var(--mf-color-brand) !important;
55
+ }
56
+
57
+ /* ── Input text ───────────────────────────────────────────────── */
58
+ .mf-autocomplete .mat-mdc-input-element {
59
+ font-family: var(--mf-font-base) !important;
60
+ color: var(--mf-color-on-surface) !important;
61
+ caret-color: var(--mf-color-brand);
62
+ }
63
+
64
+ /* ── Hint & Error ─────────────────────────────────────────────── */
65
+ .mf-autocomplete .mat-mdc-form-field-hint {
66
+ font-size: var(--mf-text-xs) !important;
67
+ color: var(--mf-color-neutral-400) !important;
68
+ }
69
+
70
+ .mf-autocomplete .mat-mdc-form-field-error {
71
+ font-size: var(--mf-text-xs) !important;
72
+ }
73
+
74
+ /* ── Prefix / Suffix icons ────────────────────────────────────── */
75
+ .mf-autocomplete .mat-icon {
76
+ color: var(--mf-color-neutral-400) !important;
77
+ font-size: 20px;
78
+ width: 20px;
79
+ height: 20px;
80
+ transition: color var(--mf-duration-base) var(--mf-ease-standard);
81
+ }
82
+
83
+ .mf-autocomplete.mat-focused .mat-icon {
84
+ color: var(--mf-color-brand) !important;
85
+ }
86
+
87
+ /* ── Sizes ─────────────────────────────────────────────────────── */
88
+ .mf-autocomplete--sm .mat-mdc-input-element {
89
+ font-size: var(--mf-text-sm) !important;
90
+ }
91
+
92
+ .mf-autocomplete--md .mat-mdc-input-element {
93
+ font-size: var(--mf-text-base) !important;
94
+ }
95
+
96
+ .mf-autocomplete--lg .mat-mdc-input-element {
97
+ font-size: var(--mf-text-lg) !important;
98
+ padding-top: var(--mf-space-1) !important;
99
+ padding-bottom: var(--mf-space-1) !important;
100
+ }
101
+
102
+ /* ── Disabled ──────────────────────────────────────────────────── */
103
+ .mf-autocomplete .mat-mdc-input-element:disabled {
104
+ color: var(--mf-color-neutral-300) !important;
105
+ }
@@ -0,0 +1,116 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { MfAutocompleteComponent } from './mf-autocomplete.component';
3
+ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4
+
5
+ describe('MfAutocompleteComponent', () => {
6
+ let fixture: ComponentFixture<MfAutocompleteComponent>;
7
+ let component: MfAutocompleteComponent;
8
+
9
+ const testOptions = [
10
+ { value: '1', label: 'Angular' },
11
+ { value: '2', label: 'React' },
12
+ { value: '3', label: 'Vue' },
13
+ { value: '4', label: 'Svelte', disabled: true },
14
+ ];
15
+
16
+ beforeEach(async () => {
17
+ await TestBed.configureTestingModule({
18
+ imports: [MfAutocompleteComponent, NoopAnimationsModule],
19
+ }).compileComponents();
20
+
21
+ fixture = TestBed.createComponent(MfAutocompleteComponent);
22
+ component = fixture.componentInstance;
23
+ fixture.componentRef.setInput('options', testOptions);
24
+ fixture.detectChanges();
25
+ });
26
+
27
+ it('should create', () => {
28
+ expect(component).toBeTruthy();
29
+ });
30
+
31
+ it('should render input with mat-autocomplete', () => {
32
+ const input = fixture.nativeElement.querySelector('input[matinput]');
33
+ expect(input).toBeTruthy();
34
+ });
35
+
36
+ it('should apply md size class by default', () => {
37
+ expect(component.hostClasses()).toContain('mf-autocomplete--md');
38
+ });
39
+
40
+ it('should apply sm size class', () => {
41
+ fixture.componentRef.setInput('size', 'sm');
42
+ expect(component.hostClasses()).toContain('mf-autocomplete--sm');
43
+ });
44
+
45
+ it('should apply lg size class', () => {
46
+ fixture.componentRef.setInput('size', 'lg');
47
+ expect(component.hostClasses()).toContain('mf-autocomplete--lg');
48
+ });
49
+
50
+ it('should include mf-autocomplete--full when fullWidth is true', () => {
51
+ fixture.componentRef.setInput('fullWidth', true);
52
+ expect(component.hostClasses()).toContain('mf-autocomplete--full');
53
+ });
54
+
55
+ it('should include mf-autocomplete--error when error is provided', () => {
56
+ fixture.componentRef.setInput('error', 'Required');
57
+ expect(component.hostClasses()).toContain('mf-autocomplete--error');
58
+ });
59
+
60
+ it('should include mf-autocomplete-panel in panelClasses by default', () => {
61
+ expect(component.autocompletePanelClasses()).toContain('mf-autocomplete-panel');
62
+ });
63
+
64
+ it('should merge extra panelClass string', () => {
65
+ fixture.componentRef.setInput('panelClass', 'custom-panel');
66
+ expect(component.autocompletePanelClasses()).toContain('mf-autocomplete-panel');
67
+ expect(component.autocompletePanelClasses()).toContain('custom-panel');
68
+ });
69
+
70
+ it('should filter options based on input text', () => {
71
+ component.inputValue = 'ang';
72
+ fixture.detectChanges();
73
+ const filtered = component.filteredOptions();
74
+ expect(filtered.length).toBe(1);
75
+ expect(filtered[0].label).toBe('Angular');
76
+ });
77
+
78
+ it('should return all options when input is empty', () => {
79
+ component.inputValue = '';
80
+ expect(component.filteredOptions().length).toBe(testOptions.length);
81
+ });
82
+
83
+ it('should emit mfInput on input change', () => {
84
+ const spy = vi.fn();
85
+ component.mfInput.subscribe(spy);
86
+ component.onInputChange('Ang');
87
+ expect(spy).toHaveBeenCalledWith('Ang');
88
+ });
89
+
90
+ it('should emit mfOptionSelected when option is selected', () => {
91
+ const spy = vi.fn();
92
+ component.mfOptionSelected.subscribe(spy);
93
+ component.onOptionSelected({ option: { value: 'Angular' } });
94
+ expect(spy).toHaveBeenCalledWith(testOptions[0]);
95
+ });
96
+
97
+ it('should render label when provided', () => {
98
+ fixture.componentRef.setInput('label', 'Framework');
99
+ fixture.detectChanges();
100
+ const label = fixture.nativeElement.querySelector('mat-label');
101
+ expect(label?.textContent).toContain('Framework');
102
+ });
103
+
104
+ it('should render hint when provided', () => {
105
+ fixture.componentRef.setInput('hint', 'Start typing...');
106
+ fixture.detectChanges();
107
+ const hint = fixture.nativeElement.querySelector('mat-hint');
108
+ expect(hint?.textContent).toContain('Start typing...');
109
+ });
110
+
111
+ it('should render error when provided', () => {
112
+ fixture.componentRef.setInput('error', 'Selection required');
113
+ fixture.detectChanges();
114
+ expect(fixture.nativeElement.textContent).toContain('Selection required');
115
+ });
116
+ });