crdx-components 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 (128) hide show
  1. package/.github/workflows/publish.yml +38 -0
  2. package/bun.lock +491 -0
  3. package/crdx-components-1.0.0.tgz +0 -0
  4. package/crdx-components-tokenized-components-1.0.1.tgz +0 -0
  5. package/ng-package.json +12 -0
  6. package/npm +0 -0
  7. package/package.json +33 -0
  8. package/src/index.ts +45 -0
  9. package/src/lib/components/breadcrumb/breadcrumb.component.css +206 -0
  10. package/src/lib/components/breadcrumb/breadcrumb.component.html +15 -0
  11. package/src/lib/components/breadcrumb/breadcrumb.component.ts +47 -0
  12. package/src/lib/components/button/button.css +371 -0
  13. package/src/lib/components/button/button.html +187 -0
  14. package/src/lib/components/button/button.ts +103 -0
  15. package/src/lib/components/card/card.css +285 -0
  16. package/src/lib/components/card/card.html +69 -0
  17. package/src/lib/components/card/card.ts +93 -0
  18. package/src/lib/components/checkbox/checkbox-showcase.component.css +42 -0
  19. package/src/lib/components/checkbox/checkbox-showcase.component.html +36 -0
  20. package/src/lib/components/checkbox/checkbox-showcase.component.ts +13 -0
  21. package/src/lib/components/checkbox/checkbox.css +10 -0
  22. package/src/lib/components/checkbox/checkbox.html +13 -0
  23. package/src/lib/components/checkbox/checkbox.ts +64 -0
  24. package/src/lib/components/circular-progress-stepper/circular-progress-stepper.css +89 -0
  25. package/src/lib/components/circular-progress-stepper/circular-progress-stepper.html +23 -0
  26. package/src/lib/components/circular-progress-stepper/circular-progress-stepper.ts +40 -0
  27. package/src/lib/components/dialogs/alert-modal/alert-modal.css +118 -0
  28. package/src/lib/components/dialogs/alert-modal/alert-modal.html +29 -0
  29. package/src/lib/components/dialogs/alert-modal/alert-modal.ts +28 -0
  30. package/src/lib/components/dialogs/confirm-modal/confirm-modal.css +219 -0
  31. package/src/lib/components/dialogs/confirm-modal/confirm-modal.html +60 -0
  32. package/src/lib/components/dialogs/confirm-modal/confirm-modal.store.ts +139 -0
  33. package/src/lib/components/dialogs/confirm-modal/confirm-modal.ts +63 -0
  34. package/src/lib/components/dialogs/container-custom/container-custom.css +11 -0
  35. package/src/lib/components/dialogs/container-custom/container-custom.html +3 -0
  36. package/src/lib/components/dialogs/container-custom/container-custom.ts +37 -0
  37. package/src/lib/components/dialogs/container-custom/custom-modal.state.ts +57 -0
  38. package/src/lib/components/dialogs/error-modal/error-modal.css +53 -0
  39. package/src/lib/components/dialogs/error-modal/error-modal.html +17 -0
  40. package/src/lib/components/dialogs/error-modal/error-modal.ts +20 -0
  41. package/src/lib/components/dialogs/side-modal/side-modal.css +80 -0
  42. package/src/lib/components/dialogs/side-modal/side-modal.html +30 -0
  43. package/src/lib/components/dialogs/side-modal/side-modal.state.ts +78 -0
  44. package/src/lib/components/dialogs/side-modal/side-modal.ts +50 -0
  45. package/src/lib/components/divider/divider.css +24 -0
  46. package/src/lib/components/divider/divider.html +7 -0
  47. package/src/lib/components/divider/divider.ts +13 -0
  48. package/src/lib/components/footer-actions/footer/footer-flow.store.ts +30 -0
  49. package/src/lib/components/footer-actions/footer/footer.html +14 -0
  50. package/src/lib/components/footer-actions/footer/footer.ts +50 -0
  51. package/src/lib/components/footer-actions/modal-footer-actions/modal-footer-actions.css +44 -0
  52. package/src/lib/components/footer-actions/modal-footer-actions/modal-footer-actions.html +7 -0
  53. package/src/lib/components/footer-actions/modal-footer-actions/modal-footer-actions.ts +12 -0
  54. package/src/lib/components/footer-actions/page-footer-actions/page-footer-actions.css +31 -0
  55. package/src/lib/components/footer-actions/page-footer-actions/page-footer-actions.html +7 -0
  56. package/src/lib/components/footer-actions/page-footer-actions/page-footer-actions.ts +12 -0
  57. package/src/lib/components/form-field/select-field.css +178 -0
  58. package/src/lib/components/form-field/select-field.html +94 -0
  59. package/src/lib/components/form-field/select-field.ts +324 -0
  60. package/src/lib/components/form-field/text-field.css +41 -0
  61. package/src/lib/components/form-field/text-field.html +38 -0
  62. package/src/lib/components/form-field/text-field.ts +102 -0
  63. package/src/lib/components/header/header.css +142 -0
  64. package/src/lib/components/header/header.html +36 -0
  65. package/src/lib/components/header/header.ts +101 -0
  66. package/src/lib/components/icon-button/icon-button.css +445 -0
  67. package/src/lib/components/icon-button/icon-button.html +15 -0
  68. package/src/lib/components/icon-button/icon-button.ts +49 -0
  69. package/src/lib/components/list-item/list-item.css +122 -0
  70. package/src/lib/components/list-item/list-item.html +79 -0
  71. package/src/lib/components/list-item/list-item.ts +104 -0
  72. package/src/lib/components/menu/menu.css +39 -0
  73. package/src/lib/components/menu/menu.html +57 -0
  74. package/src/lib/components/menu/menu.ts +159 -0
  75. package/src/lib/components/shared-table/shared-table-cell-template.directive.ts +25 -0
  76. package/src/lib/components/shared-table/shared-table.component.css +223 -0
  77. package/src/lib/components/shared-table/shared-table.component.html +96 -0
  78. package/src/lib/components/shared-table/shared-table.component.ts +172 -0
  79. package/src/lib/components/sidebar/sidebar.css +234 -0
  80. package/src/lib/components/sidebar/sidebar.html +67 -0
  81. package/src/lib/components/sidebar/sidebar.ts +92 -0
  82. package/src/lib/components/slide-toggle/slide-toggle.css +0 -0
  83. package/src/lib/components/slide-toggle/slide-toggle.html +3 -0
  84. package/src/lib/components/slide-toggle/slide-toggle.ts +18 -0
  85. package/src/lib/components/spinner/spinner.css +9 -0
  86. package/src/lib/components/spinner/spinner.html +9 -0
  87. package/src/lib/components/spinner/spinner.ts +17 -0
  88. package/src/lib/components/tooltip/tooltip.css +32 -0
  89. package/src/lib/components/tooltip/tooltip.html +3 -0
  90. package/src/lib/components/tooltip/tooltip.ts +31 -0
  91. package/src/lib/icons/configuration-countable.svg +8 -0
  92. package/src/lib/icons/edit-table.svg +3 -0
  93. package/src/lib/icons/edit.svg +3 -0
  94. package/src/lib/icons/error-circle.svg +8 -0
  95. package/src/lib/icons/hub.svg +3 -0
  96. package/src/lib/icons/icon-menu.svg +8 -0
  97. package/src/lib/icons/info-error.svg +8 -0
  98. package/src/lib/icons/keyboard_arrow_down.svg +1 -0
  99. package/src/lib/icons/logo.svg +0 -0
  100. package/src/lib/icons/logout.svg +0 -0
  101. package/src/lib/icons/notifications.svg +0 -0
  102. package/src/lib/icons/profile-user-menu.svg +0 -0
  103. package/src/lib/icons/profile.svg +0 -0
  104. package/src/lib/icons/register-icons.ts +101 -0
  105. package/src/lib/icons/visibility.svg +0 -0
  106. package/src/lib/lib-ui/lib-ui.html +1 -0
  107. package/src/lib/lib-ui/lib-ui.scss +0 -0
  108. package/src/lib/lib-ui/lib-ui.ts +9 -0
  109. package/src/lib/styles/generated/_app-tokens.scss +2757 -0
  110. package/src/lib/styles/generated/_md3-tokens.scss +179 -0
  111. package/src/lib/styles/generated/_tokens-avatars.scss +4 -0
  112. package/src/lib/styles/global-material-theme.scss +69 -0
  113. package/src/lib/styles/index.scss +16 -0
  114. package/src/lib/styles/layout.scss +29 -0
  115. package/src/lib/styles/overrides/_index.scss +11 -0
  116. package/src/lib/styles/overrides/_mat-button-overrides.scss +105 -0
  117. package/src/lib/styles/overrides/_mat-checkbox-overrides.scss +49 -0
  118. package/src/lib/styles/overrides/_mat-form-field-overrides.scss +148 -0
  119. package/src/lib/styles/overrides/_mat-icon-button-overrides.scss +20 -0
  120. package/src/lib/styles/overrides/_mat-list-overrides.scss +59 -0
  121. package/src/lib/styles/overrides/_mat-slide-toggle-overrides.scss +33 -0
  122. package/src/lib/styles/overrides/_mat-table-overrides.scss +259 -0
  123. package/src/lib/styles/overrides/_mat-tabs-overrides.scss +116 -0
  124. package/src/lib/styles/scrollbar.scss +40 -0
  125. package/src/lib/styles/text-classes.scss +116 -0
  126. package/src/lib/styles/typography.scss +14 -0
  127. package/tsconfig.json +30 -0
  128. package/tsconfig.lib.json +20 -0
