npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@onpe/ui

v1.3.16

Published

Librería de componentes UI para ONPE - Componentes e Iconos

Readme

🗳️ ONPE UI

Librería completa de componentes de interfaz de usuario para aplicaciones de la Oficina Nacional de Procesos Electorales (ONPE) del Perú.

✨ Características

  • 🎨 Colores oficiales de ONPE - Paleta de colores institucional
  • Tailwind CSS v4 - Framework CSS moderno y eficiente
  • 🔧 TypeScript - Tipado estático para mejor desarrollo
  • 📱 Responsive - Diseño adaptable a todos los dispositivos
  • 🎯 Accesible - Componentes que siguen estándares de accesibilidad
  • 📦 Tree-shakable - Solo importa lo que necesitas

🚀 Instalación

Instalación Completa de la Librería

npm install @onpe/ui

Instalación Individual de Componentes (CLI)

# Instalar la CLI globalmente
npm install -g @onpe/ui

# O usar directamente con npx
npx @onpe/ui add <componente>

🛡️ Integración Sin Conflictos

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.

✨ Características de Compatibilidad

  • Prefijos Únicos: Todas las clases usan el prefijo onpe- para evitar conflictos
  • CSS Compilado: Se genera un CSS optimizado y minificado sin @import de Tailwind
  • Variables CSS en :root: Colores con prefijos únicos (--onpe-ui-blue, etc.) disponibles globalmente
  • Sin Reset de Tailwind: No interfiere con tu configuración existente
  • Compatible con: Material UI, Shadcn, Chakra UI, Ant Design, Bootstrap, etc.
  • CSP Compatible: Genera archivos CSS externos para cumplir con Content Security Policy

🚀 Instalación Rápida

npm install @onpe/ui
// Importar estilos compilados (solo una vez en tu app)
// ⚠️ IMPORTANTE: Esto define las variables CSS en :root
import '@onpe/ui/dist/index.css';

// O usando el export específico
import '@onpe/ui/css';

// Usar componentes
import { Button } from '@onpe/ui/components';

function App() {
  return (
    <div>
      {/* Tu contenido existente con Material UI, Shadcn, etc. */}
      <Button color="blue" title="Botón ONPE" />
    </div>
  );
}

🎨 Variables CSS en :root

Los colores ONPE se definen automáticamente en :root cuando importas el CSS:

:root {
  --onpe-ui-blue: #003770;
  --onpe-ui-skyblue: #0073cf;
  --onpe-ui-skyblue-light: #69b2e8;
  --onpe-ui-yellow: #ffb81c;
  --onpe-ui-light-skyblue: #aaeff6;
  --onpe-ui-gray: #bcbcbc;
  --onpe-ui-gray-light: #bdbdbd;
  --onpe-ui-gray-extra-light: #f2f2f2;
  --onpe-ui-red: #e3002b;
  --onpe-ui-dark-gray: #4f4f4f;
  --onpe-ui-green: #76bd43;
  --onpe-ui-yellow-light: #FFF1D2;
}

Esto permite:

  • Uso directo en CSS del proyecto host
  • Sobrescribir colores si es necesario
  • Compatibilidad con CSP (Content Security Policy)
  • Acceso global a los colores ONPE

💡 Uso de Variables CSS en tu Proyecto

Puedes usar los colores ONPE directamente en tu CSS:

/* En tu archivo CSS del proyecto host */
.mi-componente {
  color: var(--onpe-ui-blue);
  background: var(--onpe-ui-skyblue-light);
  border: 2px solid var(--onpe-ui-red);
}

.mi-boton-personalizado {
  background: var(--onpe-ui-green);
  color: white;
  padding: 12px 24px;
  border-radius: 8px;
}

Sobrescribir colores si es necesario:

/* En tu archivo CSS del proyecto host */
:root {
  /* Cambiar el azul principal */
  --onpe-ui-blue: #1a365d;
  
  /* Cambiar el rojo */
  --onpe-ui-red: #c53030;
}

Uso en componentes React con estilos inline:

function MiComponente() {
  return (
    <div 
      style={{
        color: 'var(--onpe-ui-blue)',
        backgroundColor: 'var(--onpe-ui-skyblue-light)',
        padding: '16px',
        borderRadius: '8px'
      }}
    >
      Contenido con colores ONPE
    </div>
  );
}

🎯 ¿Cómo Evitamos Conflictos?

  1. Prefijos Únicos: bg-blue-500onpe-bg-onpe-ui-blue
  2. CSS Scoped: Todos los componentes están aislados
  3. Variables CSS Aisladas: --onpe-ui-blue en lugar de --blue
  4. Sin Preflight: No resetea estilos del proyecto host

📖 Uso Básico

Instalar Componentes con CLI

Instalar componentes específicos

# Componentes
npx @onpe/ui add button
npx @onpe/ui add modal
npx @onpe/ui add portal
npx @onpe/ui add overlay
npx @onpe/ui add show

# Iconos
npx @onpe/ui add icon-close
npx @onpe/ui add icon-check
npx @onpe/ui add icon-warning
npx @onpe/ui add icon-chrome
npx @onpe/ui add icon-firefox
npx @onpe/ui add icon-safari
npx @onpe/ui add icon-edge
npx @onpe/ui add icon-windows
npx @onpe/ui add icon-apple
npx @onpe/ui add icon-android

Usar componentes instalados individualmente

// Después de instalar con CLI
import { Button } from './components/onpe-ui/Button';
import { Modal } from './components/onpe-ui/Modal';
import { IconClose } from './components/onpe-icons/IconClose';
import { useState } from 'react';

function App() {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div className="p-4">
      <Button 
        color="primary" 
        title="Abrir Modal" 
        onClick={() => setIsOpen(true)} 
      />
      
      <Modal 
        isOpen={isOpen} 
        onClose={() => setIsOpen(false)}
        closeButton={true}
        overlayColor="blue"
      >
        <div className="p-6">
          <h2 className="text-xl font-bold mb-4">Contenido del Modal</h2>
          <p className="mb-4">Este es un ejemplo de modal con contenido.</p>
          <div className="flex items-center gap-2">
            <IconClose className="w-4 h-4" />
            <Button 
              color="green" 
              title="Cerrar" 
              onClick={() => setIsOpen(false)} 
            />
          </div>
        </div>
      </Modal>
    </div>
  );
}

