Repository navigation
feat: [TESIS-104] switch color schemes through CSS variables - #54
Conversation
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>
TomasMartin2004
left a comment
There was a problem hiding this comment.
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 conuiStore—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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🔗 Link
📝 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 losstyled()horneaban el hex detheme.paletteen su clase. Con carga limpia de cada ruta, el primer toggle agregaba 470<style>en/design-system, 118 en/ordersy 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:
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.uiStore.ThemeWrapperlo lleva a MUI conuseColorScheme().setMode, constorageManager={null}(si no, MUI guardaría su propia copia en localStorage y podrían no coincidir) ydisableTransitionOnChange(el cambio es instantáneo, como antes).theme.vars(var(--mui-…)). Lo que cambia de forma entre modos va entheme.applyStyles: las cuatro ramas porpalette.mode(StatusBadge, StatCard, StatusSelect y el backdrop), más el canal neutro de ProgressIndicator, que pasó acommon.onBackground. Las transparencias usantheme.alpha(), que arma elrgbadesde el canal de la variable.theme.vars.elevation) y los rellenos de los campos (palette.input). Los overrides del tema dejaron de recibir el modo y el acento: leentheme.vars, y el acento essecondary.main./design-systemimprime el hex de cada capa como texto, así que lo lee del esquema activo (theme.colorSchemes[modo]).theme.paletteahora es siempre el esquema por defecto.component-structure.mdcambia la regla a «los colores, detheme.vars».Commits: el primero es el reemplazo mecánico
theme.palette→theme.varsen los consumidores. Compila y pasa solo, porque el tema ya teníacssVariables: truey 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 ypalette.input.app/theme/components/*: los overrides, sin modo ni acento. Leentheme.varsy usanapplyStyles.app/theme/augmentations.d.ts:CssThemeVariableshabilitado (theme.varsno es opcional), elevación enColorSystem/ThemeVarsypalette.input.app/providers/ThemeWrapper.tsx: el tema por tenant y la sincronización del modo.theme.palette→theme.vars.paletteyalpha()→theme.alpha().test/renderWithTheme.tsx: el modo pedido entra como esquema activo, igual que en la app.ThemeWrapper.test.tsx(nuevo),branding.test.tspor esquema, yStatusBadge.test.tsxcon la variable exacta de cada modo.architecture.mdycomponent-structure.md.🧪 Cómo probarlo (Opcional)
/design-systemy alternar el tema varias veces, en los dos sentidos: todo cambia de esquema sin recargar.<html>alterna entredata-darkydata-light.--mui-palette-primary-mainpasa de#38bdf8a#00629d, y el conteo de<style>del documento no cambia.📸 Evidencia (Opcional)
Medido en el navegador con carga limpia de cada ruta:
<style>que agregaba el 1er toggle (antes)/design-system/orders/inventoryRepintado 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,/inventoryy/reports. Ningún elemento queda con el color del modo anterior, en los dos sentidos. Los únicos que no «vuelven» son elsvgdel 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./dashboarden oscuro)/dashboarden claro)Verificación:
npm run test(59 archivos, 539 tests, 0 fallas),npm run lint,prettier --check .ynpm run buildlimpios.Dientes: los 4 ejemplos de
ThemeWrapper.test.tsxfallan contramastery pasan con este PR. Son el cambio de esquema en los dos sentidos,primary.maindesde 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 demastercon el mismo archivo.Criterios de la card
primary.mainqueda cubierto explícitamente (ThemeWrapper.test.tsx).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 lintynpm run buildlimpios.¿Afecta la arquitectura o genera un nuevo patrón?
Sí: fija
theme.vars+applyStylescomo la forma de leer el tema (ADR-007, actualización del 25/09).Relación con otros PRs:
OriginWarehousePicker.styles.ts, porque feat: [TESIS-59] add manual order wizard step 3: carrier and confirmation #52 extraeSelectableCardde ahí, y feat: [TESIS-59] add manual order wizard step 3: carrier and confirmation #52 trae tres archivos de estilos nuevos contheme.palette(SelectableCard,QuoteOptionListyOrderConfirmCard). Compilan, pero quedarían con el hex horneado. El que mergee segundo se queda con la extracción de feat: [TESIS-59] add manual order wizard step 3: carrier and confirmation #52 y pasa esos tres archivos atheme.vars. Lo resolví así en la simulación y la combinación compila, pasa lint y build, y da 575 de 576 tests. El que falló esProductDetailPage, que se pasa del timeout de 5 s bajo carga y aislado pasa.theme.palette.theme.vars. Vercomponent-structure.md§4.🤖 Generated with Claude Code