Módulo 2: Design Tokens

Proyecto: el set de tokens de Mercado

Descripción

Las siete lecciones anteriores te enseñaron a leer un sistema de tokens: qué es un token, sus tres capas, cómo vive en CSS, cómo se nombra y cómo cambia por tema. Este proyecto te pone a construirlo. Vas a definir el set de tokens del storefront de Mercado —primitivos, semánticos y de componente, para tema claro y oscuro—, expresarlo como CSS custom properties, y resolver varios tokens con resolveToken en ambos temas para probar, con salida ejecutada, que tu sistema es coherente. En el módulo 1 hiciste el inventario (el plano); aquí fabricas la primera capa de ese plano —los cimientos— con valores reales.

El entregable tiene dos partes, y las dos importan. La parte 1 es el CSS: los tokens semánticos convertidos en custom properties, en un bloque :root para el tema claro y un bloque .dark para el oscuro —lo que de verdad pegarías en la hoja de estilos del storefront—. La parte 2 es la resolución: correr resolveToken sobre los tokens de componente en los dos temas y ver que la cadena de tres capas entrega los valores correctos. Juntas prueban que dominas lo esencial del módulo: no solo qué es un token, sino cómo definir un set completo, escribirlo para el navegador y verificar que resuelve.

Conexión con el módulo. Este proyecto es la síntesis de las siete lecciones. Define las tres capas (L3) con nombres por rol (L5) y override por tema (L6); las emite como custom properties (L4); y las resuelve con la función central del módulo (resolveToken, L3). Es también el puente al módulo 3: el CSS que emitas aquí —--color-primary, --color-surface— es exactamente lo que Tailwind consumirá como utilidades (bg-primary, bg-surface) en la próxima parada. Al terminar, la capa de tokens de Mercado deja de ser un plano y es código que corre.

Qué vas a construir

El entregable es un archivo de Node —tokens.js— que, al correrse, imprime dos cosas:

  1. El CSS de los tokens (parte 1): el bloque :root con los tokens semánticos del tema claro y el bloque .dark con los del oscuro, ambos generados desde el set con un emitVars. Es el CSS real del storefront.
  2. La resolución en ambos temas (parte 2): una tabla que, para cada token de componente de Mercado, muestra su valor final en claro y en oscuro, usando resolveToken.

No hay componentes React ni clases de Tailwind que escribir aquí: es puro modelo en Node, como todos los "Qué esperar" del módulo. La razón es la de siempre —el navegador no corre en un agente, así que mostramos el CSS que se escribiría y ejecutamos la lógica que lo valida—. Los componentes y las utilidades que consumen estos tokens llegan en los módulos 3 y 5; aquí fabricas los tokens que ellos referenciarán.

Una analogía: mezclar la paleta antes de pintar el mural

Un muralista no llega a la pared y empieza a mezclar colores sobre la marcha —un mural son semanas de trabajo y kilómetros de superficie; si cada día mezcla "más o menos el mismo azul", al final el cielo será un degradado accidental de cuatro azules—. Lo que hace es preparar, antes de tocar la pared, su paleta maestra: mezcla cada color una vez, lo guarda en un bote sellado, lo etiqueta por su papel ("cielo", "piedra", "sombra"), y prepara además una versión de cada uno para las zonas del mural que van en penumbra. Recién con la paleta maestra lista, sube al andamio.

Tu proyecto es preparar esa paleta maestra para Mercado. Los primitivos son los pigmentos base; los semánticos, los botes etiquetados por su papel; los dos temas, las versiones para luz y penumbra. El CSS que emites es la paleta escrita en la etiqueta de cada bote, lista para que cualquiera que pinte el mural (tú en el módulo 3, otro programador después) moje en el bote correcto sin re-mezclar. Y resolveToken es la prueba que haces antes de subir al andamio: destapas cada bote y confirmas que "cielo en penumbra" de verdad da el azul oscuro que esperabas. Un muralista que no prueba su paleta pinta un cielo equivocado en la parte más alta, donde bajar a corregir cuesta un día. Tú pruebas ejecutando, antes de que un solo componente dependa de un token mal resuelto.

Especificación del proyecto

Tu tokens.js debe cumplir esto:

