
El verde ya significaba peso
Semana del 24 al 30 de agosto: terminé el panel de Forma y, al ir a homologar los colores de sus gráficas, descubrí que el valor por defecto de un componente llevaba meses afirmando algo falso en cuatro pantallas distintas.
De dónde partía la semana
Dos cosas en la lista. La primera, montar la publicación automática de este recap en Mastodon y en LinkedIn, para dejar de copiar y pegar los domingos por la noche. La segunda, terminar el panel de Forma: la última pantalla grande que seguía teniendo huecos.
La primera salió como estaba previsto y ocupó lunes y martes por la mañana. La segunda ocupó el resto de la semana y acabó siendo otra cosa.
Porque el encargo, tal y como lo escribí, era cosmético: «los roscos de nutrición tienen calorías en morado, proteínas en azul, carbohidratos en verde y grasas en naranja; pon las gráficas del panel igual». Diez minutos de trabajo, pensé. Cuatro PRs después seguía tirando del hilo.
El nudo
Primera capa: dos pantallas que se contradecían
Antes incluso de tocar las gráficas apareció lo primero. El widget de nutrición del panel y la página de Nutrición dibujaban los mismos macronutrientes con colores distintos. En el panel, las proteínas eran verdes y los carbohidratos ámbar. En la página, las proteínas eran azules y los carbohidratos verdes. Las dos llevaban meses en producción, las dos estaban «bien», y nadie las había visto nunca juntas porque están en rutas diferentes.
Ganó la de la página, que ya se usaba en las etiquetas de las comidas. Pero eso no era el problema: era el síntoma.
Segunda capa: el valor por defecto era una afirmación
El componente que dibuja todas las gráficas de línea de la aplicación acepta una
prop color. Y si no se la pasas, cae en el acento de la marca. Verde.
El detalle es que verde ya significaba PESO. Lo significaba en las fichas de composición corporal y en las filas de la tarjeta de tendencia, que sí pasaban su color explícitamente. Así que cualquier gráfica escrita sin pensar en el color salía verde, y salir verde no era «salir sin color»: era decir peso.
Cuatro pantallas lo decían. La tarjeta de Evolución del panel, donde eliges la métrica en un desplegable y el trazo se quedaba verde tanto si mirabas grasa como si mirabas músculo. Las cinco gráficas de Mediciones. Las tres tarjetas de Progreso. Todas afirmando lo mismo sobre datos distintos.
Nadie escribió ese error. Lo escribió el valor por defecto.
Tercera capa: arreglarlo destapó una colisión
Al devolverle el verde a Peso, Músculo tuvo que irse al azul. Y en la leyenda de «Distribución corporal» de la página de Mediciones, Agua ya era ese azul. Dos puntos de cuatro del mismo color le dicen al lector que las dos filas son la misma cosa.
Aquí es donde el problema deja de ser de estilo y pasa a ser de accesibilidad, que es la única parte de la semana que merece una explicación larga.
La opción evidente era mover Agua a un cyan: un tono vecino, para que la fila siga leyéndose como agua en lugar de convertirse en un color arbitrario. Pero un azul y un cyan del mismo brillo son exactamente el par que una deficiencia de visión del color aplana hasta hacerlos indistinguibles. El tono es justamente lo que se pierde. La luminancia no.
Así que el token no se declara una vez, sino dos: #22d3ee en el tema oscuro y
#0e7490 en el claro. No es una concesión al contraste sobre el fondo —que
también—, es que la separación respecto al azul de Músculo tenía que ser de
brillo: 2,04:1 en oscuro y 2,20:1 en claro. Una diferencia de brillo
sobrevive a cualquier daltonismo, porque el brillo no depende del matiz.
La alternativa era devolver Agua al gris, que es lo que sugiere la propia regla que esa tarjeta documenta —color para los valores reales, gris para los estimados—. Se descartó para que la leyenda mantenga cuatro marcas legibles en lugar de dos colores y dos grises.
Y de paso, los números no se escribían igual
Mientras estaba dentro apareció el mismo patrón en otro sitio. Mediciones y
Progreso escribían «74.0» con punto. Nutrición, el generador y las fichas del
panel pasaban por Intl en español y escribían «74,0» con coma. La tarjeta de
tendencia acabó poniendo los dos separadores en tarjetas contiguas, que es
exactamente donde se ve que no es un detalle: es la aplicación contradiciéndose
a sí misma a diez píxeles de distancia.
Cómo acabó
El color dejó de ser decoración y pasó a ser un contrato: una métrica, un tono, en todas las pantallas. Calorías e IMC en violeta, proteínas y músculo en azul, carbohidratos y peso en verde, grasas en ámbar, agua en cyan. Tres PRs —una por pantalla— y una única fuente de la paleta con un test que se rompe a propósito si alguien la mueve de sitio.
En ninguna de las tres el color es el único portador de la distinción: el selector nombra la métrica, cada gráfica lleva su etiqueta accesible y cada tarjeta su título. El color añade, no sustituye.

La tarjeta de nutrición cambió a la vez. Pasó de un donut de calorías más tres barras de macros a cuatro aros concéntricos, al estilo de los anillos de Apple Fitness: las calorías son el aro exterior y dentro van proteínas, carbohidratos y grasas. Y la página de Nutrición, que partía lo mismo en dos tarjetas que podían discrepar, ahora usa exactamente el mismo bloque.

