Skip to content

Latest commit

 

History

History
335 lines (251 loc) · 13.8 KB

File metadata and controls

335 lines (251 loc) · 13.8 KB

Guías Técnicas

Bienvenido al código fuente del Design System Stack-and-Flow. Seguimos estrictamente Atomic Design combinado con el patrón Container/Presentational. Respetar estas guías es OBLIGATORIO para cualquier PR.

🇬🇧 English version


Arquitectura: Atomic Design

La UI se divide en tres niveles de complejidad:

  • Atoms: Los bloques básicos de construcción (ej: Button, Badge, Input). No dependen de otros componentes del sistema, salvo funciones utilitarias.
  • Molecules: Grupos de átomos combinados para formar una unidad funcional (ej: Modal generalmente usa botones, tipografía, etc.).
  • Organisms: Componentes de UI complejos que forman secciones diferenciadas de una interfaz (ej: un Header compuesto por logo, molécula de búsqueda y átomos de navegación).

Patrón: Container & Presentational

Cada componente DEBE separarse en lógica (Container) y renderizado (Presentational). Lo logramos mediante custom hooks y archivos .tsx.

Estructura de 6 archivos

Cada componente DEBE vivir dentro de un directorio en kebab-case (src/components/atoms/button/) y contener EXACTAMENTE estos seis archivos:

Archivo Propósito Regla
types.ts Tipos y Variantes Define props con type, JSDoc controls y variantes cva.
useButton.ts Hook Container Contiene TODA la lógica, estado, refs, handlers y consumo de clases cva.
Button.tsx Componente Presentacional SOLO JSX. Consume el hook. Sin lógica, estado ni CVA.
Button.test.tsx Tests Cubre hook y comportamiento del componente.
Button.stories.tsx Documentación Contiene Storybook autodocs, args, JSDoc encima de const meta y JSDoc encima de cada story.
index.ts API Pública Re-exporta el componente y los tipos.

1. Tipos y Variantes (types.ts)

Todas las variantes cva van aquí, nunca en el hook ni en el componente. Usa comentarios JSDoc para generar automáticamente los controles de Storybook.

import { cva, type VariantProps } from "class-variance-authority";

export const buttonVariants = cva(
  ["flex items-center justify-center font-secondary-bold"],
  {
    variants: {
      variant: {
        primary: "bg-secondary text-text-dark",
        ghost: "bg-transparent text-text-light",
      },
    },
    defaultVariants: {
      variant: "primary",
    },
  },
);

export type ButtonProps = {
  /**
   * @control select
   * @default primary
   */
  variant?: VariantProps<typeof buttonVariants>["variant"];
  className?: string;
  disabled?: boolean;
};

2. Hook Container (useButton.ts)

El hook retorna todo lo que el elemento necesita: clases CSS, manejadores de eventos, props mapeadas y atributos aria.

import { buttonVariants, type ButtonProps } from "./types";

export const useButton = ({
  variant = "primary",
  className,
  disabled = false,
  ...props
}: ButtonProps) => {
  // Logic here (refs, state, effects)
  const buttonClass = buttonVariants({ variant, className });

  return {
    buttonClass,
    disabled,
    ...props,
  };
};

3. Componente Presentacional (Button.tsx)

Solo desestructura lo que necesita del hook y lo renderiza.

import type { FC, ComponentProps } from "react";
import type { ButtonProps } from "./types";
import { useButton } from "./useButton";

const Button: FC<ButtonProps & ComponentProps<"button">> = ({ ...props }) => {
  const { buttonClass, disabled, children } = useButton(props);

  return (
    <button className={buttonClass} disabled={disabled} {...props}>
      {children}
    </button>
  );
};

export { Button };

4. API Pública (index.ts)

export { Button } from "./Button";
export type * from "./types";

Reglas de TypeScript

  • type sobre interface: USA SIEMPRE export type ComponentProps = {}. NO uses interface.
  • Sin any: El uso explícito de any está estrictamente prohibido. Si no conoces el tipo, usa unknown o reduce el tipo correctamente.
  • Props explícitas: Nunca tipifiques props implícitamente. Todo DEBE estar definido explícitamente en types.ts.
  • Definición de componente: Usa FC<ComponentProps> y exports nombrados — nunca default exports para componentes.