@@ -0,0 +1,122 @@
1
+ /* Host: flex column para que el contenedor interno ocupe toda la altura y no quede hueco antes del padding. */
2
+ :host {
3
+ display: flex;
4
+ flex-direction: column;
5
+ width: 100%;
6
+ min-height: 0;
7
+ }
8
+
9
+ .ui-list-item__container {
10
+ display: flex;
11
+ flex-direction: column;
12
+ /* Ocupa toda la altura del host para no dejar espacio vacío antes del padding */
13
+ flex: 1 1 0;
14
+ min-height: 0;
15
+ }
16
+
17
+ .ui-list-item__header {
18
+ padding-bottom: 0.5rem;
19
+ }
20
+
21
+ /* Quitar padding del MDC list item; el padding lo define el diseño Figma en .ui-list-item. */
22
+ :host ::ng-deep .ui-list-option.mdc-list-item {
23
+ padding: 0;
24
+ min-height: calc(var(--lists-wip-common-typography-one-line-list-item-container-height, 56) * 1px);
25
+ background-color: var(--lists-wip-common-color-enabled-list-list-item-container-color,
26
+ var(--app-pallete-scheme-surface-surface-container-lowest, #FFFFFF));
27
+ }
28
+
29
+ /* Layout interno del list-item según Figma (7878:52476, state-layer): gap 16px, padding 16px horizontal, 8px vertical. */
30
+ .ui-list-item {
31
+ display: flex;
32
+ align-items: center;
33
+ padding: 8px 16px;
34
+ min-height: calc(var(--lists-wip-common-typography-one-line-list-item-container-height, 56) * 1px);
35
+ position: relative;
36
+ width: 100%;
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ /* Bordes entre items según Figma (List 7878:52476, Horizontal/Full-width divider).
41
+ Se aplica al contenedor del item para línea full-width entre cada elemento. */
42
+ :host ::ng-deep mat-list .ui-list-option.ui-list-item--with-divider {
43
+ border-bottom: calc(var(--divider-enabled-container-divider-thickness, 1) * 1px) solid
44
+ var(--divider-enabled-container-divider-color, #c3c7c9);
45
+ }
46
+
47
+ /* Sin borde después del último item (diseño Figma). */
48
+ :host ::ng-deep mat-list .ui-list-option.ui-list-item--with-divider:last-child {
49
+ border-bottom: none;
50
+ }
51
+
52
+ .ui-list-item__content {
53
+ display: flex;
54
+ flex-direction: column;
55
+ justify-content: center;
56
+ min-height: 0;
57
+ flex: 1;
58
+ min-width: 0;
59
+ }
60
+
61
+ /* Tipografía del label según __app-tokens (lists-wip + date-pickers-docked menu list item) */
62
+ .ui-list-item__label {
63
+ color: var(--lists-wip-common-color-enabled-list-list-item-label-text-color, #181c1e);
64
+ font-family: var(--date-pickers-docked-enabled-label-text-menu-list-item-label-text-font, Heebo), sans-serif;
65
+ font-size: calc(var(--date-pickers-docked-enabled-label-text-menu-list-item-label-text-size, 16) * 1px);
66
+ font-style: normal;
67
+ font-weight: var(--date-pickers-docked-enabled-label-text-menu-list-item-label-text-weight, 400);
68
+ line-height: calc(var(--date-pickers-docked-enabled-label-text-menu-list-item-label-text-line-height, 24) * 1px);
69
+ letter-spacing: calc(var(--date-pickers-docked-enabled-label-text-menu-list-item-label-text-tracking, 0.5) * 1px);
70
+ }
71
+
72
+ .ui-list-item__description {
73
+ line-height: 1.25;
74
+ }
75
+
76
+ /* Estilos compartidos para checkbox (leading y trailing) — inlineados desde el placeholder @extend */
77
+ .ui-list-item__leading--checkbox {
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ flex-shrink: 0;
82
+ align-self: center;
83
+ }
84
+
85
+ .ui-list-item__leading--checkbox .mat-mdc-checkbox {
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ }
90
+
91
+ .ui-list-item__leading--checkbox .mat-mdc-checkbox .mdc-checkbox__ripple,
92
+ .ui-list-item__leading--checkbox .mat-mdc-checkbox .mdc-checkbox__state-layer {
93
+ left: 50% !important;
94
+ top: 50% !important;
95
+ right: auto !important;
96
+ bottom: auto !important;
97
+ transform: translate(-50%, -50%) !important;
98
+ }
99
+
100
+ .ui-list-item__trailing--checkbox {
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ flex-shrink: 0;
105
+ align-self: center;
106
+ margin-left: auto;
107
+ }
108
+
109
+ .ui-list-item__trailing--checkbox .mat-mdc-checkbox {
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ }
114
+
115
+ .ui-list-item__trailing--checkbox .mat-mdc-checkbox .mdc-checkbox__ripple,
116
+ .ui-list-item__trailing--checkbox .mat-mdc-checkbox .mdc-checkbox__state-layer {
117
+ left: 50% !important;
118
+ top: 50% !important;
119
+ right: auto !important;
120
+ bottom: auto !important;
121
+ transform: translate(-50%, -50%) !important;
122
+ }
@@ -0,0 +1,79 @@
1
+ <div
2
+ class="ui-list-item__container"
3
+ [style.width]="width() ?? '100%'"
4
+ [style.min-width]="width() ?? null"
5
+ [style.--lib-list-item-background]="backgroundColor() ?? null"
6
+ >
7
+ @if (headerTemplate()) {
8
+ <div class="ui-list-item__header">
9
+ <ng-container [ngTemplateOutlet]="headerTemplate()!" />
10
+ </div>
11
+ }
12
+ <mat-list role="list">
13
+ @for (item of items(); track trackById($index, item)) {
14
+ <mat-list-item
15
+ role="listitem"
16
+ class="ui-list-option"
17
+ [class.ui-list-item--disabled]="item.disabled ?? false"
18
+ [class.ui-list-item--selected]="showCheckbox() && isItemSelected(item)"
19
+ [class.ui-list-item--with-divider]="showDividers()"
20
+ [disableRipple]="true"
21
+ (click)="onRowClick(item, $event)"
22
+ >
23
+ @if (itemTemplate()) {
24
+ <ng-container [ngTemplateOutlet]="itemTemplate()!" [ngTemplateOutletContext]="{ $implicit: item }" />
25
+ } @else {
26
+ <div class="ui-list-item" [class.ui-list-item--with-divider]="showDividers()">
27
+ <span class="ui-list-item__state-layer"></span>
28
+
29
+ @if (showCheckbox() && checkboxPosition() === 'leading') {
30
+ <span class="ui-list-item__leading ui-list-item__leading--checkbox">
31
+ <mat-checkbox
32
+ [checked]="isItemSelected(item)"
33
+ [disabled]="item.disabled ?? disabled()"
34
+ (change)="onCheckboxChange(item, $event.checked)"
35
+ (click)="$event.stopPropagation()"
36
+ />
37
+ </span>
38
+ }
39
+
40
+ @if (showAvatar() && !showCheckbox()) {
41
+ <span class="ui-list-item__leading">
42
+ @if (item.avatarImage) {
43
+ <img
44
+ class="ui-list-item__avatar-image"
45
+ [src]="item.avatarImage"
46
+ [attr.alt]="item.avatarAlt || item.label"
47
+ loading="lazy"
48
+ />
49
+ } @else {
50
+ <span class="ui-list-item__avatar-text" aria-hidden="true">
51
+ {{ avatarInitial(item) }}
52
+ </span>
53
+ }
54
+ </span>
55
+ }
56
+
57
+ <span class="ui-list-item__content">
58
+ <span class="ui-list-item__label">{{ item.label }}</span>
59
+ @if (item.description) {
60
+ <span class="ui-list-item__description">{{ item.description }}</span>
61
+ }
62
+ </span>
63
+
64
+ @if (showCheckbox() && checkboxPosition() === 'trailing') {
65
+ <span class="ui-list-item__trailing ui-list-item__trailing--checkbox">
66
+ <lib-checkbox
67
+ [checked]="isItemSelected(item)"
68
+ [disabled]="item.disabled ?? disabled()"
69
+ (checkedChange)="onCheckboxChange(item, $event)"
70
+ (click)="$event.stopPropagation()"
71
+ />
72
+ </span>
73
+ }
74
+ </div>
75
+ }
76
+ </mat-list-item>
77
+ }
78
+ </mat-list>
79
+ </div>
@@ -0,0 +1,104 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ contentChild,
6
+ input,
7
+ output,
8
+ TemplateRef,
9
+ } from '@angular/core';
10
+ import { MatCheckboxModule } from '@angular/material/checkbox';
11
+ import { MatListModule } from '@angular/material/list';
12
+ import { NgTemplateOutlet } from '@angular/common';
13
+ import { LibCheckboxComponent } from '../checkbox/checkbox';
14
+
15
+ export interface LibListItemData {
16
+ id: string;
17
+ label: string;
18
+ description?: string;
19
+ avatarText?: string | null;
20
+ avatarImage?: string | null;
21
+ avatarAlt?: string;
22
+ disabled?: boolean;
23
+ }
24
+
25
+ @Component({
26
+ selector: 'lib-list-item',
27
+ standalone: true,
28
+ imports: [MatListModule, MatCheckboxModule, LibCheckboxComponent, NgTemplateOutlet],
29
+ templateUrl: './list-item.html',
30
+ styleUrl: './list-item.css',
31
+ changeDetection: ChangeDetectionStrategy.OnPush,
32
+ })
33
+ export class LibListItemComponent {
34
+ items = input<LibListItemData[]>([]);
35
+ selectedIds = input<string[]>([]);
36
+ multiple = input(true);
37
+ disabled = input(false);
38
+ showCheckbox = input(true);
39
+ showAvatar = input(true);
40
+ showDividers = input(true);
41
+ /** Posición del checkbox: 'leading' (izquierda) o 'trailing' (derecha). Por defecto 'trailing' según diseño Figma. */
42
+ checkboxPosition = input<'leading' | 'trailing'>('trailing');
43
+
44
+ /** Ancho de la lista. Acepta valores CSS (ej: '18.8rem', '300px', '100%'). Si no se pasa, la lista ocupa el ancho disponible. */
45
+ width = input<string | undefined>(undefined);
46
+ /** Fondo personalizado de la lista/items. Si no se define, usa el fondo por defecto de tokens. */
47
+ backgroundColor = input<string | undefined>(undefined);
48
+
49
+ readonly selectionChange = output<string[]>();
50
+ readonly itemClick = output<LibListItemData>();
51
+
52
+ /** Template para proyectar contenido arriba de la lista (ej: checkbox, filtros) */
53
+ readonly headerTemplate = contentChild<TemplateRef<unknown>>('headerTemplate');
54
+
55
+ /** Template para proyectar el contenido personalizado de cada item. Usar: let-item para acceder al item */
56
+ readonly itemTemplate = contentChild<TemplateRef<{ $implicit: LibListItemData }>>('itemTemplate');
57
+
58
+ readonly selectedSet = computed(() => new Set(this.selectedIds()));
59
+
60
+ trackById(_index: number, item: LibListItemData): string {
61
+ return item.id;
62
+ }
63
+
64
+ avatarInitial(item: LibListItemData): string {
65
+ const explicit = item.avatarText?.trim();
66
+ if (explicit) {
67
+ return explicit.slice(0, 2).toUpperCase();
68
+ }
69
+
70
+ const label = item.label?.trim();
71
+ return label ? label.charAt(0).toUpperCase() : '';
72
+ }
73
+
74
+ isItemSelected(item: LibListItemData): boolean {
75
+ return this.selectedSet().has(item.id);
76
+ }
77
+
78
+ onCheckboxChange(item: LibListItemData, selected: boolean): void {
79
+ const current = new Set(this.selectedIds());
80
+ if (this.multiple()) {
81
+ if (selected) {
82
+ current.add(item.id);
83
+ } else {
84
+ current.delete(item.id);
85
+ }
86
+ } else {
87
+ this.selectionChange.emit(selected ? [item.id] : []);
88
+ return;
89
+ }
90
+ this.selectionChange.emit([...current]);
91
+ }
92
+
93
+ onRowClick(item: LibListItemData, _event: Event): void {
94
+ if (this.showCheckbox() && !(item.disabled ?? this.disabled())) {
95
+ const isSelected = this.isItemSelected(item);
96
+ this.onCheckboxChange(item, !isSelected);
97
+ }
98
+ this.itemClick.emit(item);
99
+ }
100
+
101
+ onItemClick(item: LibListItemData): void {
102
+ this.itemClick.emit(item);
103
+ }
104
+ }
@@ -0,0 +1,39 @@
1
+ .lib-menu {
2
+ display: inline-flex;
3
+ }
4
+
5
+ .lib-menu__trigger {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ justify-content: space-between;
9
+ gap: 0.5rem;
10
+ min-width: 12rem;
11
+ padding-inline: 0.75rem;
12
+ text-transform: none;
13
+ }
14
+
15
+ .lib-menu__trigger-label {
16
+ font-size: 0.875rem;
17
+ color: rgba(0, 0, 0, 0.6);
18
+ }
19
+
20
+ .lib-menu__trigger-value {
21
+ flex: 1;
22
+ text-align: left;
23
+ }
24
+
25
+ .lib-menu__trigger-icon {
26
+ font-size: 20px;
27
+ }
28
+
29
+ .lib-menu__filter {
30
+ padding: 0.5rem 1rem 0.25rem;
31
+ }
32
+
33
+ .lib-menu__filter-field {
34
+ width: 100%;
35
+ }
36
+
37
+ .lib-menu__panel {
38
+ max-width: 20rem;
39
+ }
@@ -0,0 +1,57 @@
1
+ <div class="lib-menu">
2
+ <button
3
+ mat-button
4
+ class="lib-menu__trigger"
5
+ [matMenuTriggerFor]="menu"
6
+ [disabled]="disabled() || disabledByControl()"
7
+ type="button"
8
+ >
9
+ <span class="lib-menu__trigger-label">
10
+ {{ label() || '' }}
11
+ </span>
12
+ <span class="lib-menu__trigger-value">
13
+ {{ triggerLabel() }}
14
+ </span>
15
+ <mat-icon class="lib-menu__trigger-icon">
16
+ {{ panelOpen() ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}
17
+ </mat-icon>
18
+ </button>
19
+
20
+ <mat-menu
21
+ #menu="matMenu"
22
+ class="lib-menu__panel"
23
+ (menuOpened)="onMenuOpened()"
24
+ (menuClosed)="onMenuClosed()"
25
+ >
26
+ @if (filterable()) {
27
+ <div class="lib-menu__filter">
28
+ <mat-form-field appearance="outline" class="lib-menu__filter-field">
29
+ <mat-label>Buscar</mat-label>
30
+ <input
31
+ matInput
32
+ type="text"
33
+ (input)="onFilterInput($event)"
34
+ />
35
+ </mat-form-field>
36
+ </div>
37
+ }
38
+
39
+ @for (opt of filteredOptions(); track opt.value) {
40
+ @if (isMultiple()) {
41
+ <button mat-menu-item (click)="onOptionClick(opt, $event)">
42
+ <mat-checkbox
43
+ [checked]="isSelected(opt.value)"
44
+ (click)="$event.stopPropagation()"
45
+ >
46
+ {{ opt.label }}
47
+ </mat-checkbox>
48
+ </button>
49
+ } @else {
50
+ <button mat-menu-item (click)="onOptionClick(opt, $event)">
51
+ <span>{{ opt.label }}</span>
52
+ </button>
53
+ }
54
+ }
55
+ </mat-menu>
56
+ </div>
57
+
@@ -0,0 +1,159 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ forwardRef,
6
+ input,
7
+ signal,
8
+ } from '@angular/core';
9
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
10
+ import { MatMenuModule } from '@angular/material/menu';
11
+ import { MatButtonModule } from '@angular/material/button';
12
+ import { MatFormFieldModule } from '@angular/material/form-field';
13
+ import { MatInputModule } from '@angular/material/input';
14
+ import { MatIconModule } from '@angular/material/icon';
15
+ import { MatCheckboxModule } from '@angular/material/checkbox';
16
+
17
+ export interface LibMenuOption<T = string> {
18
+ value: T;
19
+ label: string;
20
+ }
21
+
22
+ export type LibMenuMode = 'single' | 'multiple';
23
+
24
+ @Component({
25
+ selector: 'lib-menu',
26
+ standalone: true,
27
+ imports: [
28
+ MatMenuModule,
29
+ MatButtonModule,
30
+ MatFormFieldModule,
31
+ MatInputModule,
32
+ MatIconModule,
33
+ MatCheckboxModule,
34
+ ],
35
+ templateUrl: './menu.html',
36
+ styleUrl: './menu.css',
37
+ changeDetection: ChangeDetectionStrategy.OnPush,
38
+ providers: [
39
+ {
40
+ provide: NG_VALUE_ACCESSOR,
41
+ useExisting: forwardRef(() => LibMenuComponent),
42
+ multi: true,
43
+ },
44
+ ],
45
+ })
46
+ /**
47
+ * @deprecated Prefer `lib-select-field` for new implementations.
48
+ * Kept for compatibility while migrating existing screens.
49
+ */
50
+ export class LibMenuComponent<T = string> implements ControlValueAccessor {
51
+ readonly label = input<string | null>(null);
52
+ readonly placeholder = input('Seleccionar');
53
+ readonly options = input<LibMenuOption<T>[]>([]);
54
+ readonly mode = input<LibMenuMode>('single');
55
+ readonly filterable = input(true);
56
+ readonly disabled = input(false);
57
+
58
+ readonly panelOpen = signal(false);
59
+ readonly filterTerm = signal('');
60
+ readonly disabledByControl = signal(false);
61
+
62
+ readonly valueSignal = signal<T | T[] | null>(null);
63
+
64
+ readonly isMultiple = computed(() => this.mode() === 'multiple');
65
+
66
+ readonly filteredOptions = computed(() => {
67
+ const opts = this.options();
68
+ const term = this.filterTerm().trim().toLowerCase();
69
+ if (!term) return opts;
70
+ return opts.filter((o) => o.label.toLowerCase().includes(term));
71
+ });
72
+
73
+ readonly triggerLabel = computed(() => {
74
+ const current = this.valueSignal();
75
+ const opts = this.options();
76
+ if (current == null) {
77
+ return this.placeholder();
78
+ }
79
+ if (Array.isArray(current)) {
80
+ if (!current.length) return this.placeholder();
81
+ const labels = current
82
+ .map((v) => opts.find((o) => o.value === v)?.label)
83
+ .filter((l): l is string => !!l);
84
+ if (!labels.length) return this.placeholder();
85
+ if (labels.length === 1) return labels[0];
86
+ const [first, ...rest] = labels;
87
+ return `${first} (+${rest.length})`;
88
+ }
89
+ const match = opts.find((o) => o.value === current);
90
+ return match?.label ?? this.placeholder();
91
+ });
92
+
93
+ onMenuOpened(): void {
94
+ this.panelOpen.set(true);
95
+ }
96
+
97
+ onMenuClosed(): void {
98
+ this.panelOpen.set(false);
99
+ this.filterTerm.set('');
100
+ }
101
+
102
+ onFilterInput(event: Event): void {
103
+ const el = event.target as HTMLInputElement;
104
+ this.filterTerm.set(el?.value ?? '');
105
+ }
106
+
107
+ onOptionClick(option: LibMenuOption<T>, event?: MouseEvent): void {
108
+ if (event) {
109
+ event.stopPropagation();
110
+ event.preventDefault();
111
+ }
112
+ if (this.isMultiple()) {
113
+ const current = this.valueSignal();
114
+ const asArray = Array.isArray(current) ? [...current] : [];
115
+ const index = asArray.findIndex((v) => v === option.value);
116
+ if (index >= 0) {
117
+ asArray.splice(index, 1);
118
+ } else {
119
+ asArray.push(option.value);
120
+ }
121
+ const next = asArray as T[];
122
+ this.valueSignal.set(next);
123
+ this.onChange(next);
124
+ } else {
125
+ const next = option.value;
126
+ this.valueSignal.set(next);
127
+ this.onChange(next);
128
+ }
129
+ }
130
+
131
+ isSelected(value: T): boolean {
132
+ const current = this.valueSignal();
133
+ if (Array.isArray(current)) {
134
+ return current.includes(value);
135
+ }
136
+ return current === value;
137
+ }
138
+
139
+ // ControlValueAccessor
140
+ private onChange: (value: T | T[] | null) => void = () => {};
141
+ private onTouched: () => void = () => {};
142
+
143
+ writeValue(value: T | T[] | null): void {
144
+ this.valueSignal.set(value);
145
+ }
146
+
147
+ registerOnChange(fn: (value: T | T[] | null) => void): void {
148
+ this.onChange = fn;
149
+ }
150
+
151
+ registerOnTouched(fn: () => void): void {
152
+ this.onTouched = fn;
153
+ }
154
+
155
+ setDisabledState(isDisabled: boolean): void {
156
+ this.disabledByControl.set(isDisabled);
157
+ }
158
+ }
159
+
@@ -0,0 +1,25 @@
1
+ import { Directive, Input, TemplateRef, inject } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: 'ng-template[cellTemplate], ng-template[libCellTemplate]',
5
+ standalone: true,
6
+ })
7
+ export class SharedTableCellTemplateDirective {
8
+ readonly template = inject<TemplateRef<unknown>>(TemplateRef);
9
+ private legacyKey = '';
10
+ private modernKey = '';
11
+
12
+ @Input()
13
+ set cellTemplate(value: string | null | undefined) {
14
+ this.legacyKey = value ?? '';
15
+ }
16
+
17
+ @Input()
18
+ set libCellTemplate(value: string | null | undefined) {
19
+ this.modernKey = value ?? '';
20
+ }
21
+
22
+ get key(): string {
23
+ return this.modernKey || this.legacyKey;
24
+ }
25
+ }