Sistemas de diseño y personalización
Un vocabulario, muchos sistemas de diseño. Cambia un dial, declara una paleta, reorienta un token, y todas las plataformas lo siguen en un solo commit.
Tanrei está construido para poder reproducir cualquier sistema de diseño, con exactitud y no por aproximación, mientras su propio vocabulario se mantiene pequeño. Dos reglas lo hacen posible: los sistemas de diseño se diferencian ante todo en los valores, y lo que un valor no puede expresar, lo añade una capa.
El vocabulario del núcleo de Tanrei es todo estado que un sistema de diseño puede adoptar dentro de un producto. De ahí sale una prueba para cada token que se propone, la prueba del producto único:
- Es un token si un solo producto puede necesitar la distinción, por significado o por lugar.
focus-ringestá separado deprimaryporque un producto puede querer un color de marca saturado y un anillo de foco tranquilo. Que el sistema predeterminado les dé valores relacionados es una coincidencia, no un motivo para fusionarlos. Button tiene tamaños porque un mismo producto usa varios. - No es un token si solo lo tiene un sistema de diseño, o un producto poco común. El
tertiaryde Material Design 3 es una paleta declarada en el preajuste que lo reproduce, y sus rolesprimary-fixedson extensiones de ese preajuste, no huecos con los que cargan todos. Veinte tamaños de botón para un producto exótico tampoco son vocabulario del núcleo; ese producto los añade a su propia capa.
Si aun así no se puede reproducir algún rasgo de un sistema de diseño (un material de fondo, una tabla de tamaños de texto propia), eso es un defecto de Tanrei, y se anota en el registro de lagunas de imitación (docs/governance/mimicry-gaps.json en el repositorio).
Un sistema de diseño debería resultar adecuado de entrada y seguirte cuando cambias una sola cosa. Bajo todo lo demás hay ocho diales numéricos:
| Dial | Valor predeterminado | Mueve |
|---|---|---|
dial.type.base |
16px | todos los tamaños de texto |
dial.type.contrast |
1 | cuánto se separan los títulos del texto de cuerpo |
dial.space.unit |
8px | todas las distancias |
dial.size.unit |
4px | las alturas de los controles, los tamaños de los iconos, las áreas de pulsación |
dial.density |
1 | la altura de los controles y las filas, y el espacio alrededor de su contenido: por debajo de 1 más juntos, por encima de 1 más holgados |
dial.shape.unit |
2px | todas las esquinas |
dial.stroke.unit |
1px | los bordes, los anillos, el indicador de foco |
dial.motion.unit |
50ms | todas las duraciones |
Cada token es de uno de tres tipos:
- derivado: una regla pequeña, normalmente un coeficiente sobre un dial:
displayestype.base × 3,space175esspace.unit × 1.75. Las escalas tipográficas reales no son geométricas, así que cada token tiene su propio coeficiente; - fijo: un literal con un motivo declarado, exento de los diales (
corner.full, una curva de aceleración, un tamaño de nota al pie impuesto por ley); - alias: una referencia que se puede reorientar (
button.container.gap→space100).
En la web, los diales también funcionan en vivo: los tokens derivados se emiten como expresiones calc(), de modo que definir --tanrei-dial-type-base en cualquier elemento reescala todo lo que contiene. Una prueba evalúa cada token derivado en un navegador real con varios ajustes de los diales y exige que sea igual al resultado de la compilación.
Un sistema de diseño es un pequeño archivo JSON que extiende otro. Sobre todo cambia valores; donde hace falta, añade al vocabulario.
{
"id": "acme",
"extends": "tanrei",
"palettes": {
"brand": { "description": "El rojo de Acme lo fijan las directrices de marca.", "source": { "seed": "#e60012" }, "roles": "intent" },
"correct": { "description": "Un cuestionario marca las respuestas correctas: ni éxito ni primario.", "source": { "seed": "#0f766e" }, "roles": "intent" }
},
"tokens": {
"tanrei.dial.type.base": { "$value": { "value": 17, "unit": "px" }, "reason": "Nuestro público lee a la distancia de un brazo." },
"tanrei.comp.button.container.padding-inline-start.medium": { "scale": 2 },
"tanrei.comp.button.container.gap.medium": { "$value": "{tanrei.sys.space.space50}" },
"tanrei.sys.typescale.label-small": { "$value": { "fontSize": { "value": 11, "unit": "px" } }, "reason": "El tamaño de la nota legal es contractual." }
}
}| Quieres | Escribes |
|---|---|
| Todo un poco más grande | ajusta un dial |
| El margen interior de Button al doble del predeterminado, y que siga obedeciendo al dial de espaciado | "scale": 2 |
| Que este token use otro paso | reorienta el alias |
| Que este valor concreto no se mueva hagan lo que hagan los diales | fija $value con un reason |
Un color de marca o de dominio con sus propios roles on- y -container, con el contraste comprobado |
declara una paleta con "roles": "intent" |
Como el código de todas las plataformas se genera a partir del mismo sistema resuelto, un solo pull request cambia a la vez la web, SwiftUI, Compose, Flutter y React Native.
Los valores no siempre bastan. Un sistema de referencia tiene tamaños de botón que el núcleo no tiene; tu producto tiene un componente de valoración propio. Por eso una capa también puede extender el vocabulario: bajo vocabulary, siempre con un rationale, y solo añadiendo:
"vocabulary": {
"tanrei.sys.space.space1200": { "$type": "dimension", "rationale": "Las secciones de cabecera necesitan un paso de ritmo de 96px.",
"derive": { "fn": "mul", "of": "{tanrei.dial.space.unit}", "by": 12 } },
"tanrei.comp.button.size.3x": { "from": "large", "scale": 1.5, "rationale": "La llamada a la acción principal de la pantalla de inicio." },
"tanrei.comp.button.variant.danger": { "from": "filled", "rationale": "Aquí, la confirmación destructiva es un énfasis distinto." },
"acme.comp.rating.star.size": { "$type": "dimension", "rationale": "La estrella del componente de valoración.",
"derive": { "fn": "mul", "of": "{tanrei.dial.size.unit}", "by": 5 } }
}| Añades | Qué ocurre |
|---|---|
Un paso en una serie: un paso de espaciado, una esquina, un rol de texto, un rol de color con el rol que se dibuja sobre él (onRole) |
Se sitúa junto a sus vecinos, sigue el mismo dial y su contraste se comprueba como el de un par del núcleo |
| Un valor en un eje de un componente: un tamaño o una variante de Button | Basta una línea. Un tamaño clona de otro tamaño todos los miembros del eje (altura, márgenes interiores, estilo de la etiqueta, icono) y los escala, sin dejar de seguir los diales. Una variante clona sus colores en reposo; las reglas de patrón del sistema dan valor por ti a sus tokens de los estados al pasar el cursor, con foco, pulsado y deshabilitado |
Tus propios tokens, en el espacio de nombres de tu capa (acme.…) |
Para tus propios componentes. Pueden hacer referencia a cualquier token tanrei.*, seguir los diales y los esquemas, y se convierten en --acme-… en la web y en un objeto con tipo en las demás plataformas |
vocabulary dice que algo existe; tokens y rules le dan un valor, exactamente igual que a los tokens del núcleo, de modo que una capa hija cambia una extensión como cualquier otra cosa. Las extensiones se heredan, nunca eliminan ni cambian el tipo de nada del núcleo, y no pueden añadir estados de interacción (esos pertenecen a la plataforma). Cada paquete generado las enumera, con quién las declaró y por qué.
Los componentes aceptan lo que añades. <tanrei-button size="3x" variant="danger"> funciona en la web, y los paquetes nativos admiten las mismas claves; un valor que el sistema de diseño activo no tiene se representa como el predeterminado. Un sistema que no añade nada no paga nada: su código generado no menciona las extensiones en absoluto.
Así se sostienen juntos dos ideales: el vocabulario propio de Tanrei se mantiene pequeño y con nombres llanos, y cualquier sistema de diseño se puede igualar con exactitud, porque no hay que añadir nada al núcleo por causa de otro.
Los componentes nunca calculan el aspecto al pasar el cursor ni el del estado pulsado. Cada estado tiene sus propios tokens (button.filled.hovered.container.color, …), y las reglas de patrón de un sistema les dan valor:
{ "match": "tanrei.comp.*.*.hovered.container.color",
"derive": { "fn": "mix", "base": "{tanrei.comp.$1.$2.container.color}", "with": "{tanrei.comp.$1.$2.label.color}", "amount": "{tanrei.sys.state.hover-state-layer-opacity}" } }Esa es la capa de estado de Tanrei. Un sistema con colores explícitos por estado escribe alias en su lugar; uno sin estado al pasar el cursor hace del estado un alias de su valor en reposo; uno que se atenúa al pulsar define pressed.opacity. El mismo vocabulario, valores distintos.
Unas pocas diferencias no son cantidades en absoluto: una pulsación a la que se responde con una onda o atenuando el control, un foco del teclado que se muestra como un anillo o como un resplandor. Son dos tokens bajo sys.behavior (pressed-feedback, focus-style), cada uno con una lista corta y cerrada de nombres, y una capa elige uno como cualquier otro valor. Cómo se curva una esquina es una cantidad, no una elección: sys.shape.corner-smoothing, en Forma. Comportamiento las describe. El conjunto de iconos y los tipos de letra también puede cambiarlos una capa: consulta Iconos y Tipografía.
El mismo <tanrei-button>, el mismo marcado. m3-like y cupertino están ahí para probar el mecanismo frente a dos sistemas de caracteres opuestos, y como puntos de partida para tu propia capa.
La exactitud se mide, no se afirma. m3-like lleva un mapa de fidelidad con los tokens publicados de material-web (Apache-2.0): cada rol de color, cada estilo de texto y cada token de Button de la referencia debe ser igual, estar cubierto por otro token o figurar con la laguna de imitación que lo explica. La cifra se muestra más abajo junto al preajuste: cuántos son iguales de entre los tokens que no están cubiertos por otro, leída de tokens/generated/<id>/fidelity.json. Se llama «-like» por motivos de marca registrada, no porque sea una aproximación.
tanrei Tanrei
- Paletas declaradas
accent- Valores cambiados
- 152
- Reglas de patrón
- 132
- WCAG
- Cumple todos los invariantes en todos los esquemas. Una infracción hace fallar la compilación.
The default design system. Near-monochrome surfaces, a monochrome primary and one accent. It takes its values from core and leaves out the components and variants it does not draw.
cupertino Cupertino (Apple HIG-inspired)
- Extiende
core- Paletas declaradas
- Ninguna
- Valores cambiados
- 581
- Reglas de patrón
- 15
- Extensiones del vocabulario
- 3
- Fidelidad (medida)
- sin medir: la referencia no publica tokens legibles por máquina
- WCAG
- 113 mediciones de contraste por debajo de los umbrales de Tanrei (todas en contraste estándar). Solo a título informativo: los criterios de accesibilidad de Tanrei no se aplican a un preajuste de imitación, en ningún esquema de contraste.
An independent approximation of the look of Apple's platforms, expressed purely as values: a saturated system tint, pure white and black canvases, gray fills with tinted labels, rounded rectangles, no hover state and a dimmed pressed state.
Tanrei is not affiliated with, endorsed by, or sponsored by Apple Inc. Apple is a trademark of Apple Inc. The values here are Tanrei's own approximations, not a transcription of Apple's documentation, and no Apple font or symbol is bundled: the system font is referenced by name only.
m3-expressive-like M3 Expressive-like (Material 3 Expressive-inspired)
- Extiende
m3-like- Paletas declaradas
- Ninguna
- Valores cambiados
- 53
- Reglas de patrón
- 0
- Extensiones del vocabulario
- 2
- Fidelidad (medida)
- 35 / 55 tokens de referencia iguales · 0 diferentes · 20 sin correspondencia, con una brecha registrada — androidx.compose.material3 button size tokensv0_11_0 (Apache-2.0)
- WCAG
- Cumple todos los invariantes en todos los esquemas. Solo a título informativo: los criterios de accesibilidad de Tanrei no se aplican a un preajuste de imitación, en ningún esquema de contraste.
m3-like with the five button sizes of Material 3 Expressive — extra small to extra large, each with its own paddings, icon size, icon-to-label gap and outline width. Colors, type, shape and states are m3-like's.
Tanrei is not affiliated with, endorsed by, or sponsored by Google. "Material Design" is Google's; this preset is an independent reproduction of published, Apache-2.0 licensed token values.
m3-like M3-like (Material 3-inspired)
- Extiende
core- Paletas declaradas
tertiary- Valores cambiados
- 711
- Reglas de patrón
- 19
- Extensiones del vocabulario
- 18
- Fidelidad (medida)
- 273 / 274 tokens de referencia iguales · 0 diferentes · 1 sin correspondencia, con una brecha registrada — material-webv0_192 (Apache-2.0)
- WCAG
- Cumple todos los invariantes en todos los esquemas. Solo a título informativo: los criterios de accesibilidad de Tanrei no se aplican a un preajuste de imitación, en ningún esquema de contraste.
An independent approximation of Material Design 3's baseline look, expressed purely as values over Tanrei's vocabulary: a chromatic primary, tonal surfaces, pill buttons, shadow elevation without a ring.
Tanrei is not affiliated with, endorsed by, or sponsored by Google. "Material Design" is Google's; this preset is an independent approximation derived from publicly available, Apache-2.0 licensed design tokens.
En la web, un preajuste es una hoja de estilos adicional, limitada a un atributo, así que los sistemas pueden incluso convivir uno al lado del otro. Su entrada registra los componentes que incluye, entre ellos los que el sistema predeterminado deja fuera (@tanrei/web registra solo los del sistema predeterminado):
import '@tanrei/web/tokens.css';
import '@tanrei/web/systems/cupertino.css';
import '@tanrei/web/cupertino'; // los componentes que incluye cupertino<body data-tanrei-system="cupertino">Lo que todavía no pueden expresar ni los valores ni las elecciones de tipo (los materiales de fondo, las etiquetas en mayúsculas como cuestión de estilo) figura como defecto de Tanrei en docs/governance/mimicry-gaps.json; no se oculta.
Un sistema puede dejar fuera un componente, o un valor de la variant de un componente: el sistema por defecto no tiene botón segmentado, ni campo de texto filled u outlined. Una capa lo dice en components, con el motivo y lo que se usa en su lugar:
"components": {
"omit": { "carousel": { "reason": "Acme shows one thing at a time.", "insteadUse": ["list"] } },
"variants": { "tabs": { "omit": ["tonal"] } }
}Lo que se deja fuera se sigue dibujando y funciona. En una compilación de desarrollo, cada elemento que lo usa avisa una vez en la consola, con el nombre de tu sistema y lo que usa en su lugar. El aviso nombra el componente que escribiste: <tanrei-text-field variant="outlined"> se informa como el outlined del campo de texto, no como la entrada de texto que dibuja dentro. Un componente que vive en la capa de otro sistema, como el menú FAB en m3-like, se informa igual, con la capa en la que vive.
Para que tus pruebas fallen por ello, haz que se informe como un error y pregunta qué se usó:
import { setAvailabilityMode, unavailableUses } from '@tanrei/web';
setAvailabilityMode('error'); // en la preparación de tus pruebas, antes de que se dibuje nada
test('el producto usa solo lo que tiene su sistema', async () => {
// …renderiza las pantallas del producto…
expect(unavailableUses()).toEqual([]);
});unavailableUses() enumera cada uso una vez, en orden alfabético (segmented-button, text-field:outlined), en cualquiera de los dos modos. Una compilación de producción no comprueba nada, así que allí la lista queda vacía. @tanrei/web/availability contiene las mismas dos funciones sin registrar ningún elemento.
Compilar un sistema produce un paquete DTCG 2025.10 autocontenido (archivos de tokens sencillos, con todos sus valores, y un resolver estándar), de modo que cualquier herramienta que lea tokens de diseño puede consumirlo sin Tanrei. Los archivos de capa que lo produjeron viajan dentro del paquete, para que Tanrei (o un agente de IA) pueda reconstruirlo o modificarlo.
Accesibilidad
Los umbrales de contraste son constantes que nadie puede cambiar. En el sistema predeterminado, un incumplimiento hace fallar la compilación. En una capa de producto, un incumplimiento es una advertencia: se imprime al compilar y se escribe en los artefactos generados, nunca en silencio. A un preajuste que imita otro sistema se le exige, en cambio, fidelidad a ese sistema: los criterios de accesibilidad de Tanrei no se le aplican, ni en el contraste estándar ni en el medio ni en el alto, y las relaciones de contraste que la compilación mide en él quedan registradas como información, no como advertencias. Un fallo funcional (un control que no se puede manejar, un nombre o un estado que falta) es un defecto en cualquier sistema, también en un preajuste de imitación.