Configuración requerida para componentes individuales

1. Instalar Tailwind CSS:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

2. Configurar PostCSS para Tailwind v4:

// postcss.config.js
export default {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
}

3. Crear archivo CSS con configuración Tailwind v4:

/* onpe-ui.css */
@import "tailwindcss";

@theme {
  --color-onpe-ui-blue: #003770;
  --color-onpe-ui-skyblue: #0073cf;
  --color-onpe-ui-skyblue-light: #69b2e8;
  --color-onpe-ui-yellow: #ffb81c;
  --color-onpe-ui-light-skyblue: #aaeff6;
  --color-onpe-ui-gray: #bcbcbc;
  --color-onpe-ui-gray-light: #bdbdbd;
  --color-onpe-ui-gray-extra-light: #f2f2f2;
  --color-onpe-ui-red: #e3002b;
  --color-onpe-ui-dark-gray: #4f4f4f;
  --color-onpe-ui-green: #76bd43;
  --color-onpe-ui-yellow-light: #FFF1D2;
}

/* Clases personalizadas ONPE */
@utility bg-onpe-ui-blue { background-color: var(--color-onpe-ui-blue); }
@utility text-onpe-ui-blue { color: var(--color-onpe-ui-blue); }
@utility border-onpe-ui-blue { border-color: var(--color-onpe-ui-blue); }
/* ... resto de clases personalizadas */

4. Importar el archivo CSS en tu aplicación:

// En tu archivo principal (index.tsx o App.tsx)
import './onpe-ui.css'; // ← IMPORTANTE: Importar primero
import { Button } from './components/onpe-ui/Button';

5. Para componentes que usan Portal, agregar en public/index.html:

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8" />
    <title>Mi App</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="portal"></div>
  </body>
</html>

🎨 Paleta de Colores ONPE

Colores Principales

  • Azul Principal: #003770 - Color institucional principal
  • Sky Blue: #0073cf - Color secundario
  • Sky Blue Light: #69b2e8 - Color claro
  • Light Sky Blue: #aaeff6 - Color muy claro

Colores de Acento

  • Amarillo: #ffb81c - Para alertas y destacados
  • Verde: #76bd43 - Para éxito y confirmaciones
  • Rojo: #e3002b - Para errores y advertencias

Escala de Grises

  • Dark Gray: #4f4f4f - Texto principal
  • Gray: #bcbcbc - Texto secundario
  • Gray Light: #bdbdbd - Texto terciario
  • Gray Extra Light: #f2f2f2 - Fondos suaves

🔗 Dependencias entre Componentes

Mapa de Dependencias

Modal
├── Portal (requerido)
├── Overlay (requerido)
└── IconClose (requerido)

ModalBrowserIncompatible
├── Modal (requerido)
├── IconWarning (requerido)
├── IconChromeColor (requerido)
├── IconSafariColor (requerido)
├── IconMozillaColor (requerido)
└── IconEdgeColor (requerido)

ModalSystemIncompatible
├── Modal (requerido)
├── IconWarning (requerido)
├── IconWindow (requerido)
├── IconAndroid (requerido)
└── IconApple (requerido)

Portal
└── react-dom (createPortal)

Overlay
└── (sin dependencias externas)

Button
└── (sin dependencias externas)

Show
└── (sin dependencias externas)

Instalación Automática de Dependencias

Modal - Instala automáticamente sus dependencias:

npx @onpe/ui add modal
# Esto instalará automáticamente:
# - Portal.tsx
# - Overlay.tsx
# - IconClose.tsx (si está disponible)

ModalBrowserIncompatible - Instala automáticamente sus dependencias:

npx @onpe/ui add modal-browser-incompatible
# Esto instalará automáticamente:
# - Modal.tsx
# - IconWarning.tsx
# - IconChromeColor.tsx
# - IconSafariColor.tsx
# - IconMozillaColor.tsx
# - IconEdgeColor.tsx

ModalSystemIncompatible - Instala automáticamente sus dependencias:

npx @onpe/ui add modal-system-incompatible
# Esto instalará automáticamente:
# - Modal.tsx
# - IconWarning.tsx
# - IconWindow.tsx
# - IconAndroid.tsx
# - IconApple.tsx

Otros componentes - Instalación independiente:

npx @onpe/ui add button    # Sin dependencias
npx @onpe/ui add portal    # Sin dependencias
npx @onpe/ui add overlay   # Sin dependencias
npx @onpe/ui add show      # Sin dependencias

Estructura de Archivos Después de la Instalación

src/
└── components/
    ├── ui/           # shadcn/ui (si está instalado)
    │   ├── button.tsx
    │   └── input.tsx
    ├── onpe-ui/      # ONPE UI - Componentes
    │   ├── Button.tsx
    │   ├── Modal.tsx
    │   ├── Overlay.tsx
    │   ├── Portal.tsx
    │   └── Show.tsx
    └── onpe-icons/   # ONPE UI - Iconos
        ├── IconClose.tsx
        ├── IconCheck.tsx
        ├── IconChrome.tsx
        ├── IconFirefox.tsx
        └── IconWindows.tsx

🧩 Componentes Disponibles

Button

Botón versátil con múltiples colores y tamaños.

Ejemplo Básico

import { Button } from '@onpe/ui/components';

