9.2 KiB
Plan: Creación del Componente Button para BL Consultores DS con Tailwind CSS
Contexto
Se necesita crear el componente de botones para el Design System de BL Consultores. El proyecto es una librería React de componentes (bl-consultores-ds) basada en create-react-component-library.
Actualmente el starter kit incluye un componente Card genérico que debe eliminarse — es solo el template por defecto del starter kit y no forma parte del diseño system real de BL Consultores.
El diseño de los botones proviene de Penpot y está documentado en Docmost con 11 variantes exactas (colores, tamaños, estados hover). Se usará Tailwind CSS para los estilos.
Paso 0: Instalar y configurar Tailwind CSS
0.1 Instalar dependencias
npm install -D tailwindcss postcss autoprefixer
0.2 Crear tailwind.config.js
Configurar con los tokens exactos del diseño de Penpot:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
],
theme: {
extend: {
colors: {
gold: '#EABF2D',
amber: '#D4880F',
black: '#1A1A2E',
danger: '#DE3626',
muted: '#9AA0A6',
gray: '#6B6B7B',
inputBorder: '#DADCE0',
},
borderRadius: {
pill: '20px',
circle: '28px',
},
fontSize: {
btn: ['11px', { lineHeight: '1', fontWeight: '700' }],
btnPrimary: ['12px', { lineHeight: '1', fontWeight: '700' }],
btnNormal: ['12px', { lineHeight: '1', fontWeight: '400' }],
},
height: {
btn: '40px',
btnLg: '60px',
btnCircle: '56px',
btnIcon: '36px',
},
width: {
btnPrimary: '170px',
btnOutlineGold: '230px',
btnOutline: '110px',
btnOutlineLight: '140px',
btnGhost: '110px',
btnFileUpload: '200px',
btnCircle: '56px',
btnIcon: '36px',
},
fontFamily: {
sans: ['sourcesanspro', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}
0.3 Crear postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
0.4 Ajustar webpack.config.ts
- Agregar
postcss-loadercomo dependencia - Agregar regla postcss en las rules de webpack (entre css-loader y sass-loader, o antes de them)
- Para componentes sin SCSS, el CSS se genera vía Tailwind classes directamente
Paso 1: Eliminar el Card del starter kit
| Archivo | Acción |
|---|---|
src/components/Card/ |
Eliminar directorio completo |
src/components/__tests__/Card.test.tsx |
Eliminar |
src/components/__tests__/Card.test.cy.tsx |
Eliminar |
src/components/index.tsx |
Remover export * from './Card' |
Paso 2: Crear el componente Button
Estructura de archivos
src/components/Button/
├── index.tsx # Componente React + tipos
└── Button.stories.tsx # Storybook stories
Sin archivo SCSS separado — los estilos se manejan con clases Tailwind.
TypeScript — Tipos
type ButtonVariant =
| 'primary'
| 'outline-gold'
| 'outline-dark'
| 'outline-light'
| 'ghost'
| 'circular'
| 'icon-edit'
| 'icon-delete'
| 'file-upload';
interface ButtonProps {
/** Variant style of the button */
variant?: ButtonVariant;
/** Button text content */
children?: React.ReactNode;
/** Click handler */
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
/** HTML button type */
type?: 'button' | 'submit' | 'reset';
/** Disabled state */
disabled?: boolean;
/** For file-upload variant: accept attribute */
accept?: string;
/** Optional className for additional styling */
className?: string;
}
JSX — Estructura
El componente usa un <button> nativo con clases Tailwind compuestas. Para file-upload, renderiza un <input type="file"> oculto dentro de un <label>.
const Button: FC<ButtonProps> = ({
variant = 'primary',
children,
onClick,
type = 'button',
disabled = false,
accept,
className = '',
}) => {
if (variant === 'file-upload') {
return (
<label
className={`btn file-upload ${className}`.trim()}
>
<input
type="file"
accept={accept}
className="file-input"
onChange={onClick as any}
disabled={disabled}
/>
<span className="btn-content">{children}</span>
</label>
);
}
return (
<button
className={`btn ${variantClasses[variant]} ${className}`.trim()}
onClick={onClick}
type={type}
disabled={disabled}
>
{children}
</button>
);
};
Variantes — Clases Tailwind exactas desde Penpot
Cada variante tiene clases Tailwind derivadas de los valores exactos de Penpot:
| Variant | Clases Tailwind |
|---|---|
| primary | w-btnPrimary h-btn bg-gold text-black font-sans font-bold text-btnPrimary px-5 rounded-btn flex items-center justify-center gap-2 transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed |
| outline-gold | w-btnOutlineGold h-btn bg-white border-[1.5px] border-gold text-amber font-sans font-bold text-btnPrimary px-5 rounded-btn flex items-center justify-center gap-2 transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed |
| outline-dark | w-btnOutline h-btn bg-white border-[1.5px] border-black text-black font-sans font-bold text-btn px-4 rounded-sm flex items-center justify-center gap-2 transition-all duration-200 cursor-pointer hover:bg-amber hover:text-white disabled:opacity-50 disabled:cursor-not-allowed |
| outline-light | w-btnOutlineLight h-btnLg bg-black border-[1.5px] border-white text-white font-sans font-bold text-btn px-4 rounded-sm flex items-center justify-center gap-2 transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed |
| ghost | w-btnGhost h-btn bg-white border-[1.5px] border-black text-black font-sans font-bold text-btn px-4 rounded-pill flex items-center justify-center gap-2 transition-all duration-200 cursor-pointer hover:bg-amber hover:text-white disabled:opacity-50 disabled:cursor-not-allowed |
| circular | w-btnCircle h-btnCircle bg-white border-[1.5px] border-black rounded-circle flex items-center justify-center transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed |
| icon-edit | w-btnIcon h-btnIcon bg-white border-[1.5px] border-gold rounded-sm flex items-center justify-center transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed |
| icon-delete | w-btnIcon h-btnIcon bg-white border-[1.5px] border-danger rounded-sm flex items-center justify-center transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed |
| file-upload | relative w-btnFileUpload h-btn bg-white border-[1.5px] border-gold text-amber font-sans font-normal text-btnPrimary px-4 rounded-sm flex items-center justify-center cursor-pointer disabled:opacity-50 |
Hover states (automáticos en las clases):
outline-darkyghost:hover:bg-amber hover:text-white
File upload input: absolute inset-0 opacity-0 cursor-pointer
Paso 3: Storybook Stories
export const Primary: Story = { args: { variant: 'primary', children: '+ Agregar' } };
export const OutlineGold: Story = { args: { variant: 'outline-gold', children: 'Editar Estructura del Clima' } };
export const OutlineDark: Story = { args: { variant: 'outline-dark', children: 'VER MÁS' } };
export const OutlineLight: Story = { args: { variant: 'outline-light', children: 'VER MÁS' } };
export const Ghost: Story = { args: { variant: 'ghost', children: 'VER MÁS' } };
export const Circular: Story = { args: { variant: 'circular', children: '↓' } };
export const IconEdit: Story = { args: { variant: 'icon-edit', children: '✏️' } };
export const IconDelete: Story = { args: { variant: 'icon-delete', children: '✕' } };
export const FileUpload: Story = { args: { variant: 'file-upload', children: 'Seleccionar CV' } };
Incluir también stories de:
- States: disabled, hover (usando
playfunction) - Sizes: mostrar cada variante con su tamaño design
- Composition: botones en grupos, con iconos
Paso 4: Tests
Jest (Button.test.tsx)
- Renderiza con variant default (primary)
- Click handler se llama al hacer click
- Estado disabled deshabilita interacción
- File upload renderiza input file
- Textos y props se renderizan correctamente
- Clases CSS correctas por variante
Cypress (Button.test.cy.tsx)
- Mount y verifica visual de cada variant
- Hover states (outline-dark, ghost)
- Disabled state visual
- File upload label structure
Paso 5: Actualizar exports
src/components/index.tsx:
export * from './Button';
Verificación
npm run storybook— verificar que todas las variantes se muestran correctamente en Storybooknpm run test— Jest tests pasannpm run cy:run— Cypress tests pasannpm run build— build exitoso sin errores- Verificar que el Card fue eliminado y no queda referencia rota
- Verificar que Tailwind se compila correctamente en el bundle final