Ir al contenido

Sistema de temas

CÉNIT usa CSS custom properties definidas en app/globals.css para todo el sistema visual. El modo oscuro es el default (no requiere atributo en <html>). El modo claro se activa con data-theme="light" desde ThemeToggle + theme-provider.tsx. Cada organización puede sobreescribir colores primarios (--azul, --rojo) sin tocar código. Regla absoluta: nunca hardcodear colores en componentes — siempre via custom property.

--bg /* fondo principal de la app */
--sidebar /* fondo del sidebar */
--g1, --g2, --g3 /* fondos de cards / glass / panels */
--border, --border2
--text, --text2, --text3 /* principal / secundario / terciario */
--azul, --azul2 /* azul Nacional (#003082 default) */
--rojo /* rojo Nacional (#C8102E default) */
--ok, --ok-bg, --ok-t, --ok-b /* verde */
--warn, --warn-bg, --warn-t, --warn-b /* amarillo */
--danger, --danger-bg, --danger-t, --danger-b /* rojo */

Cada estado tiene cuatro variantes: el color base, su fondo (-bg), el color de texto sobre el fondo (-t) y el color de borde (-b).

app/globals.css:

  • Bloque :root (sin selector adicional) define las custom properties del modo oscuro.
  • Bloque [data-theme="light"] redefine las mismas properties con los valores claros.
  • ThemeProvider (components/theme-provider.tsx) lee la preferencia del usuario (cookie + prefers-color-scheme) y setea el atributo en <html> antes del hydrate para evitar flash.
  • ThemeToggle permite cambiar manualmente.

Cada org tiene en organizations:

  • primary_color → mapea a --azul.
  • secondary_color → mapea a --rojo.
  • logo_url → consumido por componentes de header / footer / emails.

El Shell del dashboard inyecta un <style dangerouslySetInnerHTML> con las overrides de la org actual:

<style dangerouslySetInnerHTML={{
__html: `:root { --azul: ${org.primary_color}; --rojo: ${org.secondary_color}; }`
}} />

Esto significa que un componente que usa var(--azul) adopta automáticamente el color del club sin re-render condicional.

Validación: primary_color y secondary_color se sanitizan en el form de settings (regex hex #RRGGBB). No se permite javascript: ni expresiones CSS complejas.

<div className="bg-[var(--g1)] border border-[var(--border)] text-[var(--text)]" />
<div className="bg-[#0F1E35] border-[#1A2A42] text-white" />

La segunda forma rompe modo claro y branding por org. Hay ESLint rule pendiente para enforzar — por ahora es disciplina de review.

Tailwind v4 con @theme inline permite mapear las CSS custom properties a tokens de Tailwind:

@theme inline {
--color-bg: var(--bg);
--color-azul: var(--azul);
}

Eso habilita bg-bg o text-azul como utilities, sin perder el sistema de temas runtime.

  • Multi-tenant — el branding por org se persiste en organizations.primary_color/secondary_color.
  • Plans y billingcustom_branding está en pro y enterprise. Hoy sin gate: esencial también puede usarlo. Deuda técnica conocida.
  • PWAapp/manifest.ts usa el color #070D1C como fondo de splash en modo oscuro.
  • Falta gate de custom_brandingseason_comparisons y custom_branding son las dos features sin guard que esencial puede usar gratis. ~2-3 h.
  • Sin theme builder UI — el admin de la org elige solo dos colores. Para clubes con paletas complejas se requiere intervención manual.
  • Modo claro menos pulido — el producto se diseñó dark-first. Algunos componentes legacy pueden tener contraste subóptimo en light. Auditar caso por caso al recibir feedback.
  • ESLint rule para enforzar no-hardcoded colors — pendiente.