Del dibujo salieron dos decisiones que no eran obvias. La primera: son cuatro
aros y no tres porque las calorías son el número titular del día y el centro no
valía como sitio —el agujero mide unos 24 píxeles en la tarjeta del panel: cabe
446, no cabe / 2320 kcal—. La segunda: el extremo redondeado del arco se
desactiva cuando el valor es cero. Un stroke-linecap redondeado a ratio cero
pinta un punto, y ese punto afirma un dato que nadie ha registrado.
En Mediciones el cambio se ve en las cinco gráficas a la vez, y la leyenda de distribución corporal es donde vive el cyan:

Y en Progreso, las tres tarjetas que antes decían todas «peso»:

El separador decimal se unificó en punto y, sobre todo, se centralizó: ahora hay un único módulo donde se decide cómo se escribe un número en toda la aplicación. Cuatro funciones, porque hay cuatro necesidades distintas y mezclarlas era parte del lío.
De la misma tanda salió la variación respecto a la medición anterior, que ahora va al lado del valor: «74.0 kg (+0.2)». Va en tinta apagada a propósito, no en el color de la serie. Un número en verde o en ámbar diría «bien» o «mal», y bajar de peso no es lo uno ni lo otro hasta que alguien decide qué está buscando. Y como «(-0.5)» suelto no significa nada leído en voz alta, esa forma se oculta del árbol de accesibilidad y la sustituye una frase completa: «0.5 kg menos que la medición anterior».
Lo último del panel fue el navegador de fecha. Eran dos flechas: con novecientas mediciones, llegar a la de hace un año eran novecientas pulsaciones. Ahora la misma barra ofrece tres granularidades —saltos rápidos, el calendario del sistema y las flechas de siempre— y navega por las fechas que tienen medición, no por el calendario: ni los saltos ni el calendario aterrizan en una fecha exacta, caen en la medición más cercana. Los saltos, además, son relativos a lo que estás mirando y no a hoy, que es lo que permite recorrer el historial a zancadas en vez de que el botón quede inerte a partir de la segunda pulsación.

Y una historia paralela que merece un párrafo: el martes por la tarde producción empezó a devolver errores 429. La primera hipótesis fue que la aplicación pedía de más, y era verdad a medias —había peticiones duplicadas, y arreglarlas bajó la ráfaga inicial del panel de doce peticiones a ocho—, pero los 429 seguían. Los ponía el nginx de delante, con una única zona de rate limiting compartida entre los ficheros estáticos y la API. Un panel que carga ocho peticiones y un puñado de recursos se estrangulaba a sí mismo. La lección no es de código sino de método: peticiones sueltas nunca disparan un limitador. Si la prueba no es una ráfaga, no es una prueba.
Lo que me llevo
Un valor por defecto es una afirmación. «Sin color» no existe: existe «verde», y el verde ya significaba algo. Si un componente puede pintarse sin que le digas qué representa, tarde o temprano representará lo que no es. Y el error no aparecerá en ninguna revisión, porque nadie lo escribió.
Cuando el color pasa a ser dato, hereda las obligaciones del dato: una única fuente, un test que se rompe si alguien la mueve, contraste medido en los dos temas, y nunca ser el único portador del significado.
Para separar dos tonos vecinos, mueve el brillo, no el matiz. El matiz es exactamente lo que una deficiencia de visión del color aplana.
Dos pantallas que dibujan lo mismo son dos oportunidades de contradecirse. El panel y la página de Nutrición llevaban meses discrepando sin que nadie las viera juntas, y un separador decimal distinto en tarjetas contiguas es el mismo fallo con otra ropa.
Y una de trabajar con agentes: hay que comprobar siempre que un test nuevo falla ANTES del arreglo. Dos veces esta semana escribí un test verde que no demostraba nada. Una de ellas porque estaba midiendo contra fixtures donde todo responde en el mismo instante, así que dos peticiones separadas en el tiempo parecían fundirse en una: di por arreglada una petición duplicada que seguía duplicada. El usuario del otro lado del teclado acertó varias veces donde yo me equivoqué, y eso también es parte del método.
La semana que viene
El generador de plan de verdad. La petición ya se guarda desde hace dos semanas, pero todavía no hay nada que construya el plan a partir de ella. Y publicar este mismo post en Mastodon y en LinkedIn con los scripts que escribí el lunes, que hasta hoy sólo se han probado en seco.
La semana en cifras
| PRs mergeadas en Forma | 19 (#248 → #266) |
| Acumulado del proyecto | 258 PRs |
| Líneas en Forma | +9.961 / −3.199 |
| Migraciones de base de datos | 0 — primera semana 100 % frontend |
| PRs en este blog | 5 (#28 → #32) · +935 / −38 |
| Sesiones de trabajo con Claude Code | 11 |
| Ventanas de 5 h consumidas | 14 |
| Ventana semanal | no se agotó |
| Prompts míos | 149 |
| Skills usadas | branch-pr, work-unit-commits, design, dataviz |
Nota sobre las capturas: están hechas contra la aplicación en el último commit de la semana, con datos de ejemplo y sin backend real. Nada de lo que se ve aquí son mis datos de salud.