Reglas de Storybook

  • Solo en inglés: Toda la documentación de Storybook debe escribirse en inglés, incluidos headings, descripciones, comentarios, nombres de stories y labels.
  • Controles obligatorios: Usa comentarios JSDoc (/** @control text */) en types.ts para activar los controles.
  • Descripción obligatoria: Cada story de componente DEBE incluir un bloque JSDoc encima de const meta con headings en inglés:
    /**
     * ## Description
     * A versatile button component used to trigger actions.
     *
     * ## Dependencies
     * Uses `Icon` when an icon slot is provided.
     *
     * ## Usage Guide
     * Use this component for primary actions. Avoid using it for navigation.
     */
    const meta: Meta<typeof Button> = {
      title: "Atoms/Button",
      component: Button,
      parameters: {
        docs: {
          autodocs: true,
        },
      },
      tags: ["autodocs"],
    };
  • Sin parameters.docs.description.component: la documentación del componente vive en el bloque JSDoc encima de const meta.
  • Stories documentadas: añade un bloque JSDoc útil encima de cada export const StoryName; debe explicar el escenario y por qué importa, no solo repetir el nombre de la story.
  • Sin stories redundantes: cada story debe demostrar un estado, eje de variante, restricción de composición, comportamiento de accesibilidad o contexto de integración distinto.
  • Sin story genérica DarkMode: usa el toolbar dark-mode de Storybook para cobertura normal de tema; las stories dark-mode dedicadas quedan solo para scope local, herencia en portales o regresiones de tema que el toolbar no pueda expresar.
  • Args: Define args por defecto para la story base sin pisar defaultVariants.

Tokens del Sistema y Estilos

Usamos Tailwind v4 con configuraciones @theme definidas en src/styles/theme.css.

