Saltar al contenido

Product design, liderazgo y la serie Harness Design

Serie Harness DesignArtículo n.º 05

El primer resultado: cómo definimos la gobernanza de los tokens del Harness Design System

John Morais, .

Dos cuadrículas de azulejos, azul a la izquierda y naranja a la derecha, unidas azulejo a azulejo por hilos finos, con un azulejo naranja suelto, sin hilo, en la esquina inferior

En esos tres días (el inicio de las dos semanas de dedicación exclusiva al proyecto, que empezamos un miércoles) definimos un proceso de gobernanza y una forma de crear, editar y actualizar los tokens, tanto en Figma como en el código.

Nos aseguramos de resolver la gobernanza desde el principio, porque el objetivo nunca fue solo hacer que funcionara una vez. Era entregar una solución escalable, que otras personas del equipo puedan mantener después.

El primer día: poner a todos en la misma página

El kickoff fue sobre alineación. Hablamos del estado actual del design system y de los próximos pasos, y pusimos a designers y desarrolladores en la misma página antes de cualquier línea de código. Y ya empezamos el experimento central de la semana: aplicar documentación de harness a los tokens usando Claude Code.

En paralelo, el equipo de desarrollo empezó a estudiar cómo tener un repositorio en GitHub como fuente de la verdad de los tokens: un lugar único, actualizado siempre que haga falta, desde donde React y Flutter van a consumir.

Y hicimos una POC importante: el mapeo de equivalencias del cambio de tokens. La revisión que hicimos tocó muchas cosas, así que creamos un mapa que muestra cada token actual del sistema y cuál será su nuevo equivalente. La idea es simple: con ese mapa, la IA puede actualizar los tokens en el código mucho más rápido de lo que lo haríamos a mano.

El bind: el problema que no tenía una solución limpia

Antes del problema, necesito explicar un término que voy a usar bastante: bind. Hacer el bind es "amarrar" una propiedad de un elemento a un token (una variable del Design System) en lugar de dejar un valor suelto, escrito a mano. En nuestro caso, hacer (o corregir) el bind es asegurar que cada color, borde y redondeo de un botón esté amarrado al token correcto: no a un valor escrito a mano, no a un token equivocado, ni apuntando a una variable de otra biblioteca. Eso es lo que mantiene el componente consistente y fácil de mantener, y, más adelante, lo que reduce tiempo y costo.

Y justo en esa etapa apareció un problema que se llevó un buen rato: teníamos que hacer el bind de los componentes con los nuevos tokens en Figma. Parece simple. No lo fue.

Primero probé Claude Code vía MCP. No funcionó. La solución siguiente fue pedirle a Claude que generara scripts y usar el plugin Scripter para ejecutar el bind. Tampoco funcionó bien. En el segundo día recibí una actualización de Figma Agents y, después de iterar, llegué a un prompt de bind que funcionó mejor. No perfecto, pero ayudó de verdad.

El prompt quedó así:

Faça o bind das variáveis semânticas nas propriedades visuais do componente selecionado, seguindo estas regras:

Corner radius → bind cada canto individualmente (topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius) à variável correspondente da coleção Radius (ex: valor 4 → sm, 8 → md, 12 → lg, 16 → xl). Aplica em todos os nós com cornerRadius > 0 sem binding: COMPONENTs, FRAMEs, INSTANCEs, RECTANGLEs.

Stroke weight → bind cada lado individualmente (strokeTopWeight, strokeBottomWeight, strokeLeftWeight, strokeRightWeight) à variável correspondente da coleção Stroke Width (ex: valor 0.5 → sm, 1 → md, 2 → lg, 4 → xl). Aplica em qualquer nó com strokes e sem strokeWeight binding. Para VECTORs de ícone, usa setBoundVariable('strokeWeight', ...) direto.

