Muchas aplicaciones, un solo sistema
El fallo habitual: se construye un kit para la app A, los botones de la app B son 4px más altos y 2px más redondeados, así que B bifurca el botón "solo por esta vez". Cinco aplicaciones después, hay seis botones, ocho selects y tres modales, todos haciendo lo mismo.
Haz que el componente compartido sea basado en tokens: lleva comportamiento, marcado y estados, y cada tema proporciona sus medidas. Luego consolida aplicación por aplicación.
Componentes tokenizados
Lee cada medida que difiera entre aplicaciones de un token del tema:
@theme {
--spacing-btn-sm: 1.75rem; --spacing-btn-px-sm: 0.625rem;
--spacing-btn-md: 2.25rem; --spacing-btn-px-md: 0.875rem;
--spacing-field: 2.125rem; /* altura de input/select */
--text-label: 0.8125rem;
}
md: "h-btn-md px-btn-px-md text-sm",
icon: "size-btn-md",
Luego:
- Una suite sobrescribe los tokens una vez en su tema, y cada aplicación de la suite obtiene su propio control medido.
- Los radios, rellenos y sombras ya son tokens (
rounded-field,bg-accent,--shadow-card). - Los rellenos se aplican como color más una clase decorativa, por ejemplo
bg-accent accent-fill. Un tema que solo define la decoración (un brillo) nunca renderiza un botón transparente.
Consulta references/button.tsx.
Pase de consolidación, por aplicación
-
Inventario de copias locales. Busca en cada aplicación
export function (Btn|Button|IconBtn|Select|Field|Input|Modal|Dialog|Sheet|Badge|Empty|Pager|Toast|Stat|Status|Spinner). Cuenta los usos. -
Reemplaza y elimina donde el componente compartido coincida después de aplicar el tema. Mapea explícitamente los nombres de las variantes y escribe el mapeo:
Nombre local Variante compartida primary,navyaccentsecondaryoutlinequiet,softsubtledanger(sólido)danger-solid -
Mantén específico de la aplicación lo que sea específico:
- widgets de dominio, gráficos y chrome de la aplicación;
- controles cuya forma es la marca (losas de iOS, controles planos de Carbon, CTAs de píldora de 48px);
- controles con comportamiento que el compartido no tiene (checkbox tri-estado, chips removibles, un elemento de reinicio "Cualquiera", filtros multiselección).
Un envoltorio fino está bien cuando añade una sola cosa. Anota por qué en un comentario.
-
Retira los
<select>nativos y los selects basados en menú por un único Select.- Radix Select reserva
""como "sin valor": usa un centinela como"all"y mapealo de vuelta. - Reenvía
aria-labelledbyyaria-describedby. - Los scripts que llaman a
selectOption()deben hacer clic en el combobox y luego en la opción.
- Radix Select reserva
-
Verifica cada aplicación con sus propias capturas de pantalla (consulta
visual-regression-pass). Las diferencias reales suelen ser intencionadas: tamaños, brillo, escala de presión.
Trampas de tailwind-merge (cn())
tailwind-merge solo conoce la escala nativa de Tailwind. Los tokens personalizados lo rompen de tres maneras silenciosas:
- Tokens de espaciado desconocidos.
cn("h-btn-md", "h-10")conserva ambos, porque no sabe queh-btn-mdes una altura, y el orden de las hojas de estilo elige el ganador. Unh-10de quien llama se perdió frente al token y un botón de 40px se renderizó a 36px. Registra los tokens:extendTailwindMerge({ extend: { theme: { spacing: ["btn-sm","btn-md","field", …] } } }). - Tamaños de texto desconocidos.
cn("text-meta", "text-ink")tratatext-metacomo un color y lo descarta. Registra los tamaños:theme: { text: ["meta","micro","label"] }. - Sombras y colores personalizados.
text-14junto atext-t3, oshadow-[var(--x)]arbitrario junto a una sombra con nombre: el mismo problema. Cuando un código base tiene una escala personalizada cerrada en todas partes, usaclsxdirectamente allí.
Consulta references/cn.ts.
Enfoque y validación
- Un estilo
aria-invalid:ring-*prevalece sobrefocus-visible:ring-*y oculta el anillo de enfoque en un campo enfocado e inválido. Escribearia-invalid:not-focus-visible:ring-…. - Los
<button>simples dentro de formularios los envían. Si el Botón compartido no establece por defectotype="button", cada lugar de llamada convertido que no sea de envío debe configurarlo.