Skip to content

feat: [TESIS-104] switch color schemes through CSS variables - #54

Merged
Sanntinat merged 5 commits into
masterfrom
TESIS-104-theme-css-variables
Sep 25, 2026
Merged

Sanntinat merged 5 commits into
masterfrom
TESIS-104-theme-css-variables

Conversation

@Sanntinat

Copy link
Copy Markdown
Contributor

🔗 Link

https://proyectofinalfrlp.atlassian.net/browse/TESIS-104

📝 Descripción

El toggle de tema deja de rearmar el tema: un solo tema por tenant con los dos esquemas de color, y todos los estilos leen las variables CSS. Es lo que la card sospechaba detrás del componente que no se repintaba, y lo que dejó propuesto Tomás en su comentario del paso 1.

El paso 1 de la card, repetido. Como Tomás, no reproduje el síntoma en master. Sí medí el mecanismo que la card señalaba: createAppTheme(mode) armaba un tema nuevo en cada toggle y todos los styled() horneaban el hex de theme.palette en su clase. Con carga limpia de cada ruta, el primer toggle agregaba 470 <style> en /design-system, 118 en /orders y 107 en /inventory: cada componente se volvía a serializar con los colores del otro modo. El repintado dependía de que React volviera a renderizar todo el árbol.

Decisiones:

  • Un tema por tenant con colorSchemes: { light, dark }, con el esquema activo marcado en un atributo del <html> ([data-light] / [data-dark]). El tema se rearma si cambia el tenant, no el modo. El branding viaja por esquema, porque el provisorio del slug cambia de tono según el modo.
  • El modo lo sigue decidiendo uiStore. ThemeWrapper lo lleva a MUI con useColorScheme().setMode, con storageManager={null} (si no, MUI guardaría su propia copia en localStorage y podrían no coincidir) y disableTransitionOnChange (el cambio es instantáneo, como antes).
  • Los estilos leen theme.vars (var(--mui-…)). Lo que cambia de forma entre modos va en theme.applyStyles: las cuatro ramas por palette.mode (StatusBadge, StatCard, StatusSelect y el backdrop), más el canal neutro de ProgressIndicator, que pasó a common.onBackground. Las transparencias usan theme.alpha(), que arma el rgba desde el canal de la variable.
  • Lo que depende del modo y no es un color de MUI vive en cada esquema, así tiene su variable: la escala de elevación (theme.vars.elevation) y los rellenos de los campos (palette.input). Los overrides del tema dejaron de recibir el modo y el acento: leen theme.vars, y el acento es secondary.main.
  • Una sola excepción documentada: /design-system imprime el hex de cada capa como texto, así que lo lee del esquema activo (theme.colorSchemes[modo]). theme.palette ahora es siempre el esquema por defecto.
  • ADR-007 deja escrita esta forma como la oficial, que es el paso 4 de la card. component-structure.md cambia la regla a «los colores, de theme.vars».

Commits: el primero es el reemplazo mecánico theme.palette → theme.vars en los consumidores. Compila y pasa solo, porque el tema ya tenía cssVariables: true y con un esquema las variables tienen los mismos valores. El segundo es el cambio de fondo. El tercero, la documentación.

