AgentSkillsCN

Patterns

模式

SKILL.md

UI Kit - Skills & Best Practices

Overview

Este documento define las buenas prácticas y convenciones para el desarrollo del UI Kit.


1. Librerías de Utilidad

cn() - Class Name Utility

Usa cn() (de src/utils/cn.ts) para construir clases CSS. Combina clsx + tailwind-merge.

tsx
import { cn } from "../../utils/cn";

// ❌ Incorrecto: Arrays manuales
const classes = [
  "ui-button",
  variant === "primary" && "ui-button--primary",
  disabled && "ui-button--disabled",
  className,
].filter(Boolean).join(" ");

// ✅ Correcto: Usar cn()
const classes = cn(
  "ui-button",
  `ui-button--${variant}`,
  {
    "ui-button--disabled": disabled,
    "ui-button--loading": loading,
  },
  className
);

@hookform/error-message

Usa ErrorMessage para mostrar errores de validación en Fields.

tsx
import { ErrorMessage } from "@hookform/error-message";

// En el render:
<ErrorMessage
  errors={errors}
  name={name as string}
  render={({ message }) => (
    <span className="ui-field__error">{message}</span>
  )}
/>

2. Cuándo usar Tailwind CSS vs SCSS

Usar Tailwind CSS para:

Caso de usoEjemplo
Layouts y estructuraflex, grid, gap-4, justify-between
Spacing y sizingp-4, m-2, w-full, h-screen
Responsive designmd:flex-row, lg:grid-cols-3
Estados simpleshover:bg-gray-100, focus:ring-2
Página de demo/appTodos los layouts de la app Next.js
Stories de StorybookLayouts y wrappers en stories
tsx
// ✅ Correcto: Tailwind para layout
<div className="flex flex-col gap-4 p-6 md:flex-row">
  <TextField name="email" control={control} label="Email" />
</div>

Usar SCSS para:

Caso de usoEjemplo
Componentes del UI Kit.ui-input, .ui-field, .ui-button
Estados complejosError, success, warning, disabled
CSS Custom Propertiesvar(--ui-color-primary)
Theming/RebrandingVariables que se sobrescriben
Override de tercerosreact-select, react-datepicker
scss
// ✅ Correcto: SCSS para componentes con theming
.ui-input {
  border-color: var(--ui-color-gray-300);
  &:focus {
    border-color: var(--ui-color-primary);
  }
}

Regla general

code
┌─────────────────────────────────────────────────────────────┐
│  SCSS + CSS Variables  →  Componentes exportables del Kit  │
│  Tailwind CSS          →  App demo, Storybook, layouts     │
└─────────────────────────────────────────────────────────────┘

3. Arquitectura de Componentes

3 Capas

code
FIELDS      → Integración con React Hook Form + ErrorMessage
CONTAINERS  → Layout y presentación (label, error, hint)
INPUTS      → Componentes raw/unstyled

Convención de nombres

TipoEjemplo
Input rawInputText, Select, DatePicker, Button, Checkbox, Switch
ContainerFieldContainer, FloatingContainer
Field (RHF)TextField, SelectField, CheckboxField, SwitchField
CSS classesui-input, ui-field, ui-button, ui-checkbox

4. Patrón de Componente Input

tsx
"use client";

import React, { forwardRef } from "react";
import type { ComponentSize, FieldState } from "../types";
import { cn } from "../../utils/cn";

export interface MyComponentProps {
  size?: ComponentSize;
  state?: FieldState;
  disabled?: boolean;
  className?: string;
}

export const MyComponent = forwardRef<HTMLElement, MyComponentProps>(
  function MyComponent(
    {
      size = "md",
      state = "default",
      disabled = false,
      className,
      ...props
    },
    ref
  ) {
    const classes = cn(
      "ui-mycomponent",
      `ui-mycomponent--${size}`,
      state !== "default" && `ui-mycomponent--${state}`,
      { "ui-mycomponent--disabled": disabled },
      className
    );

    return <div ref={ref} className={classes} {...props} />;
  }
);

5. Patrón de Componente Field (RHF)

tsx
"use client";

import React, { useId } from "react";
import {
  useController,
  type UseControllerProps,
  type FieldValues,
  type FieldPath,
} from "react-hook-form";
import { ErrorMessage } from "@hookform/error-message";
import { MyInput, type MyInputProps } from "../inputs/MyInput";
import { cn } from "../../utils/cn";

export interface MyFieldProps<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> extends Omit<UseControllerProps<TFieldValues, TName>, "defaultValue">,
    Omit<MyInputProps, "name" | "value" | "onChange" | "state"> {
  label?: string;
  hint?: string;
}