Breakpoints responsivos (Tailwind only)

  • Fuente de verdad: usa únicamente prefijos nativos de Tailwind: sm, md, lg, xl, 2xl.

  • Prohibido introducir o reutilizar aliases custom de breakpoints (por ejemplo tablet, desktop).

  • Si un ajuste responsive requiere un corte intermedio y no hay evidencia fuerte en contra, usa md como punto de partida conservador.

  • OBLIGATORIO: DEBES usar las propiedades CSS personalizadas del design system (tokens) mediante clases de Tailwind.

  • SIN HARDCODING: Nunca escribas colores en duro (ej: #FF0000) ni valores arbitrarios en estilos inline. Las clases Tailwind arbitrarias de tamaño/typography se permiten solo dentro de CVA en types.ts para variantes compactas/densas explícitamente aprobadas; colores arbitrarios siguen prohibidos.

  • Usa las clases predefinidas: text-text-dark, bg-secondary, gap-sm, fs-h1, etc.


Accesibilidad (a11y)

La accesibilidad es una funcionalidad principal, no un agregado posterior.

  • Atributos ARIA: Los elementos interactivos DEBEN tener los atributos ARIA apropiados (aria-expanded, aria-pressed, aria-hidden, etc.).
  • aria-label dinámico: No escribas aria-label en duro. Exponlo como prop para que los consumidores puedan personalizarlo para traducciones o contexto.
  • Roles: Define role explícitamente cuando la semántica lo requiera (ej: role="status" en Badge, role="switch" en botones con toggle).
  • Navegación por teclado: Asegúrate de que los elementos sean enfocables y muestren el foco visualmente (focus-visible). Nuestros estilos globales gestionan los anillos de foco de forma nativa.

Convenciones de Nomenclatura

  • Directorios: kebab-case (ej: src/components/atoms/date-picker/).
  • Componentes y archivos: PascalCase (ej: DatePicker.tsx, DatePicker.stories.tsx).
  • Hooks: camelCase con prefijo use (ej: useDatePicker.ts).
  • Tipos: PascalCase (ej: DatePickerProps).
  • Variantes CVA: camelCase con sufijo Variants (ej: datePickerVariants).

Testing

Cada componente del design system DEBE tener un archivo de test correspondiente que siga estas convenciones.

Estrategia

Seguimos la separación Container/Presentational también en los tests:

Capa Herramienta Qué testear
useComponentName.ts (Hook) renderHook Lógica pura: valores por defecto, props computadas, manejadores de eventos, forma del retorno
ComponentName.tsx (Componente) render + screen + userEvent Comportamiento observable: accesibilidad, estado deshabilitado, loading, manejo de clicks

Nunca testees detalles de implementación: NO hagas assertions sobre cadenas de clases CSS, valores internos de refs, ni qué string de variante se aplica al DOM. Testea lo que un usuario real (o un lector de pantalla) observaría.

Ubicación y Nomenclatura del Archivo

Coloca el archivo de test junto al componente — no en un directorio __tests__ separado:

src/components/atoms/button/
  Button.tsx
  useButton.ts
  types.ts
  index.ts
  Button.stories.tsx
  Button.test.tsx   ← aquí

Cobertura Mínima Requerida

Cada archivo de test de componente DEBE cubrir:

  1. Estado por defecto — el componente renderiza sin props requeridas
  2. Prop disabled — el elemento está deshabilitado cuando disabled={true}
  3. Prop isLoading — el elemento está deshabilitado cuando isLoading={true} (aunque disabled={false})
  4. Manejador onClick — se llama cuando es interactivo y no está cargando; NO se llama cuando está cargando
  5. aria-label — el nombre accesible se aplica correctamente
  6. Valores por defecto del hookdisabled: false e isLoading: false son retornados por defecto

Mocks Requeridos

Mockeá solo los paquetes que el componente importa. lucide-react/dynamic.js y spinners-react pueden romper jsdom por módulos dinámicos o animaciones CSS, pero el mock debe existir únicamente cuando el componente usa ese paquete:

vi.mock("lucide-react/dynamic.js", () => ({
  DynamicIcon: () => null,
}));

// Solo si el componente importa spinners-react
vi.mock("spinners-react", () => ({
  SpinnerCircular: () => null,
}));

Si un componente renderiza un spinner de loading, debe usar SpinnerCircular de spinners-react, siguiendo Button como referencia. No implementes spinners CSS locales por componente.

También mockea cualquier archivo CSS importado directamente desde el componente:

vi.mock("@/components/utils/styles/index.css", () => ({}));

Ejemplos de Referencia

Test de hook — usa renderHook:

import { renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useButton } from "./useButton";

describe("useButton — logic", () => {
  it("returns disabled: false by default", () => {
    const { result } = renderHook(() => useButton({}));
    expect(result.current.disabled).toBe(false);
  });

  it("returns the correct variant when variant: ghost is passed", () => {
    const { result } = renderHook(() => useButton({ variant: "ghost" }));
    expect(result.current.variant).toBe("ghost");
  });
});

Test de componente — usa render + screen + userEvent:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { Button } from './Button';

describe('Button — component behavior', () => {
  it('is disabled when isLoading is true', () => {
    render(<Button text="Loading" isLoading disabled={false} />);
    expect(screen.getByRole('button', { name: 'Loading' })).toBeDisabled();
  });

  it('does NOT call onClick when isLoading is true', async () => {
    const handleClick = vi.fn();
    render(<Button text="Saving" isLoading onClick={handleClick} />);
    await userEvent.click(screen.getByRole('button', { name: 'Saving' }));
    expect(handleClick).not.toHaveBeenCalled();
  });
});

Ejecutar Tests

pnpm run test            # ejecución única
pnpm run test:watch      # modo watch
pnpm run test:coverage   # con reporte de cobertura

Anti-Patrones Estrictos (Lo que NO está Permitido)

Las siguientes prácticas resultan en rechazo del PR:

  1. NO combinar lógica Presentacional y Container en el mismo archivo .tsx.
  2. NO poner cva dentro del archivo .tsx o useHook.ts (pertenece a types.ts).
  3. NO usar export interface en TypeScript.
  4. NO valores arbitrarios de color en Tailwind (text-[#000]). Tamaños/typography arbitrarios solo se permiten en CVA (types.ts) para variantes compactas/densas explícitamente aprobadas.
  5. NO código en español (variables, comentarios y stories deben estar en inglés).
  6. NO tipos any.
  7. NO omitir accesibilidad (aria-* o foco por teclado ausente).
  8. NO exportar múltiples componentes desde un único archivo. Un componente = un directorio.