🛠️ Cambios realizados

  • app/theme/theme.ts: createAppTheme(branding) con los dos esquemas, la elevación por esquema y palette.input.
  • app/theme/components/*: los overrides, sin modo ni acento. Leen theme.vars y usan applyStyles.
  • app/theme/augmentations.d.ts: CssThemeVariables habilitado (theme.vars no es opcional), elevación en ColorSystem/ThemeVars y palette.input.
  • app/providers/ThemeWrapper.tsx: el tema por tenant y la sincronización del modo.
  • 33 archivos de estilos: theme.palette → theme.vars.palette y alpha() → theme.alpha().
  • test/renderWithTheme.tsx: el modo pedido entra como esquema activo, igual que en la app.
  • Tests: ThemeWrapper.test.tsx (nuevo), branding.test.ts por esquema, y StatusBadge.test.tsx con la variable exacta de cada modo.
  • Docs: ADR-007, architecture.md y component-structure.md.

🧪 Cómo probarlo (Opcional)

  1. Entrar a /design-system y alternar el tema varias veces, en los dos sentidos: todo cambia de esquema sin recargar.
  2. En DevTools, el <html> alterna entre data-dark y data-light. --mui-palette-primary-main pasa de #38bdf8 a #00629d, y el conteo de <style> del documento no cambia.
  3. Recorrer el panel, órdenes, inventario y reportes en los dos modos.

📸 Evidencia (Opcional)

Medido en el navegador con carga limpia de cada ruta:

Ruta <style> que agregaba el 1er toggle (antes) Después
/design-system +470 0
/orders +118 0
/inventory +107 0

Repintado completo, con el método de Tomás: comparé el color calculado (fondo, texto y borde) de todos los elementos visibles antes y después de alternar, en /design-system (505 elementos), /dashboard, /orders, /inventory y /reports. Ningún elemento queda con el color del modo anterior, en los dos sentidos. Los únicos que no «vuelven» son el svg del botón de tema, que se reemplaza por el otro ícono.

Un detalle de método, por si alguien repite la medición con el panel del navegador en segundo plano: en una pestaña oculta las transiciones quedan congeladas, y los elementos con transition (el indicador de las pestañas) parecen no repintarse. No es un bug: con la pestaña visible o con las transiciones neutralizadas durante la medición, repintan.

Antes Después
(adjuntar captura de /dashboard en oscuro) (adjuntar captura de /dashboard en claro)

Verificación: npm run test (59 archivos, 539 tests, 0 fallas), npm run lint, prettier --check . y npm run build limpios.

Dientes: los 4 ejemplos de ThemeWrapper.test.tsx fallan contra master y pasan con este PR. Son el cambio de esquema en los dos sentidos, primary.main desde la variable y no desde un hex horneado (el caso de Lorenzo, explícito), el valor de cada esquema y que alternar no genere estilos. Los corrí sobre un worktree de master con el mismo archivo.

Criterios de la card

  • El toggle repinta toda la UI en los dos sentidos, sin recargar. Medido elemento por elemento en cinco rutas.
  • El caso de primary.main queda cubierto explícitamente (ThemeWrapper.test.tsx).
  • No queda ningún theme.palette.* que hornee un hex donde correspondía una variable, salvo la excepción documentada en ADR-007 (el hex impreso en /design-system).
  • npm run lint y npm run build limpios.

¿Afecta la arquitectura o genera un nuevo patrón?
Sí: fija theme.vars + applyStyles como la forma de leer el tema (ADR-007, actualización del 25/09).

Relación con otros PRs:


🤖 Generated with Claude Code

Sanntinat and others added 3 commits September 25, 2026 01:11
The theme already emits every color as a CSS variable (cssVariables:
true), but no style read them: every styled() baked the hex of
theme.palette into its Emotion class. They now read theme.vars, which
resolves to var(--mui-palette-...), and alpha() over a palette color
becomes theme.alpha(), which builds the rgba from the variable channel.

Mechanical and behaviour-neutral: with a single color scheme the
variables hold the same values the hex did. The four styles that branch
on palette.mode stay as they were; the next commit moves them.

CssThemeVariables is enabled in the augmentations so theme.vars is not
optional in the types, as it is not at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The theme was rebuilt on every toggle: createAppTheme(mode) made a new
theme with a single color scheme, and repainting depended on React
re-rendering every styled component so Emotion serialized its styles
again with the colors of the other mode. That is what the card suspected
behind the component that did not repaint, and it is measurable: the
first toggle added 470 <style> tags in /design-system, 118 in /orders
and 107 in /inventory.

Now there is one theme per tenant with both schemes (colorSchemes, with
the selector on a data attribute of <html>). ThemeWrapper syncs the mode
of uiStore through useColorScheme, with storageManager={null} so uiStore
stays the only place that remembers the choice. Toggling changes the
attribute and the browser repaints from the variables: the same routes
add 0 <style> tags, and every visible element changes color both ways.

- The per-mode values that are not palette colors move into each scheme
  as variables: the elevation (theme.vars.elevation) and the input fills
  (palette.input).
- The theme overrides no longer receive the mode or the tenant accent:
  they read theme.vars (the accent is secondary.main) and put what
  changes shape between modes in theme.applyStyles.
- The four styles that branched on palette.mode use applyStyles, or
  common.onBackground for the neutral track of ProgressIndicator.
- The design system page prints layer hex codes as text, so it reads
  them from the active scheme: the one read that is not a variable.

ThemeWrapper.test covers the case Lorenzo reported with primary.main:
the scheme switches both ways, the color comes from the variable and not
from a baked hex, each scheme defines its own value, and toggling adds
no styles. All four fail against the previous theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-007 gets an update with the official way the card asked to settle,
so the two do not live side by side: one theme per tenant with both
color schemes, the mode carried from uiStore through useColorScheme,
styles reading theme.vars, mode-specific shapes in theme.applyStyles and
transparencies through theme.alpha. It records the one read that is not
a variable (the layer hex codes printed as text in /design-system), the
alternative that was discarded, and the measurement before and after.

architecture.md describes the new createAppTheme and ThemeWrapper, and
component-structure.md turns "read from the theme" into "read colors
from theme.vars".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Sanntinat
Sanntinat requested a review from a team as a code owner September 25, 2026 04:32

@TomasMartin2004 TomasMartin2004 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisión — TESIS-104 (PR #54) · El tema deja de rearmarse y pasa a variables CSS

Revisado sobre TESIS-104-theme-css-variables contra origin/master.

Check Resultado
npm run test (local) 539 tests, 0 fallas
npm run lint · npm run build Limpios

✅ Medí el mecanismo por mi cuenta, y da lo que decís

Corrí la misma sonda en las dos ramas: montar /design-system completa y contar los <style> del documento antes y después de alternar el modo.

master              inicial 1213 | tras 1 toggle 1689  (+476)
TESIS-104           inicial 1266 | tras 1 toggle 1267  (+1)
                                 | tras 3 toggles 1269 (+3)

Tu número era 470 y me dio 476: la diferencia es el par de componentes que cambiaron desde que lo mediste. El orden de magnitud es el que importa: en master cada toggle vuelve a serializar toda la página, y acá el residuo es 1 por toggle, que es el <style> con el que MUI apaga las transiciones y no depende de cuántos componentes haya montados.

Antes de eso había probado con dos tarjetas sueltas y el delta era 2 en master: la sonda chica no reproduce nada. Lo aclaro porque es el error que yo mismo hice primero y, si alguien quiere repetir la medición, conviene que la haga sobre una pantalla entera.

La conversión está completa: 146 lecturas por theme.vars.palette en los estilos y ninguna directa a theme.palette fuera de dos comentarios.

✅ Las decisiones, una por una

  • Un tema por tenant con los dos esquemas. Es lo que hace que alternar no cree un tema nuevo, y deja el rearmado donde corresponde: cuando cambia el tenant.
  • storageManager={null}. Sin eso MUI guarda su propia copia del modo en localStorage y queda una segunda fuente de verdad que puede no coincidir con uiStore —que además sabe del default del tenant—. Es el tipo de detalle que se paga meses después.
  • disableTransitionOnChange. Mantiene el cambio instantáneo, como era antes. Sin eso, cada elemento con transición de color se desvanecería de un esquema al otro.
  • El branding por esquema, porque el color derivado del slug cambia de tono según el modo. Bien visto: es el caso que rompería si se armara una sola vez.

✅ Dientes, donde importan

Volví ProgressIndicator.styles.ts a leer theme.palette en vez de theme.vars.palette y cae el ejemplo exacto:

× paints primary.main from the variable of the active scheme, not from a baked hex

Es el que protege la causa real del bug.

🟡 Una observación sobre el test que cuenta estilos

does not generate new styles when the mode changes no se pone en rojo si el tema vuelve a rearmarse con el modo: le agregué themeMode a las dependencias del useMemo y los cuatro ejemplos siguieron verdes.

Tiene sentido —con todos los estilos leyendo variables, rearmar el tema es trabajo desperdiciado pero ya no genera clases nuevas—, así que el test mide el efecto y no la causa. No está mal: el efecto es lo que le importa al usuario. Pero como la card es justamente «el tema no se rearma», vale la pena un ejemplo que lo fije, del estilo de comparar la identidad del objeto del tema entre dos modos. Si te parece de más, con que quede dicho en el comentario del test alcanza: hoy se lee como si cubriera las dos cosas.

Veredicto

APPROVE.

Hace lo que la card sospechaba y lo demuestra con números reproducibles —los reproduje—. La conversión a variables es completa y la parte más delicada, que es dónde vive el modo, quedó con una sola fuente de verdad.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

@Sanntinat
Sanntinat merged commit cbf7124 into master Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants