ng-comps 1.0.2 → 2.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 (197) hide show
  1. package/fesm2022/ng-comps.mjs +4254 -0
  2. package/package.json +54 -58
  3. package/src/styles.css +54 -0
  4. package/types/ng-comps.d.ts +1348 -0
  5. package/.editorconfig +0 -17
  6. package/.github/copilot-instructions.md +0 -55
  7. package/.github/workflows/ci.yml +0 -29
  8. package/.prettierrc +0 -12
  9. package/.storybook/main.ts +0 -21
  10. package/.storybook/preview.ts +0 -27
  11. package/.storybook/tsconfig.doc.json +0 -10
  12. package/.storybook/tsconfig.json +0 -15
  13. package/.storybook/typings.d.ts +0 -4
  14. package/.vscode/extensions.json +0 -4
  15. package/.vscode/launch.json +0 -20
  16. package/.vscode/mcp.json +0 -9
  17. package/.vscode/tasks.json +0 -42
  18. package/ACCESSIBILITY.md +0 -127
  19. package/angular.json +0 -106
  20. package/documentation.json +0 -13394
  21. package/ng-package.json +0 -27
  22. package/public/favicon.ico +0 -0
  23. package/scripts/prepare-package.mjs +0 -80
  24. package/src/app/a11y/accessibility.utils.ts +0 -35
  25. package/src/app/a11y/index.ts +0 -6
  26. package/src/app/accessibility/ng-comps.a11y.spec.ts +0 -108
  27. package/src/app/app.config.ts +0 -11
  28. package/src/app/app.css +0 -107
  29. package/src/app/app.html +0 -48
  30. package/src/app/app.routes.ts +0 -3
  31. package/src/app/app.spec.ts +0 -23
  32. package/src/app/app.ts +0 -10
  33. package/src/app/components/accordion/index.ts +0 -2
  34. package/src/app/components/accordion/mf-accordion.component.css +0 -38
  35. package/src/app/components/accordion/mf-accordion.component.spec.ts +0 -48
  36. package/src/app/components/accordion/mf-accordion.component.ts +0 -53
  37. package/src/app/components/alert/index.ts +0 -2
  38. package/src/app/components/alert/mf-alert.component.css +0 -100
  39. package/src/app/components/alert/mf-alert.component.spec.ts +0 -59
  40. package/src/app/components/alert/mf-alert.component.ts +0 -68
  41. package/src/app/components/autocomplete/index.ts +0 -5
  42. package/src/app/components/autocomplete/mf-autocomplete.component.css +0 -105
  43. package/src/app/components/autocomplete/mf-autocomplete.component.spec.ts +0 -116
  44. package/src/app/components/autocomplete/mf-autocomplete.component.ts +0 -307
  45. package/src/app/components/avatar/index.ts +0 -2
  46. package/src/app/components/avatar/mf-avatar.component.css +0 -27
  47. package/src/app/components/avatar/mf-avatar.component.spec.ts +0 -49
  48. package/src/app/components/avatar/mf-avatar.component.ts +0 -99
  49. package/src/app/components/badge/index.ts +0 -2
  50. package/src/app/components/badge/mf-badge.component.css +0 -32
  51. package/src/app/components/badge/mf-badge.component.spec.ts +0 -40
  52. package/src/app/components/badge/mf-badge.component.ts +0 -105
  53. package/src/app/components/breadcrumb/index.ts +0 -2
  54. package/src/app/components/breadcrumb/mf-breadcrumb.component.css +0 -61
  55. package/src/app/components/breadcrumb/mf-breadcrumb.component.spec.ts +0 -61
  56. package/src/app/components/breadcrumb/mf-breadcrumb.component.ts +0 -75
  57. package/src/app/components/button/index.ts +0 -2
  58. package/src/app/components/button/mf-button.component.css +0 -136
  59. package/src/app/components/button/mf-button.component.ts +0 -174
  60. package/src/app/components/card/index.ts +0 -2
  61. package/src/app/components/card/mf-card.component.css +0 -82
  62. package/src/app/components/card/mf-card.component.ts +0 -59
  63. package/src/app/components/checkbox/index.ts +0 -1
  64. package/src/app/components/checkbox/mf-checkbox.component.css +0 -75
  65. package/src/app/components/checkbox/mf-checkbox.component.ts +0 -187
  66. package/src/app/components/chip/index.ts +0 -2
  67. package/src/app/components/chip/mf-chip.component.css +0 -69
  68. package/src/app/components/chip/mf-chip.component.spec.ts +0 -47
  69. package/src/app/components/chip/mf-chip.component.ts +0 -77
  70. package/src/app/components/datepicker/index.ts +0 -2
  71. package/src/app/components/datepicker/mf-datepicker.component.css +0 -102
  72. package/src/app/components/datepicker/mf-datepicker.component.spec.ts +0 -69
  73. package/src/app/components/datepicker/mf-datepicker.component.ts +0 -233
  74. package/src/app/components/dialog/index.ts +0 -3
  75. package/src/app/components/dialog/mf-dialog.component.css +0 -73
  76. package/src/app/components/dialog/mf-dialog.component.ts +0 -160
  77. package/src/app/components/dialog/mf-dialog.service.spec.ts +0 -61
  78. package/src/app/components/dialog/mf-dialog.service.ts +0 -52
  79. package/src/app/components/divider/index.ts +0 -2
  80. package/src/app/components/divider/mf-divider.component.css +0 -38
  81. package/src/app/components/divider/mf-divider.component.spec.ts +0 -40
  82. package/src/app/components/divider/mf-divider.component.ts +0 -44
  83. package/src/app/components/form-field/index.ts +0 -1
  84. package/src/app/components/form-field/mf-form-field.component.css +0 -51
  85. package/src/app/components/form-field/mf-form-field.component.ts +0 -74
  86. package/src/app/components/grid-list/index.ts +0 -2
  87. package/src/app/components/grid-list/mf-grid-list.component.css +0 -47
  88. package/src/app/components/grid-list/mf-grid-list.component.spec.ts +0 -57
  89. package/src/app/components/grid-list/mf-grid-list.component.ts +0 -68
  90. package/src/app/components/icon/index.ts +0 -2
  91. package/src/app/components/icon/mf-icon.component.css +0 -56
  92. package/src/app/components/icon/mf-icon.component.ts +0 -41
  93. package/src/app/components/input/index.ts +0 -2
  94. package/src/app/components/input/mf-input.component.css +0 -105
  95. package/src/app/components/input/mf-input.component.ts +0 -217
  96. package/src/app/components/menu/index.ts +0 -2
  97. package/src/app/components/menu/mf-menu.component.css +0 -31
  98. package/src/app/components/menu/mf-menu.component.spec.ts +0 -49
  99. package/src/app/components/menu/mf-menu.component.ts +0 -66
  100. package/src/app/components/paginator/index.ts +0 -1
  101. package/src/app/components/paginator/mf-paginator.component.css +0 -32
  102. package/src/app/components/paginator/mf-paginator.component.spec.ts +0 -44
  103. package/src/app/components/paginator/mf-paginator.component.ts +0 -52
  104. package/src/app/components/progress-bar/index.ts +0 -2
  105. package/src/app/components/progress-bar/mf-progress-bar.component.css +0 -53
  106. package/src/app/components/progress-bar/mf-progress-bar.component.spec.ts +0 -65
  107. package/src/app/components/progress-bar/mf-progress-bar.component.ts +0 -79
  108. package/src/app/components/progress-spinner/index.ts +0 -2
  109. package/src/app/components/progress-spinner/mf-progress-spinner.component.css +0 -38
  110. package/src/app/components/progress-spinner/mf-progress-spinner.component.spec.ts +0 -59
  111. package/src/app/components/progress-spinner/mf-progress-spinner.component.ts +0 -81
  112. package/src/app/components/radio-button/index.ts +0 -2
  113. package/src/app/components/radio-button/mf-radio-button.component.css +0 -86
  114. package/src/app/components/radio-button/mf-radio-button.component.spec.ts +0 -55
  115. package/src/app/components/radio-button/mf-radio-button.component.ts +0 -219
  116. package/src/app/components/select/index.ts +0 -2
  117. package/src/app/components/select/mf-select.component.css +0 -121
  118. package/src/app/components/select/mf-select.component.spec.ts +0 -108
  119. package/src/app/components/select/mf-select.component.ts +0 -252
  120. package/src/app/components/sidenav/index.ts +0 -2
  121. package/src/app/components/sidenav/mf-sidenav.component.css +0 -168
  122. package/src/app/components/sidenav/mf-sidenav.component.spec.ts +0 -57
  123. package/src/app/components/sidenav/mf-sidenav.component.ts +0 -126
  124. package/src/app/components/slide-toggle/index.ts +0 -1
  125. package/src/app/components/slide-toggle/mf-slide-toggle.component.css +0 -42
  126. package/src/app/components/slide-toggle/mf-slide-toggle.component.spec.ts +0 -43
  127. package/src/app/components/slide-toggle/mf-slide-toggle.component.ts +0 -188
  128. package/src/app/components/snackbar/index.ts +0 -2
  129. package/src/app/components/snackbar/mf-snackbar.service.css +0 -31
  130. package/src/app/components/snackbar/mf-snackbar.service.spec.ts +0 -81
  131. package/src/app/components/snackbar/mf-snackbar.service.ts +0 -77
  132. package/src/app/components/table/index.ts +0 -2
  133. package/src/app/components/table/mf-table.component.css +0 -68
  134. package/src/app/components/table/mf-table.component.spec.ts +0 -76
  135. package/src/app/components/table/mf-table.component.ts +0 -117
  136. package/src/app/components/tabs/index.ts +0 -2
  137. package/src/app/components/tabs/mf-tabs.component.css +0 -31
  138. package/src/app/components/tabs/mf-tabs.component.spec.ts +0 -50
  139. package/src/app/components/tabs/mf-tabs.component.ts +0 -62
  140. package/src/app/components/textarea/index.ts +0 -2
  141. package/src/app/components/textarea/mf-textarea.component.css +0 -48
  142. package/src/app/components/textarea/mf-textarea.component.spec.ts +0 -55
  143. package/src/app/components/textarea/mf-textarea.component.ts +0 -227
  144. package/src/app/components/toolbar/index.ts +0 -2
  145. package/src/app/components/toolbar/mf-toolbar.component.css +0 -77
  146. package/src/app/components/toolbar/mf-toolbar.component.ts +0 -56
  147. package/src/app/components/tooltip/index.ts +0 -3
  148. package/src/app/components/tooltip/mf-tooltip.component.css +0 -7
  149. package/src/app/components/tooltip/mf-tooltip.component.spec.ts +0 -37
  150. package/src/app/components/tooltip/mf-tooltip.component.ts +0 -47
  151. package/src/app/components/tooltip/mf-tooltip.directive.ts +0 -22
  152. package/src/index.html +0 -18
  153. package/src/main.ts +0 -6
  154. package/src/public-api.ts +0 -31
  155. package/src/stories/About.mdx +0 -72
  156. package/src/stories/Accessibility.mdx +0 -59
  157. package/src/stories/Welcome.mdx +0 -26
  158. package/src/stories/assets/accessibility.png +0 -0
  159. package/src/stories/assets/accessibility.svg +0 -1
  160. package/src/stories/assets/addon-library.png +0 -0
  161. package/src/stories/assets/assets.png +0 -0
  162. package/src/stories/assets/avif-test-image.avif +0 -0
  163. package/src/stories/assets/context.png +0 -0
  164. package/src/stories/assets/discord.svg +0 -1
  165. package/src/stories/assets/docs.png +0 -0
  166. package/src/stories/assets/figma-plugin.png +0 -0
  167. package/src/stories/assets/github.svg +0 -1
  168. package/src/stories/assets/share.png +0 -0
  169. package/src/stories/assets/styling.png +0 -0
  170. package/src/stories/assets/testing.png +0 -0
  171. package/src/stories/assets/theming.png +0 -0
  172. package/src/stories/assets/tutorials.svg +0 -1
  173. package/src/stories/assets/youtube.svg +0 -1
  174. package/src/stories/mf-a11y-contracts.stories.ts +0 -472
  175. package/src/stories/mf-autocomplete.stories.ts +0 -194
  176. package/src/stories/mf-button.stories.ts +0 -152
  177. package/src/stories/mf-card.stories.ts +0 -147
  178. package/src/stories/mf-checkbox.stories.ts +0 -88
  179. package/src/stories/mf-datepicker.stories.ts +0 -118
  180. package/src/stories/mf-dialog.stories.ts +0 -159
  181. package/src/stories/mf-form-field.stories.ts +0 -108
  182. package/src/stories/mf-grid-list.stories.ts +0 -104
  183. package/src/stories/mf-icon.stories.ts +0 -133
  184. package/src/stories/mf-input.stories.ts +0 -158
  185. package/src/stories/mf-menu.stories.ts +0 -71
  186. package/src/stories/mf-progress-bar.stories.ts +0 -119
  187. package/src/stories/mf-progress-spinner.stories.ts +0 -124
  188. package/src/stories/mf-radio-button.stories.ts +0 -111
  189. package/src/stories/mf-select.stories.ts +0 -184
  190. package/src/stories/mf-sidenav.stories.ts +0 -331
  191. package/src/stories/mf-table.stories.ts +0 -80
  192. package/src/stories/mf-toolbar.stories.ts +0 -112
  193. package/src/stories/user.ts +0 -3
  194. package/tsconfig.app.json +0 -15
  195. package/tsconfig.json +0 -33
  196. package/tsconfig.spec.json +0 -15
  197. package/vercel.json +0 -6
