270 lines
9.2 KiB
Markdown
270 lines
9.2 KiB
Markdown
# 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
|
|
```bash
|
|
npm install -D tailwindcss postcss autoprefixer
|
|
```
|
|
|
|
### 0.2 Crear `tailwind.config.js`
|
|
|
|
Configurar con los tokens exactos del diseño de Penpot:
|
|
|
|
```js
|
|
/** @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`
|
|
```js
|
|
module.exports = {
|
|
plugins: {
|
|
tailwindcss: {},
|
|
autoprefixer: {},
|
|
},
|
|
}
|
|
```
|
|
|
|
### 0.4 Ajustar `webpack.config.ts`
|
|
- Agregar `postcss-loader` como 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
|
|
|
|
```tsx
|
|
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>`.
|
|
|
|
```tsx
|
|
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-dark` y `ghost`: `hover:bg-amber hover:text-white`
|
|
|
|
**File upload input**: `absolute inset-0 opacity-0 cursor-pointer`
|
|
|
|
---
|
|
|
|
## Paso 3: Storybook Stories
|
|
|
|
```tsx
|
|
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 `play` function)
|
|
- **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`:
|
|
```tsx
|
|
export * from './Button';
|
|
```
|
|
|
|
---
|
|
|
|
## Verificación
|
|
|
|
1. `npm run storybook` — verificar que todas las variantes se muestran correctamente en Storybook
|
|
2. `npm run test` — Jest tests pasan
|
|
3. `npm run cy:run` — Cypress tests pasan
|
|
4. `npm run build` — build exitoso sin errores
|
|
5. Verificar que el Card fue eliminado y no queda referencia rota
|
|
6. Verificar que Tailwind se compila correctamente en el bundle final
|