# Galappxy CSS Framework — Guía de Temas Dinámicos

> **v2.1 · Febrero 2026**
> Documentación para el equipo de desarrollo sobre cómo funciona el sistema de theming por tenant.

---

## Índice

1. [Arquitectura general](#arquitectura)
2. [Estructura de archivos](#estructura)
3. [Cómo funciona el theming dinámico](#como-funciona)
4. [Crear un tema nuevo (por archivo CSS)](#crear-tema-css)
5. [Crear un tema inline (desde el JWT)](#crear-tema-inline)
6. [Variables disponibles](#variables)
7. [Dark mode por tenant](#dark-mode)
8. [Flujo en producción](#flujo-produccion)
9. [Theme Manager API](#api)
10. [Portal de clientes](#portal-clientes)
11. [Modo embed](#modo-embed)
12. [Referencia rápida de componentes](#componentes)

---

## 1. Arquitectura general {#arquitectura}

El framework usa **CSS Custom Properties** (variables) como tokens de diseño. Cada tenant puede sobrescribir estas variables mediante dos métodos:

```
┌─────────────────────────────────────────────────┐
│  :root (variables.css)                          │
│  Valores default (School iBox purple)           │
├─────────────────────────────────────────────────┤
│  [data-theme="dark"]                            │
│  Override para dark mode                        │
├─────────────────────────────────────────────────┤
│  [data-tenant="gymflow"]                        │ ← Método 1: Archivo CSS
│  Override por tenant (archivo CSS pre-generado) │
├─────────────────────────────────────────────────┤
│  [data-theme="dark"][data-tenant="gymflow"]      │
│  Dark mode + tenant combinado                   │
├─────────────────────────────────────────────────┤
│  element.style (inline)                         │ ← Método 2: Inline desde JWT
│  Override desde theme_config_json del JWT        │
└─────────────────────────────────────────────────┘
```

La **cascada CSS** garantiza que cada capa sobrescribe a la anterior. El tenant siempre gana sobre los defaults.

---

## 2. Estructura de archivos {#estructura}

```
shell/
├── css/
│   ├── variables.css              ← Tokens de diseño (colores, espacios, etc.)
│   ├── base.css                   ← Reset, tipografía, utilidades
│   ├── layout.css                 ← Grid del app shell (sidebar, topbar, etc.)
│   ├── animations.css             ← Keyframes y micro-interacciones
│   ├── components/
│   │   ├── buttons.css
│   │   ├── cards.css
│   │   ├── dropdowns.css
│   │   ├── forms.css
│   │   ├── tabs.css
│   │   ├── badges-avatars.css
│   │   ├── tables.css
│   │   ├── modals-loaders.css
│   │   └── extensions.css         ← Smart menu, mobile fixes, embed, profile switcher
│   ├── themes/
│   │   ├── tenant-schoolibox.css  ← Tema School iBox (referencia)
│   │   ├── tenant-gymflow.css     ← Tema GymFlow (fitness)
│   │   ├── tenant-colegiopatria.css ← Tema Colegio Patria (educación)
│   │   └── tenant-medicare.css    ← Tema MediCare (salud)
│   ├── galappxy.css               ← Master import (@import de todo)
│   └── galappxy-bundle.css        ← Bundle concatenado para producción
├── js/
│   └── theme-manager.js           ← Clase ThemeManager (carga dinámica)
├── demo.html                      ← Demo admin (empresas)
└── demo_client.html               ← Demo portal clientes (alumnos, pacientes)
```

**El bundle** es la concatenación de todos los archivos source en orden. Se debe regenerar cada que se modifique un source:

```bash
cat variables.css base.css layout.css \
    components/buttons.css components/cards.css components/dropdowns.css \
    components/forms.css components/tabs.css components/badges-avatars.css \
    components/tables.css components/modals-loaders.css \
    animations.css components/extensions.css \
    > galappxy-bundle.css
```

---

## 3. Cómo funciona el theming dinámico {#como-funciona}

### Método 1: Archivo CSS por tenant (recomendado para producción)

El `ThemeManager` carga un archivo CSS desde `css/themes/tenant-{id}.css`:

```javascript
// Al hacer login, el JWT trae el tenant_id
GX.theme.init('gymflow');
// → Carga /css/themes/tenant-gymflow.css
// → Aplica data-tenant="gymflow" al <html>
// → Todas las variables se sobrescriben
```

**Ventajas:** Cache del browser, no bloquea el render, dark mode automático.

### Método 2: Config inline desde el JWT

El JWT puede traer un `theme_config_json` con los colores del tenant:

```javascript
const themeConfig = {
  primary_color: '#FF6B00',
  sidebar_bg: '#1C1C1E',
  bg_body: '#FFF8F0',
  // ... más propiedades
};

GX.theme.init('gymflow', themeConfig);
// → Aplica las variables directamente en :root via element.style
// → NO carga archivo CSS
```

**Ventajas:** No requiere archivo pre-generado, útil para preview en tiempo real.

### ¿Cuál usar?

| Escenario | Método |
|-----------|--------|
| Producción (tenant ya configurado) | Archivo CSS |
| Preview de tema en admin | Inline |
| Tenant nuevo sin archivo | Inline |
| Onboarding de cliente | Inline → generar CSS al guardar |

---

## 4. Crear un tema nuevo (por archivo CSS) {#crear-tema-css}

### Paso 1: Crear el archivo

Crear `css/themes/tenant-{id}.css` donde `{id}` es el identificador del tenant en la base de datos:

```css
/*
 * GALAPPXY — Tenant Theme: Mi Empresa
 * ID: miempresa
 * Sector: Retail
 */

[data-tenant="miempresa"] {
  /* ── Colores de marca ────────────────── */
  --gx-primary:          #E11D48;
  --gx-primary-hover:    #BE123C;
  --gx-primary-light:    #FFE4E6;
  --gx-primary-dark:     #9F1239;
  --gx-primary-rgb:      225, 29, 72;   /* IMPORTANTE: para rgba() */

  --gx-secondary:        #F59E0B;
  --gx-secondary-hover:  #D97706;
  --gx-secondary-light:  #FEF3C7;
  --gx-secondary-rgb:    245, 158, 11;

  --gx-accent:           #E11D48;

  /* ── Sidebar ─────────────────────────── */
  --gx-sidebar-bg:       #4C0519;
  --gx-sidebar-bg-hover: rgba(225, 29, 72, 0.12);
  --gx-sidebar-accent:   #FB7185;
  --gx-sidebar-text:     #FDA4AF;
  --gx-sidebar-text-active: #FFFFFF;

  /* ── Child sidebar ───────────────────── */
  --gx-childsidebar-bg:     #6B0926;
  --gx-childsidebar-hover:  rgba(251, 113, 133, 0.10);
  --gx-childsidebar-border: #881337;

  /* ── Topbar ──────────────────────────── */
  --gx-topbar-bg:        #FFFFFF;
  --gx-topbar-icon-hover: #E11D48;

  /* ── Paneles laterales ───────────────── */
  --gx-panel-header-bg:  #4C0519;
  --gx-panel-trigger-bg: #4C0519;

  /* ── Fondos del body ─────────────────── */
  --gx-bg-body:          #FFF1F2;
  --gx-bg-surface-hover: #FFE4E6;
  --gx-bg-surface-alt:   #FECDD3;
}
```

### Paso 2: Generar la variante `--gx-primary-rgb`

**Esto es OBLIGATORIO.** Muchos componentes usan `rgba(var(--gx-primary-rgb), 0.1)` para fondos translúcidos. Sin el RGB separado, esos estilos no funcionan.

Para convertir hex a rgb:
- `#E11D48` → `225, 29, 72`
- `#0077B6` → `0, 119, 182`

### Paso 3 (opcional): Dark mode del tenant

Agregar al mismo archivo:

```css
[data-theme="dark"][data-tenant="miempresa"] {
  --gx-sidebar-bg:       #2D0A16;
  --gx-childsidebar-bg:  #3D0F1F;
  --gx-bg-body:          #1A0A10;
  --gx-bg-surface:       #2D0A16;
  --gx-bg-surface-hover: #3D0F1F;
  --gx-bg-surface-alt:   #350D1B;
  --gx-bg-elevated:      #3D0F1F;
  --gx-border-light:     #4D1428;
  --gx-border-color:     #5D1A32;
}
```

**Tip:** Para generar colores dark, toma el color del sidebar y hazlo más oscuro. Los fondos deben ser versiones muy oscuras del color primario, no gris puro.

### Paso 4: Probar

Abrir `demo.html` y ejecutar en la consola del browser:

```javascript
GX.theme.init('miempresa');
```

---

## 5. Crear un tema inline (desde el JWT) {#crear-tema-inline}

### Estructura del JSON en el JWT

```json
{
  "tenant_id": "miempresa",
  "theme_config_json": {
    "primary_color": "#E11D48",
    "secondary_color": "#F59E0B",
    "accent_color": "#E11D48",
    "sidebar_bg": "#4C0519",
    "sidebar_text": "#FDA4AF",
    "childsidebar_bg": "#6B0926",
    "topbar_bg": "#FFFFFF",
    "topbar_text": "#374151",
    "bg_body": "#FFF1F2",
    "bg_surface": "#FFFFFF",
    "font_family": "'DM Sans', sans-serif",
    "border_radius": "12px",
    "panel_header_bg": "#4C0519",
    "panel_trigger_bg": "#4C0519",
    "logo_url": "https://cdn.miempresa.com/logo.png",
    "custom_css": ""
  }
}
```

### Propiedades disponibles para el JSON

| Propiedad | Variable CSS | Descripción |
|-----------|-------------|-------------|
| `primary_color` | `--gx-primary` | Color principal de la marca |
| `secondary_color` | `--gx-secondary` | Color secundario |
| `accent_color` | `--gx-accent` | Color de acento |
| `sidebar_bg` | `--gx-sidebar-bg` | Fondo del sidebar principal |
| `sidebar_text` | `--gx-sidebar-text` | Color de texto del sidebar |
| `childsidebar_bg` | `--gx-childsidebar-bg` | Fondo del child sidebar |
| `topbar_bg` | `--gx-topbar-bg` | Fondo del topbar |
| `topbar_text` | `--gx-topbar-text` | Color de texto del topbar |
| `bg_body` | `--gx-bg-body` | Fondo general de la app |
| `bg_surface` | `--gx-bg-surface` | Fondo de cards y paneles |
| `font_family` | `--gx-font-sans` | Tipografía (Google Fonts) |
| `border_radius` | `--gx-card-radius` | Radio de bordes en cards |
| `panel_header_bg` | `--gx-panel-header-bg` | Fondo del header de paneles |
| `panel_trigger_bg` | `--gx-panel-trigger-bg` | Fondo del trigger de paneles |
| `logo_url` | N/A | URL del logo (aplica a `[data-gx-logo]`) |
| `custom_css` | N/A | CSS extra (sanitizar en backend) |

### Derivaciones automáticas

Cuando envías `primary_color`, el ThemeManager genera automáticamente:
- `--gx-primary-hover` (15% más oscuro)
- `--gx-primary-light` (45% más claro)
- `--gx-primary-dark` (25% más oscuro)
- `--gx-primary-rgb` (componentes RGB separados)

Lo mismo aplica para `secondary_color`.

---

## 6. Variables disponibles {#variables}

### Colores de marca

```css
--gx-primary          /* Color principal */
--gx-primary-hover    /* Hover del principal */
--gx-primary-light    /* Versión clara (fondos) */
--gx-primary-dark     /* Versión oscura */
--gx-primary-rgb      /* RGB separado: "123, 44, 191" */

--gx-secondary        /* Color secundario */
--gx-secondary-hover
--gx-secondary-light
--gx-secondary-rgb

--gx-accent           /* Color de acento */
```

### Colores semánticos

```css
--gx-success           #10B981
--gx-danger            #EF4444
--gx-warning           #F59E0B
--gx-info              #3B82F6
```

### Superficies

```css
--gx-bg-body           /* Fondo de toda la app */
--gx-bg-surface        /* Fondo de cards, modals */
--gx-bg-surface-hover  /* Hover de superficies */
--gx-bg-surface-alt    /* Superficie alternativa */
--gx-bg-elevated       /* Superficie elevada (dropdowns) */
```

### Sidebar

```css
--gx-sidebar-bg        /* Fondo principal */
--gx-sidebar-bg-hover  /* Hover de items */
--gx-sidebar-accent    /* Indicador activo */
--gx-sidebar-text      /* Texto normal */
--gx-sidebar-text-active /* Texto activo */
--gx-childsidebar-bg   /* Fondo child sidebar */
--gx-childsidebar-hover
--gx-childsidebar-border
```

### Topbar

```css
--gx-topbar-bg
--gx-topbar-text
--gx-topbar-border
--gx-topbar-icon
--gx-topbar-icon-hover
```

---

## 7. Dark mode por tenant {#dark-mode}

El dark mode funciona en dos niveles:

### Dark mode genérico (variables.css)

Se aplica a **todos** los tenants:

```css
[data-theme="dark"] {
  --gx-bg-body: #0F0A1A;
  --gx-bg-surface: #1A1228;
  /* ... */
}
```

### Dark mode por tenant (archivo del tenant)

Se aplica **solo** a ese tenant, usando el selector combinado:

```css
[data-theme="dark"][data-tenant="gymflow"] {
  --gx-bg-body: #111111;
  --gx-sidebar-bg: #0D0D0D;
  /* ... */
}
```

**Cascada:** `dark genérico` → `tenant light` → `tenant dark`. El dark del tenant siempre gana.

### Toggle

```javascript
// El usuario cambia tema
GX.theme.toggleTheme();

// O establecer directamente
GX.theme.setTheme('dark');
GX.theme.setTheme('light');

// Auto-detect del sistema operativo
GX.theme.init('gymflow', null, { theme: 'auto' });
```

---

## 8. Flujo en producción {#flujo-produccion}

```
┌─────────────┐     ┌──────────────┐     ┌────────────────────────────────┐
│   Login      │────▶│  API Response │────▶│  Frontend                      │
│              │     │              │     │                                │
│  Credenciales│     │  JWT:        │     │  1. Extraer tenant_id del JWT  │
│              │     │  - tenant_id │     │  2. Extraer theme_config_json  │
│              │     │  - profiles[]│     │  3. GX.theme.init(id, config)  │
│              │     │  - theme_cfg │     │  4. ThemeManager decide:       │
│              │     │              │     │     config? → inline           │
│              │     │              │     │     !config? → load CSS file   │
│              │     │              │     │  5. Render app con tema        │
└─────────────┘     └──────────────┘     └────────────────────────────────┘
```

### Ejemplo de implementación en el app

```javascript
// Después del login exitoso
async function onLoginSuccess(jwt) {
  const payload = decodeJWT(jwt);

  // Aplicar tema
  await GX.theme.init(
    payload.tenant_id,
    payload.theme_config_json || null,
    { theme: 'auto' }  // Respeta preferencia del usuario
  );

  // Aplicar logo si viene en el config
  // (ThemeManager lo hace automáticamente si logo_url está en el config)

  // Render del shell
  renderApp();
}
```

### Cambio de perfil (multi-tenant)

```javascript
async function switchProfile(newTenantId, newThemeConfig) {
  // 1. Llamar al backend
  const response = await fetch('/auth/switch-profile', {
    method: 'POST',
    body: JSON.stringify({ tenant_id: newTenantId }),
  });
  const newJWT = await response.json();

  // 2. Limpiar tema anterior (importante!)
  document.documentElement.removeAttribute('style');
  const prevTheme = document.getElementById('gx-tenant-theme');
  if (prevTheme) prevTheme.remove();

  // 3. Aplicar nuevo tema
  await GX.theme.init(
    newTenantId,
    newThemeConfig,
    { theme: GX.theme.getTheme() }  // Mantener light/dark actual
  );

  // 4. Recargar datos del nuevo tenant
  reloadAppData();
}
```

---

## 9. Theme Manager API {#api}

### `GX.theme.init(tenantId, themeConfig?, options?)`

Inicializa el tema. Si `themeConfig` tiene datos, aplica inline. Si no, carga el archivo CSS.

```javascript
// Solo tenant (carga archivo CSS)
GX.theme.init('gymflow');

// Con config inline
GX.theme.init('gymflow', { primary_color: '#FF6B00' });

// Con opciones
GX.theme.init('gymflow', null, { theme: 'dark' });
GX.theme.init('gymflow', null, { theme: 'auto' }); // Detecta del OS
```

### `GX.theme.setTheme(theme)`

Establece light o dark. Guarda en localStorage.

```javascript
GX.theme.setTheme('dark');
GX.theme.setTheme('light');
```

### `GX.theme.toggleTheme()`

Alterna entre light y dark.

### `GX.theme.getTheme()` → `'light'` | `'dark'`

### `GX.theme.isDark()` → `boolean`

### Evento: `gx:theme-change`

```javascript
document.addEventListener('gx:theme-change', (e) => {
  console.log('Nuevo tema:', e.detail.theme); // 'light' o 'dark'
});
```

---

## 10. Portal de clientes {#portal-clientes}

El `demo_client.html` es un layout simplificado para usuarios finales (alumnos, pacientes). Usa el mismo `galappxy-bundle.css`, así que los temas aplican automáticamente.

### Diferencias con el admin

| Característica | Admin (`demo.html`) | Cliente (`demo_client.html`) |
|---------------|--------------------|-----------------------------|
| Sidebar | Ícono + child sidebar | Sidebar simple |
| Topbar | Complejo (search, modules, etc.) | Mínimo (logo, ID, bell, avatar) |
| Right panel | Trigger con herramientas | Solo panel de ayuda |
| Navegación móvil | Child sidebar deslizable | Bottom tabs (Inicio, Funciones, Contacto) |
| Complejidad | Muchas opciones por módulo | Opciones reducidas |

### Tabs en móvil

El portal de clientes usa tabs inferiores con 3 secciones:

1. **Inicio** — Welcome banner, pendientes, accesos rápidos, notificaciones
2. **Funciones** — Grid de módulos expandibles (click → subopciones)
3. **Contacto** — WhatsApp, email, teléfono, dirección

---

## 11. Modo embed {#modo-embed}

Para incrustar la app dentro de otras aplicaciones que ya tienen sus propios menús:

```
https://app.galappxy.com/dashboard?mode=embed
```

Esto oculta sidebar, topbar, right panel e impersonation. Solo se muestra el contenido.

### Activación programática

```javascript
// Activar
document.getElementById('app').classList.add('gx-app--embed');
document.body.classList.add('gx-embed');

// Desactivar
document.getElementById('app').classList.remove('gx-app--embed');
document.body.classList.remove('gx-embed');
```

---

## 12. Referencia rápida de componentes {#componentes}

### Botones

```html
<button class="gx-btn gx-btn--primary">Primary</button>
<button class="gx-btn gx-btn--secondary">Secondary</button>
<button class="gx-btn gx-btn--outline">Outline</button>
<button class="gx-btn gx-btn--ghost">Ghost</button>
<button class="gx-btn gx-btn--danger">Danger</button>

<!-- Tamaños -->
<button class="gx-btn gx-btn--primary gx-btn--sm">Pequeño</button>
<button class="gx-btn gx-btn--primary gx-btn--lg">Grande</button>
```

### Badges

```html
<span class="gx-badge gx-badge--primary">Primary</span>
<span class="gx-badge gx-badge--success">Activo</span>
<span class="gx-badge gx-badge--danger">Error</span>
<span class="gx-badge gx-badge--warning">Alerta</span>
<span class="gx-badge gx-badge--neutral">Draft</span>
```

### Cards

```html
<div class="gx-card">
  <div class="gx-card__header">
    <div class="gx-card__title">Título</div>
  </div>
  <div class="gx-card__body">Contenido</div>
</div>
```

### Modales

```html
<!-- Backdrop (siempre afuera del app shell) -->
<div class="gx-modal-backdrop" id="mi-modal" onclick="if(event.target===this)closeModal(this.id)">
  <div class="gx-modal">              <!-- Default: 520px -->
  <div class="gx-modal gx-modal--sm"> <!-- 400px -->
  <div class="gx-modal gx-modal--lg"> <!-- 680px -->
  <div class="gx-modal gx-modal--xl"> <!-- 900px -->
    <div class="gx-modal__header">
      <div class="gx-modal__title">Título</div>
      <button class="gx-modal__close" onclick="closeModal('mi-modal')">✕</button>
    </div>
    <div class="gx-modal__body">Contenido</div>
    <div class="gx-modal__footer">
      <button class="gx-btn gx-btn--ghost">Cancelar</button>
      <button class="gx-btn gx-btn--primary">Confirmar</button>
    </div>
  </div>
</div>
```

```javascript
function openModal(id) {
  document.getElementById(id).classList.add('is-open');
  document.body.style.overflow = 'hidden';
}
function closeModal(id) {
  document.getElementById(id).classList.remove('is-open');
  document.body.style.overflow = '';
}
```

### Forms

```html
<div class="gx-field">
  <label class="gx-field__label">Label</label>
  <input class="gx-input" placeholder="Texto">
</div>

<select class="gx-select">...</select>
<textarea class="gx-input" rows="3"></textarea>

<!-- Switch -->
<label class="gx-switch">
  <input type="checkbox">
  <span class="gx-switch__track"></span>
</label>
```

### Tablas

```html
<table class="gx-table">
  <thead><tr><th>Col</th></tr></thead>
  <tbody><tr><td>Data</td></tr></tbody>
</table>
```

### Prefijos CSS

- Clases: `gx-*` (ej: `gx-btn`, `gx-card`)
- Variables: `--gx-*` (ej: `--gx-primary`)
- Data attributes: `data-theme`, `data-tenant`, `data-gx-logo`
- Estados: `is-open`, `is-active`, `is-expanded`, `is-visible`

---

## Checklist para nuevo tenant

- [ ] Definir colores de marca (primary, secondary)
- [ ] Crear `css/themes/tenant-{id}.css` con `[data-tenant="{id}"]`
- [ ] Incluir `--gx-primary-rgb` (componentes RGB separados)
- [ ] Definir colores de sidebar y child sidebar
- [ ] Definir fondo del body (`--gx-bg-body`)
- [ ] Agregar variante dark mode (opcional pero recomendado)
- [ ] Probar en `demo.html` con `GX.theme.init('{id}')`
- [ ] Probar en `demo_client.html`
- [ ] Probar dark mode toggle
- [ ] Agregar `theme_config_json` en la tabla BRAND del backend
- [ ] Verificar que el logo se carga correctamente

---

*Galappxy Framework · Documentación interna · Febrero 2026*