@onpe/ui 1.3.3 → 1.3.5

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 CHANGED
@@ -1,1966 +1,1966 @@
1
- # 🗳️ ONPE UI
2
-
3
- Librería completa de componentes de interfaz de usuario para aplicaciones de la Oficina Nacional de Procesos Electorales (ONPE) del Perú.
4
-
5
- ## ✨ Características
6
-
7
- - 🎨 **Colores oficiales de ONPE** - Paleta de colores institucional
8
- - ⚡ **Tailwind CSS v4** - Framework CSS moderno y eficiente
9
- - 🔧 **TypeScript** - Tipado estático para mejor desarrollo
10
- - 📱 **Responsive** - Diseño adaptable a todos los dispositivos
11
- - 🎯 **Accesible** - Componentes que siguen estándares de accesibilidad
12
- - 📦 **Tree-shakable** - Solo importa lo que necesitas
13
-
14
- ## 🚀 Instalación
15
-
16
- ### Instalación Completa de la Librería
17
- ```bash
18
- npm install @onpe/ui
19
- ```
20
-
21
- ### Instalación Individual de Componentes (CLI)
22
- ```bash
23
- # Instalar la CLI globalmente
24
- npm install -g @onpe/ui
25
-
26
- # O usar directamente con npx
27
- npx @onpe/ui add <componente>
28
- ```
29
-
30
- ## 🛡️ Integración Sin Conflictos
31
-
32
- **Esta librería está diseñada para funcionar perfectamente en proyectos que ya usan Tailwind CSS, Material UI, Shadcn, o cualquier otro framework CSS sin causar conflictos de estilos.**
33
-
34
- ### ✨ Características de Compatibilidad
35
-
36
- - **Prefijos Únicos**: Todas las clases usan el prefijo `onpe-` para evitar conflictos
37
- - **CSS Compilado**: Se genera un CSS optimizado y minificado sin `@import` de Tailwind
38
- - **Variables CSS en `:root`**: Colores con prefijos únicos (`--onpe-ui-blue`, etc.) disponibles globalmente
39
- - **Sin Reset de Tailwind**: No interfiere con tu configuración existente
40
- - **Compatible con**: Material UI, Shadcn, Chakra UI, Ant Design, Bootstrap, etc.
41
- - **CSP Compatible**: Genera archivos CSS externos para cumplir con Content Security Policy
42
-
43
- ### 🚀 Instalación Rápida
44
-
45
- ```bash
46
- npm install @onpe/ui
47
- ```
48
-
49
- ```tsx
50
- // Importar estilos compilados (solo una vez en tu app)
51
- // ⚠️ IMPORTANTE: Esto define las variables CSS en :root
52
- import '@onpe/ui/dist/index.css';
53
-
54
- // O usando el export específico
55
- import '@onpe/ui/css';
56
-
57
- // Usar componentes
58
- import { Button } from '@onpe/ui/components';
59
-
60
- function App() {
61
- return (
62
- <div>
63
- {/* Tu contenido existente con Material UI, Shadcn, etc. */}
64
- <Button color="blue" title="Botón ONPE" />
65
- </div>
66
- );
67
- }
68
- ```
69
-
70
- ### 🎨 Variables CSS en `:root`
71
-
72
- **Los colores ONPE se definen automáticamente en `:root` cuando importas el CSS:**
73
-
74
- ```css
75
- :root {
76
- --onpe-ui-blue: #003770;
77
- --onpe-ui-skyblue: #0073cf;
78
- --onpe-ui-skyblue-light: #69b2e8;
79
- --onpe-ui-yellow: #ffb81c;
80
- --onpe-ui-light-skyblue: #aaeff6;
81
- --onpe-ui-gray: #bcbcbc;
82
- --onpe-ui-gray-light: #bdbdbd;
83
- --onpe-ui-gray-extra-light: #f2f2f2;
84
- --onpe-ui-red: #e3002b;
85
- --onpe-ui-dark-gray: #4f4f4f;
86
- --onpe-ui-green: #76bd43;
87
- --onpe-ui-yellow-light: #FFF1D2;
88
- }
89
- ```
90
-
91
- **Esto permite:**
92
- - ✅ **Uso directo en CSS del proyecto host**
93
- - ✅ **Sobrescribir colores si es necesario**
94
- - ✅ **Compatibilidad con CSP (Content Security Policy)**
95
- - ✅ **Acceso global a los colores ONPE**
96
-
97
- ### 💡 Uso de Variables CSS en tu Proyecto
98
-
99
- **Puedes usar los colores ONPE directamente en tu CSS:**
100
-
101
- ```css
102
- /* En tu archivo CSS del proyecto host */
103
- .mi-componente {
104
- color: var(--onpe-ui-blue);
105
- background: var(--onpe-ui-skyblue-light);
106
- border: 2px solid var(--onpe-ui-red);
107
- }
108
-
109
- .mi-boton-personalizado {
110
- background: var(--onpe-ui-green);
111
- color: white;
112
- padding: 12px 24px;
113
- border-radius: 8px;
114
- }
115
- ```
116
-
117
- **Sobrescribir colores si es necesario:**
118
-
119
- ```css
120
- /* En tu archivo CSS del proyecto host */
121
- :root {
122
- /* Cambiar el azul principal */
123
- --onpe-ui-blue: #1a365d;
124
-
125
- /* Cambiar el rojo */
126
- --onpe-ui-red: #c53030;
127
- }
128
- ```
129
-
130
- **Uso en componentes React con estilos inline:**
131
-
132
- ```tsx
133
- function MiComponente() {
134
- return (
135
- <div
136
- style={{
137
- color: 'var(--onpe-ui-blue)',
138
- backgroundColor: 'var(--onpe-ui-skyblue-light)',
139
- padding: '16px',
140
- borderRadius: '8px'
141
- }}
142
- >
143
- Contenido con colores ONPE
144
- </div>
145
- );
146
- }
147
- ```
148
-
149
- ### 🎯 ¿Cómo Evitamos Conflictos?
150
-
151
- 1. **Prefijos Únicos**: `bg-blue-500` → `onpe-bg-onpe-ui-blue`
152
- 2. **CSS Scoped**: Todos los componentes están aislados
153
- 3. **Variables CSS Aisladas**: `--onpe-ui-blue` en lugar de `--blue`
154
- 4. **Sin Preflight**: No resetea estilos del proyecto host
155
-
156
- ## 📖 Uso Básico
157
-
158
- ### Instalar Componentes con CLI
159
-
160
- #### Instalar componentes específicos
161
- ```bash
162
- # Componentes
163
- npx @onpe/ui add button
164
- npx @onpe/ui add modal
165
- npx @onpe/ui add portal
166
- npx @onpe/ui add overlay
167
- npx @onpe/ui add show
168
-
169
- # Iconos
170
- npx @onpe/ui add icon-close
171
- npx @onpe/ui add icon-check
172
- npx @onpe/ui add icon-warning
173
- npx @onpe/ui add icon-chrome
174
- npx @onpe/ui add icon-firefox
175
- npx @onpe/ui add icon-safari
176
- npx @onpe/ui add icon-edge
177
- npx @onpe/ui add icon-windows
178
- npx @onpe/ui add icon-apple
179
- npx @onpe/ui add icon-android
180
- ```
181
-
182
- #### Usar componentes instalados individualmente
183
- ```tsx
184
- // Después de instalar con CLI
185
- import { Button } from './components/onpe-ui/Button';
186
- import { Modal } from './components/onpe-ui/Modal';
187
- import { IconClose } from './components/onpe-icons/IconClose';
188
- import { useState } from 'react';
189
-
190
- function App() {
191
- const [isOpen, setIsOpen] = useState(false);
192
-
193
- return (
194
- <div className="p-4">
195
- <Button
196
- color="primary"
197
- title="Abrir Modal"
198
- onClick={() => setIsOpen(true)}
199
- />
200
-
201
- <Modal
202
- isOpen={isOpen}
203
- onClose={() => setIsOpen(false)}
204
- closeButton={true}
205
- overlayColor="blue"
206
- >
207
- <div className="p-6">
208
- <h2 className="text-xl font-bold mb-4">Contenido del Modal</h2>
209
- <p className="mb-4">Este es un ejemplo de modal con contenido.</p>
210
- <div className="flex items-center gap-2">
211
- <IconClose className="w-4 h-4" />
212
- <Button
213
- color="green"
214
- title="Cerrar"
215
- onClick={() => setIsOpen(false)}
216
- />
217
- </div>
218
- </div>
219
- </Modal>
220
- </div>
221
- );
222
- }
223
- ```
224
-
225
- #### Configuración requerida para componentes individuales
226
-
227
- **1. Instalar Tailwind CSS:**
228
- ```bash
229
- npm install -D tailwindcss postcss autoprefixer
230
- npx tailwindcss init -p
231
- ```
232
-
233
- **2. Configurar PostCSS para Tailwind v4:**
234
- ```javascript
235
- // postcss.config.js
236
- export default {
237
- plugins: {
238
- '@tailwindcss/postcss': {},
239
- autoprefixer: {},
240
- },
241
- }
242
- ```
243
-
244
- **3. Crear archivo CSS con configuración Tailwind v4:**
245
- ```css
246
- /* onpe-ui.css */
247
- @import "tailwindcss";
248
-
249
- @theme {
250
- --color-onpe-ui-blue: #003770;
251
- --color-onpe-ui-skyblue: #0073cf;
252
- --color-onpe-ui-skyblue-light: #69b2e8;
253
- --color-onpe-ui-yellow: #ffb81c;
254
- --color-onpe-ui-light-skyblue: #aaeff6;
255
- --color-onpe-ui-gray: #bcbcbc;
256
- --color-onpe-ui-gray-light: #bdbdbd;
257
- --color-onpe-ui-gray-extra-light: #f2f2f2;
258
- --color-onpe-ui-red: #e3002b;
259
- --color-onpe-ui-dark-gray: #4f4f4f;
260
- --color-onpe-ui-green: #76bd43;
261
- --color-onpe-ui-yellow-light: #FFF1D2;
262
- }
263
-
264
- /* Clases personalizadas ONPE */
265
- @utility bg-onpe-ui-blue { background-color: var(--color-onpe-ui-blue); }
266
- @utility text-onpe-ui-blue { color: var(--color-onpe-ui-blue); }
267
- @utility border-onpe-ui-blue { border-color: var(--color-onpe-ui-blue); }
268
- /* ... resto de clases personalizadas */
269
- ```
270
-
271
- **4. Importar el archivo CSS en tu aplicación:**
272
- ```tsx
273
- // En tu archivo principal (index.tsx o App.tsx)
274
- import './onpe-ui.css'; // ← IMPORTANTE: Importar primero
275
- import { Button } from './components/onpe-ui/Button';
276
- ```
277
-
278
- **5. Para componentes que usan Portal, agregar en public/index.html:**
279
- ```html
280
- <!DOCTYPE html>
281
- <html lang="es">
282
- <head>
283
- <meta charset="utf-8" />
284
- <title>Mi App</title>
285
- </head>
286
- <body>
287
- <div id="root"></div>
288
- <div id="portal"></div>
289
- </body>
290
- </html>
291
- ```
292
-
293
- ## 🎨 Paleta de Colores ONPE
294
-
295
- ### Colores Principales
296
- - **Azul Principal**: `#003770` - Color institucional principal
297
- - **Sky Blue**: `#0073cf` - Color secundario
298
- - **Sky Blue Light**: `#69b2e8` - Color claro
299
- - **Light Sky Blue**: `#aaeff6` - Color muy claro
300
-
301
- ### Colores de Acento
302
- - **Amarillo**: `#ffb81c` - Para alertas y destacados
303
- - **Verde**: `#76bd43` - Para éxito y confirmaciones
304
- - **Rojo**: `#e3002b` - Para errores y advertencias
305
-
306
- ### Escala de Grises
307
- - **Dark Gray**: `#4f4f4f` - Texto principal
308
- - **Gray**: `#bcbcbc` - Texto secundario
309
- - **Gray Light**: `#bdbdbd` - Texto terciario
310
- - **Gray Extra Light**: `#f2f2f2` - Fondos suaves
311
-
312
- ## 🔗 Dependencias entre Componentes
313
-
314
- ### Mapa de Dependencias
315
- ```
316
- Modal
317
- ├── Portal (requerido)
318
- ├── Overlay (requerido)
319
- └── IconClose (requerido)
320
-
321
- ModalBrowserIncompatible
322
- ├── Modal (requerido)
323
- ├── IconWarning (requerido)
324
- ├── IconChromeColor (requerido)
325
- ├── IconSafariColor (requerido)
326
- ├── IconMozillaColor (requerido)
327
- └── IconEdgeColor (requerido)
328
-
329
- ModalSystemIncompatible
330
- ├── Modal (requerido)
331
- ├── IconWarning (requerido)
332
- ├── IconWindow (requerido)
333
- ├── IconAndroid (requerido)
334
- └── IconApple (requerido)
335
-
336
- Portal
337
- └── react-dom (createPortal)
338
-
339
- Overlay
340
- └── (sin dependencias externas)
341
-
342
- Button
343
- └── (sin dependencias externas)
344
-
345
- Show
346
- └── (sin dependencias externas)
347
- ```
348
-
349
- ### Instalación Automática de Dependencias
350
-
351
- **Modal** - Instala automáticamente sus dependencias:
352
- ```bash
353
- npx @onpe/ui add modal
354
- # Esto instalará automáticamente:
355
- # - Portal.tsx
356
- # - Overlay.tsx
357
- # - IconClose.tsx (si está disponible)
358
- ```
359
-
360
- **ModalBrowserIncompatible** - Instala automáticamente sus dependencias:
361
- ```bash
362
- npx @onpe/ui add modal-browser-incompatible
363
- # Esto instalará automáticamente:
364
- # - Modal.tsx
365
- # - IconWarning.tsx
366
- # - IconChromeColor.tsx
367
- # - IconSafariColor.tsx
368
- # - IconMozillaColor.tsx
369
- # - IconEdgeColor.tsx
370
- ```
371
-
372
- **ModalSystemIncompatible** - Instala automáticamente sus dependencias:
373
- ```bash
374
- npx @onpe/ui add modal-system-incompatible
375
- # Esto instalará automáticamente:
376
- # - Modal.tsx
377
- # - IconWarning.tsx
378
- # - IconWindow.tsx
379
- # - IconAndroid.tsx
380
- # - IconApple.tsx
381
- ```
382
-
383
- **Otros componentes** - Instalación independiente:
384
- ```bash
385
- npx @onpe/ui add button # Sin dependencias
386
- npx @onpe/ui add portal # Sin dependencias
387
- npx @onpe/ui add overlay # Sin dependencias
388
- npx @onpe/ui add show # Sin dependencias
389
- ```
390
-
391
- ### Estructura de Archivos Después de la Instalación
392
-
393
- ```
394
- src/
395
- └── components/
396
- ├── ui/ # shadcn/ui (si está instalado)
397
- │ ├── button.tsx
398
- │ └── input.tsx
399
- ├── onpe-ui/ # ONPE UI - Componentes
400
- │ ├── Button.tsx
401
- │ ├── Modal.tsx
402
- │ ├── Overlay.tsx
403
- │ ├── Portal.tsx
404
- │ └── Show.tsx
405
- └── onpe-icons/ # ONPE UI - Iconos
406
- ├── IconClose.tsx
407
- ├── IconCheck.tsx
408
- ├── IconChrome.tsx
409
- ├── IconFirefox.tsx
410
- └── IconWindows.tsx
411
- ```
412
-
413
- ## 🧩 Componentes Disponibles
414
-
415
- ### Button
416
-
417
- Botón versátil con múltiples colores y tamaños.
418
-
419
- #### Ejemplo Básico
420
- ```tsx
421
- import { Button } from '@onpe/ui/components';
422
-
423
- function App() {
424
- return (
425
- <div className="space-y-4 p-4">
426
- <h2 className="text-2xl font-bold">Botones ONPE</h2>
427
-
428
- {/* Colores disponibles */}
429
- <div className="space-y-2">
430
- <h3 className="text-lg font-semibold">Colores:</h3>
431
- <div className="flex flex-wrap gap-2">
432
- <Button color="primary" title="Primario" />
433
- <Button color="blue" title="Azul" />
434
- <Button color="skyblue" title="Sky Blue" />
435
- <Button color="green" title="Verde" />
436
- <Button color="yellow" title="Amarillo" />
437
- <Button color="red" title="Rojo" />
438
- </div>
439
- </div>
440
-
441
- {/* Tamaños */}
442
- <div className="space-y-2">
443
- <h3 className="text-lg font-semibold">Tamaños:</h3>
444
- <div className="flex items-center gap-2">
445
- <Button color="primary" title="Pequeño" size="small" />
446
- <Button color="primary" title="Mediano" size="normal" />
447
- <Button color="primary" title="Grande" size="large" />
448
- </div>
449
- </div>
450
-
451
- {/* Estados */}
452
- <div className="space-y-2">
453
- <h3 className="text-lg font-semibold">Estados:</h3>
454
- <div className="flex gap-2">
455
- <Button color="primary" title="Normal" />
456
- <Button color="primary" title="Deshabilitado" disabled />
457
- </div>
458
- </div>
459
- </div>
460
- );
461
- }
462
- ```
463
-
464
- #### Ejemplo con Funcionalidad
465
- ```tsx
466
- import { Button } from '@onpe/ui/components';
467
- import { useState } from 'react';
468
-
469
- function VotingApp() {
470
- const [voted, setVoted] = useState(false);
471
- const [loading, setLoading] = useState(false);
472
-
473
- const handleVote = async () => {
474
- setLoading(true);
475
- // Simular llamada a API
476
- await new Promise(resolve => setTimeout(resolve, 2000));
477
- setVoted(true);
478
- setLoading(false);
479
- };
480
-
481
- return (
482
- <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
483
- <h2 className="text-xl font-bold mb-4">Sistema de Votación</h2>
484
-
485
- {!voted ? (
486
- <div className="space-y-4">
487
- <p className="text-gray-600">¿Desea votar por esta opción?</p>
488
- <Button
489
- color="primary"
490
- title={loading ? "Procesando..." : "Votar Ahora"}
491
- onClick={handleVote}
492
- disabled={loading}
493
- size="large"
494
- />
495
- </div>
496
- ) : (
497
- <div className="text-center">
498
- <p className="text-green-600 font-semibold mb-4">¡Voto registrado exitosamente!</p>
499
- <Button
500
- color="green"
501
- title="Ver Resultados"
502
- onClick={() => setVoted(false)}
503
- />
504
- </div>
505
- )}
506
- </div>
507
- );
508
- }
509
- ```
510
-
511
- ### Props del Button
512
-
513
- | Prop | Tipo | Default | Descripción |
514
- |------|------|---------|-------------|
515
- | `color` | `'primary' \| 'blue' \| 'skyblue' \| 'skyblue-light' \| 'yellow' \| 'light-skyblue' \| 'gray' \| 'gray-light' \| 'gray-extra-light' \| 'red' \| 'dark-gray' \| 'green' \| 'yellow-light'` | `'primary'` | Color del botón |
516
- | `title` | `string` | - | Texto del botón (requerido) |
517
- | `size` | `'small' \| 'normal' \| 'large'` | `'normal'` | Tamaño del botón |
518
- | `disabled` | `boolean` | `false` | Estado deshabilitado |
519
- | `className` | `string` | - | Clases CSS adicionales |
520
-
521
- ### Modal
522
-
523
- Componente modal para mostrar contenido en overlay.
524
-
525
- #### Ejemplo Básico
526
- ```tsx
527
- import { Modal } from '@onpe/ui/components';
528
- import { useState } from 'react';
529
-
530
- function App() {
531
- const [isOpen, setIsOpen] = useState(false);
532
-
533
- return (
534
- <div className="p-4">
535
- <button
536
- onClick={() => setIsOpen(true)}
537
- className="bg-blue-500 text-white px-4 py-2 rounded"
538
- >
539
- Abrir Modal
540
- </button>
541
-
542
- <Modal
543
- isOpen={isOpen}
544
- onClose={() => setIsOpen(false)}
545
- closeButton={true}
546
- overlayColor="blue"
547
- >
548
- <div className="p-6">
549
- <h2 className="text-xl font-bold mb-4">Título del Modal</h2>
550
- <p className="mb-4">Este es el contenido del modal.</p>
551
- <button
552
- onClick={() => setIsOpen(false)}
553
- className="bg-gray-500 text-white px-4 py-2 rounded"
554
- >
555
- Cerrar
556
- </button>
557
- </div>
558
- </Modal>
559
- </div>
560
- );
561
- }
562
- ```
563
-
564
- #### Ejemplo Avanzado - Modal de Confirmación
565
- ```tsx
566
- import { Modal } from '@onpe/ui/components';
567
- import { useState } from 'react';
568
-
569
- function DeleteConfirmation() {
570
- const [showModal, setShowModal] = useState(false);
571
- const [itemToDelete, setItemToDelete] = useState('');
572
-
573
- const handleDelete = (itemName) => {
574
- setItemToDelete(itemName);
575
- setShowModal(true);
576
- };
577
-
578
- const confirmDelete = () => {
579
- // Lógica para eliminar el elemento
580
- console.log(`Eliminando: ${itemToDelete}`);
581
- setShowModal(false);
582
- setItemToDelete('');
583
- };
584
-
585
- return (
586
- <div className="p-4">
587
- <div className="space-y-2">
588
- <button
589
- onClick={() => handleDelete('Usuario 1')}
590
- className="bg-red-500 text-white px-4 py-2 rounded mr-2"
591
- >
592
- Eliminar Usuario 1
593
- </button>
594
- <button
595
- onClick={() => handleDelete('Documento 2')}
596
- className="bg-red-500 text-white px-4 py-2 rounded mr-2"
597
- >
598
- Eliminar Documento 2
599
- </button>
600
- </div>
601
-
602
- <Modal
603
- isOpen={showModal}
604
- onClose={() => setShowModal(false)}
605
- closeButton={true}
606
- overlayColor="red"
607
- closeDisabled={false}
608
- >
609
- <div className="p-6 text-center">
610
- <div className="mb-4">
611
- <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
612
- <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
613
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
614
- </svg>
615
- </div>
616
- <h3 className="text-lg font-medium text-gray-900 mb-2">
617
- Confirmar Eliminación
618
- </h3>
619
- <p className="text-sm text-gray-500">
620
- ¿Está seguro de que desea eliminar <strong>{itemToDelete}</strong>?
621
- Esta acción no se puede deshacer.
622
- </p>
623
- </div>
624
-
625
- <div className="flex justify-center space-x-3">
626
- <button
627
- onClick={() => setShowModal(false)}
628
- className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md text-sm font-medium"
629
- >
630
- Cancelar
631
- </button>
632
- <button
633
- onClick={confirmDelete}
634
- className="bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium"
635
- >
636
- Eliminar
637
- </button>
638
- </div>
639
- </div>
640
- </Modal>
641
- </div>
642
- );
643
- }
644
- ```
645
-
646
- #### Ejemplo - Modal de Formulario
647
- ```tsx
648
- import { Modal } from '@onpe/ui/components';
649
- import { useState } from 'react';
650
-
651
- function UserForm() {
652
- const [isOpen, setIsOpen] = useState(false);
653
- const [formData, setFormData] = useState({
654
- name: '',
655
- email: '',
656
- phone: ''
657
- });
658
-
659
- const handleSubmit = (e) => {
660
- e.preventDefault();
661
- console.log('Datos del formulario:', formData);
662
- setIsOpen(false);
663
- setFormData({ name: '', email: '', phone: '' });
664
- };
665
-
666
- return (
667
- <div className="p-4">
668
- <button
669
- onClick={() => setIsOpen(true)}
670
- className="bg-green-500 text-white px-4 py-2 rounded"
671
- >
672
- Agregar Usuario
673
- </button>
674
-
675
- <Modal
676
- isOpen={isOpen}
677
- onClose={() => setIsOpen(false)}
678
- closeButton={true}
679
- overlayColor="skyblue"
680
- >
681
- <div className="p-6">
682
- <h2 className="text-xl font-bold mb-4">Nuevo Usuario</h2>
683
-
684
- <form onSubmit={handleSubmit} className="space-y-4">
685
- <div>
686
- <label className="block text-sm font-medium text-gray-700 mb-1">
687
- Nombre Completo
688
- </label>
689
- <input
690
- type="text"
691
- value={formData.name}
692
- onChange={(e) => setFormData({...formData, name: e.target.value})}
693
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
694
- required
695
- />
696
- </div>
697
-
698
- <div>
699
- <label className="block text-sm font-medium text-gray-700 mb-1">
700
- Correo Electrónico
701
- </label>
702
- <input
703
- type="email"
704
- value={formData.email}
705
- onChange={(e) => setFormData({...formData, email: e.target.value})}
706
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
707
- required
708
- />
709
- </div>
710
-
711
- <div>
712
- <label className="block text-sm font-medium text-gray-700 mb-1">
713
- Teléfono
714
- </label>
715
- <input
716
- type="tel"
717
- value={formData.phone}
718
- onChange={(e) => setFormData({...formData, phone: e.target.value})}
719
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
720
- />
721
- </div>
722
-
723
- <div className="flex justify-end space-x-3 pt-4">
724
- <button
725
- type="button"
726
- onClick={() => setIsOpen(false)}
727
- className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md"
728
- >
729
- Cancelar
730
- </button>
731
- <button
732
- type="submit"
733
- className="bg-blue-600 text-white px-4 py-2 rounded-md"
734
- >
735
- Guardar Usuario
736
- </button>
737
- </div>
738
- </form>
739
- </div>
740
- </Modal>
741
- </div>
742
- );
743
- }
744
- ```
745
-
746
- ### Overlay
747
-
748
- Componente overlay para superponer contenido.
749
-
750
- ```tsx
751
- import { Overlay } from '@onpe/ui/components';
752
-
753
- function App() {
754
- return (
755
- <Overlay>
756
- <div>Contenido superpuesto</div>
757
- </Overlay>
758
- );
759
- }
760
- ```
761
-
762
- ### Portal
763
-
764
- Componente portal para renderizar fuera del DOM padre.
765
-
766
- ```tsx
767
- import { Portal } from '@onpe/ui/components';
768
-
769
- function App() {
770
- return (
771
- <Portal>
772
- <div>Contenido renderizado en portal</div>
773
- </Portal>
774
- );
775
- }
776
- ```
777
-
778
- ### Show
779
-
780
- Componente condicional para mostrar/ocultar contenido.
781
-
782
- ```tsx
783
- import { Show } from '@onpe/ui/components';
784
-
785
- function App() {
786
- const [visible, setVisible] = useState(true);
787
-
788
- return (
789
- <Show when={visible}>
790
- <div>Contenido visible</div>
791
- </Show>
792
- );
793
- }
794
- ```
795
-
796
- ### ModalConfirm
797
-
798
- Modal de confirmación para acciones importantes con colores dinámicos.
799
-
800
- ```tsx
801
- import { ModalConfirm } from '@onpe/ui/components';
802
-
803
- function App() {
804
- const [showConfirm, setShowConfirm] = useState(false);
805
-
806
- return (
807
- <div className="space-y-4">
808
- {/* Modal Azul (Por Defecto) */}
809
- <ModalConfirm
810
- isOpen={showConfirm}
811
- onClose={() => setShowConfirm(false)}
812
- onConfirm={() => {
813
- // Acción a confirmar
814
- setShowConfirm(false);
815
- }}
816
- title="Confirmar acción"
817
- message="¿Estás seguro de realizar esta acción?"
818
- color="blue" // Por defecto
819
- />
820
-
821
- {/* Modal Rojo para Advertencias */}
822
- <ModalConfirm
823
- isOpen={showConfirm}
824
- onClose={() => setShowConfirm(false)}
825
- onConfirm={() => {
826
- // Acción peligrosa
827
- setShowConfirm(false);
828
- }}
829
- title="Advertencia"
830
- message="Esta acción es irreversible y no se puede deshacer."
831
- color="red" // Color rojo para advertencias
832
- icon="warning"
833
- />
834
- </div>
835
- );
836
- }
837
- ```
838
-
839
- #### Props del ModalConfirm
840
-
841
- | Prop | Tipo | Default | Descripción |
842
- |------|------|---------|-------------|
843
- | `isOpen` | `boolean` | - | Estado de apertura del modal (requerido) |
844
- | `onClose` | `function` | - | Función para cerrar el modal (requerido) |
845
- | `onConfirm` | `function` | - | Función para confirmar la acción (requerido) |
846
- | `title` | `string` | - | Título del modal (requerido) |
847
- | `message` | `string` | - | Mensaje del modal (requerido) |
848
- | `color` | `'blue' \| 'red'` | `'blue'` | Color del icono y título |
849
- | `icon` | `'warning' \| 'success'` | `'warning'` | Tipo de icono a mostrar |
850
- | `confirmText` | `string` | `'Confirmar'` | Texto del botón de confirmación |
851
- | `cancelText` | `string` | `'Cancelar'` | Texto del botón de cancelación |
852
-
853
- ### ModalLoading
854
-
855
- Modal de carga para mostrar estados de procesamiento.
856
-
857
- ```tsx
858
- import { ModalLoading } from '@onpe/ui/components';
859
-
860
- function App() {
861
- const [loading, setLoading] = useState(false);
862
-
863
- return (
864
- <ModalLoading
865
- isOpen={loading}
866
- message="Procesando información..."
867
- />
868
- );
869
- }
870
- ```
871
-
872
- ### ModalBrowserIncompatible
873
-
874
- Modal mejorado para mostrar cuando el navegador no es compatible con el sistema de votación.
875
-
876
- ```tsx
877
- import { ModalBrowserIncompatible } from '@onpe/ui/components';
878
-
879
- function App() {
880
- const [showBrowserModal, setShowBrowserModal] = useState(false);
881
-
882
- return (
883
- <ModalBrowserIncompatible
884
- isOpen={showBrowserModal}
885
- onClose={() => setShowBrowserModal(false)}
886
- />
887
- );
888
- }
889
- ```
890
-
891
- **Características del modal mejorado:**
892
- - ✅ Mensaje claro sobre incompatibilidad
893
- - ✅ Lista visual de navegadores compatibles con nombres
894
- - ✅ Información sobre seguridad y versiones
895
- - ✅ Diseño responsive y accesible
896
- - ✅ Colores institucionales ONPE
897
-
898
- ### ModalSystemIncompatible
899
-
900
- Modal mejorado para mostrar cuando el sistema operativo no es compatible con ONPEID.
901
-
902
- ```tsx
903
- import { ModalSystemIncompatible } from '@onpe/ui/components';
904
-
905
- function App() {
906
- const [showSystemModal, setShowSystemModal] = useState(false);
907
-
908
- return (
909
- <ModalSystemIncompatible
910
- isOpen={showSystemModal}
911
- onClose={() => setShowSystemModal(false)}
912
- />
913
- );
914
- }
915
- ```
916
-
917
- **Características del modal mejorado:**
918
- - ✅ Información específica sobre ONPEID
919
- - ✅ Lista de sistemas operativos compatibles con versiones mínimas
920
- - ✅ Alternativa de acceso web
921
- - ✅ Información de seguridad sobre fuentes oficiales
922
- - ✅ Diseño intuitivo y profesional
923
-
924
- ## 🎯 Hooks Disponibles
925
-
926
- ### useDebounce
927
-
928
- Hook para retrasar la ejecución de funciones.
929
-
930
- ```tsx
931
- import { useDebounce } from '@onpe/ui/hooks';
932
-
933
- function SearchComponent() {
934
- const [query, setQuery] = useState('');
935
- const debouncedQuery = useDebounce(query, 500);
936
-
937
- useEffect(() => {
938
- // Buscar solo después de 500ms sin cambios
939
- searchAPI(debouncedQuery);
940
- }, [debouncedQuery]);
941
-
942
- return (
943
- <input
944
- value={query}
945
- onChange={(e) => setQuery(e.target.value)}
946
- placeholder="Buscar..."
947
- />
948
- );
949
- }
950
- ```
951
-
952
- ### useLocalStorage
953
-
954
- Hook para manejar localStorage de forma reactiva.
955
-
956
- ```tsx
957
- import { useLocalStorage } from '@onpe/ui/hooks';
958
-
959
- function SettingsComponent() {
960
- const [theme, setTheme] = useLocalStorage('theme', 'light');
961
-
962
- return (
963
- <select value={theme} onChange={(e) => setTheme(e.target.value)}>
964
- <option value="light">Claro</option>
965
- <option value="dark">Oscuro</option>
966
- </select>
967
- );
968
- }
969
- ```
970
-
971
- ## 🎨 Iconos Disponibles
972
-
973
- La librería incluye una colección completa de iconos organizados por categorías:
974
-
975
- ### Iconos de Acciones
976
- - Iconos para acciones comunes (editar, eliminar, guardar, etc.)
977
-
978
- ### Iconos de Navegadores
979
- - Iconos de navegadores web (Chrome, Firefox, Safari, Edge, etc.)
980
-
981
- ### Iconos de Sistemas Operativos
982
- - Iconos de sistemas operativos (Windows, macOS, Linux, etc.)
983
-
984
- ### Iconos ONPE
985
- - Iconos específicos de la institución ONPE
986
-
987
- ### Logos
988
- - Logotipos oficiales y marcas
989
-
990
- ```tsx
991
- import {
992
- IconChrome,
993
- IconFirefox,
994
- IconSafari,
995
- IconWindows,
996
- IconMacOS
997
- } from '@onpe/ui/icons';
998
-
999
- function App() {
1000
- return (
1001
- <div>
1002
- <IconChrome className="w-6 h-6" />
1003
- <IconFirefox className="w-6 h-6" />
1004
- <IconSafari className="w-6 h-6" />
1005
- </div>
1006
- );
1007
- }
1008
- ```
1009
-
1010
- ## 🛠️ Utilidades
1011
-
1012
- ### formatDate
1013
-
1014
- Función para formatear fechas según estándares peruanos.
1015
-
1016
- ```tsx
1017
- import { formatDate } from '@onpe/ui/utils';
1018
-
1019
- const fecha = new Date('2024-04-14');
1020
- const fechaFormateada = formatDate(fecha, 'dd/mm/yyyy');
1021
- console.log(fechaFormateada); // "14/04/2024"
1022
- ```
1023
-
1024
- ### validateEmail
1025
-
1026
- Función para validar direcciones de correo electrónico.
1027
-
1028
- ```tsx
1029
- import { validateEmail } from '@onpe/ui/utils';
1030
-
1031
- const email = 'usuario@onpe.gob.pe';
1032
- const esValido = validateEmail(email);
1033
- console.log(esValido); // true
1034
- ```
1035
-
1036
- ## 📱 Breakpoints Responsive
1037
-
1038
- La librería incluye breakpoints personalizados para ONPE:
1039
-
1040
- - `sm`: 640px
1041
- - `md`: 768px
1042
- - `lg`: 1024px
1043
- - `2lg`: 1200px
1044
- - `xl`: 1280px
1045
- - `2xl`: 1536px
1046
- - `3xl`: 1650px
1047
-
1048
- ```css
1049
- /* Ejemplo de uso */
1050
- @media (min-width: 1200px) {
1051
- .container {
1052
- max-width: 1200px;
1053
- }
1054
- }
1055
- ```
1056
-
1057
- ## 🎨 Clases de Utilidad
1058
-
1059
- ### Colores de Texto
1060
- ```css
1061
- .text-onpe-ui-blue /* Azul principal */
1062
- .text-onpe-ui-skyblue /* Sky blue */
1063
- .text-onpe-ui-yellow /* Amarillo */
1064
- .text-onpe-ui-green /* Verde */
1065
- .text-onpe-ui-red /* Rojo */
1066
- .text-onpe-ui-gray /* Gris */
1067
- .text-onpe-ui-dark-gray /* Gris oscuro */
1068
- ```
1069
-
1070
- ### Colores de Fondo
1071
- ```css
1072
- .bg-onpe-ui-blue /* Fondo azul */
1073
- .bg-onpe-ui-skyblue /* Fondo sky blue */
1074
- .bg-onpe-ui-yellow /* Fondo amarillo */
1075
- .bg-onpe-ui-green /* Fondo verde */
1076
- .bg-onpe-ui-red /* Fondo rojo */
1077
- .bg-onpe-ui-gray /* Fondo gris */
1078
- .bg-onpe-ui-gray-light /* Fondo gris claro */
1079
- .bg-onpe-ui-gray-extra-light /* Fondo gris muy claro */
1080
- ```
1081
-
1082
- ## 🛡️ Compatibilidad con CSP (Content Security Policy)
1083
-
1084
- **La librería es completamente compatible con Content Security Policy estricto.**
1085
-
1086
- ### ✅ Características CSP
1087
-
1088
- - **Archivos CSS externos**: No usa estilos inline que violen CSP
1089
- - **Sin `'unsafe-inline'`**: Compatible con `style-src 'self'`
1090
- - **Variables CSS en `:root`**: Disponibles globalmente sin violar políticas
1091
- - **Archivos estáticos**: Todos los recursos se sirven como archivos externos
1092
-
1093
- ### 🔧 Configuración CSP Recomendada
1094
-
1095
- ```html
1096
- <!-- En tu HTML head -->
1097
- <meta http-equiv="Content-Security-Policy"
1098
- content="style-src 'self' https://fonts.googleapis.com;
1099
- script-src 'self';
1100
- img-src 'self' data:;">
1101
- ```
1102
-
1103
- ### 📋 Instrucciones para Proyectos con CSP
1104
-
1105
- **1. Instalar la librería:**
1106
- ```bash
1107
- npm install @onpe/ui@1.2.40
1108
- ```
1109
-
1110
- **2. Importar CSS (CRÍTICO):**
1111
- ```tsx
1112
- // ✅ CORRECTO - Esto carga el CSS externo
1113
- import '@onpe/ui/dist/index.css';
1114
- import { ModalConfirm, Button } from '@onpe/ui/components';
1115
- ```
1116
-
1117
- **3. Verificar que las variables CSS estén disponibles:**
1118
- ```tsx
1119
- // Las variables CSS se definen automáticamente en :root
1120
- function MiComponente() {
1121
- return (
1122
- <div style={{ color: 'var(--onpe-ui-blue)' }}>
1123
- Este texto usa el color azul ONPE
1124
- </div>
1125
- );
1126
- }
1127
- ```
1128
-
1129
- ### ⚠️ Problemas Comunes con CSP
1130
-
1131
- **Error: "Refused to apply inline style"**
1132
- ```bash
1133
- # ❌ PROBLEMA: Estilos inline bloqueados por CSP
1134
- # ✅ SOLUCIÓN: Usar archivos CSS externos (ya implementado)
1135
- ```
1136
-
1137
- **Error: "Variable CSS not defined"**
1138
- ```tsx
1139
- // ❌ PROBLEMA: No importar el CSS
1140
- import { Button } from '@onpe/ui/components';
1141
-
1142
- // ✅ SOLUCIÓN: Importar CSS primero
1143
- import '@onpe/ui/dist/index.css';
1144
- import { Button } from '@onpe/ui/components';
1145
- ```
1146
-
1147
- ## 📋 Versiones
1148
-
1149
- - **v1.2.40** - Versión actual con CSP compatible
1150
- - Compatible con React 16.8+
1151
- - TailwindCSS v4
1152
- - TypeScript 5.3+
1153
- - CSP (Content Security Policy) compatible
1154
-
1155
- ## 🔧 Desarrollo
1156
-
1157
- ### Requisitos
1158
- - Node.js 18+
1159
- - npm 9+
1160
-
1161
- ### Instalación para desarrollo
1162
- ```bash
1163
- git clone https://github.com/ricardosv46/onpe-ui.git
1164
- cd onpe-ui
1165
- npm install
1166
- ```
1167
-
1168
- ### Scripts disponibles
1169
- ```bash
1170
- npm run build # Construir para producción
1171
- npm run dev # Desarrollo con watch mode
1172
- npm run storybook # Ver componentes en Storybook
1173
- npm run lint # Verificar código
1174
- npm run lint:fix # Corregir problemas de linting
1175
- npm run type-check # Verificar tipos TypeScript
1176
- ```
1177
-
1178
- ## 📄 Licencia
1179
-
1180
- MIT © ONPE - Oficina Nacional de Procesos Electorales
1181
-
1182
- ## 🤝 Contribuir
1183
-
1184
- 1. Fork el proyecto
1185
- 2. Crea una rama para tu feature (`git checkout -b feature/nueva-funcionalidad`)
1186
- 3. Commit tus cambios (`git commit -m 'Agregar nueva funcionalidad'`)
1187
- 4. Push a la rama (`git push origin feature/nueva-funcionalidad`)
1188
- 5. Abre un Pull Request
1189
-
1190
- ## 🚀 Ejemplos Completos de Aplicaciones
1191
-
1192
- ### Aplicación de Votación Electrónica
1193
- ```tsx
1194
- import { Button, Modal, Show } from '@onpe/ui/components';
1195
- import { useState } from 'react';
1196
-
1197
- function VotingApp() {
1198
- const [currentStep, setCurrentStep] = useState(1);
1199
- const [selectedCandidate, setSelectedCandidate] = useState('');
1200
- const [showConfirmModal, setShowConfirmModal] = useState(false);
1201
- const [voted, setVoted] = useState(false);
1202
-
1203
- const candidates = [
1204
- { id: '1', name: 'Juan Pérez', party: 'Partido Democrático' },
1205
- { id: '2', name: 'María García', party: 'Partido Progresista' },
1206
- { id: '3', name: 'Carlos López', party: 'Partido Independiente' }
1207
- ];
1208
-
1209
- const handleVote = () => {
1210
- setVoted(true);
1211
- setShowConfirmModal(false);
1212
- setCurrentStep(4);
1213
- };
1214
-
1215
- return (
1216
- <div className="min-h-screen bg-gray-50 py-8">
1217
- <div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
1218
- <div className="text-center mb-8">
1219
- <h1 className="text-3xl font-bold text-gray-900 mb-2">
1220
- Sistema de Votación ONPE
1221
- </h1>
1222
- <p className="text-gray-600">
1223
- Seleccione su candidato preferido
1224
- </p>
1225
- </div>
1226
-
1227
- <Show when={currentStep === 1}>
1228
- <div className="space-y-4">
1229
- <h2 className="text-xl font-semibold mb-4">Candidatos Disponibles:</h2>
1230
- {candidates.map((candidate) => (
1231
- <div
1232
- key={candidate.id}
1233
- className={`p-4 border-2 rounded-lg cursor-pointer transition-colors ${
1234
- selectedCandidate === candidate.id
1235
- ? 'border-blue-500 bg-blue-50'
1236
- : 'border-gray-200 hover:border-gray-300'
1237
- }`}
1238
- onClick={() => setSelectedCandidate(candidate.id)}
1239
- >
1240
- <div className="flex items-center justify-between">
1241
- <div>
1242
- <h3 className="font-semibold text-lg">{candidate.name}</h3>
1243
- <p className="text-gray-600">{candidate.party}</p>
1244
- </div>
1245
- {selectedCandidate === candidate.id && (
1246
- <div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
1247
- <svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
1248
- <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
1249
- </svg>
1250
- </div>
1251
- )}
1252
- </div>
1253
- </div>
1254
- ))}
1255
-
1256
- <div className="pt-4">
1257
- <Button
1258
- color="primary"
1259
- title="Continuar"
1260
- size="large"
1261
- disabled={!selectedCandidate}
1262
- onClick={() => setCurrentStep(2)}
1263
- />
1264
- </div>
1265
- </div>
1266
- </Show>
1267
-
1268
- <Show when={currentStep === 2}>
1269
- <div className="text-center">
1270
- <h2 className="text-xl font-semibold mb-4">Confirmar Voto</h2>
1271
- <div className="bg-gray-50 p-4 rounded-lg mb-6">
1272
- <p className="text-gray-600 mb-2">Ha seleccionado:</p>
1273
- <p className="font-semibold text-lg">
1274
- {candidates.find(c => c.id === selectedCandidate)?.name}
1275
- </p>
1276
- <p className="text-gray-600">
1277
- {candidates.find(c => c.id === selectedCandidate)?.party}
1278
- </p>
1279
- </div>
1280
-
1281
- <div className="flex justify-center space-x-4">
1282
- <Button
1283
- color="gray"
1284
- title="Volver"
1285
- onClick={() => setCurrentStep(1)}
1286
- />
1287
- <Button
1288
- color="primary"
1289
- title="Confirmar Voto"
1290
- onClick={() => setShowConfirmModal(true)}
1291
- />
1292
- </div>
1293
- </div>
1294
- </Show>
1295
-
1296
- <Show when={currentStep === 4}>
1297
- <div className="text-center">
1298
- <div className="mb-6">
1299
- <div className="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4">
1300
- <svg className="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1301
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
1302
- </svg>
1303
- </div>
1304
- <h2 className="text-2xl font-bold text-green-600 mb-2">
1305
- ¡Voto Registrado Exitosamente!
1306
- </h2>
1307
- <p className="text-gray-600">
1308
- Su voto ha sido procesado correctamente. Gracias por participar en la democracia.
1309
- </p>
1310
- </div>
1311
-
1312
- <Button
1313
- color="green"
1314
- title="Ver Resultados"
1315
- size="large"
1316
- onClick={() => window.location.reload()}
1317
- />
1318
- </div>
1319
- </Show>
1320
-
1321
- {/* Modal de Confirmación */}
1322
- <Modal
1323
- isOpen={showConfirmModal}
1324
- onClose={() => setShowConfirmModal(false)}
1325
- closeButton={true}
1326
- overlayColor="blue"
1327
- >
1328
- <div className="p-6 text-center">
1329
- <div className="mb-4">
1330
- <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 mb-4">
1331
- <svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1332
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
1333
- </svg>
1334
- </div>
1335
- <h3 className="text-lg font-medium text-gray-900 mb-2">
1336
- Confirmar Voto
1337
- </h3>
1338
- <p className="text-sm text-gray-500">
1339
- ¿Está seguro de que desea votar por{' '}
1340
- <strong>{candidates.find(c => c.id === selectedCandidate)?.name}</strong>?
1341
- Esta acción no se puede deshacer.
1342
- </p>
1343
- </div>
1344
-
1345
- <div className="flex justify-center space-x-3">
1346
- <Button
1347
- color="gray"
1348
- title="Cancelar"
1349
- onClick={() => setShowConfirmModal(false)}
1350
- />
1351
- <Button
1352
- color="primary"
1353
- title="Confirmar Voto"
1354
- onClick={handleVote}
1355
- />
1356
- </div>
1357
- </div>
1358
- </Modal>
1359
- </div>
1360
- </div>
1361
- );
1362
- }
1363
-
1364
- export default VotingApp;
1365
- ```
1366
-
1367
- ### Dashboard de Administración
1368
- ```tsx
1369
- import { Button, Modal, Show } from '@onpe/ui/components';
1370
- import { useState } from 'react';
1371
-
1372
- function AdminDashboard() {
1373
- const [activeTab, setActiveTab] = useState('users');
1374
- const [showUserModal, setShowUserModal] = useState(false);
1375
- const [users, setUsers] = useState([
1376
- { id: 1, name: 'Juan Pérez', email: 'juan@onpe.gob.pe', role: 'Admin' },
1377
- { id: 2, name: 'María García', email: 'maria@onpe.gob.pe', role: 'Usuario' },
1378
- { id: 3, name: 'Carlos López', email: 'carlos@onpe.gob.pe', role: 'Usuario' }
1379
- ]);
1380
-
1381
- const [newUser, setNewUser] = useState({
1382
- name: '',
1383
- email: '',
1384
- role: 'Usuario'
1385
- });
1386
-
1387
- const addUser = () => {
1388
- const user = {
1389
- id: users.length + 1,
1390
- ...newUser
1391
- };
1392
- setUsers([...users, user]);
1393
- setNewUser({ name: '', email: '', role: 'Usuario' });
1394
- setShowUserModal(false);
1395
- };
1396
-
1397
- return (
1398
- <div className="min-h-screen bg-gray-100">
1399
- {/* Header */}
1400
- <header className="bg-white shadow-sm border-b">
1401
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
1402
- <div className="flex justify-between items-center py-4">
1403
- <div>
1404
- <h1 className="text-2xl font-bold text-gray-900">Dashboard ONPE</h1>
1405
- <p className="text-gray-600">Panel de administración del sistema electoral</p>
1406
- </div>
1407
- <div className="flex items-center space-x-4">
1408
- <span className="text-sm text-gray-500">Administrador</span>
1409
- <Button color="primary" title="Cerrar Sesión" size="small" />
1410
- </div>
1411
- </div>
1412
- </div>
1413
- </header>
1414
-
1415
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
1416
- {/* Navigation Tabs */}
1417
- <div className="mb-8">
1418
- <nav className="flex space-x-8">
1419
- <button
1420
- onClick={() => setActiveTab('users')}
1421
- className={`py-2 px-1 border-b-2 font-medium text-sm ${
1422
- activeTab === 'users'
1423
- ? 'border-blue-500 text-blue-600'
1424
- : 'border-transparent text-gray-500 hover:text-gray-700'
1425
- }`}
1426
- >
1427
- Usuarios
1428
- </button>
1429
- <button
1430
- onClick={() => setActiveTab('elections')}
1431
- className={`py-2 px-1 border-b-2 font-medium text-sm ${
1432
- activeTab === 'elections'
1433
- ? 'border-blue-500 text-blue-600'
1434
- : 'border-transparent text-gray-500 hover:text-gray-700'
1435
- }`}
1436
- >
1437
- Elecciones
1438
- </button>
1439
- <button
1440
- onClick={() => setActiveTab('reports')}
1441
- className={`py-2 px-1 border-b-2 font-medium text-sm ${
1442
- activeTab === 'reports'
1443
- ? 'border-blue-500 text-blue-600'
1444
- : 'border-transparent text-gray-500 hover:text-gray-700'
1445
- }`}
1446
- >
1447
- Reportes
1448
- </button>
1449
- </nav>
1450
- </div>
1451
-
1452
- {/* Users Tab */}
1453
- <Show when={activeTab === 'users'}>
1454
- <div className="bg-white rounded-lg shadow">
1455
- <div className="px-6 py-4 border-b border-gray-200">
1456
- <div className="flex justify-between items-center">
1457
- <h2 className="text-lg font-medium text-gray-900">Gestión de Usuarios</h2>
1458
- <Button
1459
- color="green"
1460
- title="Agregar Usuario"
1461
- onClick={() => setShowUserModal(true)}
1462
- />
1463
- </div>
1464
- </div>
1465
-
1466
- <div className="overflow-x-auto">
1467
- <table className="min-w-full divide-y divide-gray-200">
1468
- <thead className="bg-gray-50">
1469
- <tr>
1470
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1471
- Nombre
1472
- </th>
1473
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1474
- Email
1475
- </th>
1476
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1477
- Rol
1478
- </th>
1479
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1480
- Acciones
1481
- </th>
1482
- </tr>
1483
- </thead>
1484
- <tbody className="bg-white divide-y divide-gray-200">
1485
- {users.map((user) => (
1486
- <tr key={user.id}>
1487
- <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
1488
- {user.name}
1489
- </td>
1490
- <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
1491
- {user.email}
1492
- </td>
1493
- <td className="px-6 py-4 whitespace-nowrap">
1494
- <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
1495
- user.role === 'Admin'
1496
- ? 'bg-red-100 text-red-800'
1497
- : 'bg-green-100 text-green-800'
1498
- }`}>
1499
- {user.role}
1500
- </span>
1501
- </td>
1502
- <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
1503
- <div className="flex space-x-2">
1504
- <Button color="skyblue" title="Editar" size="small" />
1505
- <Button color="red" title="Eliminar" size="small" />
1506
- </div>
1507
- </td>
1508
- </tr>
1509
- ))}
1510
- </tbody>
1511
- </table>
1512
- </div>
1513
- </div>
1514
- </Show>
1515
-
1516
- {/* Elections Tab */}
1517
- <Show when={activeTab === 'elections'}>
1518
- <div className="bg-white rounded-lg shadow p-6">
1519
- <h2 className="text-lg font-medium text-gray-900 mb-4">Gestión de Elecciones</h2>
1520
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
1521
- <div className="bg-blue-50 p-4 rounded-lg">
1522
- <h3 className="font-semibold text-blue-900">Elecciones Generales</h3>
1523
- <p className="text-blue-700 text-sm mt-1">14 de abril, 2024</p>
1524
- <Button color="blue" title="Ver Detalles" size="small" />
1525
- </div>
1526
- <div className="bg-green-50 p-4 rounded-lg">
1527
- <h3 className="font-semibold text-green-900">Elecciones Regionales</h3>
1528
- <p className="text-green-700 text-sm mt-1">28 de octubre, 2024</p>
1529
- <Button color="green" title="Ver Detalles" size="small" />
1530
- </div>
1531
- <div className="bg-yellow-50 p-4 rounded-lg">
1532
- <h3 className="font-semibold text-yellow-900">Elecciones Municipales</h3>
1533
- <p className="text-yellow-700 text-sm mt-1">15 de diciembre, 2024</p>
1534
- <Button color="yellow" title="Ver Detalles" size="small" />
1535
- </div>
1536
- </div>
1537
- </div>
1538
- </Show>
1539
-
1540
- {/* Reports Tab */}
1541
- <Show when={activeTab === 'reports'}>
1542
- <div className="bg-white rounded-lg shadow p-6">
1543
- <h2 className="text-lg font-medium text-gray-900 mb-4">Reportes del Sistema</h2>
1544
- <div className="space-y-4">
1545
- <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
1546
- <div>
1547
- <h3 className="font-medium">Reporte de Participación Electoral</h3>
1548
- <p className="text-sm text-gray-600">Generado el 15 de abril, 2024</p>
1549
- </div>
1550
- <Button color="primary" title="Descargar PDF" size="small" />
1551
- </div>
1552
- <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
1553
- <div>
1554
- <h3 className="font-medium">Estadísticas de Usuarios</h3>
1555
- <p className="text-sm text-gray-600">Generado el 14 de abril, 2024</p>
1556
- </div>
1557
- <Button color="primary" title="Descargar PDF" size="small" />
1558
- </div>
1559
- </div>
1560
- </div>
1561
- </Show>
1562
-
1563
- {/* Add User Modal */}
1564
- <Modal
1565
- isOpen={showUserModal}
1566
- onClose={() => setShowUserModal(false)}
1567
- closeButton={true}
1568
- overlayColor="skyblue"
1569
- >
1570
- <div className="p-6">
1571
- <h2 className="text-xl font-bold mb-4">Agregar Nuevo Usuario</h2>
1572
-
1573
- <div className="space-y-4">
1574
- <div>
1575
- <label className="block text-sm font-medium text-gray-700 mb-1">
1576
- Nombre Completo
1577
- </label>
1578
- <input
1579
- type="text"
1580
- value={newUser.name}
1581
- onChange={(e) => setNewUser({...newUser, name: e.target.value})}
1582
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1583
- placeholder="Ingrese el nombre completo"
1584
- />
1585
- </div>
1586
-
1587
- <div>
1588
- <label className="block text-sm font-medium text-gray-700 mb-1">
1589
- Correo Electrónico
1590
- </label>
1591
- <input
1592
- type="email"
1593
- value={newUser.email}
1594
- onChange={(e) => setNewUser({...newUser, email: e.target.value})}
1595
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1596
- placeholder="usuario@onpe.gob.pe"
1597
- />
1598
- </div>
1599
-
1600
- <div>
1601
- <label className="block text-sm font-medium text-gray-700 mb-1">
1602
- Rol
1603
- </label>
1604
- <select
1605
- value={newUser.role}
1606
- onChange={(e) => setNewUser({...newUser, role: e.target.value})}
1607
- className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1608
- >
1609
- <option value="Usuario">Usuario</option>
1610
- <option value="Admin">Administrador</option>
1611
- </select>
1612
- </div>
1613
-
1614
- <div className="flex justify-end space-x-3 pt-4">
1615
- <Button
1616
- color="gray"
1617
- title="Cancelar"
1618
- onClick={() => setShowUserModal(false)}
1619
- />
1620
- <Button
1621
- color="green"
1622
- title="Agregar Usuario"
1623
- onClick={addUser}
1624
- />
1625
- </div>
1626
- </div>
1627
- </div>
1628
- </Modal>
1629
- </div>
1630
- </div>
1631
- );
1632
- }
1633
-
1634
- export default AdminDashboard;
1635
- ```
1636
-
1637
- ## 🐛 Solución de Problemas
1638
-
1639
- ### Problemas con la CLI
1640
-
1641
- **Error: "Componente no encontrado"**
1642
- ```bash
1643
- # Verificar componentes disponibles
1644
- npx @onpe/ui add --help
1645
-
1646
- # Componentes válidos:
1647
- # button, modal, overlay, portal, show
1648
- ```
1649
-
1650
- **Error: "No se pudo descargar el componente"**
1651
- ```bash
1652
- # Verificar conexión a internet
1653
- ping github.com
1654
-
1655
- # Verificar que el repositorio esté disponible
1656
- curl https://raw.githubusercontent.com/ricardosv46/onpe-ui/main/src/components/Button/Button.tsx
1657
- ```
1658
-
1659
- **Los estilos no se aplican**
1660
- ```bash
1661
- # Verificar que Tailwind esté instalado
1662
- npm list tailwindcss
1663
-
1664
- # Verificar configuración
1665
- cat tailwind.config.js
1666
- ```
1667
-
1668
- **Portal no funciona**
1669
- ```bash
1670
- # Verificar que tengas el elemento portal en HTML
1671
- grep -r "id=\"portal\"" public/
1672
- ```
1673
-
1674
- ### Problemas con la Librería Completa
1675
-
1676
- **Error: "Module not found"**
1677
- ```bash
1678
- # Verificar instalación
1679
- npm list @onpe/ui
1680
-
1681
- # Reinstalar si es necesario
1682
- npm uninstall @onpe/ui
1683
- npm install @onpe/ui
1684
- ```
1685
-
1686
- **Estilos no se cargan**
1687
- ```tsx
1688
- /* Verificar que tengas la importación correcta */
1689
- import './onpe-ui.css';
1690
- import { Button } from './components/onpe-ui/Button';
1691
- ```
1692
-
1693
- **Solución: Verificar rutas de importación**
1694
- ```tsx
1695
- // ✅ CORRECTO: Importar archivo CSS personalizado
1696
- import './onpe-ui.css';
1697
- import { Button } from './components/onpe-ui/Button';
1698
- import { IconClose } from './components/onpe-icons/IconClose';
1699
-
1700
- // ❌ INCORRECTO: No importar el archivo CSS
1701
- import { Button } from './components/onpe-ui/Button';
1702
- ```
1703
-
1704
- **Solución: Verificar configuración de bundler**
1705
- ```javascript
1706
- // webpack.config.js
1707
- module.exports = {
1708
- resolve: {
1709
- alias: {
1710
- '@onpe/ui': path.resolve(__dirname, 'node_modules/@onpe/ui')
1711
- }
1712
- }
1713
- };
1714
-
1715
- // vite.config.js
1716
- export default {
1717
- resolve: {
1718
- alias: {
1719
- '@onpe/ui': path.resolve(__dirname, 'node_modules/@onpe/ui')
1720
- }
1721
- }
1722
- };
1723
- ```
1724
-
1725
- **Solución: Verificar orden de importación**
1726
- ```tsx
1727
- // ✅ CORRECTO - Importar estilos ANTES de los componentes
1728
- import '@onpe/ui/styles';
1729
- import { Button } from '@onpe/ui/components';
1730
-
1731
- // ❌ INCORRECTO - Importar estilos DESPUÉS de los componentes
1732
- import { Button } from '@onpe/ui/components';
1733
- import '@onpe/ui/styles';
1734
- ```
1735
-
1736
- ### Problemas con Estilos
1737
-
1738
- **Los componentes se ven sin estilos**
1739
- ```bash
1740
- # Verificar que la librería esté instalada
1741
- npm list @onpe/ui
1742
-
1743
- # Verificar que los estilos estén en node_modules
1744
- ls node_modules/@onpe/ui/dist/
1745
- ```
1746
-
1747
- **Solución: Verificar importación de estilos**
1748
- ```tsx
1749
- // En tu archivo principal (index.tsx o App.tsx)
1750
- import React from 'react';
1751
- import ReactDOM from 'react-dom/client';
1752
- import '@onpe/ui/styles'; // ← IMPORTANTE: Importar estilos primero
1753
- import App from './App';
1754
-
1755
- const root = ReactDOM.createRoot(document.getElementById('root'));
1756
- root.render(<App />);
1757
- ```
1758
-
1759
- **Solución: Verificar configuración de CSS**
1760
- ```css
1761
- /* En tu archivo CSS principal */
1762
- @import "@onpe/ui/styles";
1763
-
1764
- /* O si usas CSS modules */
1765
- @import "~@onpe/ui/styles";
1766
- ```
1767
-
1768
- **Los colores personalizados no funcionan**
1769
- ```tsx
1770
- // Verificar que estés usando las clases correctas
1771
- <div className="bg-onpe-ui-blue text-white p-4">
1772
- Contenido con colores ONPE
1773
- </div>
1774
-
1775
- // Verificar que los estilos estén importados
1776
- import '@onpe/ui/styles';
1777
- ```
1778
-
1779
- **Solución: Verificar configuración de Tailwind (para componentes individuales)**
1780
- ```javascript
1781
- // tailwind.config.js
1782
- module.exports = {
1783
- content: ["./src/**/*.{js,jsx,ts,tsx}"],
1784
- theme: {
1785
- extend: {
1786
- colors: {
1787
- 'onpe-ui-blue': '#003770',
1788
- 'onpe-ui-skyblue': '#0073cf',
1789
- // ... resto de colores
1790
- }
1791
- },
1792
- },
1793
- }
1794
- ```
1795
-
1796
- ### Problemas con Storybook
1797
-
1798
- **Error: "Failed to fetch dynamically imported module"**
1799
- - Verificar que el archivo `preview.ts` importe correctamente `../src/styles.css`
1800
- - Asegurar que Tailwind CSS esté configurado correctamente
1801
- - Verificar que PostCSS esté configurado
1802
-
1803
- **Solución: Configuración completa de Storybook**
1804
- ```typescript
1805
- // .storybook/main.ts
1806
- import type { StorybookConfig } from "@storybook/react-vite";
1807
-
1808
- const config: StorybookConfig = {
1809
- stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
1810
- addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
1811
- framework: {
1812
- name: "@storybook/react-vite",
1813
- options: {},
1814
- },
1815
- viteFinal: async (config) => {
1816
- config.css = {
1817
- ...config.css,
1818
- postcss: {
1819
- plugins: [require("tailwindcss"), require("autoprefixer")],
1820
- },
1821
- };
1822
- return config;
1823
- },
1824
- };
1825
-
1826
- export default config;
1827
- ```
1828
-
1829
- ```typescript
1830
- // .storybook/preview.ts
1831
- import type { Preview } from "@storybook/react";
1832
- import "../src/styles.css"; // ← Importar estilos
1833
-
1834
- const preview: Preview = {
1835
- parameters: {
1836
- actions: { argTypesRegex: "^on[A-Z].*" },
1837
- controls: {
1838
- matchers: {
1839
- color: /(background|color)$/i,
1840
- date: /Date$/,
1841
- },
1842
- },
1843
- },
1844
- };
1845
-
1846
- export default preview;
1847
- ```
1848
-
1849
- ## 🚀 Guía Rápida de Solución
1850
-
1851
- ### ¿Los componentes no se ven con estilos?
1852
-
1853
- **Paso 1: Verificar instalación**
1854
- ```bash
1855
- npm list @onpe/ui
1856
- ```
1857
-
1858
- **Paso 2: Configurar TailwindCSS**
1859
- ```bash
1860
- # Instalar TailwindCSS
1861
- npm install -D tailwindcss postcss autoprefixer
1862
- npx tailwindcss init -p
1863
- ```
1864
-
1865
- **Paso 3: Crear archivo CSS personalizado**
1866
- ```css
1867
- /* Crear archivo onpe-ui.css */
1868
- :root {
1869
- --blue: #003770;
1870
- --skyblue: #0073cf;
1871
- /* ... resto de variables */
1872
- }
1873
-
1874
- @theme {
1875
- --color-onpe-ui-blue: var(--blue);
1876
- /* ... resto de colores */
1877
- }
1878
- ```
1879
-
1880
- **Paso 4: Importar archivo CSS**
1881
- ```tsx
1882
- // En tu archivo principal (index.tsx)
1883
- import './onpe-ui.css';
1884
- import { Button } from './components/onpe-ui/Button';
1885
- import { IconClose } from './components/onpe-icons/IconClose';
1886
- ```
1887
-
1888
- **Paso 5: Verificar orden de importación**
1889
- ```tsx
1890
- // ✅ CORRECTO
1891
- import './onpe-ui.css';
1892
- import { Button } from './components/onpe-ui/Button';
1893
- import { IconClose } from './components/onpe-icons/IconClose';
1894
-
1895
- // ❌ INCORRECTO
1896
- import { Button } from './components/onpe-ui/Button';
1897
- // Falta importar el archivo CSS
1898
- ```
1899
-
1900
- ### ¿Los colores no funcionan?
1901
-
1902
- **Solución: Usar clases correctas**
1903
- ```tsx
1904
- // ✅ CORRECTO
1905
- <div className="bg-onpe-ui-blue text-white p-4">
1906
- Contenido
1907
- </div>
1908
-
1909
- // ❌ INCORRECTO
1910
- <div className="bg-blue-500 text-white p-4">
1911
- Contenido
1912
- </div>
1913
- ```
1914
-
1915
- ### ¿Storybook no funciona?
1916
-
1917
- **Solución: Configuración completa**
1918
- ```typescript
1919
- // .storybook/preview.ts
1920
- import "../src/styles.css";
1921
-
1922
- // .storybook/main.ts
1923
- viteFinal: async (config) => {
1924
- config.css = {
1925
- ...config.css,
1926
- postcss: {
1927
- plugins: [require("tailwindcss"), require("autoprefixer")],
1928
- },
1929
- };
1930
- return config;
1931
- },
1932
- ```
1933
-
1934
- ### ¿CLI no instala componentes?
1935
-
1936
- **Solución: Verificar comandos**
1937
- ```bash
1938
- # ✅ CORRECTO
1939
- npx @onpe/ui add button
1940
- npx @onpe/ui add modal
1941
-
1942
- # ❌ INCORRECTO
1943
- npx @onpe/ui add Button
1944
- npx @onpe/ui add Modal
1945
- ```
1946
-
1947
- ### ¿Portal no funciona?
1948
-
1949
- **Solución: Agregar elemento HTML**
1950
- ```html
1951
- <!-- En public/index.html -->
1952
- <div id="root"></div>
1953
- <div id="portal"></div>
1954
- ```
1955
-
1956
- ## 📞 Soporte
1957
-
1958
- - 📧 Email: desarrollo@onpe.gob.pe
1959
- - 🐛 Issues: [GitHub Issues](https://github.com/ricardosv46/onpe-ui/issues)
1960
- - 📖 Documentación: [Storybook](https://onpe-ui-components.netlify.app)
1961
- - 🔗 Repositorio: [GitHub](https://github.com/ricardosv46/onpe-ui)
1962
- - 📦 NPM: [@onpe/ui](https://www.npmjs.com/package/@onpe/ui)
1963
-
1964
- ---
1965
-
1
+ # 🗳️ ONPE UI
2
+
3
+ Librería completa de componentes de interfaz de usuario para aplicaciones de la Oficina Nacional de Procesos Electorales (ONPE) del Perú.
4
+
5
+ ## ✨ Características
6
+
7
+ - 🎨 **Colores oficiales de ONPE** - Paleta de colores institucional
8
+ - ⚡ **Tailwind CSS v4** - Framework CSS moderno y eficiente
9
+ - 🔧 **TypeScript** - Tipado estático para mejor desarrollo
10
+ - 📱 **Responsive** - Diseño adaptable a todos los dispositivos
11
+ - 🎯 **Accesible** - Componentes que siguen estándares de accesibilidad
12
+ - 📦 **Tree-shakable** - Solo importa lo que necesitas
13
+
14
+ ## 🚀 Instalación
15
+
16
+ ### Instalación Completa de la Librería
17
+ ```bash
18
+ npm install @onpe/ui
19
+ ```
20
+
21
+ ### Instalación Individual de Componentes (CLI)
22
+ ```bash
23
+ # Instalar la CLI globalmente
24
+ npm install -g @onpe/ui
25
+
26
+ # O usar directamente con npx
27
+ npx @onpe/ui add <componente>
28
+ ```
29
+
30
+ ## 🛡️ Integración Sin Conflictos
31
+
32
+ **Esta librería está diseñada para funcionar perfectamente en proyectos que ya usan Tailwind CSS, Material UI, Shadcn, o cualquier otro framework CSS sin causar conflictos de estilos.**
33
+
34
+ ### ✨ Características de Compatibilidad
35
+
36
+ - **Prefijos Únicos**: Todas las clases usan el prefijo `onpe-` para evitar conflictos
37
+ - **CSS Compilado**: Se genera un CSS optimizado y minificado sin `@import` de Tailwind
38
+ - **Variables CSS en `:root`**: Colores con prefijos únicos (`--onpe-ui-blue`, etc.) disponibles globalmente
39
+ - **Sin Reset de Tailwind**: No interfiere con tu configuración existente
40
+ - **Compatible con**: Material UI, Shadcn, Chakra UI, Ant Design, Bootstrap, etc.
41
+ - **CSP Compatible**: Genera archivos CSS externos para cumplir con Content Security Policy
42
+
43
+ ### 🚀 Instalación Rápida
44
+
45
+ ```bash
46
+ npm install @onpe/ui
47
+ ```
48
+
49
+ ```tsx
50
+ // Importar estilos compilados (solo una vez en tu app)
51
+ // ⚠️ IMPORTANTE: Esto define las variables CSS en :root
52
+ import '@onpe/ui/dist/index.css';
53
+
54
+ // O usando el export específico
55
+ import '@onpe/ui/css';
56
+
57
+ // Usar componentes
58
+ import { Button } from '@onpe/ui/components';
59
+
60
+ function App() {
61
+ return (
62
+ <div>
63
+ {/* Tu contenido existente con Material UI, Shadcn, etc. */}
64
+ <Button color="blue" title="Botón ONPE" />
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### 🎨 Variables CSS en `:root`
71
+
72
+ **Los colores ONPE se definen automáticamente en `:root` cuando importas el CSS:**
73
+
74
+ ```css
75
+ :root {
76
+ --onpe-ui-blue: #003770;
77
+ --onpe-ui-skyblue: #0073cf;
78
+ --onpe-ui-skyblue-light: #69b2e8;
79
+ --onpe-ui-yellow: #ffb81c;
80
+ --onpe-ui-light-skyblue: #aaeff6;
81
+ --onpe-ui-gray: #bcbcbc;
82
+ --onpe-ui-gray-light: #bdbdbd;
83
+ --onpe-ui-gray-extra-light: #f2f2f2;
84
+ --onpe-ui-red: #e3002b;
85
+ --onpe-ui-dark-gray: #4f4f4f;
86
+ --onpe-ui-green: #76bd43;
87
+ --onpe-ui-yellow-light: #FFF1D2;
88
+ }
89
+ ```
90
+
91
+ **Esto permite:**
92
+ - ✅ **Uso directo en CSS del proyecto host**
93
+ - ✅ **Sobrescribir colores si es necesario**
94
+ - ✅ **Compatibilidad con CSP (Content Security Policy)**
95
+ - ✅ **Acceso global a los colores ONPE**
96
+
97
+ ### 💡 Uso de Variables CSS en tu Proyecto
98
+
99
+ **Puedes usar los colores ONPE directamente en tu CSS:**
100
+
101
+ ```css
102
+ /* En tu archivo CSS del proyecto host */
103
+ .mi-componente {
104
+ color: var(--onpe-ui-blue);
105
+ background: var(--onpe-ui-skyblue-light);
106
+ border: 2px solid var(--onpe-ui-red);
107
+ }
108
+
109
+ .mi-boton-personalizado {
110
+ background: var(--onpe-ui-green);
111
+ color: white;
112
+ padding: 12px 24px;
113
+ border-radius: 8px;
114
+ }
115
+ ```
116
+
117
+ **Sobrescribir colores si es necesario:**
118
+
119
+ ```css
120
+ /* En tu archivo CSS del proyecto host */
121
+ :root {
122
+ /* Cambiar el azul principal */
123
+ --onpe-ui-blue: #1a365d;
124
+
125
+ /* Cambiar el rojo */
126
+ --onpe-ui-red: #c53030;
127
+ }
128
+ ```
129
+
130
+ **Uso en componentes React con estilos inline:**
131
+
132
+ ```tsx
133
+ function MiComponente() {
134
+ return (
135
+ <div
136
+ style={{
137
+ color: 'var(--onpe-ui-blue)',
138
+ backgroundColor: 'var(--onpe-ui-skyblue-light)',
139
+ padding: '16px',
140
+ borderRadius: '8px'
141
+ }}
142
+ >
143
+ Contenido con colores ONPE
144
+ </div>
145
+ );
146
+ }
147
+ ```
148
+
149
+ ### 🎯 ¿Cómo Evitamos Conflictos?
150
+
151
+ 1. **Prefijos Únicos**: `bg-blue-500` → `onpe-bg-onpe-ui-blue`
152
+ 2. **CSS Scoped**: Todos los componentes están aislados
153
+ 3. **Variables CSS Aisladas**: `--onpe-ui-blue` en lugar de `--blue`
154
+ 4. **Sin Preflight**: No resetea estilos del proyecto host
155
+
156
+ ## 📖 Uso Básico
157
+
158
+ ### Instalar Componentes con CLI
159
+
160
+ #### Instalar componentes específicos
161
+ ```bash
162
+ # Componentes
163
+ npx @onpe/ui add button
164
+ npx @onpe/ui add modal
165
+ npx @onpe/ui add portal
166
+ npx @onpe/ui add overlay
167
+ npx @onpe/ui add show
168
+
169
+ # Iconos
170
+ npx @onpe/ui add icon-close
171
+ npx @onpe/ui add icon-check
172
+ npx @onpe/ui add icon-warning
173
+ npx @onpe/ui add icon-chrome
174
+ npx @onpe/ui add icon-firefox
175
+ npx @onpe/ui add icon-safari
176
+ npx @onpe/ui add icon-edge
177
+ npx @onpe/ui add icon-windows
178
+ npx @onpe/ui add icon-apple
179
+ npx @onpe/ui add icon-android
180
+ ```
181
+
182
+ #### Usar componentes instalados individualmente
183
+ ```tsx
184
+ // Después de instalar con CLI
185
+ import { Button } from './components/onpe-ui/Button';
186
+ import { Modal } from './components/onpe-ui/Modal';
187
+ import { IconClose } from './components/onpe-icons/IconClose';
188
+ import { useState } from 'react';
189
+
190
+ function App() {
191
+ const [isOpen, setIsOpen] = useState(false);
192
+
193
+ return (
194
+ <div className="p-4">
195
+ <Button
196
+ color="primary"
197
+ title="Abrir Modal"
198
+ onClick={() => setIsOpen(true)}
199
+ />
200
+
201
+ <Modal
202
+ isOpen={isOpen}
203
+ onClose={() => setIsOpen(false)}
204
+ closeButton={true}
205
+ overlayColor="blue"
206
+ >
207
+ <div className="p-6">
208
+ <h2 className="text-xl font-bold mb-4">Contenido del Modal</h2>
209
+ <p className="mb-4">Este es un ejemplo de modal con contenido.</p>
210
+ <div className="flex items-center gap-2">
211
+ <IconClose className="w-4 h-4" />
212
+ <Button
213
+ color="green"
214
+ title="Cerrar"
215
+ onClick={() => setIsOpen(false)}
216
+ />
217
+ </div>
218
+ </div>
219
+ </Modal>
220
+ </div>
221
+ );
222
+ }
223
+ ```
224
+
225
+ #### Configuración requerida para componentes individuales
226
+
227
+ **1. Instalar Tailwind CSS:**
228
+ ```bash
229
+ npm install -D tailwindcss postcss autoprefixer
230
+ npx tailwindcss init -p
231
+ ```
232
+
233
+ **2. Configurar PostCSS para Tailwind v4:**
234
+ ```javascript
235
+ // postcss.config.js
236
+ export default {
237
+ plugins: {
238
+ '@tailwindcss/postcss': {},
239
+ autoprefixer: {},
240
+ },
241
+ }
242
+ ```
243
+
244
+ **3. Crear archivo CSS con configuración Tailwind v4:**
245
+ ```css
246
+ /* onpe-ui.css */
247
+ @import "tailwindcss";
248
+
249
+ @theme {
250
+ --color-onpe-ui-blue: #003770;
251
+ --color-onpe-ui-skyblue: #0073cf;
252
+ --color-onpe-ui-skyblue-light: #69b2e8;
253
+ --color-onpe-ui-yellow: #ffb81c;
254
+ --color-onpe-ui-light-skyblue: #aaeff6;
255
+ --color-onpe-ui-gray: #bcbcbc;
256
+ --color-onpe-ui-gray-light: #bdbdbd;
257
+ --color-onpe-ui-gray-extra-light: #f2f2f2;
258
+ --color-onpe-ui-red: #e3002b;
259
+ --color-onpe-ui-dark-gray: #4f4f4f;
260
+ --color-onpe-ui-green: #76bd43;
261
+ --color-onpe-ui-yellow-light: #FFF1D2;
262
+ }
263
+
264
+ /* Clases personalizadas ONPE */
265
+ @utility bg-onpe-ui-blue { background-color: var(--color-onpe-ui-blue); }
266
+ @utility text-onpe-ui-blue { color: var(--color-onpe-ui-blue); }
267
+ @utility border-onpe-ui-blue { border-color: var(--color-onpe-ui-blue); }
268
+ /* ... resto de clases personalizadas */
269
+ ```
270
+
271
+ **4. Importar el archivo CSS en tu aplicación:**
272
+ ```tsx
273
+ // En tu archivo principal (index.tsx o App.tsx)
274
+ import './onpe-ui.css'; // ← IMPORTANTE: Importar primero
275
+ import { Button } from './components/onpe-ui/Button';
276
+ ```
277
+
278
+ **5. Para componentes que usan Portal, agregar en public/index.html:**
279
+ ```html
280
+ <!DOCTYPE html>
281
+ <html lang="es">
282
+ <head>
283
+ <meta charset="utf-8" />
284
+ <title>Mi App</title>
285
+ </head>
286
+ <body>
287
+ <div id="root"></div>
288
+ <div id="portal"></div>
289
+ </body>
290
+ </html>
291
+ ```
292
+
293
+ ## 🎨 Paleta de Colores ONPE
294
+
295
+ ### Colores Principales
296
+ - **Azul Principal**: `#003770` - Color institucional principal
297
+ - **Sky Blue**: `#0073cf` - Color secundario
298
+ - **Sky Blue Light**: `#69b2e8` - Color claro
299
+ - **Light Sky Blue**: `#aaeff6` - Color muy claro
300
+
301
+ ### Colores de Acento
302
+ - **Amarillo**: `#ffb81c` - Para alertas y destacados
303
+ - **Verde**: `#76bd43` - Para éxito y confirmaciones
304
+ - **Rojo**: `#e3002b` - Para errores y advertencias
305
+
306
+ ### Escala de Grises
307
+ - **Dark Gray**: `#4f4f4f` - Texto principal
308
+ - **Gray**: `#bcbcbc` - Texto secundario
309
+ - **Gray Light**: `#bdbdbd` - Texto terciario
310
+ - **Gray Extra Light**: `#f2f2f2` - Fondos suaves
311
+
312
+ ## 🔗 Dependencias entre Componentes
313
+
314
+ ### Mapa de Dependencias
315
+ ```
316
+ Modal
317
+ ├── Portal (requerido)
318
+ ├── Overlay (requerido)
319
+ └── IconClose (requerido)
320
+
321
+ ModalBrowserIncompatible
322
+ ├── Modal (requerido)
323
+ ├── IconWarning (requerido)
324
+ ├── IconChromeColor (requerido)
325
+ ├── IconSafariColor (requerido)
326
+ ├── IconMozillaColor (requerido)
327
+ └── IconEdgeColor (requerido)
328
+
329
+ ModalSystemIncompatible
330
+ ├── Modal (requerido)
331
+ ├── IconWarning (requerido)
332
+ ├── IconWindow (requerido)
333
+ ├── IconAndroid (requerido)
334
+ └── IconApple (requerido)
335
+
336
+ Portal
337
+ └── react-dom (createPortal)
338
+
339
+ Overlay
340
+ └── (sin dependencias externas)
341
+
342
+ Button
343
+ └── (sin dependencias externas)
344
+
345
+ Show
346
+ └── (sin dependencias externas)
347
+ ```
348
+
349
+ ### Instalación Automática de Dependencias
350
+
351
+ **Modal** - Instala automáticamente sus dependencias:
352
+ ```bash
353
+ npx @onpe/ui add modal
354
+ # Esto instalará automáticamente:
355
+ # - Portal.tsx
356
+ # - Overlay.tsx
357
+ # - IconClose.tsx (si está disponible)
358
+ ```
359
+
360
+ **ModalBrowserIncompatible** - Instala automáticamente sus dependencias:
361
+ ```bash
362
+ npx @onpe/ui add modal-browser-incompatible
363
+ # Esto instalará automáticamente:
364
+ # - Modal.tsx
365
+ # - IconWarning.tsx
366
+ # - IconChromeColor.tsx
367
+ # - IconSafariColor.tsx
368
+ # - IconMozillaColor.tsx
369
+ # - IconEdgeColor.tsx
370
+ ```
371
+
372
+ **ModalSystemIncompatible** - Instala automáticamente sus dependencias:
373
+ ```bash
374
+ npx @onpe/ui add modal-system-incompatible
375
+ # Esto instalará automáticamente:
376
+ # - Modal.tsx
377
+ # - IconWarning.tsx
378
+ # - IconWindow.tsx
379
+ # - IconAndroid.tsx
380
+ # - IconApple.tsx
381
+ ```
382
+
383
+ **Otros componentes** - Instalación independiente:
384
+ ```bash
385
+ npx @onpe/ui add button # Sin dependencias
386
+ npx @onpe/ui add portal # Sin dependencias
387
+ npx @onpe/ui add overlay # Sin dependencias
388
+ npx @onpe/ui add show # Sin dependencias
389
+ ```
390
+
391
+ ### Estructura de Archivos Después de la Instalación
392
+
393
+ ```
394
+ src/
395
+ └── components/
396
+ ├── ui/ # shadcn/ui (si está instalado)
397
+ │ ├── button.tsx
398
+ │ └── input.tsx
399
+ ├── onpe-ui/ # ONPE UI - Componentes
400
+ │ ├── Button.tsx
401
+ │ ├── Modal.tsx
402
+ │ ├── Overlay.tsx
403
+ │ ├── Portal.tsx
404
+ │ └── Show.tsx
405
+ └── onpe-icons/ # ONPE UI - Iconos
406
+ ├── IconClose.tsx
407
+ ├── IconCheck.tsx
408
+ ├── IconChrome.tsx
409
+ ├── IconFirefox.tsx
410
+ └── IconWindows.tsx
411
+ ```
412
+
413
+ ## 🧩 Componentes Disponibles
414
+
415
+ ### Button
416
+
417
+ Botón versátil con múltiples colores y tamaños.
418
+
419
+ #### Ejemplo Básico
420
+ ```tsx
421
+ import { Button } from '@onpe/ui/components';
422
+
423
+ function App() {
424
+ return (
425
+ <div className="space-y-4 p-4">
426
+ <h2 className="text-2xl font-bold">Botones ONPE</h2>
427
+
428
+ {/* Colores disponibles */}
429
+ <div className="space-y-2">
430
+ <h3 className="text-lg font-semibold">Colores:</h3>
431
+ <div className="flex flex-wrap gap-2">
432
+ <Button color="primary" title="Primario" />
433
+ <Button color="blue" title="Azul" />
434
+ <Button color="skyblue" title="Sky Blue" />
435
+ <Button color="green" title="Verde" />
436
+ <Button color="yellow" title="Amarillo" />
437
+ <Button color="red" title="Rojo" />
438
+ </div>
439
+ </div>
440
+
441
+ {/* Tamaños */}
442
+ <div className="space-y-2">
443
+ <h3 className="text-lg font-semibold">Tamaños:</h3>
444
+ <div className="flex items-center gap-2">
445
+ <Button color="primary" title="Pequeño" size="small" />
446
+ <Button color="primary" title="Mediano" size="normal" />
447
+ <Button color="primary" title="Grande" size="large" />
448
+ </div>
449
+ </div>
450
+
451
+ {/* Estados */}
452
+ <div className="space-y-2">
453
+ <h3 className="text-lg font-semibold">Estados:</h3>
454
+ <div className="flex gap-2">
455
+ <Button color="primary" title="Normal" />
456
+ <Button color="primary" title="Deshabilitado" disabled />
457
+ </div>
458
+ </div>
459
+ </div>
460
+ );
461
+ }
462
+ ```
463
+
464
+ #### Ejemplo con Funcionalidad
465
+ ```tsx
466
+ import { Button } from '@onpe/ui/components';
467
+ import { useState } from 'react';
468
+
469
+ function VotingApp() {
470
+ const [voted, setVoted] = useState(false);
471
+ const [loading, setLoading] = useState(false);
472
+
473
+ const handleVote = async () => {
474
+ setLoading(true);
475
+ // Simular llamada a API
476
+ await new Promise(resolve => setTimeout(resolve, 2000));
477
+ setVoted(true);
478
+ setLoading(false);
479
+ };
480
+
481
+ return (
482
+ <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
483
+ <h2 className="text-xl font-bold mb-4">Sistema de Votación</h2>
484
+
485
+ {!voted ? (
486
+ <div className="space-y-4">
487
+ <p className="text-gray-600">¿Desea votar por esta opción?</p>
488
+ <Button
489
+ color="primary"
490
+ title={loading ? "Procesando..." : "Votar Ahora"}
491
+ onClick={handleVote}
492
+ disabled={loading}
493
+ size="large"
494
+ />
495
+ </div>
496
+ ) : (
497
+ <div className="text-center">
498
+ <p className="text-green-600 font-semibold mb-4">¡Voto registrado exitosamente!</p>
499
+ <Button
500
+ color="green"
501
+ title="Ver Resultados"
502
+ onClick={() => setVoted(false)}
503
+ />
504
+ </div>
505
+ )}
506
+ </div>
507
+ );
508
+ }
509
+ ```
510
+
511
+ ### Props del Button
512
+
513
+ | Prop | Tipo | Default | Descripción |
514
+ |------|------|---------|-------------|
515
+ | `color` | `'primary' \| 'blue' \| 'skyblue' \| 'skyblue-light' \| 'yellow' \| 'light-skyblue' \| 'gray' \| 'gray-light' \| 'gray-extra-light' \| 'red' \| 'dark-gray' \| 'green' \| 'yellow-light'` | `'primary'` | Color del botón |
516
+ | `title` | `string` | - | Texto del botón (requerido) |
517
+ | `size` | `'small' \| 'normal' \| 'large'` | `'normal'` | Tamaño del botón |
518
+ | `disabled` | `boolean` | `false` | Estado deshabilitado |
519
+ | `className` | `string` | - | Clases CSS adicionales |
520
+
521
+ ### Modal
522
+
523
+ Componente modal para mostrar contenido en overlay.
524
+
525
+ #### Ejemplo Básico
526
+ ```tsx
527
+ import { Modal } from '@onpe/ui/components';
528
+ import { useState } from 'react';
529
+
530
+ function App() {
531
+ const [isOpen, setIsOpen] = useState(false);
532
+
533
+ return (
534
+ <div className="p-4">
535
+ <button
536
+ onClick={() => setIsOpen(true)}
537
+ className="bg-blue-500 text-white px-4 py-2 rounded"
538
+ >
539
+ Abrir Modal
540
+ </button>
541
+
542
+ <Modal
543
+ isOpen={isOpen}
544
+ onClose={() => setIsOpen(false)}
545
+ closeButton={true}
546
+ overlayColor="blue"
547
+ >
548
+ <div className="p-6">
549
+ <h2 className="text-xl font-bold mb-4">Título del Modal</h2>
550
+ <p className="mb-4">Este es el contenido del modal.</p>
551
+ <button
552
+ onClick={() => setIsOpen(false)}
553
+ className="bg-gray-500 text-white px-4 py-2 rounded"
554
+ >
555
+ Cerrar
556
+ </button>
557
+ </div>
558
+ </Modal>
559
+ </div>
560
+ );
561
+ }
562
+ ```
563
+
564
+ #### Ejemplo Avanzado - Modal de Confirmación
565
+ ```tsx
566
+ import { Modal } from '@onpe/ui/components';
567
+ import { useState } from 'react';
568
+
569
+ function DeleteConfirmation() {
570
+ const [showModal, setShowModal] = useState(false);
571
+ const [itemToDelete, setItemToDelete] = useState('');
572
+
573
+ const handleDelete = (itemName) => {
574
+ setItemToDelete(itemName);
575
+ setShowModal(true);
576
+ };
577
+
578
+ const confirmDelete = () => {
579
+ // Lógica para eliminar el elemento
580
+ console.log(`Eliminando: ${itemToDelete}`);
581
+ setShowModal(false);
582
+ setItemToDelete('');
583
+ };
584
+
585
+ return (
586
+ <div className="p-4">
587
+ <div className="space-y-2">
588
+ <button
589
+ onClick={() => handleDelete('Usuario 1')}
590
+ className="bg-red-500 text-white px-4 py-2 rounded mr-2"
591
+ >
592
+ Eliminar Usuario 1
593
+ </button>
594
+ <button
595
+ onClick={() => handleDelete('Documento 2')}
596
+ className="bg-red-500 text-white px-4 py-2 rounded mr-2"
597
+ >
598
+ Eliminar Documento 2
599
+ </button>
600
+ </div>
601
+
602
+ <Modal
603
+ isOpen={showModal}
604
+ onClose={() => setShowModal(false)}
605
+ closeButton={true}
606
+ overlayColor="red"
607
+ closeDisabled={false}
608
+ >
609
+ <div className="p-6 text-center">
610
+ <div className="mb-4">
611
+ <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
612
+ <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
613
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
614
+ </svg>
615
+ </div>
616
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
617
+ Confirmar Eliminación
618
+ </h3>
619
+ <p className="text-sm text-gray-500">
620
+ ¿Está seguro de que desea eliminar <strong>{itemToDelete}</strong>?
621
+ Esta acción no se puede deshacer.
622
+ </p>
623
+ </div>
624
+
625
+ <div className="flex justify-center space-x-3">
626
+ <button
627
+ onClick={() => setShowModal(false)}
628
+ className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md text-sm font-medium"
629
+ >
630
+ Cancelar
631
+ </button>
632
+ <button
633
+ onClick={confirmDelete}
634
+ className="bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium"
635
+ >
636
+ Eliminar
637
+ </button>
638
+ </div>
639
+ </div>
640
+ </Modal>
641
+ </div>
642
+ );
643
+ }
644
+ ```
645
+
646
+ #### Ejemplo - Modal de Formulario
647
+ ```tsx
648
+ import { Modal } from '@onpe/ui/components';
649
+ import { useState } from 'react';
650
+
651
+ function UserForm() {
652
+ const [isOpen, setIsOpen] = useState(false);
653
+ const [formData, setFormData] = useState({
654
+ name: '',
655
+ email: '',
656
+ phone: ''
657
+ });
658
+
659
+ const handleSubmit = (e) => {
660
+ e.preventDefault();
661
+ console.log('Datos del formulario:', formData);
662
+ setIsOpen(false);
663
+ setFormData({ name: '', email: '', phone: '' });
664
+ };
665
+
666
+ return (
667
+ <div className="p-4">
668
+ <button
669
+ onClick={() => setIsOpen(true)}
670
+ className="bg-green-500 text-white px-4 py-2 rounded"
671
+ >
672
+ Agregar Usuario
673
+ </button>
674
+
675
+ <Modal
676
+ isOpen={isOpen}
677
+ onClose={() => setIsOpen(false)}
678
+ closeButton={true}
679
+ overlayColor="skyblue"
680
+ >
681
+ <div className="p-6">
682
+ <h2 className="text-xl font-bold mb-4">Nuevo Usuario</h2>
683
+
684
+ <form onSubmit={handleSubmit} className="space-y-4">
685
+ <div>
686
+ <label className="block text-sm font-medium text-gray-700 mb-1">
687
+ Nombre Completo
688
+ </label>
689
+ <input
690
+ type="text"
691
+ value={formData.name}
692
+ onChange={(e) => setFormData({...formData, name: e.target.value})}
693
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
694
+ required
695
+ />
696
+ </div>
697
+
698
+ <div>
699
+ <label className="block text-sm font-medium text-gray-700 mb-1">
700
+ Correo Electrónico
701
+ </label>
702
+ <input
703
+ type="email"
704
+ value={formData.email}
705
+ onChange={(e) => setFormData({...formData, email: e.target.value})}
706
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
707
+ required
708
+ />
709
+ </div>
710
+
711
+ <div>
712
+ <label className="block text-sm font-medium text-gray-700 mb-1">
713
+ Teléfono
714
+ </label>
715
+ <input
716
+ type="tel"
717
+ value={formData.phone}
718
+ onChange={(e) => setFormData({...formData, phone: e.target.value})}
719
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
720
+ />
721
+ </div>
722
+
723
+ <div className="flex justify-end space-x-3 pt-4">
724
+ <button
725
+ type="button"
726
+ onClick={() => setIsOpen(false)}
727
+ className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md"
728
+ >
729
+ Cancelar
730
+ </button>
731
+ <button
732
+ type="submit"
733
+ className="bg-blue-600 text-white px-4 py-2 rounded-md"
734
+ >
735
+ Guardar Usuario
736
+ </button>
737
+ </div>
738
+ </form>
739
+ </div>
740
+ </Modal>
741
+ </div>
742
+ );
743
+ }
744
+ ```
745
+
746
+ ### Overlay
747
+
748
+ Componente overlay para superponer contenido.
749
+
750
+ ```tsx
751
+ import { Overlay } from '@onpe/ui/components';
752
+
753
+ function App() {
754
+ return (
755
+ <Overlay>
756
+ <div>Contenido superpuesto</div>
757
+ </Overlay>
758
+ );
759
+ }
760
+ ```
761
+
762
+ ### Portal
763
+
764
+ Componente portal para renderizar fuera del DOM padre.
765
+
766
+ ```tsx
767
+ import { Portal } from '@onpe/ui/components';
768
+
769
+ function App() {
770
+ return (
771
+ <Portal>
772
+ <div>Contenido renderizado en portal</div>
773
+ </Portal>
774
+ );
775
+ }
776
+ ```
777
+
778
+ ### Show
779
+
780
+ Componente condicional para mostrar/ocultar contenido.
781
+
782
+ ```tsx
783
+ import { Show } from '@onpe/ui/components';
784
+
785
+ function App() {
786
+ const [visible, setVisible] = useState(true);
787
+
788
+ return (
789
+ <Show when={visible}>
790
+ <div>Contenido visible</div>
791
+ </Show>
792
+ );
793
+ }
794
+ ```
795
+
796
+ ### ModalConfirm
797
+
798
+ Modal de confirmación para acciones importantes con colores dinámicos.
799
+
800
+ ```tsx
801
+ import { ModalConfirm } from '@onpe/ui/components';
802
+
803
+ function App() {
804
+ const [showConfirm, setShowConfirm] = useState(false);
805
+
806
+ return (
807
+ <div className="space-y-4">
808
+ {/* Modal Azul (Por Defecto) */}
809
+ <ModalConfirm
810
+ isOpen={showConfirm}
811
+ onClose={() => setShowConfirm(false)}
812
+ onConfirm={() => {
813
+ // Acción a confirmar
814
+ setShowConfirm(false);
815
+ }}
816
+ title="Confirmar acción"
817
+ message="¿Estás seguro de realizar esta acción?"
818
+ color="blue" // Por defecto
819
+ />
820
+
821
+ {/* Modal Rojo para Advertencias */}
822
+ <ModalConfirm
823
+ isOpen={showConfirm}
824
+ onClose={() => setShowConfirm(false)}
825
+ onConfirm={() => {
826
+ // Acción peligrosa
827
+ setShowConfirm(false);
828
+ }}
829
+ title="Advertencia"
830
+ message="Esta acción es irreversible y no se puede deshacer."
831
+ color="red" // Color rojo para advertencias
832
+ icon="warning"
833
+ />
834
+ </div>
835
+ );
836
+ }
837
+ ```
838
+
839
+ #### Props del ModalConfirm
840
+
841
+ | Prop | Tipo | Default | Descripción |
842
+ |------|------|---------|-------------|
843
+ | `isOpen` | `boolean` | - | Estado de apertura del modal (requerido) |
844
+ | `onClose` | `function` | - | Función para cerrar el modal (requerido) |
845
+ | `onConfirm` | `function` | - | Función para confirmar la acción (requerido) |
846
+ | `title` | `string` | - | Título del modal (requerido) |
847
+ | `message` | `string` | - | Mensaje del modal (requerido) |
848
+ | `color` | `'blue' \| 'red'` | `'blue'` | Color del icono y título |
849
+ | `icon` | `'warning' \| 'success'` | `'warning'` | Tipo de icono a mostrar |
850
+ | `confirmText` | `string` | `'Confirmar'` | Texto del botón de confirmación |
851
+ | `cancelText` | `string` | `'Cancelar'` | Texto del botón de cancelación |
852
+
853
+ ### ModalLoading
854
+
855
+ Modal de carga para mostrar estados de procesamiento.
856
+
857
+ ```tsx
858
+ import { ModalLoading } from '@onpe/ui/components';
859
+
860
+ function App() {
861
+ const [loading, setLoading] = useState(false);
862
+
863
+ return (
864
+ <ModalLoading
865
+ isOpen={loading}
866
+ message="Procesando información..."
867
+ />
868
+ );
869
+ }
870
+ ```
871
+
872
+ ### ModalBrowserIncompatible
873
+
874
+ Modal mejorado para mostrar cuando el navegador no es compatible con el sistema de votación.
875
+
876
+ ```tsx
877
+ import { ModalBrowserIncompatible } from '@onpe/ui/components';
878
+
879
+ function App() {
880
+ const [showBrowserModal, setShowBrowserModal] = useState(false);
881
+
882
+ return (
883
+ <ModalBrowserIncompatible
884
+ isOpen={showBrowserModal}
885
+ onClose={() => setShowBrowserModal(false)}
886
+ />
887
+ );
888
+ }
889
+ ```
890
+
891
+ **Características del modal mejorado:**
892
+ - ✅ Mensaje claro sobre incompatibilidad
893
+ - ✅ Lista visual de navegadores compatibles con nombres
894
+ - ✅ Información sobre seguridad y versiones
895
+ - ✅ Diseño responsive y accesible
896
+ - ✅ Colores institucionales ONPE
897
+
898
+ ### ModalSystemIncompatible
899
+
900
+ Modal mejorado para mostrar cuando el sistema operativo no es compatible con ONPEID.
901
+
902
+ ```tsx
903
+ import { ModalSystemIncompatible } from '@onpe/ui/components';
904
+
905
+ function App() {
906
+ const [showSystemModal, setShowSystemModal] = useState(false);
907
+
908
+ return (
909
+ <ModalSystemIncompatible
910
+ isOpen={showSystemModal}
911
+ onClose={() => setShowSystemModal(false)}
912
+ />
913
+ );
914
+ }
915
+ ```
916
+
917
+ **Características del modal mejorado:**
918
+ - ✅ Información específica sobre ONPEID
919
+ - ✅ Lista de sistemas operativos compatibles con versiones mínimas
920
+ - ✅ Alternativa de acceso web
921
+ - ✅ Información de seguridad sobre fuentes oficiales
922
+ - ✅ Diseño intuitivo y profesional
923
+
924
+ ## 🎯 Hooks Disponibles
925
+
926
+ ### useDebounce
927
+
928
+ Hook para retrasar la ejecución de funciones.
929
+
930
+ ```tsx
931
+ import { useDebounce } from '@onpe/ui/hooks';
932
+
933
+ function SearchComponent() {
934
+ const [query, setQuery] = useState('');
935
+ const debouncedQuery = useDebounce(query, 500);
936
+
937
+ useEffect(() => {
938
+ // Buscar solo después de 500ms sin cambios
939
+ searchAPI(debouncedQuery);
940
+ }, [debouncedQuery]);
941
+
942
+ return (
943
+ <input
944
+ value={query}
945
+ onChange={(e) => setQuery(e.target.value)}
946
+ placeholder="Buscar..."
947
+ />
948
+ );
949
+ }
950
+ ```
951
+
952
+ ### useLocalStorage
953
+
954
+ Hook para manejar localStorage de forma reactiva.
955
+
956
+ ```tsx
957
+ import { useLocalStorage } from '@onpe/ui/hooks';
958
+
959
+ function SettingsComponent() {
960
+ const [theme, setTheme] = useLocalStorage('theme', 'light');
961
+
962
+ return (
963
+ <select value={theme} onChange={(e) => setTheme(e.target.value)}>
964
+ <option value="light">Claro</option>
965
+ <option value="dark">Oscuro</option>
966
+ </select>
967
+ );
968
+ }
969
+ ```
970
+
971
+ ## 🎨 Iconos Disponibles
972
+
973
+ La librería incluye una colección completa de iconos organizados por categorías:
974
+
975
+ ### Iconos de Acciones
976
+ - Iconos para acciones comunes (editar, eliminar, guardar, etc.)
977
+
978
+ ### Iconos de Navegadores
979
+ - Iconos de navegadores web (Chrome, Firefox, Safari, Edge, etc.)
980
+
981
+ ### Iconos de Sistemas Operativos
982
+ - Iconos de sistemas operativos (Windows, macOS, Linux, etc.)
983
+
984
+ ### Iconos ONPE
985
+ - Iconos específicos de la institución ONPE
986
+
987
+ ### Logos
988
+ - Logotipos oficiales y marcas
989
+
990
+ ```tsx
991
+ import {
992
+ IconChrome,
993
+ IconFirefox,
994
+ IconSafari,
995
+ IconWindows,
996
+ IconMacOS
997
+ } from '@onpe/ui/icons';
998
+
999
+ function App() {
1000
+ return (
1001
+ <div>
1002
+ <IconChrome className="w-6 h-6" />
1003
+ <IconFirefox className="w-6 h-6" />
1004
+ <IconSafari className="w-6 h-6" />
1005
+ </div>
1006
+ );
1007
+ }
1008
+ ```
1009
+
1010
+ ## 🛠️ Utilidades
1011
+
1012
+ ### formatDate
1013
+
1014
+ Función para formatear fechas según estándares peruanos.
1015
+
1016
+ ```tsx
1017
+ import { formatDate } from '@onpe/ui/utils';
1018
+
1019
+ const fecha = new Date('2024-04-14');
1020
+ const fechaFormateada = formatDate(fecha, 'dd/mm/yyyy');
1021
+ console.log(fechaFormateada); // "14/04/2024"
1022
+ ```
1023
+
1024
+ ### validateEmail
1025
+
1026
+ Función para validar direcciones de correo electrónico.
1027
+
1028
+ ```tsx
1029
+ import { validateEmail } from '@onpe/ui/utils';
1030
+
1031
+ const email = 'usuario@onpe.gob.pe';
1032
+ const esValido = validateEmail(email);
1033
+ console.log(esValido); // true
1034
+ ```
1035
+
1036
+ ## 📱 Breakpoints Responsive
1037
+
1038
+ La librería incluye breakpoints personalizados para ONPE:
1039
+
1040
+ - `sm`: 640px
1041
+ - `md`: 768px
1042
+ - `lg`: 1024px
1043
+ - `2lg`: 1200px
1044
+ - `xl`: 1280px
1045
+ - `2xl`: 1536px
1046
+ - `3xl`: 1650px
1047
+
1048
+ ```css
1049
+ /* Ejemplo de uso */
1050
+ @media (min-width: 1200px) {
1051
+ .container {
1052
+ max-width: 1200px;
1053
+ }
1054
+ }
1055
+ ```
1056
+
1057
+ ## 🎨 Clases de Utilidad
1058
+
1059
+ ### Colores de Texto
1060
+ ```css
1061
+ .text-onpe-ui-blue /* Azul principal */
1062
+ .text-onpe-ui-skyblue /* Sky blue */
1063
+ .text-onpe-ui-yellow /* Amarillo */
1064
+ .text-onpe-ui-green /* Verde */
1065
+ .text-onpe-ui-red /* Rojo */
1066
+ .text-onpe-ui-gray /* Gris */
1067
+ .text-onpe-ui-dark-gray /* Gris oscuro */
1068
+ ```
1069
+
1070
+ ### Colores de Fondo
1071
+ ```css
1072
+ .bg-onpe-ui-blue /* Fondo azul */
1073
+ .bg-onpe-ui-skyblue /* Fondo sky blue */
1074
+ .bg-onpe-ui-yellow /* Fondo amarillo */
1075
+ .bg-onpe-ui-green /* Fondo verde */
1076
+ .bg-onpe-ui-red /* Fondo rojo */
1077
+ .bg-onpe-ui-gray /* Fondo gris */
1078
+ .bg-onpe-ui-gray-light /* Fondo gris claro */
1079
+ .bg-onpe-ui-gray-extra-light /* Fondo gris muy claro */
1080
+ ```
1081
+
1082
+ ## 🛡️ Compatibilidad con CSP (Content Security Policy)
1083
+
1084
+ **La librería es completamente compatible con Content Security Policy estricto.**
1085
+
1086
+ ### ✅ Características CSP
1087
+
1088
+ - **Archivos CSS externos**: No usa estilos inline que violen CSP
1089
+ - **Sin `'unsafe-inline'`**: Compatible con `style-src 'self'`
1090
+ - **Variables CSS en `:root`**: Disponibles globalmente sin violar políticas
1091
+ - **Archivos estáticos**: Todos los recursos se sirven como archivos externos
1092
+
1093
+ ### 🔧 Configuración CSP Recomendada
1094
+
1095
+ ```html
1096
+ <!-- En tu HTML head -->
1097
+ <meta http-equiv="Content-Security-Policy"
1098
+ content="style-src 'self' https://fonts.googleapis.com;
1099
+ script-src 'self';
1100
+ img-src 'self' data:;">
1101
+ ```
1102
+
1103
+ ### 📋 Instrucciones para Proyectos con CSP
1104
+
1105
+ **1. Instalar la librería:**
1106
+ ```bash
1107
+ npm install @onpe/ui@1.2.40
1108
+ ```
1109
+
1110
+ **2. Importar CSS (CRÍTICO):**
1111
+ ```tsx
1112
+ // ✅ CORRECTO - Esto carga el CSS externo
1113
+ import '@onpe/ui/dist/index.css';
1114
+ import { ModalConfirm, Button } from '@onpe/ui/components';
1115
+ ```
1116
+
1117
+ **3. Verificar que las variables CSS estén disponibles:**
1118
+ ```tsx
1119
+ // Las variables CSS se definen automáticamente en :root
1120
+ function MiComponente() {
1121
+ return (
1122
+ <div style={{ color: 'var(--onpe-ui-blue)' }}>
1123
+ Este texto usa el color azul ONPE
1124
+ </div>
1125
+ );
1126
+ }
1127
+ ```
1128
+
1129
+ ### ⚠️ Problemas Comunes con CSP
1130
+
1131
+ **Error: "Refused to apply inline style"**
1132
+ ```bash
1133
+ # ❌ PROBLEMA: Estilos inline bloqueados por CSP
1134
+ # ✅ SOLUCIÓN: Usar archivos CSS externos (ya implementado)
1135
+ ```
1136
+
1137
+ **Error: "Variable CSS not defined"**
1138
+ ```tsx
1139
+ // ❌ PROBLEMA: No importar el CSS
1140
+ import { Button } from '@onpe/ui/components';
1141
+
1142
+ // ✅ SOLUCIÓN: Importar CSS primero
1143
+ import '@onpe/ui/dist/index.css';
1144
+ import { Button } from '@onpe/ui/components';
1145
+ ```
1146
+
1147
+ ## 📋 Versiones
1148
+
1149
+ - **v1.2.40** - Versión actual con CSP compatible
1150
+ - Compatible con React 16.8+
1151
+ - TailwindCSS v4
1152
+ - TypeScript 5.3+
1153
+ - CSP (Content Security Policy) compatible
1154
+
1155
+ ## 🔧 Desarrollo
1156
+
1157
+ ### Requisitos
1158
+ - Node.js 18+
1159
+ - npm 9+
1160
+
1161
+ ### Instalación para desarrollo
1162
+ ```bash
1163
+ git clone https://github.com/ricardosv46/onpe-ui.git
1164
+ cd onpe-ui
1165
+ npm install
1166
+ ```
1167
+
1168
+ ### Scripts disponibles
1169
+ ```bash
1170
+ npm run build # Construir para producción
1171
+ npm run dev # Desarrollo con watch mode
1172
+ npm run storybook # Ver componentes en Storybook
1173
+ npm run lint # Verificar código
1174
+ npm run lint:fix # Corregir problemas de linting
1175
+ npm run type-check # Verificar tipos TypeScript
1176
+ ```
1177
+
1178
+ ## 📄 Licencia
1179
+
1180
+ MIT © ONPE - Oficina Nacional de Procesos Electorales
1181
+
1182
+ ## 🤝 Contribuir
1183
+
1184
+ 1. Fork el proyecto
1185
+ 2. Crea una rama para tu feature (`git checkout -b feature/nueva-funcionalidad`)
1186
+ 3. Commit tus cambios (`git commit -m 'Agregar nueva funcionalidad'`)
1187
+ 4. Push a la rama (`git push origin feature/nueva-funcionalidad`)
1188
+ 5. Abre un Pull Request
1189
+
1190
+ ## 🚀 Ejemplos Completos de Aplicaciones
1191
+
1192
+ ### Aplicación de Votación Electrónica
1193
+ ```tsx
1194
+ import { Button, Modal, Show } from '@onpe/ui/components';
1195
+ import { useState } from 'react';
1196
+
1197
+ function VotingApp() {
1198
+ const [currentStep, setCurrentStep] = useState(1);
1199
+ const [selectedCandidate, setSelectedCandidate] = useState('');
1200
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
1201
+ const [voted, setVoted] = useState(false);
1202
+
1203
+ const candidates = [
1204
+ { id: '1', name: 'Juan Pérez', party: 'Partido Democrático' },
1205
+ { id: '2', name: 'María García', party: 'Partido Progresista' },
1206
+ { id: '3', name: 'Carlos López', party: 'Partido Independiente' }
1207
+ ];
1208
+
1209
+ const handleVote = () => {
1210
+ setVoted(true);
1211
+ setShowConfirmModal(false);
1212
+ setCurrentStep(4);
1213
+ };
1214
+
1215
+ return (
1216
+ <div className="min-h-screen bg-gray-50 py-8">
1217
+ <div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
1218
+ <div className="text-center mb-8">
1219
+ <h1 className="text-3xl font-bold text-gray-900 mb-2">
1220
+ Sistema de Votación ONPE
1221
+ </h1>
1222
+ <p className="text-gray-600">
1223
+ Seleccione su candidato preferido
1224
+ </p>
1225
+ </div>
1226
+
1227
+ <Show when={currentStep === 1}>
1228
+ <div className="space-y-4">
1229
+ <h2 className="text-xl font-semibold mb-4">Candidatos Disponibles:</h2>
1230
+ {candidates.map((candidate) => (
1231
+ <div
1232
+ key={candidate.id}
1233
+ className={`p-4 border-2 rounded-lg cursor-pointer transition-colors ${
1234
+ selectedCandidate === candidate.id
1235
+ ? 'border-blue-500 bg-blue-50'
1236
+ : 'border-gray-200 hover:border-gray-300'
1237
+ }`}
1238
+ onClick={() => setSelectedCandidate(candidate.id)}
1239
+ >
1240
+ <div className="flex items-center justify-between">
1241
+ <div>
1242
+ <h3 className="font-semibold text-lg">{candidate.name}</h3>
1243
+ <p className="text-gray-600">{candidate.party}</p>
1244
+ </div>
1245
+ {selectedCandidate === candidate.id && (
1246
+ <div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
1247
+ <svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
1248
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
1249
+ </svg>
1250
+ </div>
1251
+ )}
1252
+ </div>
1253
+ </div>
1254
+ ))}
1255
+
1256
+ <div className="pt-4">
1257
+ <Button
1258
+ color="primary"
1259
+ title="Continuar"
1260
+ size="large"
1261
+ disabled={!selectedCandidate}
1262
+ onClick={() => setCurrentStep(2)}
1263
+ />
1264
+ </div>
1265
+ </div>
1266
+ </Show>
1267
+
1268
+ <Show when={currentStep === 2}>
1269
+ <div className="text-center">
1270
+ <h2 className="text-xl font-semibold mb-4">Confirmar Voto</h2>
1271
+ <div className="bg-gray-50 p-4 rounded-lg mb-6">
1272
+ <p className="text-gray-600 mb-2">Ha seleccionado:</p>
1273
+ <p className="font-semibold text-lg">
1274
+ {candidates.find(c => c.id === selectedCandidate)?.name}
1275
+ </p>
1276
+ <p className="text-gray-600">
1277
+ {candidates.find(c => c.id === selectedCandidate)?.party}
1278
+ </p>
1279
+ </div>
1280
+
1281
+ <div className="flex justify-center space-x-4">
1282
+ <Button
1283
+ color="gray"
1284
+ title="Volver"
1285
+ onClick={() => setCurrentStep(1)}
1286
+ />
1287
+ <Button
1288
+ color="primary"
1289
+ title="Confirmar Voto"
1290
+ onClick={() => setShowConfirmModal(true)}
1291
+ />
1292
+ </div>
1293
+ </div>
1294
+ </Show>
1295
+
1296
+ <Show when={currentStep === 4}>
1297
+ <div className="text-center">
1298
+ <div className="mb-6">
1299
+ <div className="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4">
1300
+ <svg className="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1301
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
1302
+ </svg>
1303
+ </div>
1304
+ <h2 className="text-2xl font-bold text-green-600 mb-2">
1305
+ ¡Voto Registrado Exitosamente!
1306
+ </h2>
1307
+ <p className="text-gray-600">
1308
+ Su voto ha sido procesado correctamente. Gracias por participar en la democracia.
1309
+ </p>
1310
+ </div>
1311
+
1312
+ <Button
1313
+ color="green"
1314
+ title="Ver Resultados"
1315
+ size="large"
1316
+ onClick={() => window.location.reload()}
1317
+ />
1318
+ </div>
1319
+ </Show>
1320
+
1321
+ {/* Modal de Confirmación */}
1322
+ <Modal
1323
+ isOpen={showConfirmModal}
1324
+ onClose={() => setShowConfirmModal(false)}
1325
+ closeButton={true}
1326
+ overlayColor="blue"
1327
+ >
1328
+ <div className="p-6 text-center">
1329
+ <div className="mb-4">
1330
+ <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 mb-4">
1331
+ <svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1332
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
1333
+ </svg>
1334
+ </div>
1335
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
1336
+ Confirmar Voto
1337
+ </h3>
1338
+ <p className="text-sm text-gray-500">
1339
+ ¿Está seguro de que desea votar por{' '}
1340
+ <strong>{candidates.find(c => c.id === selectedCandidate)?.name}</strong>?
1341
+ Esta acción no se puede deshacer.
1342
+ </p>
1343
+ </div>
1344
+
1345
+ <div className="flex justify-center space-x-3">
1346
+ <Button
1347
+ color="gray"
1348
+ title="Cancelar"
1349
+ onClick={() => setShowConfirmModal(false)}
1350
+ />
1351
+ <Button
1352
+ color="primary"
1353
+ title="Confirmar Voto"
1354
+ onClick={handleVote}
1355
+ />
1356
+ </div>
1357
+ </div>
1358
+ </Modal>
1359
+ </div>
1360
+ </div>
1361
+ );
1362
+ }
1363
+
1364
+ export default VotingApp;
1365
+ ```
1366
+
1367
+ ### Dashboard de Administración
1368
+ ```tsx
1369
+ import { Button, Modal, Show } from '@onpe/ui/components';
1370
+ import { useState } from 'react';
1371
+
1372
+ function AdminDashboard() {
1373
+ const [activeTab, setActiveTab] = useState('users');
1374
+ const [showUserModal, setShowUserModal] = useState(false);
1375
+ const [users, setUsers] = useState([
1376
+ { id: 1, name: 'Juan Pérez', email: 'juan@onpe.gob.pe', role: 'Admin' },
1377
+ { id: 2, name: 'María García', email: 'maria@onpe.gob.pe', role: 'Usuario' },
1378
+ { id: 3, name: 'Carlos López', email: 'carlos@onpe.gob.pe', role: 'Usuario' }
1379
+ ]);
1380
+
1381
+ const [newUser, setNewUser] = useState({
1382
+ name: '',
1383
+ email: '',
1384
+ role: 'Usuario'
1385
+ });
1386
+
1387
+ const addUser = () => {
1388
+ const user = {
1389
+ id: users.length + 1,
1390
+ ...newUser
1391
+ };
1392
+ setUsers([...users, user]);
1393
+ setNewUser({ name: '', email: '', role: 'Usuario' });
1394
+ setShowUserModal(false);
1395
+ };
1396
+
1397
+ return (
1398
+ <div className="min-h-screen bg-gray-100">
1399
+ {/* Header */}
1400
+ <header className="bg-white shadow-sm border-b">
1401
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
1402
+ <div className="flex justify-between items-center py-4">
1403
+ <div>
1404
+ <h1 className="text-2xl font-bold text-gray-900">Dashboard ONPE</h1>
1405
+ <p className="text-gray-600">Panel de administración del sistema electoral</p>
1406
+ </div>
1407
+ <div className="flex items-center space-x-4">
1408
+ <span className="text-sm text-gray-500">Administrador</span>
1409
+ <Button color="primary" title="Cerrar Sesión" size="small" />
1410
+ </div>
1411
+ </div>
1412
+ </div>
1413
+ </header>
1414
+
1415
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
1416
+ {/* Navigation Tabs */}
1417
+ <div className="mb-8">
1418
+ <nav className="flex space-x-8">
1419
+ <button
1420
+ onClick={() => setActiveTab('users')}
1421
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
1422
+ activeTab === 'users'
1423
+ ? 'border-blue-500 text-blue-600'
1424
+ : 'border-transparent text-gray-500 hover:text-gray-700'
1425
+ }`}
1426
+ >
1427
+ Usuarios
1428
+ </button>
1429
+ <button
1430
+ onClick={() => setActiveTab('elections')}
1431
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
1432
+ activeTab === 'elections'
1433
+ ? 'border-blue-500 text-blue-600'
1434
+ : 'border-transparent text-gray-500 hover:text-gray-700'
1435
+ }`}
1436
+ >
1437
+ Elecciones
1438
+ </button>
1439
+ <button
1440
+ onClick={() => setActiveTab('reports')}
1441
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
1442
+ activeTab === 'reports'
1443
+ ? 'border-blue-500 text-blue-600'
1444
+ : 'border-transparent text-gray-500 hover:text-gray-700'
1445
+ }`}
1446
+ >
1447
+ Reportes
1448
+ </button>
1449
+ </nav>
1450
+ </div>
1451
+
1452
+ {/* Users Tab */}
1453
+ <Show when={activeTab === 'users'}>
1454
+ <div className="bg-white rounded-lg shadow">
1455
+ <div className="px-6 py-4 border-b border-gray-200">
1456
+ <div className="flex justify-between items-center">
1457
+ <h2 className="text-lg font-medium text-gray-900">Gestión de Usuarios</h2>
1458
+ <Button
1459
+ color="green"
1460
+ title="Agregar Usuario"
1461
+ onClick={() => setShowUserModal(true)}
1462
+ />
1463
+ </div>
1464
+ </div>
1465
+
1466
+ <div className="overflow-x-auto">
1467
+ <table className="min-w-full divide-y divide-gray-200">
1468
+ <thead className="bg-gray-50">
1469
+ <tr>
1470
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1471
+ Nombre
1472
+ </th>
1473
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1474
+ Email
1475
+ </th>
1476
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1477
+ Rol
1478
+ </th>
1479
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
1480
+ Acciones
1481
+ </th>
1482
+ </tr>
1483
+ </thead>
1484
+ <tbody className="bg-white divide-y divide-gray-200">
1485
+ {users.map((user) => (
1486
+ <tr key={user.id}>
1487
+ <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
1488
+ {user.name}
1489
+ </td>
1490
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
1491
+ {user.email}
1492
+ </td>
1493
+ <td className="px-6 py-4 whitespace-nowrap">
1494
+ <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
1495
+ user.role === 'Admin'
1496
+ ? 'bg-red-100 text-red-800'
1497
+ : 'bg-green-100 text-green-800'
1498
+ }`}>
1499
+ {user.role}
1500
+ </span>
1501
+ </td>
1502
+ <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
1503
+ <div className="flex space-x-2">
1504
+ <Button color="skyblue" title="Editar" size="small" />
1505
+ <Button color="red" title="Eliminar" size="small" />
1506
+ </div>
1507
+ </td>
1508
+ </tr>
1509
+ ))}
1510
+ </tbody>
1511
+ </table>
1512
+ </div>
1513
+ </div>
1514
+ </Show>
1515
+
1516
+ {/* Elections Tab */}
1517
+ <Show when={activeTab === 'elections'}>
1518
+ <div className="bg-white rounded-lg shadow p-6">
1519
+ <h2 className="text-lg font-medium text-gray-900 mb-4">Gestión de Elecciones</h2>
1520
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
1521
+ <div className="bg-blue-50 p-4 rounded-lg">
1522
+ <h3 className="font-semibold text-blue-900">Elecciones Generales</h3>
1523
+ <p className="text-blue-700 text-sm mt-1">14 de abril, 2024</p>
1524
+ <Button color="blue" title="Ver Detalles" size="small" />
1525
+ </div>
1526
+ <div className="bg-green-50 p-4 rounded-lg">
1527
+ <h3 className="font-semibold text-green-900">Elecciones Regionales</h3>
1528
+ <p className="text-green-700 text-sm mt-1">28 de octubre, 2024</p>
1529
+ <Button color="green" title="Ver Detalles" size="small" />
1530
+ </div>
1531
+ <div className="bg-yellow-50 p-4 rounded-lg">
1532
+ <h3 className="font-semibold text-yellow-900">Elecciones Municipales</h3>
1533
+ <p className="text-yellow-700 text-sm mt-1">15 de diciembre, 2024</p>
1534
+ <Button color="yellow" title="Ver Detalles" size="small" />
1535
+ </div>
1536
+ </div>
1537
+ </div>
1538
+ </Show>
1539
+
1540
+ {/* Reports Tab */}
1541
+ <Show when={activeTab === 'reports'}>
1542
+ <div className="bg-white rounded-lg shadow p-6">
1543
+ <h2 className="text-lg font-medium text-gray-900 mb-4">Reportes del Sistema</h2>
1544
+ <div className="space-y-4">
1545
+ <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
1546
+ <div>
1547
+ <h3 className="font-medium">Reporte de Participación Electoral</h3>
1548
+ <p className="text-sm text-gray-600">Generado el 15 de abril, 2024</p>
1549
+ </div>
1550
+ <Button color="primary" title="Descargar PDF" size="small" />
1551
+ </div>
1552
+ <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
1553
+ <div>
1554
+ <h3 className="font-medium">Estadísticas de Usuarios</h3>
1555
+ <p className="text-sm text-gray-600">Generado el 14 de abril, 2024</p>
1556
+ </div>
1557
+ <Button color="primary" title="Descargar PDF" size="small" />
1558
+ </div>
1559
+ </div>
1560
+ </div>
1561
+ </Show>
1562
+
1563
+ {/* Add User Modal */}
1564
+ <Modal
1565
+ isOpen={showUserModal}
1566
+ onClose={() => setShowUserModal(false)}
1567
+ closeButton={true}
1568
+ overlayColor="skyblue"
1569
+ >
1570
+ <div className="p-6">
1571
+ <h2 className="text-xl font-bold mb-4">Agregar Nuevo Usuario</h2>
1572
+
1573
+ <div className="space-y-4">
1574
+ <div>
1575
+ <label className="block text-sm font-medium text-gray-700 mb-1">
1576
+ Nombre Completo
1577
+ </label>
1578
+ <input
1579
+ type="text"
1580
+ value={newUser.name}
1581
+ onChange={(e) => setNewUser({...newUser, name: e.target.value})}
1582
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1583
+ placeholder="Ingrese el nombre completo"
1584
+ />
1585
+ </div>
1586
+
1587
+ <div>
1588
+ <label className="block text-sm font-medium text-gray-700 mb-1">
1589
+ Correo Electrónico
1590
+ </label>
1591
+ <input
1592
+ type="email"
1593
+ value={newUser.email}
1594
+ onChange={(e) => setNewUser({...newUser, email: e.target.value})}
1595
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1596
+ placeholder="usuario@onpe.gob.pe"
1597
+ />
1598
+ </div>
1599
+
1600
+ <div>
1601
+ <label className="block text-sm font-medium text-gray-700 mb-1">
1602
+ Rol
1603
+ </label>
1604
+ <select
1605
+ value={newUser.role}
1606
+ onChange={(e) => setNewUser({...newUser, role: e.target.value})}
1607
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1608
+ >
1609
+ <option value="Usuario">Usuario</option>
1610
+ <option value="Admin">Administrador</option>
1611
+ </select>
1612
+ </div>
1613
+
1614
+ <div className="flex justify-end space-x-3 pt-4">
1615
+ <Button
1616
+ color="gray"
1617
+ title="Cancelar"
1618
+ onClick={() => setShowUserModal(false)}
1619
+ />
1620
+ <Button
1621
+ color="green"
1622
+ title="Agregar Usuario"
1623
+ onClick={addUser}
1624
+ />
1625
+ </div>
1626
+ </div>
1627
+ </div>
1628
+ </Modal>
1629
+ </div>
1630
+ </div>
1631
+ );
1632
+ }
1633
+
1634
+ export default AdminDashboard;
1635
+ ```
1636
+
1637
+ ## 🐛 Solución de Problemas
1638
+
1639
+ ### Problemas con la CLI
1640
+
1641
+ **Error: "Componente no encontrado"**
1642
+ ```bash
1643
+ # Verificar componentes disponibles
1644
+ npx @onpe/ui add --help
1645
+
1646
+ # Componentes válidos:
1647
+ # button, modal, overlay, portal, show
1648
+ ```
1649
+
1650
+ **Error: "No se pudo descargar el componente"**
1651
+ ```bash
1652
+ # Verificar conexión a internet
1653
+ ping github.com
1654
+
1655
+ # Verificar que el repositorio esté disponible
1656
+ curl https://raw.githubusercontent.com/ricardosv46/onpe-ui/main/src/components/Button/Button.tsx
1657
+ ```
1658
+
1659
+ **Los estilos no se aplican**
1660
+ ```bash
1661
+ # Verificar que Tailwind esté instalado
1662
+ npm list tailwindcss
1663
+
1664
+ # Verificar configuración
1665
+ cat tailwind.config.js
1666
+ ```
1667
+
1668
+ **Portal no funciona**
1669
+ ```bash
1670
+ # Verificar que tengas el elemento portal en HTML
1671
+ grep -r "id=\"portal\"" public/
1672
+ ```
1673
+
1674
+ ### Problemas con la Librería Completa
1675
+
1676
+ **Error: "Module not found"**
1677
+ ```bash
1678
+ # Verificar instalación
1679
+ npm list @onpe/ui
1680
+
1681
+ # Reinstalar si es necesario
1682
+ npm uninstall @onpe/ui
1683
+ npm install @onpe/ui
1684
+ ```
1685
+
1686
+ **Estilos no se cargan**
1687
+ ```tsx
1688
+ /* Verificar que tengas la importación correcta */
1689
+ import './onpe-ui.css';
1690
+ import { Button } from './components/onpe-ui/Button';
1691
+ ```
1692
+
1693
+ **Solución: Verificar rutas de importación**
1694
+ ```tsx
1695
+ // ✅ CORRECTO: Importar archivo CSS personalizado
1696
+ import './onpe-ui.css';
1697
+ import { Button } from './components/onpe-ui/Button';
1698
+ import { IconClose } from './components/onpe-icons/IconClose';
1699
+
1700
+ // ❌ INCORRECTO: No importar el archivo CSS
1701
+ import { Button } from './components/onpe-ui/Button';
1702
+ ```
1703
+
1704
+ **Solución: Verificar configuración de bundler**
1705
+ ```javascript
1706
+ // webpack.config.js
1707
+ module.exports = {
1708
+ resolve: {
1709
+ alias: {
1710
+ '@onpe/ui': path.resolve(__dirname, 'node_modules/@onpe/ui')
1711
+ }
1712
+ }
1713
+ };
1714
+
1715
+ // vite.config.js
1716
+ export default {
1717
+ resolve: {
1718
+ alias: {
1719
+ '@onpe/ui': path.resolve(__dirname, 'node_modules/@onpe/ui')
1720
+ }
1721
+ }
1722
+ };
1723
+ ```
1724
+
1725
+ **Solución: Verificar orden de importación**
1726
+ ```tsx
1727
+ // ✅ CORRECTO - Importar estilos ANTES de los componentes
1728
+ import '@onpe/ui/styles';
1729
+ import { Button } from '@onpe/ui/components';
1730
+
1731
+ // ❌ INCORRECTO - Importar estilos DESPUÉS de los componentes
1732
+ import { Button } from '@onpe/ui/components';
1733
+ import '@onpe/ui/styles';
1734
+ ```
1735
+
1736
+ ### Problemas con Estilos
1737
+
1738
+ **Los componentes se ven sin estilos**
1739
+ ```bash
1740
+ # Verificar que la librería esté instalada
1741
+ npm list @onpe/ui
1742
+
1743
+ # Verificar que los estilos estén en node_modules
1744
+ ls node_modules/@onpe/ui/dist/
1745
+ ```
1746
+
1747
+ **Solución: Verificar importación de estilos**
1748
+ ```tsx
1749
+ // En tu archivo principal (index.tsx o App.tsx)
1750
+ import React from 'react';
1751
+ import ReactDOM from 'react-dom/client';
1752
+ import '@onpe/ui/styles'; // ← IMPORTANTE: Importar estilos primero
1753
+ import App from './App';
1754
+
1755
+ const root = ReactDOM.createRoot(document.getElementById('root'));
1756
+ root.render(<App />);
1757
+ ```
1758
+
1759
+ **Solución: Verificar configuración de CSS**
1760
+ ```css
1761
+ /* En tu archivo CSS principal */
1762
+ @import "@onpe/ui/styles";
1763
+
1764
+ /* O si usas CSS modules */
1765
+ @import "~@onpe/ui/styles";
1766
+ ```
1767
+
1768
+ **Los colores personalizados no funcionan**
1769
+ ```tsx
1770
+ // Verificar que estés usando las clases correctas
1771
+ <div className="bg-onpe-ui-blue text-white p-4">
1772
+ Contenido con colores ONPE
1773
+ </div>
1774
+
1775
+ // Verificar que los estilos estén importados
1776
+ import '@onpe/ui/styles';
1777
+ ```
1778
+
1779
+ **Solución: Verificar configuración de Tailwind (para componentes individuales)**
1780
+ ```javascript
1781
+ // tailwind.config.js
1782
+ module.exports = {
1783
+ content: ["./src/**/*.{js,jsx,ts,tsx}"],
1784
+ theme: {
1785
+ extend: {
1786
+ colors: {
1787
+ 'onpe-ui-blue': '#003770',
1788
+ 'onpe-ui-skyblue': '#0073cf',
1789
+ // ... resto de colores
1790
+ }
1791
+ },
1792
+ },
1793
+ }
1794
+ ```
1795
+
1796
+ ### Problemas con Storybook
1797
+
1798
+ **Error: "Failed to fetch dynamically imported module"**
1799
+ - Verificar que el archivo `preview.ts` importe correctamente `../src/styles.css`
1800
+ - Asegurar que Tailwind CSS esté configurado correctamente
1801
+ - Verificar que PostCSS esté configurado
1802
+
1803
+ **Solución: Configuración completa de Storybook**
1804
+ ```typescript
1805
+ // .storybook/main.ts
1806
+ import type { StorybookConfig } from "@storybook/react-vite";
1807
+
1808
+ const config: StorybookConfig = {
1809
+ stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
1810
+ addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
1811
+ framework: {
1812
+ name: "@storybook/react-vite",
1813
+ options: {},
1814
+ },
1815
+ viteFinal: async (config) => {
1816
+ config.css = {
1817
+ ...config.css,
1818
+ postcss: {
1819
+ plugins: [require("tailwindcss"), require("autoprefixer")],
1820
+ },
1821
+ };
1822
+ return config;
1823
+ },
1824
+ };
1825
+
1826
+ export default config;
1827
+ ```
1828
+
1829
+ ```typescript
1830
+ // .storybook/preview.ts
1831
+ import type { Preview } from "@storybook/react";
1832
+ import "../src/styles.css"; // ← Importar estilos
1833
+
1834
+ const preview: Preview = {
1835
+ parameters: {
1836
+ actions: { argTypesRegex: "^on[A-Z].*" },
1837
+ controls: {
1838
+ matchers: {
1839
+ color: /(background|color)$/i,
1840
+ date: /Date$/,
1841
+ },
1842
+ },
1843
+ },
1844
+ };
1845
+
1846
+ export default preview;
1847
+ ```
1848
+
1849
+ ## 🚀 Guía Rápida de Solución
1850
+
1851
+ ### ¿Los componentes no se ven con estilos?
1852
+
1853
+ **Paso 1: Verificar instalación**
1854
+ ```bash
1855
+ npm list @onpe/ui
1856
+ ```
1857
+
1858
+ **Paso 2: Configurar TailwindCSS**
1859
+ ```bash
1860
+ # Instalar TailwindCSS
1861
+ npm install -D tailwindcss postcss autoprefixer
1862
+ npx tailwindcss init -p
1863
+ ```
1864
+
1865
+ **Paso 3: Crear archivo CSS personalizado**
1866
+ ```css
1867
+ /* Crear archivo onpe-ui.css */
1868
+ :root {
1869
+ --blue: #003770;
1870
+ --skyblue: #0073cf;
1871
+ /* ... resto de variables */
1872
+ }
1873
+
1874
+ @theme {
1875
+ --color-onpe-ui-blue: var(--blue);
1876
+ /* ... resto de colores */
1877
+ }
1878
+ ```
1879
+
1880
+ **Paso 4: Importar archivo CSS**
1881
+ ```tsx
1882
+ // En tu archivo principal (index.tsx)
1883
+ import './onpe-ui.css';
1884
+ import { Button } from './components/onpe-ui/Button';
1885
+ import { IconClose } from './components/onpe-icons/IconClose';
1886
+ ```
1887
+
1888
+ **Paso 5: Verificar orden de importación**
1889
+ ```tsx
1890
+ // ✅ CORRECTO
1891
+ import './onpe-ui.css';
1892
+ import { Button } from './components/onpe-ui/Button';
1893
+ import { IconClose } from './components/onpe-icons/IconClose';
1894
+
1895
+ // ❌ INCORRECTO
1896
+ import { Button } from './components/onpe-ui/Button';
1897
+ // Falta importar el archivo CSS
1898
+ ```
1899
+
1900
+ ### ¿Los colores no funcionan?
1901
+
1902
+ **Solución: Usar clases correctas**
1903
+ ```tsx
1904
+ // ✅ CORRECTO
1905
+ <div className="bg-onpe-ui-blue text-white p-4">
1906
+ Contenido
1907
+ </div>
1908
+
1909
+ // ❌ INCORRECTO
1910
+ <div className="bg-blue-500 text-white p-4">
1911
+ Contenido
1912
+ </div>
1913
+ ```
1914
+
1915
+ ### ¿Storybook no funciona?
1916
+
1917
+ **Solución: Configuración completa**
1918
+ ```typescript
1919
+ // .storybook/preview.ts
1920
+ import "../src/styles.css";
1921
+
1922
+ // .storybook/main.ts
1923
+ viteFinal: async (config) => {
1924
+ config.css = {
1925
+ ...config.css,
1926
+ postcss: {
1927
+ plugins: [require("tailwindcss"), require("autoprefixer")],
1928
+ },
1929
+ };
1930
+ return config;
1931
+ },
1932
+ ```
1933
+
1934
+ ### ¿CLI no instala componentes?
1935
+
1936
+ **Solución: Verificar comandos**
1937
+ ```bash
1938
+ # ✅ CORRECTO
1939
+ npx @onpe/ui add button
1940
+ npx @onpe/ui add modal
1941
+
1942
+ # ❌ INCORRECTO
1943
+ npx @onpe/ui add Button
1944
+ npx @onpe/ui add Modal
1945
+ ```
1946
+
1947
+ ### ¿Portal no funciona?
1948
+
1949
+ **Solución: Agregar elemento HTML**
1950
+ ```html
1951
+ <!-- En public/index.html -->
1952
+ <div id="root"></div>
1953
+ <div id="portal"></div>
1954
+ ```
1955
+
1956
+ ## 📞 Soporte
1957
+
1958
+ - 📧 Email: desarrollo@onpe.gob.pe
1959
+ - 🐛 Issues: [GitHub Issues](https://github.com/ricardosv46/onpe-ui/issues)
1960
+ - 📖 Documentación: [Storybook](https://onpe-ui-components.netlify.app)
1961
+ - 🔗 Repositorio: [GitHub](https://github.com/ricardosv46/onpe-ui)
1962
+ - 📦 NPM: [@onpe/ui](https://www.npmjs.com/package/@onpe/ui)
1963
+
1964
+ ---
1965
+
1966
1966
  **Desarrollado con ❤️ para la democracia peruana** 🇵🇪