function App() {
  return (
    <div className="space-y-4 p-4">
      <h2 className="text-2xl font-bold">Botones ONPE</h2>
      
      {/* Colores disponibles */}
      <div className="space-y-2">
        <h3 className="text-lg font-semibold">Colores:</h3>
        <div className="flex flex-wrap gap-2">
          <Button color="primary" title="Primario" />
          <Button color="blue" title="Azul" />
          <Button color="skyblue" title="Sky Blue" />
          <Button color="green" title="Verde" />
          <Button color="yellow" title="Amarillo" />
          <Button color="red" title="Rojo" />
        </div>
      </div>

      {/* Tamaños */}
      <div className="space-y-2">
        <h3 className="text-lg font-semibold">Tamaños:</h3>
        <div className="flex items-center gap-2">
          <Button color="primary" title="Pequeño" size="small" />
          <Button color="primary" title="Mediano" size="normal" />
          <Button color="primary" title="Grande" size="large" />
        </div>
      </div>

      {/* Estados */}
      <div className="space-y-2">
        <h3 className="text-lg font-semibold">Estados:</h3>
        <div className="flex gap-2">
          <Button color="primary" title="Normal" />
          <Button color="primary" title="Deshabilitado" disabled />
        </div>
      </div>
    </div>
  );
}

Ejemplo con Funcionalidad

import { Button } from '@onpe/ui/components';
import { useState } from 'react';

function VotingApp() {
  const [voted, setVoted] = useState(false);
  const [loading, setLoading] = useState(false);

  const handleVote = async () => {
    setLoading(true);
    // Simular llamada a API
    await new Promise(resolve => setTimeout(resolve, 2000));
    setVoted(true);
    setLoading(false);
  };

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg">
      <h2 className="text-xl font-bold mb-4">Sistema de Votación</h2>
      
      {!voted ? (
        <div className="space-y-4">
          <p className="text-gray-600">¿Desea votar por esta opción?</p>
          <Button 
            color="primary" 
            title={loading ? "Procesando..." : "Votar Ahora"}
            onClick={handleVote}
            disabled={loading}
            size="large"
          />
        </div>
      ) : (
        <div className="text-center">
          <p className="text-green-600 font-semibold mb-4">¡Voto registrado exitosamente!</p>
          <Button 
            color="green" 
            title="Ver Resultados"
            onClick={() => setVoted(false)}
          />
        </div>
      )}
    </div>
  );
}

Props del Button

| Prop | Tipo | Default | Descripción | |------|------|---------|-------------| | 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 | | title | string | - | Texto del botón (requerido) | | size | 'small' \| 'normal' \| 'large' | 'normal' | Tamaño del botón | | disabled | boolean | false | Estado deshabilitado | | className | string | - | Clases CSS adicionales |

Modal

Componente modal para mostrar contenido en overlay.

Ejemplo Básico

import { Modal } from '@onpe/ui/components';
import { useState } from 'react';

function App() {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div className="p-4">
      <button 
        onClick={() => setIsOpen(true)}
        className="bg-blue-500 text-white px-4 py-2 rounded"
      >
        Abrir Modal
      </button>
      
      <Modal 
        isOpen={isOpen} 
        onClose={() => setIsOpen(false)}
        closeButton={true}
        overlayColor="blue"
      >
        <div className="p-6">
          <h2 className="text-xl font-bold mb-4">Título del Modal</h2>
          <p className="mb-4">Este es el contenido del modal.</p>
          <button 
            onClick={() => setIsOpen(false)}
            className="bg-gray-500 text-white px-4 py-2 rounded"
          >
            Cerrar
          </button>
        </div>
      </Modal>
    </div>
  );
}

Ejemplo Avanzado - Modal de Confirmación

import { Modal } from '@onpe/ui/components';
import { useState } from 'react';

function DeleteConfirmation() {
  const [showModal, setShowModal] = useState(false);
  const [itemToDelete, setItemToDelete] = useState('');

  const handleDelete = (itemName) => {
    setItemToDelete(itemName);
    setShowModal(true);
  };

  const confirmDelete = () => {
    // Lógica para eliminar el elemento
    console.log(`Eliminando: ${itemToDelete}`);
    setShowModal(false);
    setItemToDelete('');
  };

  return (
    <div className="p-4">
      <div className="space-y-2">
        <button 
          onClick={() => handleDelete('Usuario 1')}
          className="bg-red-500 text-white px-4 py-2 rounded mr-2"
        >
          Eliminar Usuario 1
        </button>
        <button 
          onClick={() => handleDelete('Documento 2')}
          className="bg-red-500 text-white px-4 py-2 rounded mr-2"
        >
          Eliminar Documento 2
        </button>
      </div>

      <Modal 
        isOpen={showModal} 
        onClose={() => setShowModal(false)}
        closeButton={true}
        overlayColor="red"
        closeDisabled={false}
      >
        <div className="p-6 text-center">
          <div className="mb-4">
            <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
              <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <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" />
              </svg>
            </div>
            <h3 className="text-lg font-medium text-gray-900 mb-2">
              Confirmar Eliminación
            </h3>
            <p className="text-sm text-gray-500">
              ¿Está seguro de que desea eliminar <strong>{itemToDelete}</strong>? 
              Esta acción no se puede deshacer.
            </p>
          </div>
          
          <div className="flex justify-center space-x-3">
            <button
              onClick={() => setShowModal(false)}
              className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md text-sm font-medium"
            >
              Cancelar
            </button>
            <button
              onClick={confirmDelete}
              className="bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium"
            >
              Eliminar
            </button>
          </div>
        </div>
      </Modal>
    </div>
  );
}

Ejemplo - Modal de Formulario

import { Modal } from '@onpe/ui/components';
import { useState } from 'react';

