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-blue es --primary y el origen de su hover y su active, del foco en claro, de la familia info y de toda la rampa --blue-*.
  • --brand-cyan es el light-catch de los contenedores, el foco en oscuro y el glow reservado.
  • --brand-purple cierra 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.css y 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.

ChatbookEsmeraldaAtardecerOcéanoRosaBillia

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.

Guardar paleta

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.

Restablecer la marca de Unozero

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.

Plan Pro
Se aplica del 1 al 3 de noviembre a todas las habitaciones dobles.
Crear reservaVer tarifas

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.

app/layout.tsx
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.

app/ajustes/marca/brand-settings.tsx
"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>
  );
}