@@ -1,79 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- computed,
5
- effect,
6
- input,
7
- } from '@angular/core';
8
- import { MatProgressBarModule } from '@angular/material/progress-bar';
9
- import { hasAccessibleName, warnInDev } from '../../a11y';
10
-
11
- export type MfProgressBarMode = 'determinate' | 'indeterminate' | 'buffer' | 'query';
12
- export type MfProgressBarColor = 'brand' | 'accent' | 'warn';
13
-
14
- /**
15
- * Barra de progreso de la librería ng-comps.
16
- * Envuelve Angular Material `mat-progress-bar` y expone una API uniforme
17
- * con look and feel de marca.
18
- */
19
- @Component({
20
- selector: 'mf-progress-bar',
21
- imports: [MatProgressBarModule],
22
- template: `
23
- <div class="mf-progress-bar__wrapper">
24
- @if (label()) {
25
- <span class="mf-progress-bar__label">{{ label() }}</span>
26
- }
27
- <mat-progress-bar
28
- [class]="hostClasses()"
29
- [mode]="mode()"
30
- [value]="value()"
31
- [bufferValue]="bufferValue()"
32
- [attr.aria-label]="label() || null"
33
- [attr.aria-valuetext]="valueText() || null"
34
- [attr.aria-valuenow]="showsNumericValue() ? value() : null"
35
- aria-valuemin="0"
36
- aria-valuemax="100"
37
- />
38
- @if (showValue() && mode() === 'determinate') {
39
- <span class="mf-progress-bar__value" aria-hidden="true">{{ value() }}%</span>
40
- }
41
- </div>
42
- `,
43
- styleUrl: './mf-progress-bar.component.css',
44
- changeDetection: ChangeDetectionStrategy.OnPush,
45
- })
46
- export class MfProgressBarComponent {
47
- /** Modo de la barra de progreso */
48
- readonly mode = input<MfProgressBarMode>('determinate');
49
- /** Valor actual (0–100) */
50
- readonly value = input(0);
51
- /** Valor del buffer (0–100, solo en modo buffer) */
52
- readonly bufferValue = input(0);
53
- /** Color de la barra */
54
- readonly color = input<MfProgressBarColor>('brand');
55
- /** Etiqueta accesible */
56
- readonly label = input<string | undefined>(undefined);
57
- /** Texto accesible opcional para lectores de pantalla */
58
- readonly valueText = input<string | undefined>(undefined);
59
- /** Muestra el porcentaje junto a la barra */
60
- readonly showValue = input(false);
61
- /** Altura de la barra en px */
62
- readonly height = input(4);
63
-
64
- constructor() {
65
- effect(() => {
66
- if (!hasAccessibleName(this.label())) {
67
- warnInDev(
68
- 'mf-progress-bar debería incluir `label` para anunciar el progreso de forma accesible.',
69
- );
70
- }
71
- });
72
- }
73
-
74
- readonly showsNumericValue = computed(() => this.mode() === 'determinate');
75
-
76
- readonly hostClasses = computed(() => {
77
- return `mf-progress-bar mf-progress-bar--${this.color()}`;
78
- });
79
- }
@@ -1,2 +0,0 @@
1
- export { MfProgressSpinnerComponent } from './mf-progress-spinner.component';
2
- export type { MfProgressSpinnerMode, MfProgressSpinnerColor } from './mf-progress-spinner.component';
@@ -1,38 +0,0 @@
1
- :host {
2
- display: inline-flex;
3
- align-items: center;
4
- justify-content: center;
5
- }
6
-
7
- /* ── Wrapper ───────────────────────────────────────────────────── */
8
- .mf-progress-spinner__wrapper {
9
- display: inline-flex;
10
- align-items: center;
11
- gap: var(--mf-space-3);
12
- }
13
-
14
- .mf-progress-spinner__wrapper--labeled {
15
- flex-direction: row;
16
- }
17
-
18
- .mf-progress-spinner__label {
19
- font-family: var(--mf-font-base);
20
- font-size: var(--mf-text-sm);
21
- font-weight: var(--mf-weight-medium);
22
- color: var(--mf-color-on-surface);
23
- }
24
-
25
- /* ── Color: brand ─────────────────────────────────────────────── */
26
- .mf-progress-spinner--brand.mat-mdc-progress-spinner {
27
- --mdc-circular-progress-active-indicator-color: var(--mf-color-brand) !important;
28
- }
29
-
30
- /* ── Color: accent ────────────────────────────────────────────── */
31
- .mf-progress-spinner--accent.mat-mdc-progress-spinner {
32
- --mdc-circular-progress-active-indicator-color: var(--mf-color-accent-500) !important;
33
- }
34
-
35
- /* ── Color: warn ──────────────────────────────────────────────── */
36
- .mf-progress-spinner--warn.mat-mdc-progress-spinner {
37
- --mdc-circular-progress-active-indicator-color: var(--mf-color-error-500) !important;
38
- }
@@ -1,59 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { MfProgressSpinnerComponent } from './mf-progress-spinner.component';
3
- import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4
-
5
- describe('MfProgressSpinnerComponent', () => {
6
- let fixture: ComponentFixture<MfProgressSpinnerComponent>;
7
- let component: MfProgressSpinnerComponent;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [MfProgressSpinnerComponent, NoopAnimationsModule],
12
- }).compileComponents();
13
-
14
- fixture = TestBed.createComponent(MfProgressSpinnerComponent);
15
- component = fixture.componentInstance;
16
- fixture.detectChanges();
17
- });
18
-
19
- it('should create', () => {
20
- expect(component).toBeTruthy();
21
- });
22
-
23
- it('should render mat-progress-spinner', () => {
24
- const spinner = fixture.nativeElement.querySelector('mat-progress-spinner');
25
- expect(spinner).toBeTruthy();
26
- });
27
-
28
- it('should apply brand class by default', () => {
29
- expect(component.hostClasses()).toContain('mf-progress-spinner--brand');
30
- });
31
-
32
- it('should apply accent class when color is accent', () => {
33
- fixture.componentRef.setInput('color', 'accent');
34
- expect(component.hostClasses()).toContain('mf-progress-spinner--accent');
35
- });
36
-
37
- it('should apply warn class when color is warn', () => {
38
- fixture.componentRef.setInput('color', 'warn');
39
- expect(component.hostClasses()).toContain('mf-progress-spinner--warn');
40
- });
41
-
42
- it('should render label when provided', () => {
43
- fixture.componentRef.setInput('label', 'Processing...');
44
- fixture.detectChanges();
45
- const label = fixture.nativeElement.querySelector('.mf-progress-spinner__label');
46
- expect(label?.textContent).toContain('Processing...');
47
- });
48
-
49
- it('should not render label when not provided', () => {
50
- fixture.detectChanges();
51
- const label = fixture.nativeElement.querySelector('.mf-progress-spinner__label');
52
- expect(label).toBeFalsy();
53
- });
54
-
55
- it('should add labeled class when label is provided', () => {
56
- fixture.componentRef.setInput('label', 'Loading');
57
- expect(component.wrapperClasses()).toContain('mf-progress-spinner__wrapper--labeled');
58
- });
59
- });
@@ -1,81 +0,0 @@
1
- import {
2
- ChangeDetectionStrategy,
3
- Component,
4
- computed,
5
- effect,
6
- input,
7
- } from '@angular/core';
8
- import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
9
- import { hasAccessibleName, warnInDev } from '../../a11y';
10
-
11
- export type MfProgressSpinnerMode = 'determinate' | 'indeterminate';
12
- export type MfProgressSpinnerColor = 'brand' | 'accent' | 'warn';
13
-
14
- /**
15
- * Spinner de progreso de la librería ng-comps.
16
- * Envuelve Angular Material `mat-progress-spinner` y expone una API uniforme
17
- * con look and feel de marca.
18
- */
19
- @Component({
20
- selector: 'mf-progress-spinner',
21
- imports: [MatProgressSpinnerModule],
22
- template: `
23
- <div [class]="wrapperClasses()">
24
- <mat-progress-spinner
25
- [class]="hostClasses()"
26
- [mode]="mode()"
27
- [value]="value()"
28
- [diameter]="diameter()"
29
- [strokeWidth]="strokeWidth()"
30
- [attr.aria-label]="label() || null"
31
- [attr.aria-valuetext]="valueText() || null"
32
- [attr.aria-valuenow]="showsNumericValue() ? value() : null"
33
- aria-valuemin="0"
34
- aria-valuemax="100"
35
- />
36
- @if (label()) {
37
- <span class="mf-progress-spinner__label">{{ label() }}</span>
38
- }
39
- </div>
40
- `,
41
- styleUrl: './mf-progress-spinner.component.css',
42
- changeDetection: ChangeDetectionStrategy.OnPush,
43
- })
44
- export class MfProgressSpinnerComponent {
45
- /** Modo del spinner */
46
- readonly mode = input<MfProgressSpinnerMode>('indeterminate');
47
- /** Valor actual (0–100, solo en modo determinate) */
48
- readonly value = input(0);
49
- /** Diámetro en px */
50
- readonly diameter = input(40);
51
- /** Grosor del trazo en px */
52
- readonly strokeWidth = input(4);
53
- /** Color del spinner */
54
- readonly color = input<MfProgressSpinnerColor>('brand');
55
- /** Etiqueta accesible y visible */
56
- readonly label = input<string | undefined>(undefined);
57
- /** Texto accesible opcional para lectores de pantalla */
58
- readonly valueText = input<string | undefined>(undefined);
59
-
60
- constructor() {
61
- effect(() => {
62
- if (!hasAccessibleName(this.label())) {
63
- warnInDev(
64
- 'mf-progress-spinner debería incluir `label` para anunciar el estado de carga de forma accesible.',
65
- );
66
- }
67
- });
68
- }
69
-
70
- readonly showsNumericValue = computed(() => this.mode() === 'determinate');
71
-
72
- readonly hostClasses = computed(() => {
73
- return `mf-progress-spinner mf-progress-spinner--${this.color()}`;
74
- });
75
-
76
- readonly wrapperClasses = computed(() => {
77
- const classes = ['mf-progress-spinner__wrapper'];
78
- if (this.label()) classes.push('mf-progress-spinner__wrapper--labeled');
79
- return classes.join(' ');
80
- });
81
- }
@@ -1,2 +0,0 @@
1
- export { MfRadioButtonComponent } from './mf-radio-button.component';
2
- export type { MfRadioOption, MfRadioDirection } from './mf-radio-button.component';
@@ -1,86 +0,0 @@
1
- :host {
2
- display: block;
3
- }
4
-
5
- .mf-radio__fieldset {
6
- margin: 0;
7
- padding: 0;
8
- border: 0;
9
- }
10
-
11
- .mf-radio__legend {
12
- margin-bottom: var(--mf-space-2);
13
- font-size: var(--mf-text-sm);
14
- font-weight: var(--mf-weight-medium);
15
- color: var(--mf-color-on-surface);
16
- }
17
-
18
- /* ── Group layout ──────────────────────────────────────────────── */
19
- .mf-radio {
20
- display: flex;
21
- }
22
-
23
- .mf-radio--vertical {
24
- flex-direction: column;
25
- gap: var(--mf-space-2);
26
- }
27
-
28
- .mf-radio--horizontal {
29
- flex-direction: row;
30
- flex-wrap: wrap;
31
- gap: var(--mf-space-4);
32
- }
33
-
34
- /* ── Option ────────────────────────────────────────────────────── */
35
- .mf-radio__option {
36
- font-family: var(--mf-font-base) !important;
37
- font-size: var(--mf-text-sm) !important;
38
- color: var(--mf-color-on-surface) !important;
39
- }
40
-
41
- /* ── Radio button circle ──────────────────────────────────────── */
42
- .mf-radio__option .mdc-radio .mdc-radio__outer-circle {
43
- border-color: var(--mf-color-border) !important;
44
- transition: border-color var(--mf-duration-base) var(--mf-ease-standard);
45
- }
46
-
47
- .mf-radio__option.mat-mdc-radio-checked .mdc-radio .mdc-radio__outer-circle {
48
- border-color: var(--mf-color-brand) !important;
49
- }
50
-
51
- .mf-radio__option.mat-mdc-radio-checked .mdc-radio .mdc-radio__inner-circle {
52
- border-color: var(--mf-color-brand) !important;
53
- }
54
-
55
- /* ── Ripple ────────────────────────────────────────────────────── */
56
- .mf-radio__option .mat-ripple-element {
57
- background-color: var(--mf-color-brand-light) !important;
58
- }
59
-
60
- /* ── Label text ────────────────────────────────────────────────── */
61
- .mf-radio__option .mdc-label {
62
- font-family: var(--mf-font-base) !important;
63
- color: var(--mf-color-on-surface) !important;
64
- cursor: pointer;
65
- }
66
-
67
- /* ── Disabled ──────────────────────────────────────────────────── */
68
- .mf-radio__option.mat-mdc-radio-disabled .mdc-label {
69
- color: var(--mf-color-neutral-400) !important;
70
- cursor: default;
71
- }
72
-
73
- .mf-radio__hint,
74
- .mf-radio__error {
75
- margin: var(--mf-space-2) 0 0;
76
- font-size: var(--mf-text-xs);
77
- line-height: var(--mf-leading-normal);
78
- }
79
-
80
- .mf-radio__hint {
81
- color: var(--mf-color-neutral-600);
82
- }
83
-
84
- .mf-radio__error {
85
- color: var(--mf-color-error-500);
86
- }
@@ -1,55 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { MfRadioButtonComponent } from './mf-radio-button.component';
3
- import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4
-
5
- const SAMPLE_OPTIONS = [
6
- { value: 'a', label: 'Opción A' },
7
- { value: 'b', label: 'Opción B' },
8
- { value: 'c', label: 'Opción C', disabled: true },
9
- ];
10
-
11
- describe('MfRadioButtonComponent', () => {
12
- let fixture: ComponentFixture<MfRadioButtonComponent>;
13
- let component: MfRadioButtonComponent;
14
-
15
- beforeEach(async () => {
16
- await TestBed.configureTestingModule({
17
- imports: [MfRadioButtonComponent, NoopAnimationsModule],
18
- }).compileComponents();
19
-
20
- fixture = TestBed.createComponent(MfRadioButtonComponent);
21
- component = fixture.componentInstance;
22
- fixture.componentRef.setInput('options', SAMPLE_OPTIONS);
23
- fixture.detectChanges();
24
- });
25
-
26
- it('should create', () => {
27
- expect(component).toBeTruthy();
28
- });
29
-
30
- it('should render mat-radio-group', () => {
31
- const group = fixture.nativeElement.querySelector('mat-radio-group');
32
- expect(group).toBeTruthy();
33
- });
34
-
35
- it('should render all options', () => {
36
- const buttons = fixture.nativeElement.querySelectorAll('mat-radio-button');
37
- expect(buttons.length).toBe(SAMPLE_OPTIONS.length);
38
- });
39
-
40
- it('should apply vertical class by default', () => {
41
- expect(component.hostClasses()).toContain('mf-radio--vertical');
42
- });
43
-
44
- it('should apply horizontal class when direction is horizontal', () => {
45
- fixture.componentRef.setInput('direction', 'horizontal');
46
- expect(component.hostClasses()).toContain('mf-radio--horizontal');
47
- });
48
-
49
- it('should emit mfChange when selection changes', () => {
50
- const spy = vi.fn();
51
- component.mfChange.subscribe(spy);
52
- component.onChange({ value: 'b' });
53
- expect(spy).toHaveBeenCalledWith('b');
54
- });
55
- });
@@ -1,219 +0,0 @@
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 { MatRadioModule } from '@angular/material/radio';
19
- import {
20
- createUniqueId,
21
- hasAccessibleName,
22
- mergeAriaIds,
23
- warnInDev,
24
- } from '../../a11y';
25
-
26
- export interface MfRadioOption {
27
- /** Valor de la opción */
28
- value: string;
29
- /** Texto visible */
30
- label: string;
31
- /** Deshabilitada individualmente */
32
- disabled?: boolean;
33
- }
34
-
35
- export type MfRadioDirection = 'horizontal' | 'vertical';
36
-
37
- /**
38
- * Grupo de radio buttons de la librería ng-comps.
39
- * Envuelve Angular Material `mat-radio-group` + `mat-radio-button`
40
- * y expone una API uniforme con look and feel de marca.
41
- */
42
- @Component({
43
- selector: 'mf-radio-button',
44
- imports: [MatRadioModule],
45
- providers: [
46
- {
47
- provide: NG_VALUE_ACCESSOR,
48
- useExisting: forwardRef(() => MfRadioButtonComponent),
49
- multi: true,
50
- },
51
- ],
52
- template: `
53
- <fieldset class="mf-radio__fieldset">
54
- @if (label()) {
55
- <legend class="mf-radio__legend" [id]="legendId()">
56
- {{ label() }}
57
- @if (required()) {
58
- <span aria-hidden="true"> *</span>
59
- }
60
- </legend>
61
- }
62
-
63
- <mat-radio-group
64
- [class]="hostClasses()"
65
- [disabled]="isDisabled()"
66
- [name]="groupName()"
67
- [required]="required()"
68
- [value]="internalValue()"
69
- [attr.aria-label]="resolvedAriaLabel()"
70
- [attr.aria-labelledby]="computedLabelledby()"
71
- [attr.aria-describedby]="describedBy()"
72
- [attr.aria-invalid]="isInvalid() ? 'true' : null"
73
- [attr.aria-required]="required() ? 'true' : null"
74
- (change)="onChange($event)"
75
- >
76
- @for (option of options(); track option.value) {
77
- <mat-radio-button
78
- class="mf-radio__option"
79
- [value]="option.value"
80
- [disabled]="option.disabled ?? false"
81
- >
82
- {{ option.label }}
83
- </mat-radio-button>
84
- }
85
- </mat-radio-group>
86
-
87
- @if (hint()) {
88
- <p class="mf-radio__hint" [attr.id]="hintId()">{{ hint() }}</p>
89
- }
90
-
91
- @if (error()) {
92
- <p class="mf-radio__error" [attr.id]="errorId()" role="alert">
93
- {{ error() }}
94
- </p>
95
- }
96
- </fieldset>
97
- `,
98
- styleUrl: './mf-radio-button.component.css',
99
- changeDetection: ChangeDetectionStrategy.OnPush,
100
- })
101
- export class MfRadioButtonComponent implements ControlValueAccessor {
102
- private readonly cdr = inject(ChangeDetectorRef);
103
- private readonly ngControl = inject(NgControl, { self: true, optional: true });
104
- private readonly generatedId = createUniqueId('mf-radio');
105
- private readonly disabledFromForm = signal(false);
106
- protected readonly internalValue = signal<string | undefined>(undefined);
107
- private onControlChange: (value: string) => void = () => undefined;
108
- private onControlTouched: () => void = () => undefined;
109
-
110
- /** Opciones del grupo */
111
- readonly options = input.required<MfRadioOption[]>();
112
- /** ID del control */
113
- readonly id = input<string | undefined>(undefined);
114
- /** Etiqueta visible del grupo */
115
- readonly label = input<string | undefined>(undefined);
116
- /** Valor seleccionado */
117
- readonly value = input<string | undefined>(undefined);
118
- /** Deshabilitado */
119
- readonly disabled = input(false);
120
- /** Requerido */
121
- readonly required = input(false);
122
- /** Dirección del grupo */
123
- readonly direction = input<MfRadioDirection>('vertical');
124
- /** Nombre accesible alternativo del grupo */
125
- readonly ariaLabel = input<string | undefined>(undefined);
126
- /** Etiqueta accesible para el grupo */
127
- readonly ariaLabelledby = input<string | undefined>(undefined);
128
- /** Referencia externa a elementos descriptivos adicionales */
129
- readonly ariaDescribedby = input<string | undefined>(undefined);
130
- /** Texto de ayuda */
131
- readonly hint = input<string | undefined>(undefined);
132
- /** Mensaje de error */
133
- readonly error = input<string | undefined>(undefined);
134
- /** Name del grupo */
135
- readonly name = input<string | undefined>(undefined);
136
-
137
- readonly mfChange = output<string>();
138
-
139
- constructor() {
140
- effect(() => {
141
- this.internalValue.set(this.value());
142
- });
143
-
144
- effect(() => {
145
- if (
146
- !hasAccessibleName(
147
- this.label(),
148
- this.ariaLabel(),
149
- this.ariaLabelledby(),
150
- )
151
- ) {
152
- warnInDev(
153
- 'mf-radio-button requiere `label`, `ariaLabel` o `ariaLabelledby` para exponer un nombre accesible.',
154
- );
155
- }
156
- });
157
- }
158
-
159
- readonly controlId = computed(() => this.id() ?? this.generatedId);
160
- readonly legendId = computed(() => `${this.controlId()}-legend`);
161
- readonly hintId = computed(() =>
162
- this.hint() ? `${this.controlId()}-hint` : null,
163
- );
164
- readonly errorId = computed(() =>
165
- this.error() ? `${this.controlId()}-error` : null,
166
- );
167
- readonly computedLabelledby = computed(() =>
168
- mergeAriaIds(
169
- this.ariaLabelledby(),
170
- this.label() ? this.legendId() : null,
171
- ),
172
- );
173
- readonly describedBy = computed(() =>
174
- mergeAriaIds(this.ariaDescribedby(), this.hintId(), this.errorId()),
175
- );
176
- readonly groupName = computed(() => this.name() ?? this.controlId());
177
- readonly resolvedAriaLabel = computed(() =>
178
- this.label() ? null : this.ariaLabel() ?? null,
179
- );
180
- readonly isDisabled = computed(
181
- () => this.disabled() || this.disabledFromForm(),
182
- );
183
-
184
- readonly hostClasses = computed(() => {
185
- return `mf-radio mf-radio--${this.direction()}`;
186
- });
187
-
188
- writeValue(value: string | null): void {
189
- this.internalValue.set(value ?? undefined);
190
- this.cdr.markForCheck();
191
- }
192
-
193
- registerOnChange(fn: (value: string) => void): void {
194
- this.onControlChange = fn;
195
- }
196
-
197
- registerOnTouched(fn: () => void): void {
198
- this.onControlTouched = fn;
199
- }
200
-
201
- setDisabledState(isDisabled: boolean): void {
202
- this.disabledFromForm.set(isDisabled);
203
- this.cdr.markForCheck();
204
- }
205
-
206
- isInvalid(): boolean {
207
- const control = this.ngControl?.control;
208
- return Boolean(
209
- this.error() || (control?.invalid && (control.touched || control.dirty)),
210
- );
211
- }
212
-
213
- onChange(event: { value: string }): void {
214
- this.internalValue.set(event.value);
215
- this.onControlChange(event.value);
216
- this.onControlTouched();
217
- this.mfChange.emit(event.value);
218
- }
219
- }
@@ -1,2 +0,0 @@
1
- export { MfSelectComponent } from './mf-select.component';
2
- export type { MfSelectOption, MfSelectSize } from './mf-select.component';