Cores de stroke de ícones → ícones que renderizam via stroke (VECTORs sem fill, apenas strokes) devem ter o stroke color bound a variável da coleção Colour Usage > Icon/*. Identifica a variável correta pelo contexto da variante (cor do botão/estado). Não tocar nos fills de INSTANCE que envolvem ícones — se o ícone é stroke-based, limpar qualquer fill residual da instância wrapper em vez de bindá-lo.

Importante: Antes de executar, escaneia todas as variantes do component set para mapear o que está sem bind. Divide as operações em scripts separados por tipo de propriedade para evitar timeout (corner radius primeiro, depois stroke weight nos components, depois nos VECTORs internos). Verifica o resultado exportando uma imagem e checando node.boundVariables (atenção: cornerRadius é armazenado como topLeftRadius etc., e strokeWeight como strokeTopWeight etc. — checar esses campos, não cornerRadius diretamente).

Pero ni siquiera ese prompt era la solución definitiva. Esa llegó después, y de una forma que refuerza una de las cosas que más defiendo. Construimos un plugin propio para Figma, de manera multidisciplinaria, designers y desarrolladores juntos. Esa colaboración fue lo que permitió llegar a una solución de verdad: el plugin entregó resultados mucho mejores y personalizados exactamente a lo que necesitábamos, en lugar de depender de un script genérico o de un prompt que a veces acierta y a veces no. Y no resolvió solo el bind, también empezó a exportar los tokens como queríamos.

Este es el tipo de cosa que no aparece en la diapositiva bonita. Pruebas un camino, falla, pruebas otro, falla de nuevo, y lo que de verdad destraba, al final, fue que el equipo se sentó junto y construyó la herramienta que faltaba. Es parte del trabajo.

En ese mismo período también encontramos problemas estructurales en la semántica de los nombres de los tokens y los ajustamos. El nombre de un token importa más de lo que parece cuando quien lo va a leer es un LLM.

El tercer día: el schema, el momento que destrabó todo

El tercer día fue el más importante. Fue cuando paramos para estructurar de verdad cómo se construiría el harness: dónde viviría esa información, cómo se actualizaría, quién la toca. Y fue ahí donde definimos nuestro estándar de Schema para la description de los tokens, y por fin aplicamos la capa de Harness Design.

Este fue el ejemplo de schema que creamos:

{
  "purpose": "Fill sólido de alta ênfase de marca — identidade de marca e ação primária. Passo Bold base: superfície protagonista (ação primária, item selecionado, badge sólido).",
  "useWhen": ["Ação primária ou elemento de máxima proeminência da marca."],
  "doNotUseWhen": [
    { "scenario": "Sinalizar status (sucesso, erro, atenção, processo)", "reason": "marca não comunica estado; usá-la para status gera leitura ambígua de severidade", "alternative": "Background|Text|Icon/Status/*" },
    { "scenario": "Conteúdo apenas informativo, que não é a ação principal", "alternative": "Status/Info/* (mesma cor, papel semântico correto)" }
  ],
  "pairsWith": { "text": "Text/Brand/Inverse", "icon": "Icon/Brand/Inverse" },
  "note": null
}

Crear este schema solo fue posible por una decisión de estrategia: en este primer momento, pusimos la documentación directamente en la description de cada variable, dentro del propio Figma. Y validamos algo que era la pieza que faltaba: Claude Code puede traer esa información junto con el JSON de las variables. Es decir, tanto los tokens como el schema de harness van a vivir en Figma.

Me importa decir una cosa: esta estructura es totalmente funcional para nuestro contexto, con las herramientas y el equipo que tenemos. No es la arquitectura definitiva del universo. Es la que tiene sentido para nosotros ahora.

El flujo que quedó decidido

Con el schema definido, el flujo de aplicación de harness a los tokens quedó así:

  1. Definir primero el schema. Antes que nada, el contrato de documentación que va en el campo description, usando el W3C DTCG como base (que ya garantiza name, title y value como campos estáticos) y agregando por encima las propiedades de harness. Este esquema queda accesible para Claude en Prolog Claude, nuestro repositorio con la información de Prolog que Claude consume, en el archivo CLAUDE.md.

  2. Carga inicial vía Claude Code: el experimento que hicimos. Con el schema listo, le pedimos a Claude Code que entrara a Figma vía MCP y analizara cómo se usa cada token. De ese contexto saca lo que necesita para escribir el schema de cada uno de los ~600 tokens. Después de escribir los schemas, vuelve a Figma, también vía MCP, e inserta cada uno en la description de la variable correspondiente, en lugar de que alguien llene 600 tokens a mano. No esperamos que salga perfecto a la primera; la idea es probar la eficacia de este camino y mejorarlo después.

  3. Mantenimiento cuando un token cambia. El designer crea o edita la variable en Figma y escribe la description siguiendo el schema. Una skill en Claude Code se asegura de que no olvide aplicar el esquema, no borre nada de lo que ya existe y edite de la forma correcta.

  4. Export y commit, la parte manual de verdad. Un parser en Claude Code lee las descriptions vía Figma MCP, desglosa el harness, descarga el JSON actual del repositorio, compara el diff y hace commit en Prolog Claude. Ese JSON es lo que React y Flutter consumen después.

Por qué manual, y no la versión "ideal"

La pregunta obvia es: ¿por qué no automatizar todo? Sí diseñamos versiones más automatizadas, incluso una con una herramienta nuestra que centralizaba todo. Pero llegamos a una conclusión que vale la pena registrar: actualizar un token es raro. Ocurre menos de una vez al mes, es puntual.

Construir toda la automatización ahora no valía la pena. Cada paso automático abre la puerta a un edge case, el proceso se cae a la mitad, la IA edita mal, alguien olvida exportar, y el costo de diseñar esto bien, lidiando con cada una de esas fallas, es alto. Para una tarea que ocurre una vez al mes, el camino manual es más barato y más confiable.

Esto conversa directamente con lo que escribí en "Antes de AI-first, human-first". La solución ideal en el papel es hermosa. La solución real es la que el equipo puede operar hoy, sin ahogarse en los casos de excepción. Elegimos la real y gastamos bastante tiempo de discusión en una sola sesión de brainstorm para llegar a esta elección aparentemente simple. Valió cada minuto.

El harness para un token no es harness para un componente

Algo que quedó claro en este proceso: el harness para un token es diferente del harness para un componente. Lo normal es que el LLM construya interfaces usando los componentes que ya existen; no se la pasa creando componentes nuevos todo el tiempo. La necesidad de que entienda cómo usar un token aparece más en el escenario en que tiene que tomar decisiones al crear algo desde cero, sin un componente listo para reutilizar.

Ese escenario tiende a volverse más raro a medida que aplicamos harness a los componentes y ampliamos la cantidad de componentes utilizables. Por eso, incluso, el schema de los tokens terminó más pequeño de lo que yo imaginaba: no necesita cubrirlo todo, porque la mayor parte de las decisiones de interfaz va a venir de los componentes, no de los tokens sueltos.

Lo que viene: refinamiento y pruebas para harness en tokens

Lo que haremos a continuación es probar cómo el LLM está entendiendo los design tokens y validar todo el flujo que creamos para actualizar tokens. Después de eso, por fin llegamos a las pruebas con componentes.