ten-minds-ui-kit 0.0.1

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.
package/README.md ADDED
@@ -0,0 +1,2212 @@
1
+ # @10mindssoftware/tm-ui-kit
2
+
3
+ Librería de componentes UI para Angular, construida con Tailwind CSS v4 y Lucide Icons.
4
+
5
+ ---
6
+
7
+ ## Instalación
8
+
9
+ ```bash
10
+ npm install @10mindssoftware/tm-ui-kit
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Configuración inicial
16
+
17
+ 1.- Instalar la sgte libreria lucide-angular
18
+ `npm i lucide-angular`
19
+
20
+ ### 1. Agregar los estilos de la librería
21
+
22
+ Importá los estilos de la librería en el `styles.css` de tu proyecto:
23
+
24
+ ```css
25
+ @import '@10mindssoftware/tm-ui-kit/styles/styles.css';
26
+ ```
27
+
28
+ ---
29
+
30
+ # Customización de Estilos
31
+
32
+ La librería **@10mindssoftware/tm-ui-kit** ofrece tres niveles de personalización según el alcance que necesites.
33
+
34
+ ---
35
+
36
+ ## Nivel 1 — Global: sobreescribir tokens CSS
37
+
38
+ Afecta **todos** los componentes que usen ese token en toda la app.
39
+
40
+ Agrega esto en el `styles.css` de tu proyecto:
41
+
42
+ ```css
43
+ :root {
44
+ --tm-gray-900: #your-brand-color;
45
+ }
46
+ ```
47
+
48
+ O directamente el token semántico:
49
+
50
+ ```css
51
+ :root {
52
+ --color-tm-brand: #your-brand-color;
53
+ --color-tm-primary: #your-text-color;
54
+ }
55
+ ```
56
+
57
+ ### Tokens disponibles
58
+
59
+ | Token | Descripción | Default (light) |
60
+ | -------------------------------- | --------------------------- | --------------- |
61
+ | `--color-tm-brand` | Color principal de la marca | `#141414` |
62
+ | `--color-tm-primary` | Texto principal | `#141414` |
63
+ | `--color-tm-secondary` | Texto secundario | `#737373` |
64
+ | `--color-tm-tertiary` | Texto terciario | `#A0A0A0` |
65
+ | `--color-tm-inverse` | Texto sobre fondos oscuros | `#FFFFFF` |
66
+ | `--color-tm-canvas` | Fondo general de la app | `#FAFAFA` |
67
+ | `--color-tm-surface` | Fondo de tarjetas y paneles | `#FFFFFF` |
68
+ | `--color-tm-subtle` | Fondo sutil | `#F0F0F0` |
69
+ | `--color-tm-muted` | Fondo atenuado | `#E4E4E4` |
70
+ | `--color-tm-brand-hover-default` | Hover del botón brand | `#363636` |
71
+ | `--color-tm-brand-hover-subtle` | Hover sutil del botón brand | `#F0F0F0` |
72
+ | `--color-tm-default` | Borde por defecto | `#E4E4E4` |
73
+ | `--color-tm-strong` | Borde fuerte | `#C8C8C8` |
74
+
75
+ ---
76
+
77
+ ## Nivel 2 — Scoped: cambiar un componente en una sección
78
+
79
+ Afecta solo los componentes dentro de un contenedor específico. Se usa CSS puro — **no Tailwind**.
80
+
81
+ ```css
82
+ /* styles.css del proyecto consumidor */
83
+ .mi-seccion tm-btn-primary button {
84
+ background-color: #your-color !important;
85
+ font-size: 1.25rem !important;
86
+ border-radius: 999px !important;
87
+ }
88
+ ```
89
+
90
+ ```html
91
+ <div class="mi-seccion">
92
+ <tm-btn-primary>Solo este botón cambia</tm-btn-primary>
93
+ </div>
94
+
95
+ <tm-btn-primary>Este queda igual</tm-btn-primary>
96
+ ```
97
+
98
+ > ⚠️ CSS puro únicamente. Tailwind no funciona aquí porque se compila en build time y las clases de la librería no están disponibles en el proyecto consumidor.
99
+
100
+ ---
101
+
102
+ ## Nivel 3 — Puntual: un solo componente con `customClass`
103
+
104
+ Afecta **únicamente** el componente donde lo aplicas. Aquí sí puedes usar clases de Tailwind porque se compilan en el proyecto consumidor.
105
+
106
+ ```html
107
+ <!-- Tailwind funciona aquí -->
108
+ <tm-btn-primary customClass="!bg-orange-500 !text-black"> Botón personalizado </tm-btn-primary>
109
+
110
+ <!-- CSS también funciona con clase propia -->
111
+ <tm-btn-primary customClass="mi-boton-especial"> Botón personalizado </tm-btn-primary>
112
+ ```
113
+
114
+ ```css
115
+ /* styles.css */
116
+ .mi-boton-especial {
117
+ background-color: orange !important;
118
+ font-size: 1.25rem !important;
119
+ }
120
+ ```
121
+
122
+ > ℹ️ El modificador `!` de Tailwind es necesario para sobreescribir estilos ya definidos por el componente.
123
+
124
+ ---
125
+
126
+ ## Resumen
127
+
128
+ | Método | Alcance | Tailwind | Cuándo usarlo |
129
+ | -------------------------- | ------------- | -------- | ----------------------------------------- |
130
+ | Token CSS en `:root` | Toda la app | ✗ | Cambiar la paleta de colores del proyecto |
131
+ | CSS scoped con clase padre | Una sección | ✗ | Variante especial en una página o sección |
132
+ | `customClass` input | Un componente | ✓ | Ajuste puntual en un solo componente |
133
+
134
+ ### 2. Importar los componentes
135
+
136
+ Los componentes son standalone — solo importa los que necesites directamente en tu componente o módulo:
137
+
138
+ ```typescript
139
+ import { BtnPrimary, Chip, CheckboxButton } from '@10mindssoftware/tm-ui-kit';
140
+
141
+ @Component({
142
+ imports: [BtnPrimary, Chip, CheckboxButton],
143
+ })
144
+ ```
145
+
146
+ ### 3. Usar íconos
147
+
148
+ Los íconos se importan directamente desde `lucide-angular` como objetos. **No se requiere ninguna configuración global en `app.config.ts`.**
149
+
150
+ ```typescript
151
+ import { Download, Trash2, Check } from 'lucide-angular';
152
+
153
+ export class MiComponente {
154
+ download = Download;
155
+ trash = Trash2;
156
+ check = Check;
157
+ }
158
+ ```
159
+
160
+ ```html
161
+ <tm-btn-primary [iconLeft]="download">Exportar</tm-btn-primary>
162
+ <tm-chip [iconLeft]="check">Activo</tm-chip>
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Componentes disponibles
168
+
169
+ | Componente | Selector | Descripción |
170
+ | ---------------- | ------------------ | --------------------------------------- |
171
+ | `BtnPrimary` | `tm-btn-primary` | Botón principal con múltiples variantes |
172
+ | `Chip` | `tm-chip` | Etiqueta de estado o categoría |
173
+ | `CheckboxButton` | `tm-checkbox` | Checkbox personalizado |
174
+ | `Toggle` | `tm-toggle` | Switch on/off |
175
+ | `RadioButton` | `tm-radio-button` | Botón de selección única |
176
+ | `InputTen` | `tm-input` | Campo de texto |
177
+ | `Avatar` | `tm-avatar` | Avatar con iniciales y estado |
178
+ | `SnackbarHost` | `tm-snackbar-host` | Contenedor de notificaciones |
179
+
180
+ ---
181
+
182
+ ## Dark Mode
183
+
184
+ La librería soporta dark mode mediante la clase `.dark` en el elemento raíz. Agrégala o quítala desde tu componente:
185
+
186
+ ```typescript
187
+ onThemeChange(mode: 'light' | 'dark') {
188
+ if (mode === 'dark') {
189
+ document.documentElement.classList.add('dark');
190
+ } else {
191
+ document.documentElement.classList.remove('dark');
192
+ }
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Componentes
199
+
200
+ # Avatar
201
+
202
+ Componente de avatar de la librería **ten-minds-beta**. Soporta imagen, iniciales como fallback, múltiples tamaños e indicador de estado.
203
+
204
+ ---
205
+
206
+ ## Importación
207
+
208
+ ```typescript
209
+ import { Avatar } from '@10mindssoftware/tm-ui-kit';
210
+
211
+ @Component({
212
+ imports: [Avatar],
213
+ })
214
+ ```
215
+
216
+ ---
217
+
218
+ ## API
219
+
220
+ ### Inputs
221
+
222
+ | Prop | Tipo | Default | Descripción |
223
+ | ---------- | ---------------------- | ---------- | ---------------------------------------------------------- |
224
+ | `src` | `string` | `''` | URL de la imagen. Si está vacío, se muestran las iniciales |
225
+ | `initials` | `string` | `''` | Texto de iniciales, se muestra cuando no hay imagen |
226
+ | `alt` | `string` | `'Avatar'` | Texto alternativo para accesibilidad |
227
+ | `size` | `AvatarSize` | `'md'` | Tamaño del avatar |
228
+ | `status` | `AvatarStatus \| null` | `null` | Indicador de estado visible sobre el avatar |
229
+
230
+ ### Tipos
231
+
232
+ ```typescript
233
+ type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
234
+ type AvatarStatus = 'online' | 'busy' | 'disconnect';
235
+ ```
236
+
237
+ ### Tamaños de referencia
238
+
239
+ | Valor | Tamaño |
240
+ | ----- | ------ |
241
+ | `xs` | 28px |
242
+ | `sm` | 40px |
243
+ | `md` | 48px |
244
+ | `lg` | 60px |
245
+ | `xl` | 80px |
246
+
247
+ ---
248
+
249
+ ## Ejemplos
250
+
251
+ ### Con imagen
252
+
253
+ ```html
254
+ <tm-avatar src="https://ejemplo.com/foto.jpg" />
255
+ <tm-avatar src="https://ejemplo.com/foto.jpg" alt="Foto de Juan" />
256
+ ```
257
+
258
+ ### Con iniciales (fallback sin imagen)
259
+
260
+ ```html
261
+ <tm-avatar initials="JP" /> <tm-avatar initials="AB" />
262
+ ```
263
+
264
+ > Si `src` está vacío e `initials` tiene valor, se muestran las iniciales sobre el fondo de marca.
265
+
266
+ ### Tamaños
267
+
268
+ ```html
269
+ <tm-avatar src="..." size="xs" />
270
+ <tm-avatar src="..." size="sm" />
271
+ <tm-avatar src="..." size="md" />
272
+ <tm-avatar src="..." size="lg" />
273
+ <tm-avatar src="..." size="xl" />
274
+ ```
275
+
276
+ ### Indicador de estado
277
+
278
+ ```html
279
+ <tm-avatar src="..." status="online" />
280
+ <tm-avatar src="..." status="busy" />
281
+ <tm-avatar src="..." status="disconnect" />
282
+ ```
283
+
284
+ ### Sin estado (por defecto)
285
+
286
+ ```html
287
+ <tm-avatar src="..." />
288
+ <!-- status es null por defecto, no se muestra el indicador -->
289
+ ```
290
+
291
+ ### Combinaciones comunes
292
+
293
+ ```html
294
+ <!-- Avatar con iniciales y estado online -->
295
+ <tm-avatar initials="MG" status="online" />
296
+
297
+ <!-- Avatar grande con imagen y estado ocupado -->
298
+ <tm-avatar src="https://ejemplo.com/foto.jpg" alt="María García" size="lg" status="busy" />
299
+
300
+ <!-- Avatar pequeño desconectado -->
301
+ <tm-avatar src="https://ejemplo.com/foto.jpg" size="sm" status="disconnect" />
302
+
303
+ <!-- Avatar extra grande solo con iniciales -->
304
+ <tm-avatar initials="TM" size="xl" />
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Notas
310
+
311
+ - Si `src` tiene valor, se muestra la imagen. Si no, se recurre a las `initials`. Si tampoco hay iniciales, el avatar queda vacío con el fondo de marca.
312
+ - El indicador de `status` escala automáticamente según el `size` del avatar y se posiciona en la esquina inferior derecha.
313
+ - El indicador incluye `role="status"` y `aria-label` con el valor del estado para accesibilidad.
314
+
315
+ ---
316
+
317
+ # Componente BtnIcon
318
+
319
+ Componente de botón de ícono de la librería **@10mindssoftware/tm-ui-kit**. Versión cuadrada del botón principal, diseñado para acciones que se representan únicamente con un ícono. Soporta los mismos colores, variantes, tamaños y bordes redondeados que `BtnPrimary`.
320
+
321
+ ---
322
+
323
+ ## Importación
324
+
325
+ ```typescript
326
+ import { BtnIcon } from '@10mindssoftware/tm-ui-kit';
327
+
328
+ @Component({
329
+ imports: [BtnIcon],
330
+ })
331
+ ```
332
+
333
+ ---
334
+
335
+ ## API
336
+
337
+ ### Inputs
338
+
339
+ | Prop | Tipo | Default | Descripción |
340
+ | ----------- | ---------------- | ----------- | ------------------------------------------------------------------------ |
341
+ | `icon` | `LucideIconData` | — | **Requerido.** Ícono a mostrar dentro del botón |
342
+ | `color` | `ButtonColor` | `'primary'` | Color del botón |
343
+ | `size` | `ButtonSize` | `'md'` | Tamaño del botón |
344
+ | `type` | `ButtonType` | `'filled'` | Variante visual |
345
+ | `rounded` | `ButtonRounded` | `'default'` | Forma del borde |
346
+ | `active` | `boolean` | `false` | Aplica estado activo (brightness) |
347
+ | `disabled` | `boolean` | `false` | Desactiva el botón |
348
+ | `ariaLabel` | `string` | `''` | Texto accesible del botón. Recomendado siempre que no haya texto visible |
349
+
350
+ ### Outputs
351
+
352
+ | Evento | Tipo | Descripción |
353
+ | --------- | ------------ | --------------------------------------------------- |
354
+ | `clicked` | `MouseEvent` | Se emite al hacer click (no emite si está disabled) |
355
+
356
+ ### Tipos
357
+
358
+ ```typescript
359
+ type ButtonColor = 'primary' | 'danger' | 'success' | 'warning';
360
+ type ButtonSize = 'lg' | 'md' | 'sm';
361
+ type ButtonType = 'filled' | 'outlined' | 'ghost';
362
+ type ButtonRounded = 'default' | 'full';
363
+ ```
364
+
365
+ ### Tamaños de referencia
366
+
367
+ | Valor | Tamaño | Ícono |
368
+ | ----- | ------ | ----- |
369
+ | `lg` | 44px | 20px |
370
+ | `md` | 40px | 18px |
371
+ | `sm` | 36px | 16px |
372
+
373
+ ---
374
+
375
+ ## Ejemplos
376
+
377
+ ### Uso básico
378
+
379
+ El prop `icon` es requerido. Siempre importa el ícono desde `lucide-angular` como objeto.
380
+
381
+ ```typescript
382
+ import { Trash2, Edit, Plus } from 'lucide-angular';
383
+
384
+ export class App {
385
+ trash = Trash2;
386
+ edit = Edit;
387
+ plus = Plus;
388
+ }
389
+ ```
390
+
391
+ ```html
392
+ <tm-icon-button [icon]="edit" ariaLabel="Editar" />
393
+ ```
394
+
395
+ ### Colores
396
+
397
+ ```html
398
+ <tm-icon-button [icon]="edit" color="primary" ariaLabel="Editar" />
399
+ <tm-icon-button [icon]="trash" color="danger" ariaLabel="Eliminar" />
400
+ <tm-icon-button [icon]="plus" color="success" ariaLabel="Agregar" />
401
+ <tm-icon-button [icon]="edit" color="warning" ariaLabel="Advertencia" />
402
+ ```
403
+
404
+ ### Variantes
405
+
406
+ ```html
407
+ <tm-icon-button [icon]="edit" type="filled" ariaLabel="Editar" />
408
+ <tm-icon-button [icon]="edit" type="outlined" ariaLabel="Editar" />
409
+ <tm-icon-button [icon]="edit" type="ghost" ariaLabel="Editar" />
410
+ ```
411
+
412
+ ### Tamaños
413
+
414
+ ```html
415
+ <tm-icon-button [icon]="edit" size="lg" ariaLabel="Editar" />
416
+ <tm-icon-button [icon]="edit" size="md" ariaLabel="Editar" />
417
+ <tm-icon-button [icon]="edit" size="sm" ariaLabel="Editar" />
418
+ ```
419
+
420
+ ### Bordes redondeados
421
+
422
+ ```html
423
+ <tm-icon-button [icon]="plus" rounded="default" ariaLabel="Agregar" />
424
+ <tm-icon-button [icon]="plus" rounded="full" ariaLabel="Agregar" />
425
+ ```
426
+
427
+ ### Disabled
428
+
429
+ ```html
430
+ <tm-icon-button [icon]="trash" [disabled]="true" ariaLabel="Eliminar" />
431
+ <tm-icon-button
432
+ [icon]="trash"
433
+ color="danger"
434
+ type="outlined"
435
+ [disabled]="true"
436
+ ariaLabel="Eliminar"
437
+ />
438
+ ```
439
+
440
+ ### Manejo de eventos
441
+
442
+ ```html
443
+ <tm-icon-button [icon]="trash" color="danger" ariaLabel="Eliminar" (clicked)="onEliminar($event)" />
444
+ ```
445
+
446
+ ```typescript
447
+ onEliminar(event: MouseEvent) {
448
+ console.log('Eliminando...', event);
449
+ }
450
+ ```
451
+
452
+ ### Combinaciones comunes
453
+
454
+ ```html
455
+ <!-- Botón de acción flotante (FAB) -->
456
+ <tm-icon-button
457
+ [icon]="plus"
458
+ color="primary"
459
+ size="lg"
460
+ rounded="full"
461
+ ariaLabel="Crear nuevo"
462
+ (clicked)="onCreate($event)"
463
+ />
464
+
465
+ <!-- Botón destructivo ghost -->
466
+ <tm-icon-button
467
+ [icon]="trash"
468
+ color="danger"
469
+ type="ghost"
470
+ size="sm"
471
+ ariaLabel="Eliminar fila"
472
+ (clicked)="onDelete($event)"
473
+ />
474
+
475
+ <!-- Botón de edición outlined -->
476
+ <tm-icon-button
477
+ [icon]="edit"
478
+ color="primary"
479
+ type="outlined"
480
+ ariaLabel="Editar registro"
481
+ (clicked)="onEdit($event)"
482
+ />
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Notas
488
+
489
+ - El prop `icon` es **requerido**. El componente no renderiza nada sin él.
490
+ - El evento `clicked` **no se emite** cuando `disabled` es `true`, no es necesario manejarlo manualmente.
491
+ - Se recomienda proporcionar siempre un `ariaLabel` descriptivo ya que el botón no contiene texto visible, lo cual es importante para accesibilidad.
492
+ - Los íconos deben importarse desde `lucide-angular` como objetos y asignarse a propiedades del componente. Pasar strings directamente **no funciona**.
493
+ - El tamaño del ícono se ajusta automáticamente según el `size` del botón.
494
+
495
+ ---
496
+
497
+ # tm-button — Componente de Botón
498
+
499
+ Componente de botón de la librería **@10mindssoftware/tm-ui-kit**. Soporta múltiples colores, variantes, tamaños, bordes redondeados e íconos.
500
+
501
+ ---
502
+
503
+ ## Instalación
504
+
505
+ Importa el componente en tu componente standalone:
506
+
507
+ ```typescript
508
+ import { BtnPrimary } from '@10mindssoftware/tm-ui-kit';
509
+
510
+ @Component({
511
+ imports: [BtnPrimary],
512
+ })
513
+ ```
514
+
515
+ ---
516
+
517
+ ## API
518
+
519
+ ### Inputs
520
+
521
+ | Prop | Tipo | Default | Descripción |
522
+ | ------------- | ---------------- | ----------- | ---------------------- |
523
+ | `color` | `ButtonColor` | `'primary'` | Color del botón |
524
+ | `size` | `ButtonSize` | `'md'` | Tamaño del botón |
525
+ | `type` | `ButtonType` | `'filled'` | Variante visual |
526
+ | `rounded` | `ButtonRounded` | `'default'` | Forma del borde |
527
+ | `disabled` | `boolean` | `false` | Desactiva el botón |
528
+ | `active` | `boolean` | `false` | Aplica estado activo |
529
+ | `iconLeft` | `LucideIconData` | — | Ícono a la izquierda |
530
+ | `iconRight` | `LucideIconData` | — | Ícono a la derecha |
531
+ | `customClass` | `string` | `''` | Clases CSS adicionales |
532
+
533
+ ### Outputs
534
+
535
+ | Evento | Tipo | Descripción |
536
+ | ----------- | ------------ | --------------------------------------------------- |
537
+ | `(clicked)` | `MouseEvent` | Se emite al hacer click (no emite si está disabled) |
538
+
539
+ ### Tipos
540
+
541
+ ```typescript
542
+ type ButtonColor = 'primary' | 'danger' | 'success' | 'warning';
543
+ type ButtonSize = 'lg' | 'md' | 'sm';
544
+ type ButtonType = 'filled' | 'outlined' | 'ghost';
545
+ type ButtonRounded = 'default' | 'full';
546
+ ```
547
+
548
+ ---
549
+
550
+ ## Ejemplos
551
+
552
+ ### Uso básico
553
+
554
+ ```html
555
+ <tm-button>Guardar</tm-button>
556
+ ```
557
+
558
+ ### Colores
559
+
560
+ ```html
561
+ <tm-button color="primary">Primary</tm-button>
562
+ <tm-button color="danger">Danger</tm-button>
563
+ <tm-button color="success">Success</tm-button>
564
+ <tm-button color="warning">Warning</tm-button>
565
+ ```
566
+
567
+ ### Variantes
568
+
569
+ ```html
570
+ <tm-button type="filled">Filled</tm-button>
571
+ <tm-button type="outlined">Outlined</tm-button>
572
+ <tm-button type="ghost">Ghost</tm-button>
573
+ ```
574
+
575
+ ### Tamaños
576
+
577
+ ```html
578
+ <tm-button size="lg">Large</tm-button>
579
+ <tm-button size="md">Medium</tm-button>
580
+ <tm-button size="sm">Small</tm-button>
581
+ ```
582
+
583
+ ### Bordes redondeados
584
+
585
+ ```html
586
+ <tm-button rounded="default">Default</tm-button> <tm-button rounded="full">Pill</tm-button>
587
+ ```
588
+
589
+ ### Disabled
590
+
591
+ ```html
592
+ <tm-button [disabled]="true">No disponible</tm-button>
593
+ <tm-button color="danger" type="outlined" [disabled]="true">Eliminar</tm-button>
594
+ ```
595
+
596
+ ### Con íconos
597
+
598
+ Los íconos se importan desde `lucide-angular` como objetos y se asignan a propiedades del componente:
599
+
600
+ ```typescript
601
+ import { Download, Trash2, Check } from 'lucide-angular';
602
+
603
+ export class MiComponente {
604
+ download = Download;
605
+ trash = Trash2;
606
+ check = Check;
607
+ }
608
+ ```
609
+
610
+ ```html
611
+ <!-- Ícono izquierdo -->
612
+ <tm-button [iconLeft]="download">Exportar</tm-button>
613
+
614
+ <!-- Ícono derecho -->
615
+ <tm-button color="success" [iconRight]="check">Confirmar</tm-button>
616
+
617
+ <!-- Ambos íconos -->
618
+ <tm-button color="danger" type="outlined" [iconLeft]="trash" [iconRight]="check">
619
+ Eliminar
620
+ </tm-button>
621
+ ```
622
+
623
+ ### Manejo de eventos
624
+
625
+ ```html
626
+ <tm-button color="success" (clicked)="onGuardar($event)">Guardar</tm-button>
627
+ ```
628
+
629
+ ```typescript
630
+ onGuardar(event: MouseEvent) {
631
+ console.log('Guardando...', event);
632
+ }
633
+ ```
634
+
635
+ ### Combinaciones comunes
636
+
637
+ ```html
638
+ <!-- Botón de acción principal -->
639
+ <tm-button color="primary" size="md" [iconLeft]="download">Exportar</tm-button>
640
+
641
+ <!-- Botón destructivo -->
642
+ <tm-button color="danger" type="outlined" size="md" [iconLeft]="trash">Eliminar</tm-button>
643
+
644
+ <!-- Botón pill -->
645
+ <tm-button color="primary" rounded="full" [iconLeft]="check">Confirmar</tm-button>
646
+
647
+ <!-- Botón pequeño ghost -->
648
+ <tm-button color="primary" type="ghost" size="sm">Ver más</tm-button>
649
+ ```
650
+
651
+ ---
652
+
653
+ ## Notas
654
+
655
+ - El evento `(clicked)` no se emite cuando `disabled` es `true`.
656
+ - Los íconos deben importarse desde `lucide-angular` como objetos. Pasar strings no funciona.
657
+ - Usa `customClass` para agregar clases adicionales sin sobreescribir las base.
658
+
659
+ ---
660
+
661
+ # Componente Btn Theme
662
+
663
+ Componente de botón para alternar entre modo claro y oscuro. Cambia el icono según el modo actual y emite el nuevo modo al padre.
664
+
665
+ ---
666
+
667
+ ## Instalación
668
+
669
+ ```typescript
670
+ import { BtnTheme } from '@10mindssoftware/tm-ui-kit';
671
+
672
+ @Component({
673
+ imports: [BtnTheme],
674
+ })
675
+ ```
676
+
677
+ ---
678
+
679
+ ## Uso básico
680
+
681
+ ```html
682
+ <lib-btn-theme [mode]="currentTheme" (modeChange)="onThemeChange($event)" />
683
+ ```
684
+
685
+ ```typescript
686
+ import { ThemeMode } from '@10mindssoftware/tm-ui-kit';
687
+
688
+ currentTheme: ThemeMode = 'light';
689
+
690
+ onThemeChange(mode: ThemeMode) {
691
+ this.currentTheme = mode;
692
+ if (mode === 'dark') {
693
+ document.documentElement.classList.add('dark');
694
+ } else {
695
+ document.documentElement.classList.remove('dark');
696
+ }
697
+ }
698
+ ```
699
+
700
+ ---
701
+
702
+ ## Inputs y Outputs
703
+
704
+ | Input | Tipo | Default | Descripción |
705
+ | ---------- | ----------- | --------- | ------------------------ |
706
+ | `mode` | `ThemeMode` | `'light'` | Modo actual del tema |
707
+ | `disabled` | `boolean` | `false` | Desactiva la interacción |
708
+
709
+ | Output | Tipo | Descripción |
710
+ | ------------ | ----------- | ---------------------------------- |
711
+ | `modeChange` | `ThemeMode` | Emite el nuevo modo al hacer click |
712
+
713
+ ---
714
+
715
+ ## Tipos
716
+
717
+ ```typescript
718
+ type ThemeMode = 'light' | 'dark';
719
+ ```
720
+
721
+ ---
722
+
723
+ ## Estados
724
+
725
+ ### Light (modo claro)
726
+
727
+ ```html
728
+ <lib-btn-theme mode="light" (modeChange)="onThemeChange($event)" />
729
+ ```
730
+
731
+ ### Dark (modo oscuro)
732
+
733
+ ```html
734
+ <lib-btn-theme mode="dark" (modeChange)="onThemeChange($event)" />
735
+ ```
736
+
737
+ ### Disabled
738
+
739
+ ```html
740
+ <lib-btn-theme [disabled]="true" />
741
+ ```
742
+
743
+ ---
744
+
745
+ ## Casos de uso reales
746
+
747
+ ### Toggle global en el navbar
748
+
749
+ ```html
750
+ <nav class="flex items-center justify-between px-6 py-4">
751
+ <span>Mi App</span>
752
+ <lib-btn-theme [mode]="currentTheme" (modeChange)="onThemeChange($event)" />
753
+ </nav>
754
+ ```
755
+
756
+ ```typescript
757
+ currentTheme: ThemeMode = 'light';
758
+
759
+ onThemeChange(mode: ThemeMode) {
760
+ this.currentTheme = mode;
761
+ if (mode === 'dark') {
762
+ document.documentElement.classList.add('dark');
763
+ } else {
764
+ document.documentElement.classList.remove('dark');
765
+ }
766
+ }
767
+ ```
768
+
769
+ ### Persistir en localStorage
770
+
771
+ ```typescript
772
+ currentTheme: ThemeMode = 'light';
773
+
774
+ ngOnInit() {
775
+ const saved = localStorage.getItem('theme') as ThemeMode;
776
+ if (saved) {
777
+ this.currentTheme = saved;
778
+ document.documentElement.classList.toggle('dark', saved === 'dark');
779
+ }
780
+ }
781
+
782
+ onThemeChange(mode: ThemeMode) {
783
+ this.currentTheme = mode;
784
+ localStorage.setItem('theme', mode);
785
+ document.documentElement.classList.toggle('dark', mode === 'dark');
786
+ }
787
+ ```
788
+
789
+ # Componente CardButton
790
+
791
+ Componente de botón de carrito de compras de la librería **@10mindssoftware/tm-ui-kit**. Muestra un ícono de carrito con un badge numérico opcional según la cantidad de ítems.
792
+
793
+ ---
794
+
795
+ ## Importación
796
+
797
+ ```typescript
798
+ import { CardButton } from '@10mindssoftware/tm-ui-kit';
799
+
800
+ @Component({
801
+ imports: [CardButton],
802
+ })
803
+ ```
804
+
805
+ ---
806
+
807
+ ## API
808
+
809
+ ### Inputs
810
+
811
+ | Prop | Tipo | Default | Descripción |
812
+ | ----------- | --------- | ---------------------- | -------------------------------------------------------------------------------------- |
813
+ | `count` | `number` | `0` | Cantidad de ítems en el carrito. Si es `0`, no se muestra el badge |
814
+ | `disabled` | `boolean` | `false` | Desactiva el botón |
815
+ | `ariaLabel` | `string` | `'Carrito de compras'` | Texto accesible del botón. Se complementa automáticamente con la cantidad si hay ítems |
816
+
817
+ ### Outputs
818
+
819
+ | Evento | Tipo | Descripción |
820
+ | --------- | ------ | --------------------------------------------------- |
821
+ | `clicked` | `void` | Se emite al hacer click (no emite si está disabled) |
822
+
823
+ ---
824
+
825
+ ## Ejemplos
826
+
827
+ ### Uso básico
828
+
829
+ ```html
830
+ <tm-card-button />
831
+ ```
832
+
833
+ ### Con ítems en el carrito
834
+
835
+ ```html
836
+ <tm-card-button [count]="3" /> <tm-card-button [count]="9" />
837
+ ```
838
+
839
+ > Cuando `count` supera 9, el badge muestra `9+` en lugar del número exacto.
840
+
841
+ ```html
842
+ <tm-card-button [count]="15" />
843
+ <!-- Badge muestra: 9+ -->
844
+ ```
845
+
846
+ ### Deshabilitado
847
+
848
+ ```html
849
+ <tm-card-button [disabled]="true" /> <tm-card-button [count]="4" [disabled]="true" />
850
+ ```
851
+
852
+ ### Manejo de eventos
853
+
854
+ ```html
855
+ <tm-card-button [count]="itemCount" (clicked)="onCartClick()" />
856
+ ```
857
+
858
+ ```typescript
859
+ itemCount = 2;
860
+
861
+ onCartClick() {
862
+ console.log('Abriendo carrito...');
863
+ }
864
+ ```
865
+
866
+ ### Con aria-label personalizado
867
+
868
+ ```html
869
+ <tm-card-button ariaLabel="Ver mi pedido" [count]="2" />
870
+ <!-- aria-label resultante: "Ver mi pedido, 2 items" -->
871
+ ```
872
+
873
+ ### Combinaciones comunes
874
+
875
+ ```html
876
+ <!-- Carrito vacío -->
877
+ <tm-card-button (clicked)="abrirCarrito()" />
878
+
879
+ <!-- Carrito con items -->
880
+ <tm-card-button [count]="totalItems" (clicked)="abrirCarrito()" />
881
+
882
+ <!-- Carrito deshabilitado durante carga -->
883
+ <tm-card-button [count]="totalItems" [disabled]="cargando" (clicked)="abrirCarrito()" />
884
+ ```
885
+
886
+ ---
887
+
888
+ ## Notas
889
+
890
+ - El badge solo es visible cuando `count` es mayor a `0`.
891
+ - Cuando `count` supera 9, el badge muestra `9+` para evitar desbordamiento visual.
892
+ - El evento `clicked` **no se emite** cuando `disabled` es `true`, no es necesario manejarlo manualmente.
893
+ - El `aria-label` se construye dinámicamente: si hay ítems, añade la cantidad al texto base (ej. `"Carrito de compras, 3 items"`). El badge tiene `aria-hidden="true"` para evitar duplicados en lectores de pantalla.
894
+ - El ícono del carrito es fijo (`ShoppingCart` de `lucide-angular`) y no es configurable.
895
+
896
+ ---
897
+
898
+ # Componente Checkbox
899
+
900
+ Componente de selección para formularios. Permite marcar o desmarcar una opción individual. Semánticamente correcto y accesible.
901
+
902
+ ---
903
+
904
+ ## Instalación
905
+
906
+ ```typescript
907
+ import { CheckboxButton } from '@10mindssoftware/tm-ui-kit';
908
+
909
+ @Component({
910
+ imports: [CheckboxButton],
911
+ })
912
+ ```
913
+
914
+ ---
915
+
916
+ ## Uso básico
917
+
918
+ ```html
919
+ <tm-checkbox-button [checked]="isChecked" (changed)="isChecked = $event" />
920
+ ```
921
+
922
+ ---
923
+
924
+ ## API
925
+
926
+ ### Inputs
927
+
928
+ | Input | Tipo | Default | Descripción |
929
+ | ---------- | --------- | ------------- | ----------------------------------------- |
930
+ | `checked` | `boolean` | `false` | Estado actual del checkbox |
931
+ | `disabled` | `boolean` | `false` | Desactiva la interacción |
932
+ | `label` | `string` | — | Texto descriptivo al lado del checkbox |
933
+ | `id` | `string` | auto-generado | ID del input para asociar labels externos |
934
+
935
+ ### Outputs
936
+
937
+ | Output | Tipo | Descripción |
938
+ | ----------- | --------- | ------------------------------------ |
939
+ | `(changed)` | `boolean` | Emite el nuevo estado al hacer click |
940
+
941
+ ---
942
+
943
+ ## Estados
944
+
945
+ ```html
946
+ <!-- Desmarcado -->
947
+ <tm-checkbox-button [checked]="false" (changed)="isChecked = $event" />
948
+
949
+ <!-- Marcado -->
950
+ <tm-checkbox-button [checked]="true" (changed)="isChecked = $event" />
951
+
952
+ <!-- Disabled desmarcado -->
953
+ <tm-checkbox-button [checked]="false" [disabled]="true" />
954
+
955
+ <!-- Disabled marcado -->
956
+ <tm-checkbox-button [checked]="true" [disabled]="true" />
957
+ ```
958
+
959
+ ---
960
+
961
+ ## Con label
962
+
963
+ ```html
964
+ <tm-checkbox-button
965
+ [checked]="isChecked"
966
+ label="Aceptar términos y condiciones"
967
+ (changed)="isChecked = $event"
968
+ />
969
+ ```
970
+
971
+ ---
972
+
973
+ ## Manejo del estado
974
+
975
+ El checkbox no guarda estado internamente. El padre es responsable de actualizar el valor:
976
+
977
+ ```typescript
978
+ isChecked = false;
979
+
980
+ onChanged(value: boolean) {
981
+ this.isChecked = value;
982
+ }
983
+ ```
984
+
985
+ ```html
986
+ <tm-checkbox-button [checked]="isChecked" (changed)="onChanged($event)" />
987
+ ```
988
+
989
+ ---
990
+
991
+ ## Casos de uso reales
992
+
993
+ ### Aceptar términos
994
+
995
+ ```html
996
+ <tm-checkbox-button
997
+ [checked]="aceptaTerminos"
998
+ label="Acepto los términos y condiciones"
999
+ (changed)="aceptaTerminos = $event"
1000
+ />
1001
+
1002
+ <tm-button [disabled]="!aceptaTerminos" (clicked)="continuar()"> Continuar </tm-button>
1003
+ ```
1004
+
1005
+ ### Lista de permisos
1006
+
1007
+ ```html
1008
+ @for (permiso of permisos; track permiso.id) {
1009
+ <tm-checkbox-button
1010
+ [checked]="permiso.activo"
1011
+ [label]="permiso.nombre"
1012
+ (changed)="togglePermiso(permiso.id, $event)"
1013
+ />
1014
+ }
1015
+ ```
1016
+
1017
+ ```typescript
1018
+ togglePermiso(id: string, value: boolean) {
1019
+ const permiso = this.permisos.find(p => p.id === id);
1020
+ if (permiso) permiso.activo = value;
1021
+ }
1022
+ ```
1023
+
1024
+ ### Con disabled según permisos
1025
+
1026
+ ```html
1027
+ <tm-checkbox-button
1028
+ [checked]="opcion.activa"
1029
+ [disabled]="!tienePermisos"
1030
+ [label]="opcion.nombre"
1031
+ (changed)="opcion.activa = $event"
1032
+ />
1033
+ ```
1034
+
1035
+ # Componente de Chip
1036
+
1037
+ Componente para mostrar estados, etiquetas o valores visuales. Puede ser estático o clickeable.
1038
+
1039
+ ---
1040
+
1041
+ ## Instalación
1042
+
1043
+ ```typescript
1044
+ import { Chip } from '@10mindssoftware/tm-ui-kit';
1045
+
1046
+ @Component({
1047
+ imports: [Chip],
1048
+ })
1049
+ ```
1050
+
1051
+ ---
1052
+
1053
+ ## Uso básico
1054
+
1055
+ ```html
1056
+ <tm-chip status="success">Activo</tm-chip>
1057
+ ```
1058
+
1059
+ ---
1060
+
1061
+ ## Status (colores)
1062
+
1063
+ | Status | Uso recomendado |
1064
+ | --------- | --------------------------------- |
1065
+ | `success` | Activo, completado, aprobado |
1066
+ | `warning` | Pendiente, por vencer, precaución |
1067
+ | `danger` | Inactivo, error, rechazado |
1068
+ | `info` | Informativo, en proceso |
1069
+ | `indigo` | Categorías, etiquetas neutras |
1070
+ | `brand` | Destacado, principal |
1071
+
1072
+ ```html
1073
+ <tm-chip status="success">Activo</tm-chip>
1074
+ <tm-chip status="warning">Pendiente</tm-chip>
1075
+ <tm-chip status="danger">Inactivo</tm-chip>
1076
+ <tm-chip status="info">En proceso</tm-chip>
1077
+ <tm-chip status="indigo">Categoría</tm-chip>
1078
+ <tm-chip status="brand">Destacado</tm-chip>
1079
+ ```
1080
+
1081
+ ---
1082
+
1083
+ ## Type (estilo visual)
1084
+
1085
+ | Type | Descripción |
1086
+ | ---------- | --------------------------------------- |
1087
+ | `filled` | Fondo sólido con texto blanco |
1088
+ | `subtle` | Fondo suave con texto del color |
1089
+ | `outlined` | Sin fondo, solo borde y texto del color |
1090
+
1091
+ ```html
1092
+ <tm-chip status="success" type="filled">Filled</tm-chip>
1093
+ <tm-chip status="success" type="subtle">Subtle</tm-chip>
1094
+ <tm-chip status="success" type="outlined">Outlined</tm-chip>
1095
+ ```
1096
+
1097
+ ---
1098
+
1099
+ ## Rounded (forma)
1100
+
1101
+ | Rounded | Uso recomendado |
1102
+ | --------- | ---------------------------- |
1103
+ | `default` | Estados, badges informativos |
1104
+ | `full` | Filtros, tags interactivos |
1105
+
1106
+ ```html
1107
+ <tm-chip status="info" rounded="default">Estado</tm-chip>
1108
+ <tm-chip status="info" rounded="full">Etiqueta</tm-chip>
1109
+ ```
1110
+
1111
+ ---
1112
+
1113
+ ## Íconos
1114
+
1115
+ Los íconos se importan desde `lucide-angular` como objetos:
1116
+
1117
+ ```typescript
1118
+ import { Check, TriangleAlert, X } from 'lucide-angular';
1119
+
1120
+ export class MiComponente {
1121
+ check = Check;
1122
+ alert = TriangleAlert;
1123
+ x = X;
1124
+ }
1125
+ ```
1126
+
1127
+ ```html
1128
+ <!-- Ícono izquierdo -->
1129
+ <tm-chip status="success" [iconLeft]="check">Activo</tm-chip>
1130
+
1131
+ <!-- Ícono derecho -->
1132
+ <tm-chip status="info" [iconRight]="x">Etiqueta</tm-chip>
1133
+
1134
+ <!-- Ambos íconos -->
1135
+ <tm-chip status="warning" [iconLeft]="alert" [iconRight]="x">Advertencia</tm-chip>
1136
+ ```
1137
+
1138
+ ---
1139
+
1140
+ ## Clickeable
1141
+
1142
+ Por defecto el chip es estático. Para hacerlo interactivo usa `[clickable]="true"` y escucha el evento `(clicked)`:
1143
+
1144
+ ```html
1145
+ <tm-chip
1146
+ status="danger"
1147
+ type="filled"
1148
+ rounded="full"
1149
+ [iconLeft]="x"
1150
+ [clickable]="true"
1151
+ (clicked)="onDesactivar()"
1152
+ >
1153
+ Desactivar
1154
+ </tm-chip>
1155
+ ```
1156
+
1157
+ ```typescript
1158
+ onDesactivar() {
1159
+ console.log('chip clickeado');
1160
+ }
1161
+ ```
1162
+
1163
+ > Si `clickable` es `false` (default), el chip no emite eventos ni muestra cursor pointer.
1164
+
1165
+ ---
1166
+
1167
+ ## API
1168
+
1169
+ ### Inputs
1170
+
1171
+ | Input | Tipo | Default | Descripción |
1172
+ | ----------- | ---------------- | ----------- | ------------------------------- |
1173
+ | `status` | `ChipStatus` | `'brand'` | Color del chip |
1174
+ | `type` | `ChipType` | `'filled'` | Estilo visual |
1175
+ | `rounded` | `ChipRounded` | `'default'` | Forma del borde |
1176
+ | `iconLeft` | `LucideIconData` | — | Ícono a la izquierda |
1177
+ | `iconRight` | `LucideIconData` | — | Ícono a la derecha |
1178
+ | `clickable` | `boolean` | `false` | Habilita click y cursor pointer |
1179
+
1180
+ ### Outputs
1181
+
1182
+ | Output | Tipo | Descripción |
1183
+ | ----------- | ------ | --------------------------------------------- |
1184
+ | `(clicked)` | `void` | Se emite al hacer click si `clickable="true"` |
1185
+
1186
+ ### Tipos
1187
+
1188
+ ```typescript
1189
+ type ChipStatus = 'success' | 'warning' | 'indigo' | 'danger' | 'info' | 'brand';
1190
+ type ChipType = 'filled' | 'subtle' | 'outlined';
1191
+ type ChipRounded = 'default' | 'full';
1192
+ ```
1193
+
1194
+ ---
1195
+
1196
+ ## Casos de uso reales
1197
+
1198
+ ```html
1199
+ <!-- Estado de un producto -->
1200
+ <tm-chip status="success" type="subtle" rounded="full" [iconLeft]="check"> Activo </tm-chip>
1201
+
1202
+ <!-- Licencia por vencer -->
1203
+ <tm-chip status="warning" type="filled" rounded="default" [iconLeft]="alert"> Por vencer </tm-chip>
1204
+
1205
+ <!-- Filtro clickeable -->
1206
+ <tm-chip status="indigo" type="outlined" rounded="full" [clickable]="true" (clicked)="filtrar()">
1207
+ Electrónica
1208
+ </tm-chip>
1209
+
1210
+ <!-- Notificación de error -->
1211
+ <tm-chip status="danger" type="subtle" rounded="default" [iconLeft]="x"> Pago rechazado </tm-chip>
1212
+ ```
1213
+
1214
+ # Componente CloseDialog
1215
+
1216
+ Componente de botón de cierre de la librería **@10mindssoftware/tm-ui-kit**. Diseñado para cerrar diálogos, modales o paneles. Muestra un ícono de `X` con fondo circular al hacer hover.
1217
+
1218
+ ---
1219
+
1220
+ ## Importación
1221
+
1222
+ ```typescript
1223
+ import { CloseDialog } from '@10mindssoftware/tm-ui-kit';
1224
+
1225
+ @Component({
1226
+ imports: [CloseDialog],
1227
+ })
1228
+ ```
1229
+
1230
+ ---
1231
+
1232
+ ## API
1233
+
1234
+ ### Inputs
1235
+
1236
+ | Prop | Tipo | Default | Descripción |
1237
+ | ----------- | --------- | ---------- | ------------------------- |
1238
+ | `disabled` | `boolean` | `false` | Desactiva el botón |
1239
+ | `ariaLabel` | `string` | `'Cerrar'` | Texto accesible del botón |
1240
+
1241
+ ### Outputs
1242
+
1243
+ | Evento | Tipo | Descripción |
1244
+ | -------- | ------ | --------------------------------------------------- |
1245
+ | `closed` | `void` | Se emite al hacer click (no emite si está disabled) |
1246
+
1247
+ ---
1248
+
1249
+ ## Ejemplos
1250
+
1251
+ ### Uso básico
1252
+
1253
+ ```html
1254
+ <tm-close-dialog (closed)="onClose()" />
1255
+ ```
1256
+
1257
+ ### Deshabilitado
1258
+
1259
+ ```html
1260
+ <tm-close-dialog [disabled]="true" />
1261
+ ```
1262
+
1263
+ ### Con aria-label personalizado
1264
+
1265
+ ```html
1266
+ <tm-close-dialog ariaLabel="Cerrar modal de confirmación" (closed)="onClose()" />
1267
+ ```
1268
+
1269
+ ### Combinaciones comunes
1270
+
1271
+ ```html
1272
+ <!-- En la cabecera de un modal -->
1273
+ <div class="flex items-center justify-between">
1274
+ <h2>Título del modal</h2>
1275
+ <tm-close-dialog (closed)="cerrarModal()" />
1276
+ </div>
1277
+
1278
+ <!-- Deshabilitado durante procesamiento -->
1279
+ <tm-close-dialog [disabled]="procesando" (closed)="cerrarModal()" />
1280
+ ```
1281
+
1282
+ ```typescript
1283
+ procesando = false;
1284
+
1285
+ cerrarModal() {
1286
+ console.log('Cerrando modal...');
1287
+ }
1288
+ ```
1289
+
1290
+ ---
1291
+
1292
+ ## Notas
1293
+
1294
+ - El evento `closed` **no se emite** cuando `disabled` es `true`, no es necesario manejarlo manualmente.
1295
+ - El ícono de cierre es fijo (`X` de `lucide-angular`) y no es configurable.
1296
+ - El botón tiene forma circular y aplica un fondo sutil (`hover:bg-tm-subtle`) al pasar el cursor, sin afectar el layout del contenedor.
1297
+
1298
+ ---
1299
+
1300
+ # Componente InputTen
1301
+
1302
+ Componente de campo de texto de la librería **@10mindssoftware/tm-ui-kit**. Soporta múltiples tipos de input, tamaños, íconos, estados de error, hints y labels.
1303
+
1304
+ ---
1305
+
1306
+ ## Uso
1307
+
1308
+ Importa el componente en tu módulo o componente standalone:
1309
+
1310
+ ```typescript
1311
+ import { InputTen } from '@10mindssoftware/tm-ui-kit';
1312
+
1313
+ @Component({
1314
+ imports: [InputTen],
1315
+ })
1316
+ ```
1317
+
1318
+ ---
1319
+
1320
+ ## API
1321
+
1322
+ ### Inputs
1323
+
1324
+ | Prop | Tipo | Default | Descripción |
1325
+ | ------------- | ---------------- | --------------- | ------------------------------------------------------------ |
1326
+ | `value` | `string` | `''` | Valor del input (two-way binding con `model`) |
1327
+ | `size` | `InputSize` | `'md'` | Tamaño del input |
1328
+ | `type` | `InputType` | `'text'` | Tipo de input nativo |
1329
+ | `placeholder` | `string` | `'placeholder'` | Texto de placeholder |
1330
+ | `label` | `string` | `''` | Etiqueta visible sobre el input |
1331
+ | `hint` | `string` | `''` | Texto de ayuda debajo del input (no se muestra si hay error) |
1332
+ | `disabled` | `boolean` | `false` | Desactiva el input |
1333
+ | `required` | `boolean` | `false` | Marca el campo como requerido (agrega `*` al label) |
1334
+ | `error` | `string` | `''` | Mensaje de error (activa el estado visual de error) |
1335
+ | `iconLeft` | `LucideIconData` | — | Ícono a la izquierda del input |
1336
+ | `iconRight` | `LucideIconData` | — | Ícono a la derecha del input |
1337
+
1338
+ ### Outputs
1339
+
1340
+ | Evento | Tipo | Descripción |
1341
+ | --------- | ------------ | --------------------------------------- |
1342
+ | `focused` | `FocusEvent` | Se emite cuando el input recibe el foco |
1343
+ | `blurred` | `FocusEvent` | Se emite cuando el input pierde el foco |
1344
+
1345
+ ### Tipos
1346
+
1347
+ ```typescript
1348
+ type InputSize = 'sm' | 'md';
1349
+ type InputType = 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
1350
+ ```
1351
+
1352
+ ---
1353
+
1354
+ ## Ejemplos
1355
+
1356
+ ### Uso básico
1357
+
1358
+ ```html
1359
+ <tm-input placeholder="Escribe algo..." />
1360
+ ```
1361
+
1362
+ ### Con label y hint
1363
+
1364
+ ```html
1365
+ <tm-input
1366
+ label="Correo electrónico"
1367
+ placeholder="ejemplo@correo.com"
1368
+ hint="Nunca compartiremos tu correo."
1369
+ />
1370
+ ```
1371
+
1372
+ ### Campo requerido
1373
+
1374
+ ```html
1375
+ <tm-input label="Nombre completo" placeholder="Juan Pérez" [required]="true" />
1376
+ ```
1377
+
1378
+ ### Tamaños
1379
+
1380
+ ```html
1381
+ <tm-input size="md" placeholder="Medium (default)" /> <tm-input size="sm" placeholder="Small" />
1382
+ ```
1383
+
1384
+ ### Tipos de input
1385
+
1386
+ ```html
1387
+ <tm-input type="text" placeholder="Texto" />
1388
+ <tm-input type="email" placeholder="Correo" />
1389
+ <tm-input type="password" placeholder="Contraseña" />
1390
+ <tm-input type="number" placeholder="Número" />
1391
+ <tm-input type="search" placeholder="Buscar..." />
1392
+ <tm-input type="tel" placeholder="Teléfono" />
1393
+ <tm-input type="url" placeholder="https://..." />
1394
+ ```
1395
+
1396
+ ### Estado deshabilitado
1397
+
1398
+ ```html
1399
+ <tm-input [disabled]="true" placeholder="No disponible" />
1400
+ <tm-input [disabled]="true" label="Usuario" placeholder="admin" />
1401
+ ```
1402
+
1403
+ ### Estado de error
1404
+
1405
+ ```html
1406
+ <tm-input
1407
+ label="Correo electrónico"
1408
+ placeholder="ejemplo@correo.com"
1409
+ error="El correo ingresado no es válido."
1410
+ />
1411
+ ```
1412
+
1413
+ > Cuando `error` tiene valor, el hint queda oculto y el borde cambia a rojo.
1414
+
1415
+ ### Con íconos
1416
+
1417
+ Los íconos se pasan como objetos `LucideIconData`, **no como strings**.
1418
+
1419
+ ```typescript
1420
+ // app.ts
1421
+ import { Mail, Lock, Search, Eye } from 'lucide-angular';
1422
+
1423
+ export class App {
1424
+ mail = Mail;
1425
+ lock = Lock;
1426
+ search = Search;
1427
+ eye = Eye;
1428
+ }
1429
+ ```
1430
+
1431
+ ```html
1432
+ <!-- Ícono izquierdo -->
1433
+ <tm-input [iconLeft]="mail" placeholder="Correo electrónico" />
1434
+
1435
+ <!-- Ícono derecho -->
1436
+ <tm-input [iconRight]="eye" type="password" placeholder="Contraseña" />
1437
+
1438
+ <!-- Ambos íconos -->
1439
+ <tm-input [iconLeft]="search" [iconRight]="eye" placeholder="Buscar..." />
1440
+ ```
1441
+
1442
+ ### Two-way binding
1443
+
1444
+ ```html
1445
+ <tm-input [(value)]="correo" placeholder="Correo electrónico" />
1446
+ <p>Valor actual: {{ correo }}</p>
1447
+ ```
1448
+
1449
+ ```typescript
1450
+ correo = '';
1451
+ ```
1452
+
1453
+ ### Manejo de eventos de foco
1454
+
1455
+ ```html
1456
+ <tm-input placeholder="Nombre" (focused)="onFocus($event)" (blurred)="onBlur($event)" />
1457
+ ```
1458
+
1459
+ ```typescript
1460
+ onFocus(event: FocusEvent) {
1461
+ console.log('Input enfocado', event);
1462
+ }
1463
+
1464
+ onBlur(event: FocusEvent) {
1465
+ console.log('Input desenfocado', event);
1466
+ }
1467
+ ```
1468
+
1469
+ ### Combinaciones comunes
1470
+
1471
+ ```html
1472
+ <!-- Campo de búsqueda -->
1473
+ <tm-input type="search" size="sm" [iconLeft]="search" placeholder="Buscar usuarios..." />
1474
+
1475
+ <!-- Campo de contraseña con label y error -->
1476
+ <tm-input
1477
+ type="password"
1478
+ label="Contraseña"
1479
+ placeholder="Mínimo 8 caracteres"
1480
+ [iconLeft]="lock"
1481
+ error="La contraseña es demasiado corta."
1482
+ />
1483
+
1484
+ <!-- Campo de email requerido con hint -->
1485
+ <tm-input
1486
+ type="email"
1487
+ label="Correo electrónico"
1488
+ placeholder="ejemplo@correo.com"
1489
+ [iconLeft]="mail"
1490
+ hint="Te enviaremos un enlace de confirmación."
1491
+ [required]="true"
1492
+ [(value)]="email"
1493
+ />
1494
+ ```
1495
+
1496
+ ---
1497
+
1498
+ ## Notas
1499
+
1500
+ - El prop `value` usa `model()` de Angular Signals, lo que permite two-way binding con la sintaxis `[(value)]`.
1501
+ - Cuando `error` tiene contenido, el `hint` **no se muestra**, independientemente de su valor.
1502
+ - El estado visual del wrapper cambia automáticamente entre `default`, `active`, `error` y `disabled` sin configuración adicional.
1503
+ - Los íconos deben importarse desde `lucide-angular` como objetos y asignarse a propiedades del componente. Pasar strings directamente **no funciona**.
1504
+ - No se requiere ninguna configuración global de íconos en `app.config.ts`.
1505
+
1506
+ ---
1507
+
1508
+ # Componente de Menú
1509
+
1510
+ Componente de menú desplegable flexible para Angular. Soporta cualquier trigger personalizado (avatar, botón, ícono de 3 puntos, etc.) y contenido variable dentro del panel.
1511
+
1512
+ ---
1513
+
1514
+ ## Instalación
1515
+
1516
+ Importa `TmMenu` en tu componente:
1517
+
1518
+ ```typescript
1519
+ import { TmMenu } from '@10mindssoftware/tm-ui-kit';
1520
+
1521
+ @Component({
1522
+ imports: [TmMenu],
1523
+ })
1524
+ export class MiComponente {}
1525
+ ```
1526
+
1527
+ > `TmMenu` ya incluye `MenuComponent`, `MenuTriggerDirective` y `MenuItem` — no necesitas importarlos por separado.
1528
+
1529
+ ---
1530
+
1531
+ ## Uso básico
1532
+
1533
+ ```html
1534
+ <tm-menu placement="bottom-end" triggerOn="click">
1535
+ <ng-template tmMenuTrigger>
1536
+ <button>Abrir menú</button>
1537
+ </ng-template>
1538
+
1539
+ <tm-menu-item label="Mi perfil" [icon]="userIcon" />
1540
+ <tm-menu-item label="Configuración" [icon]="settingsIcon" />
1541
+ <tm-menu-item label="Cerrar sesión" [danger]="true" [divider]="true" />
1542
+ </tm-menu>
1543
+ ```
1544
+
1545
+ ---
1546
+
1547
+ ## Inputs de `tm-menu`
1548
+
1549
+ | Input | Tipo | Default | Descripción |
1550
+ | ----------- | ------------------------------------------------------------ | -------------- | ----------------------- |
1551
+ | `placement` | `'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end'` | `'bottom-end'` | Posición del panel |
1552
+ | `triggerOn` | `'click' \| 'hover' \| 'both'` | `'click'` | Cómo se dispara el menú |
1553
+
1554
+ ## Outputs de `tm-menu`
1555
+
1556
+ | Output | Descripción |
1557
+ | ---------- | --------------------------------- |
1558
+ | `(opened)` | Se emite cuando el menú se abre |
1559
+ | `(closed)` | Se emite cuando el menú se cierra |
1560
+
1561
+ ---
1562
+
1563
+ ## Inputs de `tm-menu-item`
1564
+
1565
+ | Input | Tipo | Default | Descripción |
1566
+ | ---------- | ---------------- | --------- | ----------------------------- |
1567
+ | `label` | `string` | requerido | Texto del ítem |
1568
+ | `icon` | `LucideIconData` | — | Ícono a la izquierda |
1569
+ | `danger` | `boolean` | `false` | Estilo de color rojo |
1570
+ | `divider` | `boolean` | `false` | Muestra línea divisora encima |
1571
+ | `disabled` | `boolean` | `false` | Deshabilita el ítem |
1572
+
1573
+ ## Outputs de `tm-menu-item`
1574
+
1575
+ | Output | Descripción |
1576
+ | ------------ | -------------------------------------------- |
1577
+ | `(selected)` | Se emite al hacer click (respeta `disabled`) |
1578
+
1579
+ ---
1580
+
1581
+ ## Ícono dinámico de abierto/cerrado
1582
+
1583
+ Para mostrar un chevron que cambia según el estado del menú, usa los outputs `(opened)` y `(closed)` junto con un `signal` en tu componente:
1584
+
1585
+ **En tu `.ts`:**
1586
+
1587
+ ```typescript
1588
+ import { signal } from '@angular/core';
1589
+ import { ChevronDown, ChevronUp } from 'lucide-angular';
1590
+
1591
+ isMenuOpen = signal(false);
1592
+ chevronDown = ChevronDown;
1593
+ chevronUp = ChevronUp;
1594
+ ```
1595
+
1596
+ **En tu `.html`:**
1597
+
1598
+ ```html
1599
+ <tm-menu placement="bottom-end" (opened)="isMenuOpen.set(true)" (closed)="isMenuOpen.set(false)">
1600
+ <ng-template tmMenuTrigger>
1601
+ <div class="flex items-center gap-2">
1602
+ <span>Jhon Doe</span>
1603
+ <lucide-icon [img]="isMenuOpen() ? chevronUp : chevronDown" [size]="16" />
1604
+ </div>
1605
+ </ng-template>
1606
+
1607
+ <tm-menu-item label="Mi perfil" [icon]="userIcon" />
1608
+ <tm-menu-item label="Cerrar sesión" [danger]="true" />
1609
+ </tm-menu>
1610
+ ```
1611
+
1612
+ > Si tienes múltiples menús en el mismo componente, crea un signal separado para cada uno: `isMenu1Open`, `isMenu2Open`, etc.
1613
+
1614
+ ---
1615
+
1616
+ ## Menú con header de perfil (caso mobile/especial)
1617
+
1618
+ Usa el slot `menuHeader` para agregar contenido encima de los ítems, como el avatar y nombre del usuario:
1619
+
1620
+ ```html
1621
+ <tm-menu placement="bottom-start">
1622
+ <ng-template tmMenuTrigger>
1623
+ <img src="avatar.png" class="rounded-full w-10 h-10" />
1624
+ </ng-template>
1625
+
1626
+ <!-- Header opcional -->
1627
+ <div menuHeader class="px-3 py-3 flex items-center gap-3 border-b border-tm-subtle w-full">
1628
+ <img src="avatar.png" class="rounded-full w-10 h-10" />
1629
+ <div>
1630
+ <p class="font-medium text-sm">Jhon Doe</p>
1631
+ <p class="text-xs text-tm-secondary">Super Admin</p>
1632
+ </div>
1633
+ </div>
1634
+
1635
+ <tm-menu-item label="Mi perfil" [icon]="userIcon" />
1636
+ <tm-menu-item label="Cerrar sesión" [danger]="true" [divider]="true" />
1637
+ </tm-menu>
1638
+ ```
1639
+
1640
+ ---
1641
+
1642
+ ## Menú de acciones en tabla (ícono de 3 puntos)
1643
+
1644
+ ```html
1645
+ <tm-menu placement="bottom-end">
1646
+ <ng-template tmMenuTrigger>
1647
+ <lucide-icon [img]="dotsIcon" [size]="16" />
1648
+ </ng-template>
1649
+
1650
+ <tm-menu-item label="Editar" [icon]="editIcon" />
1651
+ <tm-menu-item label="Eliminar" [icon]="trashIcon" [danger]="true" [divider]="true" />
1652
+ </tm-menu>
1653
+ ```
1654
+
1655
+ ---
1656
+
1657
+ ## Divider
1658
+
1659
+ El `divider` muestra una línea separadora **encima** del ítem. Úsalo en el primer ítem de una sección secundaria, como "Cerrar sesión":
1660
+
1661
+ ```html
1662
+ <tm-menu-item label="Cerrar sesión" [danger]="true" [divider]="true" />
1663
+ ```
1664
+
1665
+ ## Componente de Radio Button
1666
+
1667
+ Componente de selección única para formularios. Se usa en grupos donde solo una opción puede estar seleccionada a la vez.
1668
+
1669
+ ---
1670
+
1671
+ ## Instalación
1672
+
1673
+ ```typescript
1674
+ import { RadioButton } from '@10mindssoftware/tm-ui-kit';
1675
+
1676
+ @Component({
1677
+ imports: [RadioButton],
1678
+ })
1679
+ ```
1680
+
1681
+ ---
1682
+
1683
+ ## Uso básico
1684
+
1685
+ ```html
1686
+ <tm-radio-button
1687
+ value="opcion1"
1688
+ name="mi-grupo"
1689
+ [checked]="selected === 'opcion1'"
1690
+ (changed)="selected = $event"
1691
+ />
1692
+ ```
1693
+
1694
+ ---
1695
+
1696
+ ## API
1697
+
1698
+ ### Inputs
1699
+
1700
+ | Input | Tipo | Default | Descripción |
1701
+ | ---------- | --------- | --------------- | ------------------------------------------------------- |
1702
+ | `value` | `string` | requerido | Valor que emite cuando se selecciona |
1703
+ | `name` | `string` | `'radio-group'` | Nombre del grupo, debe ser igual en todos los del grupo |
1704
+ | `checked` | `boolean` | `false` | Estado actual del radio |
1705
+ | `disabled` | `boolean` | `false` | Desactiva la interacción |
1706
+ | `label` | `string` | — | Texto descriptivo al lado del radio |
1707
+ | `id` | `string` | auto-generado | ID del input para asociar labels externos |
1708
+
1709
+ ### Outputs
1710
+
1711
+ | Output | Tipo | Descripción |
1712
+ | ----------- | -------- | --------------------------------------- |
1713
+ | `(changed)` | `string` | Emite el `value` del radio seleccionado |
1714
+
1715
+ ---
1716
+
1717
+ ## Estados
1718
+
1719
+ ```html
1720
+ <!-- Sin seleccionar -->
1721
+ <tm-radio-button value="opcion1" name="grupo" [checked]="false" (changed)="onChanged($event)" />
1722
+
1723
+ <!-- Seleccionado -->
1724
+ <tm-radio-button value="opcion1" name="grupo" [checked]="true" (changed)="onChanged($event)" />
1725
+
1726
+ <!-- Disabled desmarcado -->
1727
+ <tm-radio-button value="opcion1" name="grupo" [checked]="false" [disabled]="true" />
1728
+
1729
+ <!-- Disabled marcado -->
1730
+ <tm-radio-button value="opcion1" name="grupo" [checked]="true" [disabled]="true" />
1731
+ ```
1732
+
1733
+ ---
1734
+
1735
+ ## Uso en grupo
1736
+
1737
+ El `name` debe ser el mismo en todos los radio del grupo. El padre controla cuál está seleccionado comparando `value` con el valor actual:
1738
+
1739
+ ```typescript
1740
+ selected = 'mensual';
1741
+
1742
+ onChanged(value: string) {
1743
+ this.selected = value;
1744
+ }
1745
+ ```
1746
+
1747
+ ```html
1748
+ <div class="flex flex-col gap-3">
1749
+ <tm-radio-button
1750
+ value="mensual"
1751
+ name="plan"
1752
+ label="Mensual"
1753
+ [checked]="selected === 'mensual'"
1754
+ (changed)="onChanged($event)"
1755
+ />
1756
+ <tm-radio-button
1757
+ value="anual"
1758
+ name="plan"
1759
+ label="Anual"
1760
+ [checked]="selected === 'anual'"
1761
+ (changed)="onChanged($event)"
1762
+ />
1763
+ <tm-radio-button
1764
+ value="lifetime"
1765
+ name="plan"
1766
+ label="De por vida"
1767
+ [checked]="selected === 'lifetime'"
1768
+ (changed)="onChanged($event)"
1769
+ />
1770
+ </div>
1771
+ ```
1772
+
1773
+ ---
1774
+
1775
+ ## Casos de uso reales
1776
+
1777
+ ### Selección de plan
1778
+
1779
+ ```html
1780
+ <div class="flex flex-col gap-3">
1781
+ @for (plan of planes; track plan.id) {
1782
+ <tm-radio-button
1783
+ [value]="plan.id"
1784
+ name="planes"
1785
+ [label]="plan.nombre"
1786
+ [checked]="selectedPlan === plan.id"
1787
+ (changed)="selectedPlan = $event"
1788
+ />
1789
+ }
1790
+ </div>
1791
+ ```
1792
+
1793
+ ### Con opción desactivada
1794
+
1795
+ ```html
1796
+ <tm-radio-button
1797
+ value="premium"
1798
+ name="plan"
1799
+ label="Premium (próximamente)"
1800
+ [checked]="false"
1801
+ [disabled]="true"
1802
+ />
1803
+ ```
1804
+
1805
+ ### Selección de rol en formulario
1806
+
1807
+ ```html
1808
+ <div class="flex flex-col gap-3">
1809
+ <tm-radio-button
1810
+ value="admin"
1811
+ name="rol"
1812
+ label="Administrador"
1813
+ [checked]="rol === 'admin'"
1814
+ (changed)="rol = $event"
1815
+ />
1816
+ <tm-radio-button
1817
+ value="editor"
1818
+ name="rol"
1819
+ label="Editor"
1820
+ [checked]="rol === 'editor'"
1821
+ (changed)="rol = $event"
1822
+ />
1823
+ <tm-radio-button
1824
+ value="viewer"
1825
+ name="rol"
1826
+ label="Solo lectura"
1827
+ [checked]="rol === 'viewer'"
1828
+ (changed)="rol = $event"
1829
+ />
1830
+ </div>
1831
+ ```
1832
+
1833
+ ```typescript
1834
+ rol = 'viewer';
1835
+ ```
1836
+
1837
+ ## Componente de Select
1838
+
1839
+ Combobox con búsqueda integrada, soporte para avatar, ícono y grupos de opciones. Usa CDK Overlay para funcionar correctamente dentro de modales y tablas.
1840
+
1841
+ ---
1842
+
1843
+ ## Instalación
1844
+
1845
+ Importa `TmSelect` en tu componente:
1846
+
1847
+ ```typescript
1848
+ import { TmSelect } from '@10mindssoftware/tm-ui-kit';
1849
+
1850
+ @Component({
1851
+ imports: [TmSelect],
1852
+ })
1853
+ export class MiComponente {}
1854
+ ```
1855
+
1856
+ ---
1857
+
1858
+ ## Uso básico
1859
+
1860
+ ```html
1861
+ <div class="w-[273px]">
1862
+ <tm-select
1863
+ label="Asignar a"
1864
+ placeholder="Selecciona una persona"
1865
+ [options]="myOptions"
1866
+ (changed)="onSelect($event)"
1867
+ />
1868
+ </div>
1869
+ ```
1870
+
1871
+ ```typescript
1872
+ import { SelectOption } from '@10mindssoftware/tm-ui-kit';
1873
+
1874
+ myOptions: SelectOption[] = [
1875
+ { value: 1, label: 'Ana García' },
1876
+ { value: 2, label: 'Luis Pérez' },
1877
+ { value: 3, label: 'Sin asignar', disabled: true },
1878
+ ];
1879
+
1880
+ onSelect(option: SelectOption | null) {
1881
+ console.log('value:', option?.value); // el ID que mandas al backend
1882
+ }
1883
+ ```
1884
+
1885
+ > El select ocupa el 100% del contenedor padre. Define el ancho desde afuera con un `div` wrapper.
1886
+
1887
+ ---
1888
+
1889
+ ## API de inputs
1890
+
1891
+ | Input | Tipo | Default | Descripción |
1892
+ | ------------- | ---------------- | ------------------ | ----------------------------- |
1893
+ | `options` | `SelectOption[]` | `[]` | Lista de opciones |
1894
+ | `placeholder` | `string` | `'Seleccionar...'` | Texto cuando no hay selección |
1895
+ | `size` | `'sm' \| 'md'` | `'md'` | Tamaño del campo |
1896
+ | `label` | `string` | `''` | Etiqueta encima del campo |
1897
+ | `disabled` | `boolean` | `false` | Deshabilita el select |
1898
+ | `error` | `string` | `''` | Mensaje de error |
1899
+
1900
+ ## API de outputs
1901
+
1902
+ | Output | Tipo | Descripción |
1903
+ | ----------- | ---------------------- | --------------------------------- |
1904
+ | `(changed)` | `SelectOption \| null` | Se emite al seleccionar o limpiar |
1905
+
1906
+ ## Model
1907
+
1908
+ | Model | Tipo | Descripción |
1909
+ | ------- | -------------------------- | ---------------------------------------------- |
1910
+ | `value` | `string \| number \| null` | Valor seleccionado (bindeable con `[(value)]`) |
1911
+
1912
+ ---
1913
+
1914
+ ## Ejemplo con avatar e ícono
1915
+
1916
+ Cada opción puede tener un `avatar` (imagen) o un `icon` (Lucide). Si tiene ambos, el avatar tiene prioridad.
1917
+
1918
+ ```typescript
1919
+ import { User, Package } from 'lucide-angular';
1920
+
1921
+ myOptions: SelectOption[] = [
1922
+ { value: 1, label: 'Ana García', avatar: 'https://ejemplo.com/ana.jpg' },
1923
+ { value: 2, label: 'Luis Pérez', icon: User },
1924
+ { value: 3, label: 'Sin asignar', disabled: true },
1925
+ ];
1926
+ ```
1927
+
1928
+ ```html
1929
+ <div class="w-[273px]">
1930
+ <tm-select
1931
+ label="Responsable"
1932
+ placeholder="Selecciona una persona"
1933
+ [options]="myOptions"
1934
+ (changed)="onSelect($event)"
1935
+ />
1936
+ </div>
1937
+ ```
1938
+
1939
+ ---
1940
+
1941
+ ## Ejemplo con grupos
1942
+
1943
+ Agrega la propiedad `group` a cada opción para agruparlas con un encabezado:
1944
+
1945
+ ```typescript
1946
+ myOptions: SelectOption[] = [
1947
+ { value: 1, label: 'Ana García', avatar: 'https://...', group: 'Equipo A' },
1948
+ { value: 2, label: 'Luis Pérez', icon: User, group: 'Equipo A' },
1949
+ { value: 3, label: 'Sin asignar', group: 'Equipo B', disabled: true },
1950
+ ];
1951
+ ```
1952
+
1953
+ Las opciones sin `group` se muestran sin encabezado.
1954
+
1955
+ ---
1956
+
1957
+ ## Ejemplo con error y disabled
1958
+
1959
+ ```html
1960
+ <div class="w-[273px] flex flex-col gap-4">
1961
+ <!-- Con error -->
1962
+ <tm-select
1963
+ label="Asignar a"
1964
+ [options]="myOptions"
1965
+ error="Este campo es requerido"
1966
+ (changed)="onSelect($event)"
1967
+ />
1968
+
1969
+ <!-- Deshabilitado -->
1970
+ <tm-select label="Asignar a" [options]="myOptions" [disabled]="true" />
1971
+ </div>
1972
+ ```
1973
+
1974
+ ---
1975
+
1976
+ ## Obtener el valor para el backend
1977
+
1978
+ El `value` de cada opción es el ID que usas para mandar al backend:
1979
+
1980
+ ```typescript
1981
+ onSelect(option: SelectOption | null) {
1982
+ const payload = {
1983
+ asignado_a: option?.value ?? null
1984
+ };
1985
+ this.api.post('/tarea', payload).subscribe();
1986
+ }
1987
+ ```
1988
+
1989
+ ---
1990
+
1991
+ ## Interface SelectOption
1992
+
1993
+ ```typescript
1994
+ export interface SelectOption {
1995
+ value: string | number;
1996
+ label: string;
1997
+ avatar?: string;
1998
+ icon?: LucideIconData;
1999
+ group?: string;
2000
+ disabled?: boolean;
2001
+ }
2002
+ ```
2003
+
2004
+ # Componente SnackbarService
2005
+
2006
+ Sistema de notificaciones globales para mostrar feedback visual al usuario.
2007
+
2008
+ ### Configuración (una sola vez)
2009
+
2010
+ **`app.html`:**
2011
+
2012
+ ```html
2013
+ <snackbar-host /> <router-outlet />
2014
+ ```
2015
+
2016
+ **`app.ts`:**
2017
+
2018
+ ```typescript
2019
+ import { SnackbarHost } from '@10mindssoftware/tm-ui-kit';
2020
+
2021
+ @Component({
2022
+ imports: [SnackbarHost],
2023
+ })
2024
+ ```
2025
+
2026
+ ### Uso
2027
+
2028
+ ```typescript
2029
+ import { SnackbarService } from '@10mindssoftware/tm-ui-kit';
2030
+
2031
+ export class MiComponente {
2032
+ snackbar = inject(SnackbarService);
2033
+
2034
+ async onGuardar() {
2035
+ try {
2036
+ await this.service.guardar(data);
2037
+ this.snackbar.alert('success', 'Guardado', 'Los cambios fueron guardados correctamente');
2038
+ } catch {
2039
+ this.snackbar.alert('error', 'Error', 'No pudimos guardar los cambios');
2040
+ }
2041
+ }
2042
+ }
2043
+ ```
2044
+
2045
+ ### Métodos
2046
+
2047
+ | Método | Descripción |
2048
+ | --------------------------------------------------------- | -------------------------------- |
2049
+ | `alert(color, title, description?, duration?, position?)` | Desaparece automáticamente |
2050
+ | `notify(color, title, description?, position?)` | El usuario la cierra manualmente |
2051
+
2052
+ ### Parámetros
2053
+
2054
+ | Parámetro | Tipo | Default | Valores |
2055
+ | ------------- | ------------------ | ----------- | -------------------------------------------------------------------------------- |
2056
+ | `color` | `SnackbarColor` | — | `success` `error` `warning` `info` |
2057
+ | `title` | `string` | — | — |
2058
+ | `description` | `string` | `undefined` | — |
2059
+ | `duration` | `number` | `5000` | Milisegundos |
2060
+ | `position` | `SnackbarPosition` | `top-right` | `top-left` `top-center` `top-right` `bottom-left` `bottom-center` `bottom-right` |
2061
+
2062
+ ### Ejemplos
2063
+
2064
+ ```typescript
2065
+ // Mínimo
2066
+ this.snackbar.alert('success', 'Guardado');
2067
+
2068
+ // Con descripción
2069
+ this.snackbar.alert('error', 'Error de pago', 'No pudimos procesar tu método de pago');
2070
+
2071
+ // Con duración personalizada
2072
+ this.snackbar.alert('warning', 'Licencia por expirar', 'Expira en 7 días', 2000);
2073
+
2074
+ // Con posición personalizada
2075
+ this.snackbar.alert('info', 'Nueva versión', 'Yana 2.0 disponible', 5000, 'bottom-center');
2076
+
2077
+ // Notificación persistente
2078
+ this.snackbar.notify('success', 'Compra exitosa', 'Tu licencia fue activada');
2079
+
2080
+ // Notificación con posición
2081
+ this.snackbar.notify('error', 'Sin conexión', undefined, 'bottom-right');
2082
+ ```
2083
+
2084
+ ---
2085
+
2086
+ # Componente Toggle
2087
+
2088
+ Componente de interruptor para activar o desactivar estados. Emite el nuevo valor al padre para que maneje la lógica correspondiente.
2089
+
2090
+ ---
2091
+
2092
+ ## Instalación
2093
+
2094
+ ```typescript
2095
+ import { Toggle } from '@10mindssoftware/tm-ui-kit';
2096
+
2097
+ @Component({
2098
+ imports: [Toggle],
2099
+ })
2100
+ ```
2101
+
2102
+ ---
2103
+
2104
+ ## Uso básico
2105
+
2106
+ ```html
2107
+ <tm-toggle [value]="isActive" (changed)="isActive = $event" />
2108
+ ```
2109
+
2110
+ ---
2111
+
2112
+ ## API
2113
+
2114
+ ### Inputs
2115
+
2116
+ | Input | Tipo | Default | Descripción |
2117
+ | ---------- | --------- | ------- | ------------------------ |
2118
+ | `value` | `boolean` | `false` | Estado actual del toggle |
2119
+ | `disabled` | `boolean` | `false` | Desactiva la interacción |
2120
+
2121
+ ### Outputs
2122
+
2123
+ | Output | Tipo | Descripción |
2124
+ | ----------- | --------- | ------------------------------------ |
2125
+ | `(changed)` | `boolean` | Emite el nuevo estado al hacer click |
2126
+
2127
+ ---
2128
+
2129
+ ## Estados
2130
+
2131
+ ```html
2132
+ <!-- Apagado -->
2133
+ <tm-toggle [value]="false" (changed)="onChanged($event)" />
2134
+
2135
+ <!-- Encendido -->
2136
+ <tm-toggle [value]="true" (changed)="onChanged($event)" />
2137
+
2138
+ <!-- Disabled apagado -->
2139
+ <tm-toggle [value]="false" [disabled]="true" />
2140
+
2141
+ <!-- Disabled encendido -->
2142
+ <tm-toggle [value]="true" [disabled]="true" />
2143
+ ```
2144
+
2145
+ ---
2146
+
2147
+ ## Manejo del estado
2148
+
2149
+ El toggle no guarda estado internamente. El padre es responsable de actualizar el valor:
2150
+
2151
+ ```typescript
2152
+ isActive = false;
2153
+
2154
+ onChanged(value: boolean) {
2155
+ this.isActive = value;
2156
+ }
2157
+ ```
2158
+
2159
+ ```html
2160
+ <tm-toggle [value]="isActive" (changed)="onChanged($event)" />
2161
+ ```
2162
+
2163
+ O de forma inline:
2164
+
2165
+ ```html
2166
+ <tm-toggle [value]="isActive" (changed)="isActive = $event" />
2167
+ ```
2168
+
2169
+ ---
2170
+
2171
+ ## Casos de uso reales
2172
+
2173
+ ### Activar notificaciones
2174
+
2175
+ ```html
2176
+ <div class="flex items-center gap-3">
2177
+ <tm-toggle [value]="notificaciones" (changed)="notificaciones = $event" />
2178
+ <span>Notificaciones</span>
2179
+ </div>
2180
+
2181
+ @if (notificaciones) {
2182
+ <p>Recibirás notificaciones de nuevos eventos.</p>
2183
+ } @else {
2184
+ <p>Las notificaciones están desactivadas.</p>
2185
+ }
2186
+ ```
2187
+
2188
+ ### Activar/desactivar un producto
2189
+
2190
+ ```html
2191
+ <div class="flex items-center gap-3">
2192
+ <tm-toggle [value]="producto.activo" (changed)="toggleProducto($event)" />
2193
+ <span>{{ producto.activo ? 'Activo' : 'Inactivo' }}</span>
2194
+ </div>
2195
+ ```
2196
+
2197
+ ```typescript
2198
+ toggleProducto(value: boolean) {
2199
+ this.producto.activo = value;
2200
+ this.productoService.actualizarEstado(this.producto.id, value);
2201
+ }
2202
+ ```
2203
+
2204
+ ### Con disabled según permisos
2205
+
2206
+ ```html
2207
+ <tm-toggle
2208
+ [value]="configuracion.habilitado"
2209
+ [disabled]="!tienePermisos"
2210
+ (changed)="configuracion.habilitado = $event"
2211
+ />
2212
+ ```