function UserForm() {
  const [isOpen, setIsOpen] = useState(false);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: ''
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Datos del formulario:', formData);
    setIsOpen(false);
    setFormData({ name: '', email: '', phone: '' });
  };

  return (
    <div className="p-4">
      <button 
        onClick={() => setIsOpen(true)}
        className="bg-green-500 text-white px-4 py-2 rounded"
      >
        Agregar Usuario
      </button>

      <Modal 
        isOpen={isOpen} 
        onClose={() => setIsOpen(false)}
        closeButton={true}
        overlayColor="skyblue"
      >
        <div className="p-6">
          <h2 className="text-xl font-bold mb-4">Nuevo Usuario</h2>
          
          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Nombre Completo
              </label>
              <input
                type="text"
                value={formData.name}
                onChange={(e) => setFormData({...formData, name: e.target.value})}
                className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                required
              />
            </div>
            
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Correo Electrónico
              </label>
              <input
                type="email"
                value={formData.email}
                onChange={(e) => setFormData({...formData, email: e.target.value})}
                className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                required
              />
            </div>
            
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">
                Teléfono
              </label>
              <input
                type="tel"
                value={formData.phone}
                onChange={(e) => setFormData({...formData, phone: e.target.value})}
                className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
              />
            </div>
            
            <div className="flex justify-end space-x-3 pt-4">
              <button
                type="button"
                onClick={() => setIsOpen(false)}
                className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md"
              >
                Cancelar
              </button>
              <button
                type="submit"
                className="bg-blue-600 text-white px-4 py-2 rounded-md"
              >
                Guardar Usuario
              </button>
            </div>
          </form>
        </div>
      </Modal>
    </div>
  );
}

Overlay

Componente overlay para superponer contenido.

import { Overlay } from '@onpe/ui/components';

function App() {
  return (
    <Overlay>
      <div>Contenido superpuesto</div>
    </Overlay>
  );
}

Portal

Componente portal para renderizar fuera del DOM padre.

import { Portal } from '@onpe/ui/components';

function App() {
  return (
    <Portal>
      <div>Contenido renderizado en portal</div>
    </Portal>
  );
}

Show

Componente condicional para mostrar/ocultar contenido.

import { Show } from '@onpe/ui/components';

function App() {
  const [visible, setVisible] = useState(true);
  
  return (
    <Show when={visible}>
      <div>Contenido visible</div>
    </Show>
  );
}

ModalConfirm

Modal de confirmación para acciones importantes con colores dinámicos.

import { ModalConfirm } from '@onpe/ui/components';

function App() {
  const [showConfirm, setShowConfirm] = useState(false);
  
  return (
    <div className="space-y-4">
      {/* Modal Azul (Por Defecto) */}
      <ModalConfirm
        isOpen={showConfirm}
        onClose={() => setShowConfirm(false)}
        onConfirm={() => {
          // Acción a confirmar
          setShowConfirm(false);
        }}
        title="Confirmar acción"
        message="¿Estás seguro de realizar esta acción?"
        color="blue" // Por defecto
      />

      {/* Modal Rojo para Advertencias */}
      <ModalConfirm
        isOpen={showConfirm}
        onClose={() => setShowConfirm(false)}
        onConfirm={() => {
          // Acción peligrosa
          setShowConfirm(false);
        }}
        title="Advertencia"
        message="Esta acción es irreversible y no se puede deshacer."
        color="red" // Color rojo para advertencias
        icon="warning"
      />
    </div>
  );
}

Props del ModalConfirm

| Prop | Tipo | Default | Descripción | |------|------|---------|-------------| | isOpen | boolean | - | Estado de apertura del modal (requerido) | | onClose | function | - | Función para cerrar el modal (requerido) | | onConfirm | function | - | Función para confirmar la acción (requerido) | | title | string | - | Título del modal (requerido) | | message | string | - | Mensaje del modal (requerido) | | color | 'blue' \| 'red' | 'blue' | Color del icono y título | | icon | 'warning' \| 'success' | 'warning' | Tipo de icono a mostrar | | confirmText | string | 'Confirmar' | Texto del botón de confirmación | | cancelText | string | 'Cancelar' | Texto del botón de cancelación |

ModalLoading

Modal de carga para mostrar estados de procesamiento.

import { ModalLoading } from '@onpe/ui/components';

function App() {
  const [loading, setLoading] = useState(false);
  
  return (
    <ModalLoading
      isOpen={loading}
      message="Procesando información..."
    />
  );
}

ModalBrowserIncompatible

Modal mejorado para mostrar cuando el navegador no es compatible con el sistema de votación.

import { ModalBrowserIncompatible } from '@onpe/ui/components';

function App() {
  const [showBrowserModal, setShowBrowserModal] = useState(false);
  
  return (
    <ModalBrowserIncompatible
      isOpen={showBrowserModal}
      onClose={() => setShowBrowserModal(false)}
    />
  );
}

Características del modal mejorado:

  • ✅ Mensaje claro sobre incompatibilidad
  • ✅ Lista visual de navegadores compatibles con nombres
  • ✅ Información sobre seguridad y versiones
  • ✅ Diseño responsive y accesible
  • ✅ Colores institucionales ONPE

ModalSystemIncompatible

Modal mejorado para mostrar cuando el sistema operativo no es compatible con ONPEID.

import { ModalSystemIncompatible } from '@onpe/ui/components';

function App() {
  const [showSystemModal, setShowSystemModal] = useState(false);
  
  return (
    <ModalSystemIncompatible
      isOpen={showSystemModal}
      onClose={() => setShowSystemModal(false)}
    />
  );
}

Características del modal mejorado:

  • ✅ Información específica sobre ONPEID
  • ✅ Lista de sistemas operativos compatibles con versiones mínimas
  • ✅ Alternativa de acceso web
  • ✅ Información de seguridad sobre fuentes oficiales
  • ✅ Diseño intuitivo y profesional

🎯 Hooks Disponibles

useDebounce

Hook para retrasar la ejecución de funciones.

import { useDebounce } from '@onpe/ui/hooks';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);
  
  useEffect(() => {
    // Buscar solo después de 500ms sin cambios
    searchAPI(debouncedQuery);
  }, [debouncedQuery]);
  
  return (
    <input 
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Buscar..."
    />
  );
}

useLocalStorage

Hook para manejar localStorage de forma reactiva.

import { useLocalStorage } from '@onpe/ui/hooks';

function SettingsComponent() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value)}>
      <option value="light">Claro</option>
      <option value="dark">Oscuro</option>
    </select>
  );
}

🎨 Iconos Disponibles

