Temas empresariales
Paleta por modo, tokens, radio, fuentes, contraste AA.
En esta página
Cada empresa (dominio) puede definir toda su paleta y toda la aplicación se adapta a ella: modo claro y oscuro, radio de esquinas, tipografía y qué temas genéricos puede elegir el usuario. El motor garantiza contraste WCAG 2.1 AA por defecto. El tema se guarda en Domain.theme (backend) y se sirve por GET /api/config.
Modelo de configuración#
| Campo | Tipo | Significado |
|---|---|---|
palette.light / palette.dark | objeto {token: "#rrggbb"} | Cualquiera de los 49 tokens, por modo. Un valor explícito gana sobre los campos antiguos y sobre la derivación. Solo overlay admite alfa |
primaryColor, accentColor, backgroundColor, textColor, cardColor, mutedColor, borderColor, secondaryColor, inputColor, ringColor y sus *Foreground | hex #rgb/#rrggbb | Campos antiguos (siguen funcionando). Primario y acento valen para ambos modos; los neutros van al modo que corresponda al fondo (claro u oscuro) |
defaultMode | light · dark · system | Modo inicial solo cuando el usuario aún no ha elegido tema. system sigue prefers-color-scheme |
radius | none · sm · md · lg · xl (o número de rem 0–3) | Escala de esquinas: 0, 0.25, 0.5 (por defecto), 0.75 y 1 rem |
fontFamily | inter · system · humanist · geometric · rounded · serif · mono | Familia de la lista blanca (cuerpo y títulos). titleFont/bodyFont (nombre alfanumérico) la refinan solo si no hay fontFamily |
allowedThemes | lista de ids | Temas genéricos elegibles: light, dark, midnight, amoled, ocean, forest, rose, contrast. Vacío u omitido = todos |
lockBrand | booleano | true: el usuario solo ve los temas de la empresa (brand-light/brand-dark). Solo tiene efecto si hay colores de empresa |
autoFixContrast | booleano (por defecto true) | false: los tokens explícitos que incumplan AA no se corrigen (solo se avisa) |
landing | objeto | Landing y login: ver Landing y login |
Cómo se interpreta#
- Con algún color (campo antiguo o
palette) se generan dos temas completos,brand-lightybrand-dark. Radio, fuente o modo solos no crean temas de empresa. - Si defines un solo modo, el otro se deriva del fondo del primero (conserva el matiz; luminosidad 8 % en oscuro y 97.5 % en claro).
- Un fondo de esquema contrario (oscuro en
palette.lighto claro enpalette.dark) se descarta con aviso: nunca se pinta un lienzo oscuro con tokens claros. - Un muted, borde o secundario casi idéntico al fondo (por ejemplo
#ffffffsobre#ffffff) se descarta y se deriva uno visible (solo conautoFixContrastdistinto defalse). - Con
autoFixContrast: true, un color que no cumple AA se ajusta al más cercano que sí cumple. El editor del panel muestra el color elegido, el aplicado y su ratio. - Las claves desconocidas se descartan al guardar (el backend y el frontend usan el mismo saneador, con hex normalizado a
#rrggbb).
Tokens y su función#
Hay 49 tokens (--color-<token> y utilidades Tailwind bg-<token>, text-<token>, border-<token>, ring-<token>, con opacidad como bg-primary/10). Los componentes de BloomX solo usan estos tokens, nunca hex ni paletas crudas.
| Token | Función |
|---|---|
background / foreground | Lienzo de la app y texto principal |
card / card-foreground | Superficies elevadas: tarjetas, paneles, modales |
popover / popover-foreground | Menús desplegables, tooltips, selects |
primary / primary-foreground | Acción principal y texto de marca (text-primary ≥ 4.5:1 sobre fondo y tarjeta) |
secondary / secondary-foreground | Botones secundarios |
muted / muted-foreground | Zonas apagadas y texto secundario (AA sobre fondo, tarjeta, muted, popover, sidebar, no leído y filas) |
accent / accent-foreground | Resaltado neutro de hover/selección |
destructive, success, warning, info (+ -foreground) | Estados: relleno y texto (text-success), tintes bg-success/10 |
border | Divisores y bordes decorativos |
input | Borde de controles de formulario (≥ 3:1) |
ring | Anillo de foco (≥ 3:1) |
brand-accent / brand-accent-foreground | Segundo color de marca |
sidebar, sidebar-foreground, sidebar-accent, sidebar-accent-foreground, sidebar-border | Panel lateral (carpetas, etiquetas), su ítem activo y divisor |
header / header-foreground | Barra superior |
unread / unread-foreground | Fila de correo no leída |
row-hover, row-selected / row-selected-foreground | Fila en hover y fila seleccionada |
link / link-hover | Enlaces en texto |
code / code-foreground | Código en línea y bloques |
overlay | Fondo translucido de modales (único token que admite alfa #rrggbbaa) |
chip / chip-foreground | Etiquetas y badges suaves |
selection / selection-foreground | Selección de texto |
scrollbar | Pulgar de la barra de scroll |
Además del color: --radius (todas las clases rounded-* derivan de él; rounded-full no cambia) y las variables de fuente --font-body y --font-title.
Guía para diseñadores#
- Empieza con 2–3 colores:
primary(marca),brand-accent(segundo color) ybackground. El motor deriva los otros 40+ tokens y garantiza AA. Defineforeground,card,muted,bordersolo si la marca los exige. - Elige `primary` pensando en dos usos: relleno de botón (con
primary-foregroundencima) y texto/enlace sobre el fondo (text-primarydebe llegar a 4.5:1 sobrebackgroundycard). Un primario muy claro suele fallar como texto en modo claro; usa uno más oscuro enpalette.lighty uno más claro enpalette.dark. - Define el modo oscuro por separado cuando tengas una versión de marca oscura: los tonos que funcionan sobre blanco casi nunca funcionan sobre
#101418. - Fondos oscuros: evita negros puros (
#000000) salvo intención AMOLED; un fondo muy oscuro y otrocardapenas más claro mantienen la jerarquía. - Estados (
destructive,success,warning,info): conservan su matiz semántico y solo se corrige el contraste. No los cambies a menos que la marca lo exija. - Radio y fuente cambian la personalidad sin tocar contraste:
radius: "none"para un estilo corporativo recto,"xl"para uno amable;fontFamilysolo admite la lista blanca (sin fuentes web propias). - Verifica con
npm run check:themesañadiendo tu paleta aBRAND_FIXTURES(src/lib/theme-fixtures.ts) y con el editor del panel, que avisa en vivo de cada par que incumple.
Contraste AA: qué se exige#
| Par | Mínimo |
|---|---|
Texto (foreground, card-foreground, popover-foreground, sidebar-foreground, header-foreground…) sobre su superficie | 4.5:1 |
muted-foreground sobre background, card, muted, popover, sidebar, no leído y filas hover/seleccionada | 4.5:1 |
primary, destructive, success, warning, info, brand-accent, link y link-hover como texto sobre background/card | 4.5:1 |
Botones: primary-foreground sobre primary (y las demás parejas relleno/texto) | 4.5:1 |
input (borde de controles) y ring (foco) sobre background y card | 3:1 |
scrollbar sobre background | 1.5:1 |
text-muted-foreground no está garantizado sobre secondary, header, code, chip, primary, imágenes ni degradados. Los avisos de autoFixContrast: false solo cubren los pares de la tabla anterior.
Ejemplos JSON#
{
"primaryColor": "#0b5fff",
"accentColor": "#f59e0b"
}{
"defaultMode": "system",
"radius": "lg",
"fontFamily": "humanist",
"allowedThemes": ["midnight", "contrast"],
"lockBrand": false,
"autoFixContrast": true,
"palette": {
"light": {
"background": "#fafaf7",
"foreground": "#1c1b18",
"primary": "#0b5fff",
"primary-foreground": "#ffffff",
"brand-accent": "#b45309",
"sidebar": "#f1efe8",
"header": "#ffffff"
},
"dark": {
"background": "#101418",
"foreground": "#eef1f5",
"primary": "#6ea8ff",
"primary-foreground": "#0a0f1a",
"brand-accent": "#fbbf24",
"sidebar": "#0b0e12"
}
}
}{
"lockBrand": true,
"defaultMode": "light",
"palette": {
"light": { "primary": "#7c3aed", "background": "#faf5ff" },
"dark": { "primary": "#c4b5fd", "background": "#120a1f" }
}
}# Guardar el tema de la empresa (sesión de manager). El cuerpo REEMPLAZA todo el tema, incluida la landing.
curl -sS -X PUT "$BACKEND/api/admin/domain" \
-H 'content-type: application/json' -b 'auth_session=<cookie>' \
-d '{"theme": { "primaryColor": "#0b5fff", "radius": "lg", "landing": { "layout": "split-right" } } }'PUT reemplaza el tema completo
PUT /api/admin/domain guarda el theme entero que envíes (incluida landing). Envía siempre el objeto completo. Si guardas desde una pantalla del panel que solo conoce los campos de color y fuentes, revisa que no descarte palette ni landing.
Qué ve el usuario#
- Con colores de empresa, el selector muestra primero
"<Empresa> · Claro"y"<Empresa> · Oscuro"y luego los genéricos permitidos (lightydarkse ocultan porque los sustituyen, salvo que estén enallowedThemes). defaultModesolo aplica si el usuario no ha elegido (sin cookie,localStorageni BD). Una elección explícita, incluido "Sistema", gana siempre.- Una preferencia guardada que ya no es elegible se remapea al tema por defecto de su mismo esquema, sin borrar lo guardado. Con
lockBrand, todas. - Sin configuración de empresa: los 8 temas genéricos y
system, como siempre. El tema se inyecta en el servidor (<style id="bx-brand">) y hay un script anti-parpadeo.
Probar en local#
- Sin backend: define
NEXT_PUBLIC_BLOOMX_THEME_OVERRIDEen.env.localy reinicianext dev. Sustituye el tema de/api/config(servidor y cliente) y se ignora en producción. Borra la cookiebloomx-themeylocalStorage["bloomx:theme:v1"]para verdefaultMode. - Contraste:
npm run check:themes. - Tests: los de tema y selección.
- Avisos por token:
buildBrandThemes(cfg)?.warnings(oanalyzeBrandTheme(cfg)) devuelve, por token, el valor elegido, el aplicado, los ratios y si se corrigió.
# .env.local (solo desarrollo; se ignora en producción). Una línea, JSON válido.
NEXT_PUBLIC_BLOOMX_THEME_OVERRIDE={"primaryColor":"#7c3aed","backgroundColor":"#faf5ff","defaultMode":"dark","radius":"lg","fontFamily":"humanist","palette":{"dark":{"background":"#120a1f","sidebar":"#0d0716"}}}npm run check:themes # contraste AA de los 8 temas y de los 12 ejemplos de empresa
npm run check:themes -- --md # tabla en Markdown
npx vitest run src/lib/__tests__/brand-theme.test.ts src/lib/__tests__/theme-config.test.ts src/lib/__tests__/theme-selection.test.tsTemas genéricos#
Sin configuración de empresa hay 8 temas: light, dark, midnight, amoled, ocean, forest, rose y contrast (alto contraste), más Sistema. La preferencia se guarda en cookie, localStorage y en la cuenta. El correo entrante tiene además su propia opción de modo (oscurecer, por defecto, o papel claro; mailDarkMode).
Límites conocidos#
- Solo
overlayadmite alfa; el resto de colores se rechaza si trae alfa distinto deff. - La red de seguridad que remapea las escalas crudas de Tailwind en temas oscuros existe para código antiguo; el código nuevo no debe contar con ella.
- Las fuentes son solo las de la lista blanca (sin cargar fuentes propias).
- Los colores de agenda, calendarios y etiquetas de usuario los elige cada usuario; el texto encima se calcula por contraste.