Parte 1 — el CSS de los tokens.

  • Define un objeto tokens con las tres capas: primitives (valores crudos), semantics (rol → primitivo, con override { light, dark }) y component (pieza → semántico).
  • Incluye al menos estos roles semánticos: color.primary, color.surface, color.text, y un color.muted (un fondo sutil, distinto de surface, para superficies como la SearchBar).
  • Los primitivos se nombran por valor (blue.600), los semánticos por rol (color.primary, nunca blue), y los de componente apuntan a semánticos (nunca a primitivos).
  • Escribe un emitVars(selector, theme) que, para cada semántico, emita --nombre: valorResuelto;. Imprime el bloque :root (tema claro) y el bloque .dark (tema oscuro).

Parte 2 — la resolución.

  • Escribe resolveToken(name, theme) que siga la cadena componente → semántico → primitivo → valor.
  • Imprime una tabla con el valor en light y en dark de al menos cuatro tokens de componente (por ejemplo button.bg, card.bg, card.text, searchbar.bg).

Restricciones (las convenciones del módulo):

  • Todo identificador, token, nombre de custom property y valor en inglés; solo comentarios y textos en español.
  • Sin dependencias: puro JavaScript, corre con node tokens.js.
  • Salida literal y reproducible.
  • El override por tema vive solo en la capa semántica; los primitivos son fijos y los de componente heredan.

Solución de referencia

Aquí está una solución completa que cumple la especificación. Estúdiala después de intentarlo por tu cuenta; el valor del proyecto está en construirlo tú, no en leer la respuesta:

// L8 proyecto — el set de tokens de Mercado en 3 capas, su CSS y su resolucion.
const tokens = {
  // TIER 1 — primitivos: la paleta cruda, sin tema.
  primitives: {
    'blue.600': '#2563eb',
    'blue.400': '#60a5fa',
    'gray.50':  '#f9fafb',
    'gray.100': '#f3f4f6',
    'gray.800': '#1f2937',
    'gray.900': '#111827',
    'white':    '#ffffff',
  },
  // TIER 2 — semanticos: rol -> primitivo, con override por tema.
  semantics: {
    'color.primary': { light: 'blue.600', dark: 'blue.400' },
    'color.surface': { light: 'white',    dark: 'gray.900' },
    'color.muted':   { light: 'gray.100', dark: 'gray.800' },
    'color.text':    { light: 'gray.900', dark: 'gray.50'  },
  },
  // TIER 3 — de componente: pieza -> semantico.
  component: {
    'button.bg':   'color.primary',
    'card.bg':     'color.surface',
    'card.text':   'color.text',
    'searchbar.bg':'color.muted',
  },
};

// resolveToken: sigue la cadena hasta el valor final, segun el tema.
function resolveToken(name, theme) {
  if (name in tokens.component)  return resolveToken(tokens.component[name], theme);
  if (name in tokens.semantics)  return resolveToken(tokens.semantics[name][theme], theme);
  if (name in tokens.primitives) return tokens.primitives[name];
  throw new Error('Token desconocido: ' + name);
}

// emitVars: los tokens semanticos como CSS custom properties de un tema.
function emitVars(selector, theme) {
  const lines = [selector + ' {'];
  for (const name of Object.keys(tokens.semantics)) {
    lines.push('  --' + name.split('.').join('-') + ': ' + resolveToken(name, theme) + ';');
  }
  lines.push('}');
  return lines.join('\n');
}

console.log('=== 1) CSS custom properties de Mercado ===\n');
console.log(emitVars(':root', 'light'));
console.log(emitVars('.dark', 'dark'));

console.log('\n=== 2) resolveToken en ambos temas ===');
console.log('token'.padEnd(16) + 'light'.padEnd(12) + 'dark');
console.log('-'.repeat(38));
for (const name of ['button.bg', 'card.bg', 'card.text', 'searchbar.bg']) {
  console.log(name.padEnd(16) + resolveToken(name, 'light').padEnd(12) + resolveToken(name, 'dark'));
}

Qué esperar. Al correr node tokens.js, la salida es exactamente esta:

=== 1) CSS custom properties de Mercado ===

:root {
  --color-primary: #2563eb;
  --color-surface: #ffffff;
  --color-muted: #f3f4f6;
  --color-text: #111827;
}
.dark {
  --color-primary: #60a5fa;
  --color-surface: #111827;
  --color-muted: #1f2937;
  --color-text: #f9fafb;
}

