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 +2212 -0
- package/assets/fonts/PlusJakartaSans-VariableFont_wght.woff2 +0 -0
- package/fesm2022/ten-minds-ui-kit.mjs +921 -0
- package/fesm2022/ten-minds-ui-kit.mjs.map +1 -0
- package/package.json +41 -0
- package/styles/_typography.css +64 -0
- package/styles/_variables.css +297 -0
- package/styles/styles.css +2 -0
- package/types/ten-minds-ui-kit.d.ts +357 -0
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
|
+
```
|