Un pipeline de compilación para aislar habilidades de diseño externas de Claude en el código de producción
Para un desarrollador que crea un servicio full-stack solo y sin diseñador, los documentos de habilidades de diseño externas son atractivos. Copiar un prompt de sistema de diseño de 5,000 líneas encontrado en Twitter o GitHub y colocarlo en las instrucciones del sistema de Claude da la impresión de que la interfaz quedará impecable.
En la realidad, ocurre exactamente lo contrario. Tan pronto como se envía el primer mensaje, salta una advertencia de límite de tokens y el tiempo de espera de la respuesta de la API aumenta indefinidamente. El código generado por el modelo inserta estilos en línea desconocidos en los componentes o importa un módulo de Framer Motion que jamás se ha instalado en el proyecto. Al final, después de corregir manualmente la UI rota hasta altas horas de la madrugada, se regresa a la plantilla básica original de shadcn/ui.
El problema no es la sensibilidad estética del modelo. La causa principal es el método de verter texto Markdown no estructurado directamente en el prompt del sistema. Es necesario convertir las descripciones de estilo en lenguaje natural a especificaciones de tokens legibles por máquina y construir un pipeline que realice pruebas de aislamiento del código generado por el modelo antes de que este toque los archivos de producción.
Desechar el Markdown emocional y reemplazarlo por la especificación JSON W3C DTCG
Pegar por completo el Markdown de un sistema de diseño en el prompt genera un desperdicio severo de tokens. Los modificadores estéticos como "un azul marino profundo que inspira confianza al usuario" no sirven para absolutamente nada en la generación de diseños. Estas oraciones consumen recursos de cálculo dentro del modelo de lenguaje que deberían destinarse a las interfaces de TypeScript o a la lógica de validación que realmente se deben cumplir.
Se eliminan las descripciones en lenguaje natural y se reconstruye el prompt del sistema utilizando objetos JSON que cumplen con la especificación W3C Design Tokens Community Group (DTCG). Solo se conservan los colores, espaciados y radios de curvatura.
`json
{
"color": {
"background": {
"surface": { "value":"oklch(0.980.005250)","type": "color" },
"canvas": { "value":"oklch(1.000)","type": "color" }
},
"action": {
"primary": { "value":"oklch(0.550.20250)","type": "color" },
"primary-hover": { "value":"oklch(0.480.22250)","type": "color" }
}
},
"spacing": {
"compact": { "value":"0.5rem","type": "dimension" },
"comfortable": { "value":"1rem","type": "dimension" }
},
"radius": {
"md": { "value":"0.5rem","type": "dimension" }
}
}
`
Se combina la especificación de tokens estandarizada con Anthropic Prompt Caching. Los tokens básicos y el rol del sistema se fijan en un bloque estático aplicando cache_control: { type: "ephemeral" }.
`typescript
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
export async function requestComponentGeneration(componentBrief: string) {
return await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 4096,
system: [
{
type: "text",
text: Generador de componentes de Next.js y Tailwind CSS. Utiliza estrictamente solo las clases definidas en la especificación de tokens DTCG a continuación y no emitas estilos en línea (atributo style). [DTCG_TOKENS_JSON_DATA],
cache_control: { type: "ephemeral" },
},
],
messages: [
{
role: "user",
content: componentBrief,
},
],
});
}
`
Al eliminar los documentos de texto plano, el tamaño del prompt se reduce de alrededor de 10,000 tokens a un nivel de 1,200 tokens. Dado que, según la documentación de Anthropic, la lectura de caché de prompts cobra solo el 10% de la tarifa de tokens de entrada básica, los costos de la API se reducen en más del 70% durante las llamadas repetidas.
Reglas de linter para prevenir estilos en línea arbitrarios y conflictos de clases
Al inyectar habilidades externas, Claude suele introducir sigilosamente valores arbitrarios como style={{ marginTop: '13px' }}. También genera clases arbitrarias entre corchetes (w-[342px]) de manera indiscriminada. Este tipo de código destruye poco a poco el sistema de estilos global.
Se configuran eslint-plugin-react y eslint-plugin-tailwindcss en el Flat Config de eslint.config.mjs para filtrarlos en la fase de compilación.
`javascript
import reactPlugin from "eslint-plugin-react";
import tailwindPlugin from "eslint-plugin-tailwindcss";
export default [
{
files: ["**/.{ts,tsx}"],
plugins: {
react: reactPlugin,
tailwindcss: tailwindPlugin,
},
settings: {
tailwindcss: {
callees: ["cn", "cva"],
config: "./tailwind.config.ts",
},
},
rules: {
"react/forbid-dom-props": [
"error",
{
forbid: [
{
propName: "style",
message: "No se permiten estilos en línea. Utiliza las utilidades de Tailwind especificadas.",
},
],
},
],
"tailwindcss/no-arbitrary-value": "error",
"tailwindcss/no-custom-classname": [
"error",
{
whitelist: ["animate-."],
},
],
},
},
];
`
Los tokens extraídos se mapean en theme.extend dentro de tailwind.config.ts. Si se sobrescribe la paleta predeterminada, los colores a los que hacen referencia las primitivas internas de shadcn/ui se corrompen.
`typescript
import type { Config } from "tailwindcss";
import themeTokens from "./build/tailwind/theme.json";
const config: Config = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
surface: themeTokens.color.background.surface,
canvas: themeTokens.color.background.canvas,
action: {
primary: themeTokens.color.action.primary,
"primary-hover": themeTokens.color.action["primary-hover"],
},
},
borderRadius: {
token: themeTokens.radius.md,
},
},
},
plugins: [require("tailwindcss-animate")],
};
export default config;
`
Las interacciones se aíslan mediante variantes de Class Variance Authority (CVA). Alterar directamente las primitivas de Radix UI destruye las propiedades del árbol de accesibilidad (ARIA).
`typescript
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-token text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-action-primary text-white hover:bg-action-hover active:scale-[0.98]",
secondary: "bg-surface text-foreground hover:bg-surface/80",
ghost: "hover:bg-surface text-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-12 px-8 text-base",
},
motion: {
subtle: "transition-all duration-150 ease-out",
expressive: "transition-all duration-300 cubic-bezier(0.16, 1, 0.3, 1)",
},
},
defaultVariants: {
variant: "default",
size: "default",
motion: "subtle",
},
}
);
export interface ActionButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
({ className, variant, size, motion, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, motion, className }))}
ref={ref}
{...props}
/>
);
}
);
ActionButton.displayName = "ActionButton";
`
Limitación de animaciones para proteger el tamaño del bundle
Cuando se le piden movimientos vistosos, Claude recurre habitualmente a Framer Motion. Según las mediciones de Bundlephobia, Framer Motion ocupa aproximadamente 60 KB incluso después de la compresión Gzip. En el momento en que se añaden 60 KB solo para animar un par de botones, el indicador de carga inicial LCP se ve afectado.
Se prohíbe en el prompt del sistema la importación de bibliotecas de terceros y se restringe el uso exclusivo a propiedades orientadas al hilo del sintetizador del navegador (Compositor Thread).
`typescript
// Forma no recomendada que provoca reflujo de diseño en el hilo principal
//
// Forma recomendada procesada en el hilo del sintetizador de la GPU
`
Las propiedades geométricas como top, left, width y height recalculan el árbol de diseño en cada fotograma. Por el contrario, la utilidad transform-gpu y la opacidad opacity se procesan en una capa independiente de la GPU sin pasar por el hilo principal, lo que permite mantener fluidos los 60 FPS.
Asimismo, el efecto de desenfoque de fondo (backdrop-filter: blur()) se omite en las áreas de desplazamiento. En navegadores móviles de bajas prestaciones, un filtro de desenfoque dentro del viewport de desplazamiento recalcula las texturas en cada fotograma, haciendo que la frecuencia de actualización de la pantalla caiga por debajo de los 20 FPS. Usar un canal alfa semitransparente de color sólido (bg-background/80 y un borde de 1px) en lugar de desenfoque evita problemas de ocupación de memoria de video.
Pipeline de verificación automática a través de Storybook y Playwright
Si el código generado por Claude se inserta directamente en el directorio app/ de Next.js, el diseño se rompe y se producen errores de hidratación. Los componentes se renderizan primero en un entorno independiente de Storybook y solo aquellos que superan las pruebas se fusionan en producción.
Se integra axe-playwright en .storybook/test-runner.js para comprobar la accesibilidad según los estándares WCAG 2.1 AA.
`javascript
const { injectAxe, checkA11y } = require("axe-playwright");
module.exports = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await page.waitForSelector("#storybook-root", { state: "attached" });
await checkA11y(page, "#storybook-root", {
detailedReport: true,
axeOptions: {
runOnly: {
type: "tag",
values: ["wcag2a", "wcag2aa"],
},
},
});
},
};
`
Si una prueba falla, el registro de errores se devuelve directamente a Claude.
`text
[Bucle de retroalimentación de IA: Prompt de calibración de componentes]
El código generado ha fallado en la prueba de accesibilidad de axe-core.
Analice los registros y emita un código que conserve la estructura original corrigiendo únicamente los defectos.
[Detalles de la falla]
Elemento violado: color-contrast
Nodo fallido:
Motivo: La relación de contraste de color es 2.8:1, lo cual no alcanza el estándar WCAG AA de 4.5:1.
Directriz de solución: Reemplace la utilidad de texto por text-foreground o text-action-primary-hover.
`
Se configura un script de shell que toma instantáneas de regresión visual (Visual Regression) con Playwright y crea una rama únicamente cuando se superan todas las comprobaciones.
`typescript
import { test, expect } from "@playwright/test";
import storybookManifest from "../../storybook-static/index.json";
const stories = Object.values(storybookManifest.entries).filter(
(entry) => entry.type === "story"
);
for (const story of stories) {
test(Verificación de instantánea: ${story.title} - ${story.name}, async ({ page }) => {
await page.goto(/iframe.html?id=${story.id}&viewMode=story);
await page.waitForSelector("#storybook-root");
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot(`${story.id}.png`, {
animations: "disabled",
maxDiffPixelRatio: 0.01,
threshold: 0.2,
});
});
}
`
`bash
#!/usr/bin/env bash
set -e
COMPONENT_NAME=$1
if [ -z "$COMPONENT_NAME" ]; then
echo "Error: Ingrese el nombre del componente a verificar."
exit 1
fi
echo "1. Comprobación de guardarraíles de linter"
pnpm eslint "src/components/ui/${COMPONENT_NAME}.tsx" --max-warnings=0
echo "2. Compilación estática de Storybook"
pnpm build-storybook --quiet
echo "3. Pruebas CLI de accesibilidad en Storybook"
pnpm test-storybook
echo "4. Comparación de instantáneas de regresión visual con Playwright"
pnpm playwright test tests/visual/component-regression.spec.ts
echo "5. Aprobado: Creación de rama y envío de PR"
git checkout -b "feature/ui-COMPONENTNAME"gitadd"src/components/ui/{COMPONENT_NAME}.tsx"
git commit -m "feat(ui): COMPONENTNAMEverificacioˊndeaislamientocompletada"gitpushorigin"feature/ui−{COMPONENT_NAME}"
gh pr create --title "feat(ui): Adición de ${COMPONENT_NAME}" --body "Componente que ha superado las pruebas de accesibilidad A11y en Storybook y de regresión en Playwright."
`
Ya no es necesario abrir las herramientas de desarrollo del navegador cada vez que la interfaz se distorsiona para ajustar los márgenes píxel a píxel. Al delegar las reglas de validación a un script de terminal, es posible diseñar UIs de forma segura incluso estando a solas.