La librería incluye una colección completa de iconos organizados por categorías:

Iconos de Acciones

  • Iconos para acciones comunes (editar, eliminar, guardar, etc.)

Iconos de Navegadores

  • Iconos de navegadores web (Chrome, Firefox, Safari, Edge, etc.)

Iconos de Sistemas Operativos

  • Iconos de sistemas operativos (Windows, macOS, Linux, etc.)

Iconos ONPE

  • Iconos específicos de la institución ONPE

Logos

  • Logotipos oficiales y marcas
import { 
  IconChrome, 
  IconFirefox, 
  IconSafari,
  IconWindows,
  IconMacOS 
} from '@onpe/ui/icons';

function App() {
  return (
    <div>
      <IconChrome className="w-6 h-6" />
      <IconFirefox className="w-6 h-6" />
      <IconSafari className="w-6 h-6" />
    </div>
  );
}

🛠️ Utilidades

formatDate

Función para formatear fechas según estándares peruanos.

import { formatDate } from '@onpe/ui/utils';

const fecha = new Date('2024-04-14');
const fechaFormateada = formatDate(fecha, 'dd/mm/yyyy');
console.log(fechaFormateada); // "14/04/2024"

validateEmail

Función para validar direcciones de correo electrónico.

import { validateEmail } from '@onpe/ui/utils';

const email = '[email protected]';
const esValido = validateEmail(email);
console.log(esValido); // true

📱 Breakpoints Responsive

La librería incluye breakpoints personalizados para ONPE:

  • sm: 640px
  • md: 768px
  • lg: 1024px
  • 2lg: 1200px
  • xl: 1280px
  • 2xl: 1536px
  • 3xl: 1650px
/* Ejemplo de uso */
@media (min-width: 1200px) {
  .container {
    max-width: 1200px;
  }
}

🎨 Clases de Utilidad

Colores de Texto

.text-onpe-ui-blue          /* Azul principal */
.text-onpe-ui-skyblue       /* Sky blue */
.text-onpe-ui-yellow        /* Amarillo */
.text-onpe-ui-green         /* Verde */
.text-onpe-ui-red           /* Rojo */
.text-onpe-ui-gray          /* Gris */
.text-onpe-ui-dark-gray     /* Gris oscuro */

Colores de Fondo

.bg-onpe-ui-blue            /* Fondo azul */
.bg-onpe-ui-skyblue         /* Fondo sky blue */
.bg-onpe-ui-yellow          /* Fondo amarillo */
.bg-onpe-ui-green           /* Fondo verde */
.bg-onpe-ui-red             /* Fondo rojo */
.bg-onpe-ui-gray            /* Fondo gris */
.bg-onpe-ui-gray-light      /* Fondo gris claro */
.bg-onpe-ui-gray-extra-light /* Fondo gris muy claro */

🛡️ Compatibilidad con CSP (Content Security Policy)

La librería es completamente compatible con Content Security Policy estricto.

✅ Características CSP

  • Archivos CSS externos: No usa estilos inline que violen CSP
  • Sin 'unsafe-inline': Compatible con style-src 'self'
  • Variables CSS en :root: Disponibles globalmente sin violar políticas
  • Archivos estáticos: Todos los recursos se sirven como archivos externos

🔧 Configuración CSP Recomendada

<!-- En tu HTML head -->
<meta http-equiv="Content-Security-Policy" 
      content="style-src 'self' https://fonts.googleapis.com; 
               script-src 'self'; 
               img-src 'self' data:;">

📋 Instrucciones para Proyectos con CSP

1. Instalar la librería:

npm install @onpe/[email protected]

2. Importar CSS (CRÍTICO):

// ✅ CORRECTO - Esto carga el CSS externo
import '@onpe/ui/dist/index.css';
import { ModalConfirm, Button } from '@onpe/ui/components';

3. Verificar que las variables CSS estén disponibles:

// Las variables CSS se definen automáticamente en :root
function MiComponente() {
  return (
    <div style={{ color: 'var(--onpe-ui-blue)' }}>
      Este texto usa el color azul ONPE
    </div>
  );
}

⚠️ Problemas Comunes con CSP

Error: "Refused to apply inline style"

# ❌ PROBLEMA: Estilos inline bloqueados por CSP
# ✅ SOLUCIÓN: Usar archivos CSS externos (ya implementado)

Error: "Variable CSS not defined"

// ❌ PROBLEMA: No importar el CSS
import { Button } from '@onpe/ui/components';

// ✅ SOLUCIÓN: Importar CSS primero
import '@onpe/ui/dist/index.css';
import { Button } from '@onpe/ui/components';

📋 Versiones

  • v1.2.40 - Versión actual con CSP compatible
  • Compatible con React 16.8+
  • TailwindCSS v4
  • TypeScript 5.3+
  • CSP (Content Security Policy) compatible

🔧 Desarrollo

Requisitos

  • Node.js 18+
  • npm 9+

Instalación para desarrollo

git clone https://github.com/ricardosv46/onpe-ui.git
cd onpe-ui
npm install

Scripts disponibles

npm run build          # Construir para producción
npm run dev            # Desarrollo con watch mode
npm run storybook      # Ver componentes en Storybook
npm run lint           # Verificar código
npm run lint:fix       # Corregir problemas de linting
npm run type-check     # Verificar tipos TypeScript

📄 Licencia

MIT © ONPE - Oficina Nacional de Procesos Electorales

🤝 Contribuir

  1. Fork el proyecto
  2. Crea una rama para tu feature (git checkout -b feature/nueva-funcionalidad)
  3. Commit tus cambios (git commit -m 'Agregar nueva funcionalidad')
  4. Push a la rama (git push origin feature/nueva-funcionalidad)
  5. Abre un Pull Request

🚀 Ejemplos Completos de Aplicaciones

Aplicación de Votación Electrónica

import { Button, Modal, Show } from '@onpe/ui/components';
import { useState } from 'react';

