Fase 2: 04_sanitize.py - scrubbing de secretos de skills/agentes/plans (10 secretos unicos, gate en verde)
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user