shirkasoft-ui-components 1.0.21 → 1.0.23

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,678 @@
1
+ # Shirkasoft UI Components
2
+
3
+ <p align="center">
4
+ Creado por <a href="https://shirkasoft.com" target="_blank"><strong>Shirkasoft</strong></a>
5
+ </p>
6
+
7
+ Librería de componentes UI para Angular 21+ con **Tailwind CSS**, **standalone components**, señales (Angular signals) y soporte de **tema claro/oscuro** mediante CSS custom properties.
8
+
9
+ ## Instalación
10
+
11
+ ```bash
12
+ pnpm add shirkasoft-ui-components
13
+ # o
14
+ npm install shirkasoft-ui-components
15
+ ```
16
+
17
+ ### Dependencias pares
18
+
19
+ | Paquete | Versión mínima |
20
+ |---|---|
21
+ | @angular/common | ^21.2.0 |
22
+ | @angular/core | ^21.2.0 |
23
+ | @angular/forms | ^21.2.0 |
24
+ | rxjs | ^7.8.0 |
25
+ | @jsverse/transloco | >= 8.0.0 |
26
+ | @lucide/angular | ^1.17.0 |
27
+ | chart.js | ^4.4.0 |
28
+
29
+ ## Configuración del tema
30
+
31
+ Importar el tema en `styles.css` global:
32
+
33
+ ```css
34
+ @import '@shirkasoft/ui-components/theme.css';
35
+ ```
36
+
37
+ El tema se basa en variables CSS `--shk-*`. Para personalizar colores, sobrescribí las variables en `:root` (modo claro) y `.dark` (modo oscuro):
38
+
39
+ ```css
40
+ :root {
41
+ --shk-primary-500: #3b82f6;
42
+ --shk-primary-600: #2563eb;
43
+ /* ... */
44
+ }
45
+ ```
46
+
47
+ ### Dark mode
48
+
49
+ Agregá la clase `dark` al elemento `<html>` para activar el modo oscuro.
50
+
51
+ ```typescript
52
+ document.documentElement.classList.toggle('dark');
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Componentes
58
+
59
+ | # | Componente | Selector | ControlValueAccessor | Servicio asociado |
60
+ |---|---|---|---|---|
61
+ | 1 | TextField | `shk-text-field` | ✅ | — |
62
+ | 2 | Toggle | `shk-toggle` | ✅ | — |
63
+ | 3 | Select | `shk-select` | ✅ | — |
64
+ | 4 | DatePicker | `shk-date-picker` | ✅ | — |
65
+ | 5 | PriceInput | `shk-price-input` | — | — |
66
+ | 6 | FileUpload | `shk-file-upload` | — | — |
67
+ | 7 | Table | `shk-table` | — | — |
68
+ | 8 | Modal | `shk-modal` | — | `ModalService` |
69
+ | 9 | Notification | `shk-notification` | — | `NotificationService` |
70
+ | 10 | ConfirmDialog | `shk-confirm-dialog` | — | `ConfirmDialogService` |
71
+ | 11 | Chart | `shk-chart` | — | — |
72
+ | 12 | Tooltip | `shk-tooltip` | — | — |
73
+
74
+ ---
75
+
76
+ ### TextField (`shk-text-field`)
77
+
78
+ Campo de texto con soporte para input, textarea, password, búsqueda, formato de tarjeta de crédito, y validación de errores. Implementa `ControlValueAccessor`.
79
+
80
+ ```html
81
+ <shk-text-field
82
+ [label]="'Nombre'"
83
+ [control]="myControl"
84
+ [type]="'text'"
85
+ [placeholder]="'Ingresá tu nombre'"
86
+ [disabled]="false"
87
+ [errorMessages]="{ required: 'Campo obligatorio' }"
88
+ [isTextArea]="false"
89
+ [searchMode]="false"
90
+ (searchButtonClick)="onSearch()"
91
+ />
92
+ ```
93
+
94
+ | Input | Tipo | Default |
95
+ |---|---|---|
96
+ | `id` | `string` | `''` |
97
+ | `label` | `string` | `''` |
98
+ | `type` | `string` | `'text'` |
99
+ | `placeholder` | `string` | `''` |
100
+ | `disabled` | `boolean` | `false` |
101
+ | `control` | `FormControl` | `undefined` |
102
+ | `formGroup` | `FormGroup` | `undefined` |
103
+ | `errorMessage` | `string` | `''` |
104
+ | `errorMessages` | `{ [key: string]: string }` | `{}` |
105
+ | `isTextArea` | `boolean` | `false` |
106
+ | `textAreaHeight` | `string` | `'h-28'` |
107
+ | `searchMode` | `boolean` | `false` |
108
+ | `searchButtonClick` | `output<void>` | — |
109
+ | `numbersOnly` | `boolean` | `false` |
110
+ | `allowDecimals` | `boolean` | `false` |
111
+ | `maxLength` | `number` | `undefined` |
112
+ | `formatCard` | `boolean` | `false` |
113
+ | `preventNegative` | `boolean` | `false` |
114
+ | `displayValue` | `string` | `''` |
115
+ | `autocomplete` | `string` | `'new-password'` |
116
+
117
+ > También expone `inputElement` y `textareaElement` como `viewChild`.
118
+
119
+ ---
120
+
121
+ ### Toggle (`shk-toggle`)
122
+
123
+ Switch o checkbox. Implementa `ControlValueAccessor`.
124
+
125
+ ```html
126
+ <shk-toggle
127
+ [label]="'Activar notificaciones'"
128
+ [mode]="'toggle'"
129
+ [(checked)]="isChecked"
130
+ (checkedChange)="onChange($event)"
131
+ />
132
+ ```
133
+
134
+ | Input | Tipo | Default |
135
+ |---|---|---|
136
+ | `label` | `string` | `''` |
137
+ | `mode` | `'toggle' \| 'checkbox'` | `'toggle'` |
138
+ | `checked` | `boolean` | `undefined` |
139
+
140
+ | Output | Tipo |
141
+ |---|---|
142
+ | `checkedChange` | `boolean` |
143
+
144
+ ---
145
+
146
+ ### Select (`shk-select`)
147
+
148
+ Dropdown seleccionable con búsqueda, paginación, selección múltiple, opciones personalizadas y validación. Implementa `ControlValueAccessor`.
149
+
150
+ ```html
151
+ <shk-select
152
+ [options]="options"
153
+ [label]="'País'"
154
+ [control]="countryControl"
155
+ [multiple]="false"
156
+ [isSearchable]="true"
157
+ [usePagination]="true"
158
+ [itemsPerPage]="10"
159
+ [allowCustomEntries]="false"
160
+ [placeholder]="'Seleccionar...'"
161
+ [isLoading]="false"
162
+ (selectionChange)="onSelect($event)"
163
+ (search)="onSearch($event)"
164
+ />
165
+ ```
166
+
167
+ ```typescript
168
+ interface SelectOption {
169
+ label: string;
170
+ value: any;
171
+ custom?: boolean;
172
+ }
173
+ ```
174
+
175
+ | Input | Tipo | Default |
176
+ |---|---|---|
177
+ | `options` | `SelectOption[]` | `[]` |
178
+ | `label` | `string` | `undefined` |
179
+ | `placeholder` | `string` | `'Seleccionar...'` |
180
+ | `disabled` | `boolean` | `false` |
181
+ | `control` | `FormControl` | `undefined` |
182
+ | `formGroup` | `FormGroup` | `undefined` |
183
+ | `multiple` | `boolean` | `false` |
184
+ | `isSearchable` | `boolean` | `false` |
185
+ | `usePagination` | `boolean` | `false` |
186
+ | `itemsPerPage` | `number` | `10` |
187
+ | `allowCustomEntries` | `boolean` | `false` |
188
+ | `isLoading` | `boolean` | `false` |
189
+ | `isAllDataLoaded` | `boolean` | `false` |
190
+ | `preserveSearchOnLoad` | `boolean` | `false` |
191
+ | `dropdownUpward` | `boolean` | `false` |
192
+ | `showEmptyOption` | `boolean` | `true` |
193
+ | `emptyMessageKey` | `string` | `'common.no_records'` |
194
+ | `validCombinations` | `string[][]` | `undefined` |
195
+ | `errorMessages` | `{ [key: string]: string }` | `{ required: 'Este campo es requerido' }` |
196
+
197
+ | Output | Tipo |
198
+ |---|---|
199
+ | `selectionChange` | `any` |
200
+ | `search` | `string` |
201
+
202
+ ---
203
+
204
+ ### DatePicker (`shk-date-picker`)
205
+
206
+ Selector de fecha, mes o año. Implementa `ControlValueAccessor`. Soporta formatos `'date'`, `'month'`, `'year'`.
207
+
208
+ ```html
209
+ <shk-date-picker
210
+ [label]="'Fecha de inicio'"
211
+ [control]="dateControl"
212
+ [view]="'date'"
213
+ [locale]="'es'"
214
+ [minValue]="'2024-01-01'"
215
+ [maxValue]="'2026-12-31'"
216
+ />
217
+ ```
218
+
219
+ | Input | Tipo | Default |
220
+ |---|---|---|
221
+ | `id` | `string` | `''` |
222
+ | `label` | `string` | `''` |
223
+ | `placeholder` | `string` | `''` |
224
+ | `view` | `'date' \| 'month' \| 'year'` | `'date'` |
225
+ | `control` | `FormControl` | `undefined` |
226
+ | `formGroup` | `FormGroup` | `undefined` |
227
+ | `disabled` | `boolean` | `false` |
228
+ | `errorMessage` | `string` | `''` |
229
+ | `errorMessages` | `{ [key: string]: string }` | `{}` |
230
+ | `minValue` | `string` | `''` |
231
+ | `maxValue` | `string` | `''` |
232
+ | `locale` | `string` | `'es'` |
233
+
234
+ **Valor devuelto** según el `view`:
235
+ - `'date'` → `'YYYY-MM-DD'`
236
+ - `'month'` → `'YYYY-MM'`
237
+ - `'year'` → `'YYYY'`
238
+
239
+ ---
240
+
241
+ ### PriceInput (`shk-price-input`
242
+
243
+ Input numérico con toggle entre modo monto (`$`) y porcentaje (`%`). Usa `model()` para doble vinculación.
244
+
245
+ ```html
246
+ <shk-price-input
247
+ [(value)]="price"
248
+ [(isPercentage)]="isPercent"
249
+ [label]="'Precio'"
250
+ [min]="0"
251
+ [max]="10000"
252
+ [required]="true"
253
+ [showError]="hasError"
254
+ [errorMessage]="'Valor inválido'"
255
+ />
256
+ ```
257
+
258
+ | Input | Tipo | Default |
259
+ |---|---|---|
260
+ | `value` | `model<number>` | **required** |
261
+ | `isPercentage` | `model<boolean>` | `false` |
262
+ | `label` | `string` | `''` |
263
+ | `inputId` | `string` | `''` |
264
+ | `required` | `boolean` | `false` |
265
+ | `disabled` | `boolean` | `false` |
266
+ | `min` | `number` | `0` |
267
+ | `max` | `number` | `Infinity` |
268
+ | `showError` | `boolean` | `false` |
269
+ | `errorMessage` | `string` | `''` |
270
+
271
+ ---
272
+
273
+ ### FileUpload (`shk-file-upload`)
274
+
275
+ Subida de archivos con arrastrar y soltar, vista previa de imágenes, validación de tipo y tamaño.
276
+
277
+ ```html
278
+ <shk-file-upload
279
+ [label]="'Subí tu foto'"
280
+ [accept]="'image/*'"
281
+ [maxFileSize]="5 * 1024 * 1024"
282
+ [maxFiles]="3"
283
+ [multiple]="true"
284
+ (fileSelected)="onFilesSelected($event)"
285
+ (fileRemoved)="onFileRemoved()"
286
+ (fileError)="onFileError($event)"
287
+ />
288
+ ```
289
+
290
+ ```typescript
291
+ interface FileUploadError {
292
+ type: 'size' | 'type';
293
+ message: string;
294
+ file: File;
295
+ }
296
+ ```
297
+
298
+ | Input | Tipo | Default |
299
+ |---|---|---|
300
+ | `label` | `string` | `'Cargar archivo'` |
301
+ | `accept` | `string` | `'image/*'` |
302
+ | `maxFileSize` | `number` | `2 * 1024 * 1024` (2MB) |
303
+ | `maxFiles` | `number` | `0` (sin límite) |
304
+ | `multiple` | `boolean` | `false` |
305
+ | `fileUploadText` | `string` | `'Seleccionar archivo'` |
306
+ | `changeFilesText` | `string` | `'Cambiar archivos'` |
307
+ | `fileRecommendation` | `string` | `''` |
308
+
309
+ | Output | Tipo |
310
+ |---|---|
311
+ | `fileSelected` | `File[]` |
312
+ | `fileRemoved` | `void` |
313
+ | `fileError` | `FileUploadError` |
314
+ | `fileWarning` | `FileUploadError` |
315
+
316
+ ---
317
+
318
+ ### Table (`shk-table`)
319
+
320
+ Tabla de datos con ordenamiento, filtros por columna, búsqueda global, paginación, acciones por fila y acciones de cabecera. Soporta modo cliente y servidor.
321
+
322
+ ```html
323
+ <shk-table
324
+ [data]="users"
325
+ [columns]="columns"
326
+ [loading]="isLoading"
327
+ [serverSide]="true"
328
+ [totalRecords]="totalUsers"
329
+ [rowActions]="rowActions"
330
+ [headerActions]="headerActions"
331
+ [rowsPerPage]="25"
332
+ [showSearch]="true"
333
+ (pageChange)="onPageChange($event)"
334
+ (searchChange)="onSearch($event)"
335
+ (refresh)="loadData()"
336
+ />
337
+ ```
338
+
339
+ ```typescript
340
+ interface Column {
341
+ field: string;
342
+ header: string;
343
+ sortable?: boolean;
344
+ filter?: boolean;
345
+ filterPlaceholder?: string;
346
+ width?: string;
347
+ defaultSort?: true;
348
+ filterType?: 'text' | 'exact' | 'select';
349
+ filterOptions?: { label: string; value: any }[];
350
+ template?: 'text' | 'tag';
351
+ format?: (row: any) => string;
352
+ tagValue?: (row: any) => string;
353
+ tagSeverity?: (row: any) => 'success' | 'danger' | 'warn' | 'info' | undefined;
354
+ tagStyle?: (row: any) => { [key: string]: string } | undefined;
355
+ }
356
+
357
+ interface TableAction {
358
+ label: string;
359
+ icon: string;
360
+ onClick: () => void;
361
+ class?: string;
362
+ isVisible?: () => boolean;
363
+ isDisabled?: () => boolean;
364
+ }
365
+
366
+ interface RowAction {
367
+ label: string | ((data: any) => string);
368
+ icon: string | ((data: any) => string);
369
+ onClick: (rowData: any) => void;
370
+ class?: string | ((data: any) => string);
371
+ isVisible?: (rowData: any) => boolean;
372
+ isDisabled?: (rowData: any) => boolean;
373
+ }
374
+
375
+ interface PageChangeEvent {
376
+ first: number;
377
+ rows: number;
378
+ page: number;
379
+ pageCount: number;
380
+ }
381
+
382
+ interface FilterChangeEvent {
383
+ filters: { [key: string]: any };
384
+ }
385
+ ```
386
+
387
+ | Input | Tipo | Default |
388
+ |---|---|---|
389
+ | `data` | `any[]` | `[]` |
390
+ | `columns` | `Column[]` | `[]` |
391
+ | `rowsPerPage` | `number` | `10` |
392
+ | `rowsPerPageOptions` | `number[]` | `[10, 25, 50]` |
393
+ | `loading` | `boolean` | `false` |
394
+ | `showActionRow` | `boolean` | `true` |
395
+ | `headerActions` | `TableAction[]` | `[]` |
396
+ | `rowActions` | `RowAction[]` | `[]` |
397
+ | `hasShadow` | `boolean` | `true` |
398
+ | `defaultSortField` | `string` | `''` |
399
+ | `defaultSortOrder` | `number` | `1` |
400
+ | `showSearch` | `boolean` | `true` |
401
+ | `searchPlaceholder` | `string` | `''` |
402
+ | `emptyMessage` | `string` | `''` |
403
+ | `serverSide` | `boolean` | `false` |
404
+ | `totalRecords` | `number` | `0` |
405
+ | `filters` | `{ label: string; value: string }[]` | `[]` |
406
+ | `activeFilter` | `string` | `''` |
407
+ | `customTemplates` | `{ [key: string]: any }` | `{}` |
408
+
409
+ | Output | Tipo |
410
+ |---|---|
411
+ | `pageChange` | `PageChangeEvent` |
412
+ | `filterChange` | `FilterChangeEvent` |
413
+ | `searchChange` | `string` |
414
+ | `filterClick` | `string` |
415
+ | `refresh` | `void` |
416
+
417
+ ---
418
+
419
+ ### Modal (`shk-modal`)
420
+
421
+ Modal dinámico que carga componentes a partir de un `ModalService`. Soporta formularios, expansión a pantalla completa y botones de aceptar/cancelar.
422
+
423
+ ```html
424
+ <shk-modal />
425
+ ```
426
+
427
+ ```typescript
428
+ interface ModalConfig {
429
+ title: string;
430
+ component: Type<any>;
431
+ data?: Record<string, any>;
432
+ width?: string;
433
+ showButtons?: boolean;
434
+ showExpandButton?: boolean;
435
+ acceptLabel?: string;
436
+ cancelLabel?: string;
437
+ onClose?: () => void;
438
+ }
439
+ ```
440
+
441
+ El componente inyectado **debe** exponer:
442
+ - `form?: FormGroup` (opcional, para validación)
443
+ - `onSubmit()` → llama `submitSuccess` o `submitError`
444
+ - `submitSuccess?: EventEmitter<void>` (opcional)
445
+ - `submitError?: EventEmitter<void>` (opcional)
446
+ - `handleCancel?: () => void` (opcional)
447
+
448
+ ```typescript
449
+ constructor(private modalSrv: ModalService) {}
450
+
451
+ openModal() {
452
+ this.modalSrv.open({
453
+ title: 'Editar usuario',
454
+ component: EditUserComponent,
455
+ data: { userId: 123 },
456
+ width: '600px',
457
+ showButtons: true,
458
+ showExpandButton: true,
459
+ });
460
+ }
461
+
462
+ closeModal() {
463
+ this.modalSrv.close();
464
+ }
465
+ ```
466
+
467
+ **Métodos del servicio:**
468
+
469
+ | Método | Descripción |
470
+ |---|---|
471
+ | `open(config: ModalConfig)` | Abre un nuevo modal |
472
+ | `close()` | Cierra el modal actual |
473
+ | `accept()` | Cierra el modal (alias de close) |
474
+ | `clear()` | Cierra todos los modales |
475
+
476
+ ---
477
+
478
+ ### Notification (`shk-notification`)
479
+
480
+ Sistema de notificaciones toast con posiciones configurables y barra de progreso opcional.
481
+
482
+ ```html
483
+ <shk-notification position="right-top" />
484
+ ```
485
+
486
+ ```typescript
487
+ interface Notification {
488
+ id: string;
489
+ message: string;
490
+ type: 'success' | 'error' | 'warning' | 'info';
491
+ progress?: number;
492
+ showProgress?: boolean;
493
+ }
494
+ ```
495
+
496
+ ```typescript
497
+ constructor(private notifSrv: NotificationService) {}
498
+
499
+ showNotif() {
500
+ const id = this.notifSrv.addNotification(
501
+ 'Operación exitosa',
502
+ 'success',
503
+ false, // showProgress
504
+ 3000 // duration (ms)
505
+ );
506
+ }
507
+
508
+ updateProgress(id: string, progress: number) {
509
+ this.notifSrv.updateProgress(id, progress);
510
+ }
511
+ ```
512
+
513
+ | Input | Tipo | Default |
514
+ |---|---|---|
515
+ | `position` | `'center-top' \| 'right-top' \| 'left-top'` | `'center-top'` |
516
+
517
+ **Métodos del servicio:**
518
+
519
+ | Método | Descripción |
520
+ |---|---|
521
+ | `addNotification(msg, type?, showProgress?, duration?)` | Agrega una notificación |
522
+ | `updateProgress(id, progress)` | Actualiza la barra de progreso |
523
+ | `removeNotification(notification)` | Elimina una notificación |
524
+ | `removeNotificationById(id)` | Elimina por ID |
525
+ | `clearAll()` | Elimina todas las notificaciones |
526
+
527
+ ---
528
+
529
+ ### ConfirmDialog (`shk-confirm-dialog`)
530
+
531
+ Diálogo de confirmación programático. Se usa a través del `ConfirmDialogService` que lo crea dinámicamente.
532
+
533
+ ```html
534
+ <!-- No hace falta agregarlo al template -->
535
+ ```
536
+
537
+ ```typescript
538
+ interface ConfirmConfig {
539
+ title?: string;
540
+ message: string;
541
+ confirmLabel?: string;
542
+ cancelLabel?: string;
543
+ loadingText?: string;
544
+ type?: 'danger' | 'info' | 'warning';
545
+ showCancel?: boolean;
546
+ loading?: boolean;
547
+ }
548
+ ```
549
+
550
+ ```typescript
551
+ constructor(private confirmSrv: ConfirmDialogService) {}
552
+
553
+ async deleteItem() {
554
+ const confirmed = await this.confirmSrv.confirm({
555
+ title: 'Eliminar usuario',
556
+ message: '¿Estás seguro de eliminar este usuario?',
557
+ confirmLabel: 'Eliminar',
558
+ type: 'danger',
559
+ });
560
+
561
+ if (confirmed) {
562
+ // proceder con la eliminación
563
+ }
564
+ }
565
+ ```
566
+
567
+ | Input | Tipo | Default |
568
+ |---|---|---|
569
+ | `title` | `string` | `'Confirmar acción'` |
570
+ | `message` | `string` | `'¿Está seguro de realizar esta acción?'` |
571
+ | `confirmLabel` | `string` | `'Confirmar'` |
572
+ | `cancelLabel` | `string` | `'Cancelar'` |
573
+ | `loadingText` | `string` | `'Procesando…'` |
574
+ | `type` | `'danger' \| 'info' \| 'warning'` | `'danger'` |
575
+ | `loading` | `boolean` | `false` |
576
+ | `showCancel` | `boolean` | `true` |
577
+
578
+ | Output | Tipo |
579
+ |---|---|
580
+ | `closed` | `void` |
581
+
582
+ ---
583
+
584
+ ### Chart (`shk-chart`)
585
+
586
+ Wrapper de Chart.js que soporta los tipos de gráfico principales.
587
+
588
+ ```html
589
+ <shk-chart
590
+ [type]="'bar'"
591
+ [data]="chartData"
592
+ [options]="chartOptions"
593
+ />
594
+ ```
595
+
596
+ ```typescript
597
+ import { COLOR_PALETTE, readThemeColors } from 'shirkasoft-ui-components';
598
+
599
+ const colors = readThemeColors(); // colores del tema actual
600
+ ```
601
+
602
+ | Input | Tipo | Default |
603
+ |---|---|---|
604
+ | `type` | `'line' \| 'bar' \| 'pie' \| 'doughnut' \| 'polarArea'` | `'bar'` |
605
+ | `data` | `any` | `{ datasets: [] }` |
606
+ | `options` | `any` | `{}` |
607
+
608
+ **Utils exportadas:**
609
+
610
+ | Export | Descripción |
611
+ |---|---|
612
+ | `COLOR_PALETTE` | Array de colores predefinidos (bg + border) |
613
+ | `readThemeColors()` | Lee colores del tema actual (`--shk-surface-*`) |
614
+
615
+ ---
616
+
617
+ ### Tooltip (`shk-tooltip`)
618
+
619
+ Tooltip con dos modos: `hover` (posicionamiento CSS) y `fixed` (posicionamiento calculado con JS).
620
+
621
+ ```html
622
+ <shk-tooltip [text]="'Info adicional'" [position]="'bottom'" [mode]="'hover'">
623
+ <button>Pasar el mouse</button>
624
+ </shk-tooltip>
625
+ ```
626
+
627
+ | Input | Tipo | Default |
628
+ |---|---|---|
629
+ | `text` | `string` | `''` |
630
+ | `position` | `'top' \| 'bottom' \| 'right'` | `'bottom'` |
631
+ | `mode` | `'hover' \| 'fixed'` | `'hover'` |
632
+ | `offset` | `number` | `8` |
633
+
634
+ ---
635
+
636
+ ## Desarrollo
637
+
638
+ ```bash
639
+ # Construir la librería
640
+ pnpm build
641
+
642
+ # Servir la showcase (app de demostración)
643
+ pnpm serve
644
+
645
+ # Construir la showcase
646
+ pnpm build:showcase
647
+
648
+ # Escuchar cambios en la librería
649
+ pnpm watch
650
+ ```
651
+
652
+ ### Versionado
653
+
654
+ ```bash
655
+ pnpm version:patch # 1.0.22 → 1.0.23
656
+ pnpm version:minor # 1.0.22 → 1.1.0
657
+ pnpm version:major # 1.0.22 → 2.0.0
658
+ ```
659
+
660
+ ---
661
+
662
+ ## Publicación
663
+
664
+ CI/CD vía GitHub Actions:
665
+ - Al pushear un tag `v*` se publica a npm automáticamente.
666
+ - Al pushear a `main`/`master` se despliega la showcase a GitHub Pages.
667
+
668
+ Manual:
669
+
670
+ ```bash
671
+ bash scripts/publish-lib.sh
672
+ ```
673
+
674
+ ---
675
+
676
+ ## Licencia
677
+
678
+ MIT © Shirkasoft