function VotingApp() {
  const [currentStep, setCurrentStep] = useState(1);
  const [selectedCandidate, setSelectedCandidate] = useState('');
  const [showConfirmModal, setShowConfirmModal] = useState(false);
  const [voted, setVoted] = useState(false);

  const candidates = [
    { id: '1', name: 'Juan Pérez', party: 'Partido Democrático' },
    { id: '2', name: 'María García', party: 'Partido Progresista' },
    { id: '3', name: 'Carlos López', party: 'Partido Independiente' }
  ];

  const handleVote = () => {
    setVoted(true);
    setShowConfirmModal(false);
    setCurrentStep(4);
  };

  return (
    <div className="min-h-screen bg-gray-50 py-8">
      <div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-bold text-gray-900 mb-2">
            Sistema de Votación ONPE
          </h1>
          <p className="text-gray-600">
            Seleccione su candidato preferido
          </p>
        </div>

        <Show when={currentStep === 1}>
          <div className="space-y-4">
            <h2 className="text-xl font-semibold mb-4">Candidatos Disponibles:</h2>
            {candidates.map((candidate) => (
              <div 
                key={candidate.id}
                className={`p-4 border-2 rounded-lg cursor-pointer transition-colors ${
                  selectedCandidate === candidate.id 
                    ? 'border-blue-500 bg-blue-50' 
                    : 'border-gray-200 hover:border-gray-300'
                }`}
                onClick={() => setSelectedCandidate(candidate.id)}
              >
                <div className="flex items-center justify-between">
                  <div>
                    <h3 className="font-semibold text-lg">{candidate.name}</h3>
                    <p className="text-gray-600">{candidate.party}</p>
                  </div>
                  {selectedCandidate === candidate.id && (
                    <div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
                      <svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
                        <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" />
                      </svg>
                    </div>
                  )}
                </div>
              </div>
            ))}
            
            <div className="pt-4">
              <Button 
                color="primary" 
                title="Continuar" 
                size="large"
                disabled={!selectedCandidate}
                onClick={() => setCurrentStep(2)}
              />
            </div>
          </div>
        </Show>

        <Show when={currentStep === 2}>
          <div className="text-center">
            <h2 className="text-xl font-semibold mb-4">Confirmar Voto</h2>
            <div className="bg-gray-50 p-4 rounded-lg mb-6">
              <p className="text-gray-600 mb-2">Ha seleccionado:</p>
              <p className="font-semibold text-lg">
                {candidates.find(c => c.id === selectedCandidate)?.name}
              </p>
              <p className="text-gray-600">
                {candidates.find(c => c.id === selectedCandidate)?.party}
              </p>
            </div>
            
            <div className="flex justify-center space-x-4">
              <Button 
                color="gray" 
                title="Volver" 
                onClick={() => setCurrentStep(1)}
              />
              <Button 
                color="primary" 
                title="Confirmar Voto" 
                onClick={() => setShowConfirmModal(true)}
              />
            </div>
          </div>
        </Show>

        <Show when={currentStep === 4}>
          <div className="text-center">
            <div className="mb-6">
              <div className="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4">
                <svg className="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                </svg>
              </div>
              <h2 className="text-2xl font-bold text-green-600 mb-2">
                ¡Voto Registrado Exitosamente!
              </h2>
              <p className="text-gray-600">
                Su voto ha sido procesado correctamente. Gracias por participar en la democracia.
              </p>
            </div>
            
            <Button 
              color="green" 
              title="Ver Resultados" 
              size="large"
              onClick={() => window.location.reload()}
            />
          </div>
        </Show>

        {/* Modal de Confirmación */}
        <Modal 
          isOpen={showConfirmModal} 
          onClose={() => setShowConfirmModal(false)}
          closeButton={true}
          overlayColor="blue"
        >
          <div className="p-6 text-center">
            <div className="mb-4">
              <div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 mb-4">
                <svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <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" />
                </svg>
              </div>
              <h3 className="text-lg font-medium text-gray-900 mb-2">
                Confirmar Voto
              </h3>
              <p className="text-sm text-gray-500">
                ¿Está seguro de que desea votar por{' '}
                <strong>{candidates.find(c => c.id === selectedCandidate)?.name}</strong>?
                Esta acción no se puede deshacer.
              </p>
            </div>
            
            <div className="flex justify-center space-x-3">
              <Button
                color="gray"
                title="Cancelar"
                onClick={() => setShowConfirmModal(false)}
              />
              <Button
                color="primary"
                title="Confirmar Voto"
                onClick={handleVote}
              />
            </div>
          </div>
        </Modal>
      </div>
    </div>
  );
}

export default VotingApp;

Dashboard de Administración

import { Button, Modal, Show } from '@onpe/ui/components';
import { useState } from 'react';

