15 KiB
Tokens de @pi/ui
Todo el aspecto visual de la librería (color, forma, sombra, tipografía de controles) se resuelve en custom properties de CSS, nunca en valores hardcodeados dentro de un componente. Hay dos familias con propósitos distintos, más un puñado de primitivas base:
| Familia | Dónde vive | Qué representa | Cuántos temas la redefinen |
|---|---|---|---|
--pi-color-* |
:root (_pi-color-variables.scss) |
Paleta de marca (primary/accent/highlighted/warn/success/grey/white/black/background) | Todos excepto Minimal |
--pi-theme-* |
.pi-shell (themes/_*.scss) |
Forma y comportamiento por superficie: botón, campo, card, panel, chip, overlay | Todos |
| Primitivas base | :root (_pi-global-variables.scss, _pi-font-sizes-variables.scss, _pi-public-variables.scss) |
Espaciado, radios, sombras, tamaños de fuente, animaciones — sin tematizar | Ninguno |
Los componentes de common/ (botón, campo, card, panel, chip, modal,
snackbar, tooltip, accordion…) no leen la paleta ni la forma directamente:
consumen --pi-theme-*, y son los ficheros de styles/themes/ los que deciden
qué valor tiene cada token. Así, un tema nuevo no toca un solo selector de
componente — solo redefine tokens (ver _alternative.scss, _minimal.scss,
_gradient.scss como ejemplo).
Cómo se aplica un tema
import { ShellClass } from '@pi/ui';
// en el elemento raíz (normalmente <body> o el contenedor de la app)
document.body.className = ShellClass.Alternative; // 'pi-shell pi-shell__alternative'
ShellClass expone Default, Alternative, Minimal y Gradient
(libs/ui/src/lib/styles/configs.ts). Cada uno corresponde a un mixin en
libs/ui/src/lib/styles/themes/_<nombre>.scss, incluido desde
themes/index.scss. Dentro de un shell se puede forzar el aspecto contrario en
un subárbol con .is-private/.is-public (p. ej. el input context de
pi-card).
El patrón de fallback
Un token puede consumirse de dos formas, según si el tema por defecto lo declara o no:
// El tema Default declara este token → todos los temas lo tienen.
border-radius: var(--pi-theme-card-radius);
// El tema Default NO lo declara → el segundo argumento de var() reproduce
// el valor original hardcodeado. Solo Minimal (u otro tema futuro) lo pisa.
border-radius: var(--pi-theme-chip-radius, #{$chip-border-radius});
Esto es intencional: los tokens que todos los temas existentes necesitan
personalizar (radio/altura/tipografía de botón, forma de campo) se declaran en
_default.scss. Los que solo un tema concreto toca (radio de overlays en
Minimal, degradado de foco en Gradient) se dejan sin declarar en Default —así
su valor por defecto es exactamente el que tenía el componente antes de
tokenizar, byte a byte, y ningún tema existente cambia de aspecto sin querer.
--pi-theme-*: tabla completa
D = Default · A = Alternative · M = Minimal · G = Gradient.
Una celda vacía significa "no lo redefine, hereda el valor de la fila D (o el
fallback de var() si D tampoco lo declara, marcado como (fallback))".
Botón (pi-button/_pi-button.scss, _pi-button-theme.scss, menu/dynamic-menu)
| Token | D | A | M | G |
|---|---|---|---|---|
--pi-theme-button-height |
var(--pi-spacing-xxx-large) (2.5rem) |
2.875rem |
2.5rem |
— |
--pi-theme-button-radius |
var(--pi-border-radius-medium) |
999px (píldora) |
var(--pi-border-radius-xx-small) |
— |
--pi-theme-button-font-weight |
600 |
700 |
500 |
— |
--pi-theme-button-letter-spacing |
normal |
0.04em |
normal |
— |
--pi-theme-button-text-transform |
none |
uppercase |
none |
— |
--pi-theme-button-shadow |
none |
0 6px 16px -6px rgba(106,63,212,.55) |
none |
— |
--pi-theme-button-transition |
background-color .16s ease-in |
all .2s ease-out |
background-color .12s ease-out, border-color .12s ease-out |
— |
--pi-theme-button-fill-primary |
none (relleno plano) |
degradado primary-900→primary-400 |
none |
degradado primary-400→primary-900 |
--pi-theme-button-fill-accent |
none |
degradado accent-900→accent-400 |
none |
degradado accent-400→accent-900 |
--pi-theme-button-fill-warn |
none |
— | none |
— |
--pi-theme-button-fill-success |
none |
— | none |
— |
--pi-theme-button-fill-highlighted |
none |
degradado highlighted→highlighted-300 |
none |
degradado highlighted-300→highlighted |
--pi-theme-button-fill-#{$color} se interpola por nombre de color en
_pi-button-theme.scss:47 (background-image: var(--pi-theme-button-fill-#{$color}, none)),
así que un tema nuevo que quiera degradado en un color que no sea
primary/accent/highlighted solo tiene que declarar
--pi-theme-button-fill-<color>.
Campos de formulario (_mat-form-field.scss, file-upload, _mat-datepicker.scss, _mat-select.scss)
| Token | D | A | M | G |
|---|---|---|---|---|
--pi-theme-field-radius |
0.875rem |
999px |
var(--pi-border-radius-xx-small) |
— |
--pi-theme-field-border-width |
1px |
2px |
1px |
— |
--pi-theme-field-border-color |
var(--pi-color-grey-200) |
var(--pi-color-primary-100) |
var(--pi-color-grey-200) |
— |
--pi-theme-field-border-color-hover |
var(--pi-color-grey-300) |
var(--pi-color-primary-300) |
var(--pi-color-grey-400) |
— |
--pi-theme-field-border-color-focus |
var(--pi-color-primary) |
var(--pi-color-primary) |
var(--pi-color-black) |
transparent (deja ver el degradado) |
--pi-theme-field-surface |
var(--pi-color-white) |
var(--pi-color-primary-25) |
var(--pi-color-white) |
— |
--pi-theme-field-label-color |
var(--pi-color-grey-300) |
var(--pi-color-primary-900) |
var(--pi-color-grey-400) |
— |
--pi-theme-field-label-transform (fallback: none) |
— | uppercase |
none |
— |
--pi-theme-field-label-letter-spacing (fallback: normal) |
— | 0.08em |
normal |
— |
--pi-theme-field-label-weight (fallback: inherit) |
— | 700 |
500 |
— |
--pi-theme-field-focus-gradient (fallback: none) |
— | — | — | linear-gradient(120deg, primary, accent, highlighted) |
Superficies: card / panel / chip
| Token | D | A | M | G | Consumidores |
|---|---|---|---|---|---|
--pi-theme-card-radius |
var(--pi-border-radius-large) |
1.5rem |
var(--pi-border-radius-xx-small) |
— | _pi-card.scss, skeleton.component.scss |
--pi-theme-card-shadow |
var(--pi-box-shadow-panel) |
0 10px 30px -12px rgba(59,13,107,.28) |
none |
— | _pi-card.scss |
--pi-theme-panel-radius |
var(--pi-border-radius-large) |
1.5rem |
var(--pi-border-radius-xx-small) |
— | panel.component.scss, _mat-datepicker.scss, _mat-select.scss, dynamic-menu |
--pi-theme-chip-radius (fallback: radio original por tamaño) |
— | 0.25rem |
0 |
— | chip/styles/_size.scss (×3, por tamaño y por affix) |
Overlays: modal / notification / tooltip / accordion
Ningún tema salvo Minimal los redefine — Default/Alternative/Gradient usan
siempre el fallback de var() (el valor que tenían antes de tokenizar).
| Token | Fallback (D/A/G) | M | Consumidores |
|---|---|---|---|
--pi-theme-modal-radius |
var(--pi-border-radius-medium) (panel completo) / var(--pi-border-radius-large) (.pi-modal__panel--full) |
var(--pi-border-radius-xx-small) |
_mat-dialog.scss (×2) |
--pi-theme-notification-radius |
var(--pi-border-radius-large) |
var(--pi-border-radius-xx-small) |
snack-bar.component.scss, _mat-snackbar.scss (×2) |
--pi-theme-notification-shadow |
var(--pi-box-shadow) |
none |
_mat-snackbar.scss |
--pi-theme-tooltip-radius |
var(--pi-border-radius-medium) |
var(--pi-border-radius-xx-small) |
_mat-tooltip.scss |
--pi-theme-tooltip-shadow |
var(--pi-box-shadow-panel-double) |
none |
_mat-tooltip.scss |
--pi-theme-accordion-radius |
var(--pi-border-radius-large) |
var(--pi-border-radius-xx-small) |
accordion.component.scss, _horizontal-accordion.scss, _vertical-accordion.scss (×6, incluye un calc(... - 0.0625rem)) |
Puente con Angular Material (--mat-sys-*)
Los componentes de Material (slider, checkbox, radio, progress…) no leen
--pi-color-*: pintan con sus propios roles --mat-sys-*, generados por
mat.theme() a partir de una paleta separada. _default.scss reengancha una
cosa con otra una sola vez, por indirección:
--mat-sys-primary: var(--pi-color-primary);
--mat-sys-on-primary: var(--pi-color-white);
--mat-sys-primary-container: var(--pi-color-primary-100);
--mat-sys-on-primary-container: var(--pi-color-primary-900);
--mat-sys-secondary: var(--pi-color-accent);
--mat-sys-on-secondary: var(--pi-color-white);
--mat-sys-secondary-container: var(--pi-color-accent-100);
--mat-sys-on-secondary-container: var(--pi-color-accent-900);
--mat-sys-tertiary: var(--pi-color-highlighted);
--mat-sys-on-tertiary: var(--pi-color-white);
--mat-sys-error: var(--pi-color-warn);
--mat-sys-on-error: var(--pi-color-white);
Como es indirección (var(--pi-color-*), no un valor literal), un tema que
redefina --pi-color-primary etc. (como Alternative) tiñe también los
controles de Material sin tener que repetir este bloque.
--pi-color-*: paleta de marca
Declarada una vez en :root (_pi-color-variables.scss) con los valores
Default, y redeclarada completa dentro de .pi-shell__alternative (Minimal y
Gradient no tocan la paleta — ver comentario en _minimal.scss: "minimalista
aquí es una cuestión de forma, no de color"). Se consume directamente por
nombre (no hay indirección --pi-theme-* de por medio) en ~100 ficheros de
la librería: bordes, fondos, texto, iconos, estados de badge/chip/status, etc.
Cada paleta (primary, accent, highlighted, warn, success) sigue la
misma escala de 10 pasos + variante rgba del tono 200:
10 → 25 → 50 → 75 → 100 → 200 → 200-rgba → 300 → 400 → (base) → 900
| Escala | primary (D) | accent (D) | highlighted (D) | warn (D) | success (D) |
|---|---|---|---|---|---|
| 10 | #fdfdff |
#fffdf9 |
#f9fbff |
#fff9fa |
#f7fcf9 |
| 25 | #fafbff |
#fffaed |
#f2f5ff |
#ffedf0 |
#e6f6ed |
| 50 | #f0f4ff |
#fdf5e5 |
#e9eeff |
#fcebeb |
#def5e8 |
| 75 | #eaefff |
#fae5bf |
#dae2ff |
#ffe7ea |
#c7ebdc |
| 100 | #d7e2fb |
#fce0ad |
#bccdfa |
#f7cdcd |
#bbe8d5 |
| 200 | #c3d5ff |
#f6d495 |
#6482d7 |
#f2acac |
#83dbb6 |
| 200-rgba | rgba(196,213,255,.15) |
rgba(252,224,173,.15) |
rgba(168,193,255,.15) |
rgba(237,129,129,.15) |
(no tiene) |
| 300 | #a3c0ff |
#f2c26a |
#436ac1 |
#ec8b8b |
#60c99d |
| 400 | #8cacf2 |
#f6bc63 |
#1d3f8a |
#e87272 |
#3ebe88 |
| base | #799ef0 |
#eda82a |
#0d2a6b |
#e45959 |
#1cb273 |
| 900 | #5772d4 |
#d78f0f |
#030f3c |
#c13737 |
#009758 |
Neutros y utilidad, sin escala de tema (no cambian entre Default/Alternative):
| Token | Valor | Uso |
|---|---|---|
--pi-color-white |
#ffffff |
— |
--pi-color-white-900 |
#f4f4f4 |
— |
--pi-color-white-rgba-03/05/06/09 |
blanco a 33/50/66/88% opacidad | overlays, disabled |
--pi-color-black-200 |
#323748 |
— |
--pi-color-black |
#1a1a1a |
texto, foco (Minimal) |
--pi-color-grey-10…900 |
escala de 10 pasos, #fcfcfd → #3d3d3d |
bordes, texto secundario |
--pi-color-background-300/400/500 |
#aeaeb6 / #fcfcfc / #f7f7f7 |
fondos de página |
En _alternative.scss la paleta completa (primary, accent, highlighted)
se redeclara con una gama azul→violeta / turquesa / violeta oscuro,
respectivamente. warn y success se dejan intactos a propósito en todos los
temas: son colores semánticos y cambiarlos perjudicaría la lectura del estado.
Nota:
_pi-color-variables.scssdeclara--pi-color-grey-200dos veces (líneas 328-329) y--pi-color-white/--pi-color-background-400con el mismo patrón en otro punto del fichero — inofensivo (la segunda declaración gana y tiene el mismo valor) pero es limpieza pendiente si se retoca el fichero.
Primitivas base (sin tematizar)
Estas variables no cambian entre temas — son la escala de espaciado/forma/sombra
de toda la librería, y los tokens --pi-theme-* casi siempre se expresan en
función de ellas (var(--pi-border-radius-large), var(--pi-spacing-xxx-large)…).
| Familia | Tokens | Rango |
|---|---|---|
| Espaciado | --pi-spacing-xxxxx-small … --pi-spacing-xxxx-large |
0.0625rem (1px) … 3rem (48px), 11 pasos |
| Radio | --pi-border-radius-xx-small … --pi-border-radius-x-large |
0.125rem (2px) … 1.375rem (22px), 5 pasos |
| Sombra | --pi-box-shadow, -hover, -double, -panel, -panel-double, -panel-disabled, -top, -inset |
valores rgba fijos, ver _pi-global-variables.scss |
| Animación | --pi-animation, -fast, -slow |
all .16s/.25s/.4s ease-out |
| Tamaño de fuente | --pi-font-size-xxx-small … -xxx-large |
0.5rem (8px) … 2.875rem (46px), 9 pasos |
| Layout (header/footer/navbar/aside/card) | --pi-height-*, --pi-width-* |
ver _pi-global-variables.scss |
| Landing/marketing | --pi-public-font-size-*, --pi-public-height-*, --pi-public-width-* |
ver _pi-public-variables.scss |
_pi-gradient-variables.scss ($pi-gradient-primary-light, etc.) son
variables Sass, no custom properties de CSS — se resuelven en tiempo de
compilación y no forman parte del contrato de theming en runtime.
Cómo tokenizar una superficie nueva
- En el consumidor, cambia el valor hardcodeado por
var(--pi-theme-<superficie>-<propiedad>, <valor-original>). El fallback garantiza que ningún tema existente cambia de aspecto hasta que decida redefinir el token. - Si todos los temas van a necesitar tocarlo pronto, decláralo también en
defaultThemeTokens(_default.scss) con el valor original — así queda documentado como parte del contrato en vez de vivir solo como fallback disperso en el CSS. - Añade el override en el/los temas que lo necesiten
(
_alternative.scss,_minimal.scss,_gradient.scss). - Actualiza este documento.
Cómo añadir un tema nuevo
- Crea
libs/ui/src/lib/styles/themes/_<nombre>.scsscon un mixin<nombre>ThemeTokensque redefina solo los tokens--pi-theme-*(y, si aplica,--pi-color-*) que cambian respecto a Default. - Aplícalo a
.pi-shell__<nombre>. - Impórtalo desde
themes/index.scss. - Añade la entrada a
ShellClassenstyles/configs.ts.
No hace falta tocar ningún componente: si consumen los tokens en vez de valores literales, el tema nuevo se propaga solo.