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 uso | Ejemplo |
|---|---|
| Layouts y estructura | flex, grid, gap-4, justify-between |
| Spacing y sizing | p-4, m-2, w-full, h-screen |
| Responsive design | md:flex-row, lg:grid-cols-3 |
| Estados simples | hover:bg-gray-100, focus:ring-2 |
| Página de demo/app | Todos los layouts de la app Next.js |
| Stories de Storybook | Layouts 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 uso | Ejemplo |
|---|---|
| Componentes del UI Kit | .ui-input, .ui-field, .ui-button |
| Estados complejos | Error, success, warning, disabled |
| CSS Custom Properties | var(--ui-color-primary) |
| Theming/Rebranding | Variables que se sobrescriben |
| Override de terceros | react-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
| Tipo | Ejemplo |
|---|---|
| Input raw | InputText, Select, DatePicker, Button, Checkbox, Switch |
| Container | FieldContainer, FloatingContainer |
| Field (RHF) | TextField, SelectField, CheckboxField, SwitchField |
| CSS classes | ui-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ía | Componentes |
|---|---|
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
| Prefijo | Uso |
|---|---|
--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
- •
forwardRefpara todos los inputs - •
useIdpara IDs únicos - •
cn()para clases condicionales - •Composición sobre herencia
- •
useController+ErrorMessagepara integración con RHF
10. Accesibilidad
- •
label+htmlFor/id - •
aria-invaliden 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
ErrorMessageen Fields
12. Dependencias principales
| Librería | Uso |
|---|---|
primereact (unstyled) | Componentes base (Button, Checkbox, etc.) |
react-hook-form | Manejo de formularios |
@hookform/error-message | Mensajes de error de validación |
react-select | Selectores avanzados |
react-datepicker | Date/time picker |
react-number-format | Inputs numéricos formateados |
clsx | Clases condicionales |
tailwind-merge | Merge 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