export function MyField<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
  name,
  control,
  rules,
  shouldUnregister,
  label,
  hint,
  className,
  ...inputProps
}: MyFieldProps<TFieldValues, TName>) {
  const generatedId = useId();
  const id = `field-${name}-${generatedId}`;

  const {
    field,
    fieldState: { error, invalid },
    formState: { errors },
  } = useController({ name, control, rules, shouldUnregister });

  const state = invalid ? "error" : "default";

  return (
    <div className={cn("ui-myfield", className)}>
      {label && <label htmlFor={id}>{label}</label>}
      <MyInput
        {...inputProps}
        id={id}
        name={field.name}
        value={field.value}
        onChange={field.onChange}
        state={state}
        ref={field.ref}
      />
      <div className="ui-myfield__helper">
        <ErrorMessage
          errors={errors}
          name={name as string}
          render={({ message }) => (
            <span className="ui-myfield__error">{message}</span>
          )}
        />
        {!error && hint && (
          <span className="ui-myfield__hint">{hint}</span>
        )}
      </div>
    </div>
  );
}

6. Storybook

Estructura de stories

code
src/components/{layer}/__stories__/{Component}.stories.tsx

Formato de story

tsx
import type { Meta, StoryObj } from "@storybook/react";
import { ComponentName } from "../ComponentName";

/**
 * Descripción del componente para autodocs.
 *
 * ## Uso
 * ```tsx
 * <ComponentName prop="value" />
 * ```
 */
const meta: Meta<typeof ComponentName> = {
  title: "Category/ComponentName",
  component: ComponentName,
  tags: ["autodocs"],
  argTypes: {
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
      description: "Tamaño del componente",
    },
  },
};

export default meta;
type Story = StoryObj<typeof ComponentName>;

export const Default: Story = {
  args: {
    // props por defecto
  },
};

// IMPORTANTE: Evitar duplicar nombres de funciones y exports
const VariantExample = () => <ComponentName variant="special" />;

export const Variant: Story = {
  render: () => <VariantExample />,
};

Categorías

CategoríaComponentes
Inputs/InputText, Button, Checkbox, Switch, RadioGroup, Select, DatePicker, NumberInput, PasswordInput
Fields/TextField, CheckboxField, SwitchField, RadioGroupField, SelectField, DateField, NumberField
Containers/FieldContainer, FloatingContainer

7. TypeScript & Docstrings

Docstrings obligatorios

tsx
/**
 * ComponentName - Breve descripción
 *
 * @description
 * Descripción detallada.
 *
 * @example
 * ```tsx
 * <ComponentName prop="value" />
 * ```
 */

Props documentadas

tsx
export interface Props {
  /**
   * Descripción de la prop
   * @default "md"
   */
  size?: ComponentSize;
}

8. CSS Custom Properties

Nomenclatura

code
--ui-{category}-{name}-{variant}

Categorías

PrefijoUso
--ui-color-*Colores
--ui-font-*Tipografía
--ui-spacing-*Espaciado
--ui-radius-*Border radius
--ui-shadow-*Sombras
--ui-transition-*Transiciones
--ui-z-*Z-index

9. React Patterns

  • forwardRef para todos los inputs
  • useId para IDs únicos
  • cn() para clases condicionales
  • Composición sobre herencia
  • useController + ErrorMessage para integración con RHF

10. Accesibilidad

  • label + htmlFor / id
  • aria-invalid en errors
  • role="alert" en mensajes de error
  • Focus visible
  • Contraste WCAG AA

11. Checklist nuevos componentes

  • Crear en la capa correcta (inputs/, fields/, containers/)
  • Usar forwardRef
  • Usar cn() para clases
  • Docstrings completos
  • CSS variables para theming
  • Props: size, state, disabled, className
  • Estilos en _*.scss (BEM: .ui-{componente}--{modificador})
  • Exportar desde index.ts
  • Crear story en __stories__/
  • Usar ErrorMessage en Fields

12. Dependencias principales

LibreríaUso
primereact (unstyled)Componentes base (Button, Checkbox, etc.)
react-hook-formManejo de formularios
@hookform/error-messageMensajes de error de validación
react-selectSelectores avanzados
react-datepickerDate/time picker
react-number-formatInputs numéricos formateados
clsxClases condicionales
tailwind-mergeMerge inteligente de clases Tailwind

13. Scripts disponibles

bash
npm run dev           # Next.js dev
npm run build         # Next.js build
npm run build:lib     # Build librería (tsup)
npm run storybook     # Storybook dev
npm run build-storybook # Storybook build
npm run lint          # ESLint

Última actualización: Enero 2026