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

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-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

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-dark y ghost: 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 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:

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