=== 2) resolveToken en ambos temas ===
token           light       dark
--------------------------------------
button.bg       #2563eb     #60a5fa
card.bg         #ffffff     #111827
card.text       #111827     #f9fafb
searchbar.bg    #f3f4f6     #1f2937

Lee la salida como el entregable que es, en sus dos partes.

La parte 1 es tu paleta escrita para el navegador. El bloque :root es el tema claro y el .dark es el oscuro: los mismos cuatro nombres (--color-primary, --color-surface, --color-muted, --color-text), otros cuatro valores. Fíjate en --color-muted: en claro es #f3f4f6 (un gris casi blanco, apenas separado del --color-surface blanco) y en oscuro es #1f2937 (un gris apenas más claro que el --color-surface casi negro). Ese "apenas más claro/oscuro que la superficie" es justo el rol de un fondo muted —y funciona en los dos temas porque el semántico apunta a un primitivo distinto en cada uno—. Este bloque lo pegas en tu CSS y tienes theming completo; los componentes solo tendrán que usar var(--color-muted) y compañía.

La parte 2 es tu prueba de que el sistema resuelve. Cuatro tokens de componente, en dos temas, ocho valores —y todos coherentes con el CSS de arriba, porque salen del mismo set—. searchbar.bg da #f3f4f6 en claro y #1f2937 en oscuro: exactamente los valores de --color-muted, porque searchbar.bg → color.muted y hereda su valor de tema. Ningún token de componente tiene valores propios; todos toman los del rol que referencian. La tabla es la evidencia dura de que la cadena de tres capas —pieza → rol → pigmento → valor— entrega lo correcto en ambos temas.

Junta las dos partes y tienes la capa 1 de Mercado terminada: el CSS que el navegador lee (parte 1) y la prueba de que resuelve como debe (parte 2). Es el plano del módulo 1 convertido en cimientos reales —sobre los que el módulo 3 levantará las utilidades, y el 5 los componentes—.

Extensiones (opcionales, para ir más lejos)

Si quieres exprimir el proyecto, prueba estas ampliaciones —cada una refuerza una lección del módulo—:

  • Emite también los tokens de componente (L4). Extiende emitVars para emitir, además de los semánticos, los tokens de componente como custom properties que referencian al semántico: --button-bg: var(--color-primary);. Verás la cadena de tres capas escrita en CSS puro, con var() anidados.
  • Detecta primitivos duplicados (L7). Agrega una verificación que recorra primitives y avise si dos nombres distintos tienen el mismo hex —la prueba de "un valor, un primitivo" aplicada a tu propia caja de pigmentos—.
  • Agrega un tercer tema (L6). Suma highContrast a cada semántico (texto negro puro sobre blanco puro) y emite un tercer bloque. Confirma que no tocaste ningún token de componente.
  • Un rol nuevo, cero valores nuevos (L3/L7). Agrega un color.danger (rojo) con su primitivo, y un badge.bg → color.danger. Mide cuántas líneas costó sumar una pieza que usa un rol nuevo.

Ninguna es necesaria para cumplir el proyecto; todas son buen entrenamiento para el resto de la guía.

Errores comunes

Emitir el CSS con nombres por valor. Qué pasa: se define el semántico como blue en vez de color.primary, y emitVars produce --blue: #2563eb;. Por qué pasa: se saltó la regla de la L5 al armar el set. Cómo detectarlo: tu bloque :root tiene custom properties con nombres de color (--blue, --white) en vez de roles (--color-primary, --color-surface). Cómo corregirlo: el CSS hereda el nombre del token —si el token está mal nombrado, el CSS también—. Y el síntoma aparece en el bloque .dark: tendrías --white: #111827 (un absurdo). Nombra los semánticos por rol antes de emitir; el CSS correcto sale solo de un set correcto.

Poner el override de tema en la capa equivocada. Qué pasa: se le dan valores { light, dark } a un primitivo o a un token de componente, en vez de al semántico. Por qué pasa: no queda claro cuál capa "sabe" del tema. Cómo detectarlo: emitVars o resolveToken fallan o dan valores raros, porque intentan leer [theme] en una capa que no lo tiene (o lo tiene de más). Cómo corregirlo: solo semantics lleva { light, dark }. Los primitives son strings de valor fijo; los component son strings que apuntan a un semántico. Si tu resolveToken tiene que leer el tema en más de una capa, algo está en el lugar equivocado —el tema vive en el medio, y solo ahí—.

