Marca blanca
Cada hotel puede reescribir la tríada de marca y la interfaz entera responde: primario, enlaces, foco, gradiente y el filo de las tarjetas. Los mandos de esta página cambian la marca de toda la web mientras la recorres.
Qué mueve cada color
--brand-bluees--primaryy el origen de su hover y su active, del foco en claro, de la familiainfoy de toda la rampa--blue-*.--brand-cyanes el light-catch de los contenedores, el foco en oscuro y el glow reservado.--brand-purplecierra el gradiente de marca y tiñe la selección de texto.- Éxito, advertencia y error no dependen de la marca: tienen valores propios en
tokens.cssy no cambian con el tenant.
La Regla del Tenant
Ningún significado puede descansar en el azul. Un estado que solo se lee como estado porque es azul, o un error que solo se lee como error porque es morado, deja de leerse en el tenant siguiente. El significado lo cargan la forma, la posición, el contraste y la palabra; el color lo confirma.
Presets
Paletas completas de un clic, de BRAND_PRESETS —la tríada por defecto, «Chatbook»; cuatro alternativas y «Billia», un verde oscurecido hasta que el texto blanco de sus botones pasa AA—, más las que guardes abajo. Es el mismo estado que el selector de marca de la cabecera y se recuerda entre visitas. Antes de dar por terminada una pantalla, mírala con la paleta de otro tenant.
Generar una paleta
La de Unozero es una análoga: el cian y el morado están a unos 38° de tono del azul, a cada lado. Al girar hacia el morado, el color se oscurece y se satura. El oscuro es el punto medio entre azul y morado, casi sin croma. Elige el principal y una armonía, y el resto sale con la misma rampa de luz y croma. Guardada, aparece en los presets y en la cabecera.
Cian Señal
#00d1fd
Azul Operativo
#0973fc
Violeta Profundo
#8720fd
Grafito Violado
#22243a
Texto blanco sobre el principal: 4,3:1 — por debajo de 4,5:1, el texto de los botones primarios no se lee bien. Oscurece el principal.
Color por color
Cada selector es el ColorPicker del kit conectado a setBrandColor, que escribe la variable y su triplete -rgb en el style de <html>. «Restablecer» llama a resetBrandColors y vuelve a los valores de tokens.css. Las ediciones sueltas duran hasta que eliges un preset o recargas.
Lo que responde
Componentes del kit sin una línea de marca propia: la barra de progreso, el interruptor, el aviso informativo, el botón brand con su gradiente y su glow y el enlace se re-tiñen con cada cambio de arriba.
En tu producto
presets.ts da los datos y las funciones; dónde se guarda la marca de cada tenant lo decide tu backend. Lo habitual es inyectarla en el servidor, en el style de <html>, para que la primera pintada ya salga con ella. Pon siempre las dos variables de cada color: el triplete -rgb alimenta el light-catch, el glow y la selección.
import type { CSSProperties } from "react";
import { BRAND_TOKENS, hexToRgbTriplet } from "@unozero-ai/design-tokens/presets";
import { brandFontVariables } from "@unozero-ai/design-tokens/fonts";
import { getTenantBrand } from "@/lib/tenant"; // tu backend: { cyan, blue, purple }
import { Providers } from "./providers";
function brandStyle(brand: { cyan: string; blue: string; purple: string }): CSSProperties {
const [cyan, blue, purple] = BRAND_TOKENS;
return Object.fromEntries(
([[cyan, brand.cyan], [blue, brand.blue], [purple, brand.purple]] as const).flatMap(([token, hex]) => [
[token.name, hex],
[token.rgbName, hexToRgbTriplet(hex)],
]),
);
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const brand = await getTenantBrand();
return (
<html lang="es" className={brandFontVariables} style={brandStyle(brand)} suppressHydrationWarning>
<body className="font-sans antialiased">
<Providers>{children}</Providers>
</body>
</html>
);
}En la pantalla de ajustes del tenant, setBrandColor da la vista previa en vivo mientras el usuario elige. Escribe en el mismo style donde el servidor puso la marca, así que resetBrandColors borraría las dos: para descartar cambios, vuelve a aplicar la guardada con applyBrandPreset.
"use client";
import * as React from "react";
import { Button, ColorPicker } from "@unozero-ai/ui/react";
import { applyBrandPreset, BRAND_TOKENS, setBrandColor } from "@unozero-ai/design-tokens/presets";
type Brand = { cyan: string; blue: string; purple: string };
const [cyan, blue, purple] = BRAND_TOKENS;
export function BrandSettings({ saved, onSave }: { saved: Brand; onSave: (brand: Brand) => Promise<void> }) {
const [brand, setBrand] = React.useState(saved);
// `input` llega con cada color válido: vista previa en toda la app mientras se elige.
const edit = (key: keyof Brand, token: typeof cyan) => (e: Event) => {
const hex = (e.currentTarget as HTMLInputElement).value;
setBrandColor(token, hex);
setBrand((b) => ({ ...b, [key]: hex }));
};
function discard() {
applyBrandPreset({ name: "guardada", ...saved });
setBrand(saved);
}
return (
<form action={() => onSave(brand)} className="grid max-w-md gap-4">
<ColorPicker label="Color principal" value={brand.blue} onInput={edit("blue", blue)} />
<ColorPicker label="Acento" value={brand.cyan} onInput={edit("cyan", cyan)} />
<ColorPicker label="Cierre del gradiente" value={brand.purple} onInput={edit("purple", purple)} />
<div className="flex gap-3">
<Button type="submit" variant="primary">Guardar marca</Button>
<Button variant="ghost" onClick={discard}>Descartar cambios</Button>
</div>
</form>
);
}