Files
qwen3-6-lora/data/raw/sanitized/plans/podemos-planificar-la-creaci-n-structured-sundae.md
T

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