Inventar la salida en vez de ejecutarla. Qué pasa: se escribe el bloque "Qué esperar" a mano, calculando los valores mentalmente, sin correr el archivo. Por qué pasa: parece que uno "ya sabe" qué va a dar. Cómo detectarlo: tu salida reportada no coincide carácter por carácter con la real —un espacio de más en el padEnd, un hex mal copiado—. Cómo corregirlo: corre node tokens.js de verdad y pega su salida literal. Todo el módulo se para sobre la honestidad de "esto es lo que la máquina imprimió, no lo que creo que imprimiría". Un valor resuelto mal calculado a mano es justo el tipo de error que el sistema existe para eliminar; no lo reintroduzcas en la verificación.

Rúbrica de autoevaluación

Marca cada punto; si todos están, dominaste el módulo:

  • Tres capas. Tu tokens tiene primitives, semantics y component, y cada uno apunta a la capa de abajo.
  • Nombres correctos por capa. Primitivos por valor (blue.600), semánticos por rol (color.primary, nunca blue), componentes apuntando a semánticos.
  • Override solo en semánticos. El { light, dark } vive únicamente en la capa semántica; primitivos fijos, componentes que heredan.
  • CSS emitido. emitVars produce el bloque :root (claro) y el .dark (oscuro) con los mismos nombres y valores distintos.
  • Resolución ejecutada. resolveToken da los valores de al menos cuatro tokens de componente en ambos temas, coherentes con el CSS.
  • Salida literal. Corriste node tokens.js de verdad y la salida coincide con lo que reportas —no inventaste el output—.

Resumen y cierre del módulo

Con este proyecto cerraste el módulo 2 haciendo, no solo leyendo. Definiste el set de tokens de Mercado en sus tres capas —primitivos por valor, semánticos por rol con override por tema, de componente atados a roles—, lo emitiste como CSS custom properties (el :root claro y el .dark oscuro, listos para pegar) y probaste con resolveToken que cada token de componente resuelve al valor correcto en ambos temas. Con el muralista que prepara su paleta antes de subir al andamio viste por qué se hace así: mezclar una vez, etiquetar por rol, preparar la versión en penumbra, y probar la paleta antes de que un solo componente dependa de ella.

Da un paso atrás y mira lo que aprendiste en las ocho lecciones. Sabes qué es un token —un valor nombrado por su rol, no solo de color— y que la UI se construye referenciándolos (L2). Sabes que se organizan en tres capas —primitivos, semánticos, de componente— que resolveToken recorre hasta el valor (L3). Sabes que su vehículo en el navegador son las CSS custom properties, declaradas en :root y referenciadas con var() (L4). Sabes nombrarlos por su rol para que sobrevivan a un rebranding (L5). Sabes hacer theming cambiando el valor del semántico, no el componente (L6). Y viste la paleta completa de Mercado funcionando de punta a punta (L7). La capa más baja del sistema —los cimientos— ya no es un plano: es código que corre.

Lo que no hiciste todavía —a propósito— es consumir estos tokens desde los componentes. No escribiste una clase de Tailwind, no construiste el Button. Eso empieza ahora. El módulo 3 — Utility-first con Tailwind toma tu set de tokens y lo pone a trabajar: vas a ver cómo --color-primary se vuelve la utilidad bg-primary, por qué las utilidades funcionan por debajo (la cascada de web-fundamentals), y cómo se configura Tailwind para que lea tus tokens en vez de los suyos por defecto. Los cimientos que fabricaste aquí empiezan a sostener las paredes.

Recursos

  • MDN, "Using CSS custom properties (variables)" — developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties. El vehículo del CSS que emitiste; repásalo para pegar tu :root y tu .dark en un proyecto real. En inglés.
  • Design Tokens Community Group (W3C) — design-tokens.github.io/community-group. El estándar para describir el set que construiste; útil si más adelante exportas tus tokens a herramientas de diseño. En inglés.
  • shadcn/ui, "Theming" — ui.shadcn.com/docs/theming. Un set de tokens semánticos en custom properties con tema claro/oscuro, muy cercano al que hiciste; buen espejo de tu solución. En inglés.
  • Tailwind CSS, "Theme" — tailwindcss.com/docs/theme. Cómo Tailwind lee tus tokens para generar utilidades —la puerta directa al módulo 3—. En inglés.