function AdminDashboard() {
  const [activeTab, setActiveTab] = useState('users');
  const [showUserModal, setShowUserModal] = useState(false);
  const [users, setUsers] = useState([
    { id: 1, name: 'Juan Pérez', email: '[email protected]', role: 'Admin' },
    { id: 2, name: 'María García', email: '[email protected]', role: 'Usuario' },
    { id: 3, name: 'Carlos López', email: '[email protected]', role: 'Usuario' }
  ]);

  const [newUser, setNewUser] = useState({
    name: '',
    email: '',
    role: 'Usuario'
  });

  const addUser = () => {
    const user = {
      id: users.length + 1,
      ...newUser
    };
    setUsers([...users, user]);
    setNewUser({ name: '', email: '', role: 'Usuario' });
    setShowUserModal(false);
  };

  return (
    <div className="min-h-screen bg-gray-100">
      {/* Header */}
      <header className="bg-white shadow-sm border-b">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between items-center py-4">
            <div>
              <h1 className="text-2xl font-bold text-gray-900">Dashboard ONPE</h1>
              <p className="text-gray-600">Panel de administración del sistema electoral</p>
            </div>
            <div className="flex items-center space-x-4">
              <span className="text-sm text-gray-500">Administrador</span>
              <Button color="primary" title="Cerrar Sesión" size="small" />
            </div>
          </div>
        </div>
      </header>

      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        {/* Navigation Tabs */}
        <div className="mb-8">
          <nav className="flex space-x-8">
            <button
              onClick={() => setActiveTab('users')}
              className={`py-2 px-1 border-b-2 font-medium text-sm ${
                activeTab === 'users'
                  ? 'border-blue-500 text-blue-600'
                  : 'border-transparent text-gray-500 hover:text-gray-700'
              }`}
            >
              Usuarios
            </button>
            <button
              onClick={() => setActiveTab('elections')}
              className={`py-2 px-1 border-b-2 font-medium text-sm ${
                activeTab === 'elections'
                  ? 'border-blue-500 text-blue-600'
                  : 'border-transparent text-gray-500 hover:text-gray-700'
              }`}
            >
              Elecciones
            </button>
            <button
              onClick={() => setActiveTab('reports')}
              className={`py-2 px-1 border-b-2 font-medium text-sm ${
                activeTab === 'reports'
                  ? 'border-blue-500 text-blue-600'
                  : 'border-transparent text-gray-500 hover:text-gray-700'
              }`}
            >
              Reportes
            </button>
          </nav>
        </div>

        {/* Users Tab */}
        <Show when={activeTab === 'users'}>
          <div className="bg-white rounded-lg shadow">
            <div className="px-6 py-4 border-b border-gray-200">
              <div className="flex justify-between items-center">
                <h2 className="text-lg font-medium text-gray-900">Gestión de Usuarios</h2>
                <Button 
                  color="green" 
                  title="Agregar Usuario" 
                  onClick={() => setShowUserModal(true)}
                />
              </div>
            </div>
            
            <div className="overflow-x-auto">
              <table className="min-w-full divide-y divide-gray-200">
                <thead className="bg-gray-50">
                  <tr>
                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Nombre
                    </th>
                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Email
                    </th>
                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Rol
                    </th>
                    <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                      Acciones
                    </th>
                  </tr>
                </thead>
                <tbody className="bg-white divide-y divide-gray-200">
                  {users.map((user) => (
                    <tr key={user.id}>
                      <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
                        {user.name}
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                        {user.email}
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap">
                        <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
                          user.role === 'Admin' 
                            ? 'bg-red-100 text-red-800' 
                            : 'bg-green-100 text-green-800'
                        }`}>
                          {user.role}
                        </span>
                      </td>
                      <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
                        <div className="flex space-x-2">
                          <Button color="skyblue" title="Editar" size="small" />
                          <Button color="red" title="Eliminar" size="small" />
                        </div>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </Show>

        {/* Elections Tab */}
        <Show when={activeTab === 'elections'}>
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-lg font-medium text-gray-900 mb-4">Gestión de Elecciones</h2>
            <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
              <div className="bg-blue-50 p-4 rounded-lg">
                <h3 className="font-semibold text-blue-900">Elecciones Generales</h3>
                <p className="text-blue-700 text-sm mt-1">14 de abril, 2024</p>
                <Button color="blue" title="Ver Detalles" size="small" />
              </div>
              <div className="bg-green-50 p-4 rounded-lg">
                <h3 className="font-semibold text-green-900">Elecciones Regionales</h3>
                <p className="text-green-700 text-sm mt-1">28 de octubre, 2024</p>
                <Button color="green" title="Ver Detalles" size="small" />
              </div>
              <div className="bg-yellow-50 p-4 rounded-lg">
                <h3 className="font-semibold text-yellow-900">Elecciones Municipales</h3>
                <p className="text-yellow-700 text-sm mt-1">15 de diciembre, 2024</p>
                <Button color="yellow" title="Ver Detalles" size="small" />
              </div>
            </div>
          </div>
        </Show>

        {/* Reports Tab */}
        <Show when={activeTab === 'reports'}>
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-lg font-medium text-gray-900 mb-4">Reportes del Sistema</h2>
            <div className="space-y-4">
              <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
                <div>
                  <h3 className="font-medium">Reporte de Participación Electoral</h3>
                  <p className="text-sm text-gray-600">Generado el 15 de abril, 2024</p>
                </div>
                <Button color="primary" title="Descargar PDF" size="small" />
              </div>
              <div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg">
                <div>
                  <h3 className="font-medium">Estadísticas de Usuarios</h3>
                  <p className="text-sm text-gray-600">Generado el 14 de abril, 2024</p>
                </div>
                <Button color="primary" title="Descargar PDF" size="small" />
              </div>
            </div>
          </div>
        </Show>

        {/* Add User Modal */}
        <Modal 
          isOpen={showUserModal} 
          onClose={() => setShowUserModal(false)}
          closeButton={true}
          overlayColor="skyblue"
        >
          <div className="p-6">
            <h2 className="text-xl font-bold mb-4">Agregar Nuevo Usuario</h2>
            
            <div className="space-y-4">
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Nombre Completo
                </label>
                <input
                  type="text"
                  value={newUser.name}
                  onChange={(e) => setNewUser({...newUser, name: e.target.value})}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                  placeholder="Ingrese el nombre completo"
                />
              </div>
              
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Correo Electrónico
                </label>
                <input
                  type="email"
                  value={newUser.email}
                  onChange={(e) => setNewUser({...newUser, email: e.target.value})}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                  placeholder="[email protected]"
                />
              </div>
              
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Rol
                </label>
                <select
                  value={newUser.role}
                  onChange={(e) => setNewUser({...newUser, role: e.target.value})}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                >
                  <option value="Usuario">Usuario</option>
                  <option value="Admin">Administrador</option>
                </select>
              </div>
              
              <div className="flex justify-end space-x-3 pt-4">
                <Button
                  color="gray"
                  title="Cancelar"
                  onClick={() => setShowUserModal(false)}
                />
                <Button
                  color="green"
                  title="Agregar Usuario"
                  onClick={addUser}
                />
              </div>
            </div>
          </div>
        </Modal>
      </div>
    </div>
  );
}

export default AdminDashboard;

🐛 Solución de Problemas

Problemas con la CLI

Error: "Componente no encontrado"

# Verificar componentes disponibles
npx @onpe/ui add --help

# Componentes válidos:
# button, modal, overlay, portal, show

Error: "No se pudo descargar el componente"

# Verificar conexión a internet
ping github.com

# Verificar que el repositorio esté disponible
curl https://raw.githubusercontent.com/ricardosv46/onpe-ui/main/src/components/Button/Button.tsx

Los estilos no se aplican

# Verificar que Tailwind esté instalado
npm list tailwindcss

# Verificar configuración
cat tailwind.config.js

Portal no funciona

# Verificar que tengas el elemento portal en HTML
grep -r "id=\"portal\"" public/

Problemas con la Librería Completa

Error: "Module not found"

# Verificar instalación
npm list @onpe/ui

# Reinstalar si es necesario
npm uninstall @onpe/ui
npm install @onpe/ui

Estilos no se cargan

/* Verificar que tengas la importación correcta */
import './onpe-ui.css';
import { Button } from './components/onpe-ui/Button';

Solución: Verificar rutas de importación

// ✅ CORRECTO: Importar archivo CSS personalizado
import './onpe-ui.css';
import { Button } from './components/onpe-ui/Button';
import { IconClose } from './components/onpe-icons/IconClose';

// ❌ INCORRECTO: No importar el archivo CSS
import { Button } from './components/onpe-ui/Button';

Solución: Verificar configuración de bundler

// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      '@onpe/ui': path.resolve(__dirname, 'node_modules/@onpe/ui')
    }
  }
};

// vite.config.js
export default {
  resolve: {
    alias: {
      '@onpe/ui': path.resolve(__dirname, 'node_modules/@onpe/ui')
    }
  }
};

Solución: Verificar orden de importación

// ✅ CORRECTO - Importar estilos ANTES de los componentes
import '@onpe/ui/styles';
import { Button } from '@onpe/ui/components';

// ❌ INCORRECTO - Importar estilos DESPUÉS de los componentes
import { Button } from '@onpe/ui/components';
import '@onpe/ui/styles';

Problemas con Estilos

Los componentes se ven sin estilos

# Verificar que la librería esté instalada
npm list @onpe/ui

# Verificar que los estilos estén en node_modules
ls node_modules/@onpe/ui/dist/

Solución: Verificar importación de estilos

// En tu archivo principal (index.tsx o App.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import '@onpe/ui/styles'; // ← IMPORTANTE: Importar estilos primero
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Solución: Verificar configuración de CSS

/* En tu archivo CSS principal */
@import "@onpe/ui/styles";

/* O si usas CSS modules */
@import "~@onpe/ui/styles";

Los colores personalizados no funcionan

// Verificar que estés usando las clases correctas
<div className="bg-onpe-ui-blue text-white p-4">
  Contenido con colores ONPE
</div>

// Verificar que los estilos estén importados
import '@onpe/ui/styles';

Solución: Verificar configuración de Tailwind (para componentes individuales)

// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      colors: {
        'onpe-ui-blue': '#003770',
        'onpe-ui-skyblue': '#0073cf',
        // ... resto de colores
      }
    },
  },
}

Problemas con Storybook

Error: "Failed to fetch dynamically imported module"

  • Verificar que el archivo preview.ts importe correctamente ../src/styles.css
  • Asegurar que Tailwind CSS esté configurado correctamente
  • Verificar que PostCSS esté configurado

Solución: Configuración completa de Storybook

// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  viteFinal: async (config) => {
    config.css = {
      ...config.css,
      postcss: {
        plugins: [require("tailwindcss"), require("autoprefixer")],
      },
    };
    return config;
  },
};

export default config;
// .storybook/preview.ts
import type { Preview } from "@storybook/react";
import "../src/styles.css"; // ← Importar estilos

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;

🚀 Guía Rápida de Solución

¿Los componentes no se ven con estilos?

Paso 1: Verificar instalación

npm list @onpe/ui

Paso 2: Configurar TailwindCSS

# Instalar TailwindCSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Paso 3: Crear archivo CSS personalizado

/* Crear archivo onpe-ui.css */
:root {
  --blue: #003770;
  --skyblue: #0073cf;
  /* ... resto de variables */
}

@theme {
  --color-onpe-ui-blue: var(--blue);
  /* ... resto de colores */
}

Paso 4: Importar archivo CSS

// En tu archivo principal (index.tsx)
import './onpe-ui.css';
import { Button } from './components/onpe-ui/Button';
import { IconClose } from './components/onpe-icons/IconClose';

Paso 5: Verificar orden de importación

// ✅ CORRECTO
import './onpe-ui.css';
import { Button } from './components/onpe-ui/Button';
import { IconClose } from './components/onpe-icons/IconClose';

// ❌ INCORRECTO
import { Button } from './components/onpe-ui/Button';
// Falta importar el archivo CSS

¿Los colores no funcionan?

Solución: Usar clases correctas

// ✅ CORRECTO
<div className="bg-onpe-ui-blue text-white p-4">
  Contenido
</div>

// ❌ INCORRECTO
<div className="bg-blue-500 text-white p-4">
  Contenido
</div>

¿Storybook no funciona?

Solución: Configuración completa

// .storybook/preview.ts
import "../src/styles.css";

// .storybook/main.ts
viteFinal: async (config) => {
  config.css = {
    ...config.css,
    postcss: {
      plugins: [require("tailwindcss"), require("autoprefixer")],
    },
  };
  return config;
},

¿CLI no instala componentes?

Solución: Verificar comandos

# ✅ CORRECTO
npx @onpe/ui add button
npx @onpe/ui add modal

# ❌ INCORRECTO
npx @onpe/ui add Button
npx @onpe/ui add Modal

¿Portal no funciona?

Solución: Agregar elemento HTML

<!-- En public/index.html -->
<div id="root"></div>
<div id="portal"></div>

📞 Soporte


Desarrollado con ❤️ para la democracia peruana 🇵🇪