From afc01ec1d94008ca4daf051ac22bd910ffd77120 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 18 Apr 2026 19:30:14 -0400 Subject: [PATCH] fix(shopfloor): proper dark-mode via \$o-webclient-color-scheme branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dug deeper after the user reported shop-floor pages staying white in dark mode. Traced through Odoo 19 source: _dependencies/web_enterprise/static/src/ webclient/color_scheme/color_scheme_service.js <- reads cookie scss/primary_variables.scss \$o-webclient-color-scheme: bright scss/primary_variables.dark.scss \$o-webclient-color-scheme: dark Odoo compiles TWO separate CSS bundles: web.assets_backend -> compiled with \$...scheme: bright web.assets_web_dark -> compiled with \$...scheme: dark (the .dark.scss files are layered in front of the light ones) Our shop-floor SCSS is in web.assets_backend, which means it gets compiled into BOTH bundles. But the previous CSS-variable fallback chain (var(--fp-page-bg, var(--bs-tertiary-bg, #hex))) baked the SAME hex fallback into both bundles, so cards stayed white in dark. Odoo's own code doesn't redefine --bs-* CSS custom properties at runtime either — it just bakes the dark palette straight into the dark bundle via SCSS \$-variables during compile. Fix: _fp_shopfloor_tokens.scss now branches at compile time: \$o-webclient-color-scheme: bright !default; \$_fp-page-hex: #f3f4f6; // light defaults \$_fp-card-hex: #ffffff; ... @if \$o-webclient-color-scheme == dark { \$_fp-page-hex: #1a1d21 !global; \$_fp-card-hex: #22262d !global; ... } \$fp-page: var(--fp-page-bg, \$_fp-page-hex); \$fp-card: var(--fp-card-bg, \$_fp-card-hex); The CSS-custom-property fallback stays so deployments can still skin via --fp-* without touching SCSS; the underlying hex changes between bundles. Verified via odoo-shell: LIGHT bundle: .o_fp_plant_overview { background-color: var(...#f3f4f6) } .o_fp_po_card { background-color: var(...#ffffff); border: ... #d8dadd } DARK bundle: .o_fp_plant_overview { background-color: var(...#1a1d21) } .o_fp_po_card { background-color: var(...#22262d); border: ... #343942 } Two separate bundle URLs generated: /web/assets/a593157/web.assets_backend.min.css /web/assets/a9dba7d/web.assets_web_dark.min.css === CLAUDE.md === Replaced the previous (incorrect) .o_dark_mode override advice with a proper "Branch on \$o-webclient-color-scheme at SCSS compile time" section, including the bundle names and the verify-via-odoo-shell snippet. Future redesigns now have a single, correct pattern to follow. Version bumped 19.0.4.0.0 -> 19.0.5.0.0 to force asset hash change. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 37 +++++++-- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../static/src/scss/_fp_shopfloor_tokens.scss | 80 ++++++++++++------- 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 17f9f32b..bee454de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,16 +29,39 @@ For custom OWL dashboards / client actions use the same approach: $fp-border: var(--fp-border-color, #d8dadd); ``` - Reference those tokens everywhere (never `var(--bs-border-color)` directly) -- Override the custom properties for dark mode at the bottom of the tokens file: - ```scss - :root[data-bs-theme="dark"], body.o_dark_mode, .o_dark_mode { - --fp-card-bg: #22262d; - --fp-border-color: #343942; - } - ``` - Three-layer contrast: **page** (grayest) → **container/column** (mid) → **card** (brightest). That's what makes cards pop. - Reference implementation: `fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss`. +## Dark Mode — Branch on `$o-webclient-color-scheme` at SCSS Compile Time +Odoo 19 does NOT flip dark mode via a runtime DOM class. It compiles TWO asset bundles: +- `web.assets_backend` — compiled with `$o-webclient-color-scheme: bright` +- `web.assets_web_dark` — compiled with `$o-webclient-color-scheme: dark` (dark variant primary variables loaded first) + +Your SCSS file is compiled into BOTH bundles. To make the dark bundle have different colors, **branch at compile time** using the SCSS variable Odoo sets: + +```scss +$o-webclient-color-scheme: bright !default; + +$_my-page-hex: #f3f4f6; +$_my-card-hex: #ffffff; + +@if $o-webclient-color-scheme == dark { + $_my-page-hex: #1a1d21 !global; + $_my-card-hex: #22262d !global; +} + +$my-page: var(--my-page-bg, $_my-page-hex); +$my-card: var(--my-card-bg, $_my-card-hex); +``` + +**Do NOT use** `.o_dark_mode` class selectors, `[data-bs-theme="dark"]`, or `@media (prefers-color-scheme: dark)` — none of those fire reliably in Odoo 19. The user toggles dark mode via the user profile, which sets a `color_scheme` cookie and reloads the page; Odoo then serves the dark bundle. Your SCSS `@if` handles the rest at compile time. + +Verify by inspecting the attachments — you should see two files with different URLs for the two bundles: +```python +env['ir.qweb']._get_asset_bundle('web.assets_backend').css() # light +env['ir.qweb']._get_asset_bundle('web.assets_web_dark').css() # dark +``` + ## Asset Bundle Cache Busting Odoo content-hashes the compiled bundle URL (`/web/assets//...`). When CSS changes but the hash doesn't update, the browser serves the old bundle. Fixes in order of escalation: 1. Bump the module `version` in `__manifest__.py` diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index b007ec94..5632be3e 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.4.0.0', + 'version': '19.0.5.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, ' 'first-piece inspection gates.', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss index d7899f58..ad408037 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss @@ -31,40 +31,60 @@ $fp-radius-lg : 20px; $fp-radius-xl : 28px; $fp-radius-pill: 999px; -// ---------- Surfaces — tied to Odoo's Bootstrap tokens ----------------------- +// ---------- Surfaces — COMPILE-TIME branch on Odoo's dark scheme ------------- // -// Odoo 19's dark mode is NOT a runtime class toggle — it's a separate -// compiled CSS bundle where --bs-body-bg, --bs-card-bg, --bs-tertiary-bg, -// --bs-border-color are baked in with dark values. So if we chain our -// tokens through those Bootstrap variables, they auto-invert when Odoo -// swaps the bundle; no .o_dark_mode override needed. +// Odoo 19 compiles TWO asset bundles: web.assets_backend (light) and +// web.assets_web_dark (dark). The two bundles differ only in the value +// of the SCSS variable $o-webclient-color-scheme — `bright` for light, +// `dark` for dark (defined in primary_variables.scss / +// primary_variables.dark.scss in web_enterprise). // -// Fallback order: -// 1. --fp-* (custom override, e.g. shop-branded skin) -// 2. --bs-* (Odoo/Bootstrap semantic token — inverts on dark) -// 3. explicit hex (absolute fallback if vars are missing) -$fp-page : var(--fp-page-bg, - var(--bs-tertiary-bg, #f3f4f6)); -$fp-card : var(--fp-card-bg, - var(--bs-card-bg, - var(--o-view-background-color, #ffffff))); -$fp-card-soft : var(--fp-card-soft-bg, - var(--bs-secondary-bg, #f1f3f5)); -$fp-border : var(--fp-border-color, - var(--bs-border-color, #d8dadd)); -$fp-border-strong : var(--fp-border-strong, - var(--bs-border-color-translucent, #b6babf)); +// Odoo does NOT redefine --bs-body-bg / --bs-card-bg as CSS custom +// properties at runtime. It bakes the chosen palette into the bundle +// at compile time via Bootstrap SCSS variables. So our tokens must do +// the same: branch on $o-webclient-color-scheme at compile time and +// emit the right hex values into each bundle. -// ---------- Text tiers — same chain pattern ---------------------------------- -$fp-ink : var(--fp-ink, var(--bs-body-color, #1f2937)); -$fp-ink-soft : var(--fp-ink-soft, var(--bs-secondary-color, #4b5563)); -$fp-ink-mute : var(--fp-ink-mute, var(--bs-tertiary-color, #6b7280)); -$fp-ink-faint : var(--fp-ink-faint, - color-mix(in srgb, var(--bs-body-color, #000) 40%, transparent)); +$o-webclient-color-scheme: bright !default; -// Action colour — Odoo's primary. Explicit hex fallback to Odoo's -// default brand purple so .btn-primary doesn't render transparent. -$fp-accent : var(--o-action, #714B67); +// Default (light / bright) palette +$_fp-page-hex : #f3f4f6; +$_fp-card-hex : #ffffff; +$_fp-card-soft-hex : #f1f3f5; +$_fp-border-hex : #d8dadd; +$_fp-border-strong-hex : #b6babf; +$_fp-ink-hex : #1f2937; +$_fp-ink-soft-hex : #4b5563; +$_fp-ink-mute-hex : #6b7280; +$_fp-ink-faint-hex : #9ca3af; + +// Dark palette — engaged when the dark bundle is compiled +@if $o-webclient-color-scheme == dark { + $_fp-page-hex : #1a1d21 !global; + $_fp-card-hex : #22262d !global; + $_fp-card-soft-hex : #1c2027 !global; + $_fp-border-hex : #343942 !global; + $_fp-border-strong-hex : #4a505a !global; + $_fp-ink-hex : #e5e7eb !global; + $_fp-ink-soft-hex : #c8ccd2 !global; + $_fp-ink-mute-hex : #8a909a !global; + $_fp-ink-faint-hex : #5a606b !global; +} + +// Public tokens — CSS custom property fallback chain remains so a +// deployment can still override via --fp-* without touching SCSS. +$fp-page : var(--fp-page-bg, $_fp-page-hex); +$fp-card : var(--fp-card-bg, $_fp-card-hex); +$fp-card-soft : var(--fp-card-soft-bg, $_fp-card-soft-hex); +$fp-border : var(--fp-border-color, $_fp-border-hex); +$fp-border-strong : var(--fp-border-strong, $_fp-border-strong-hex); +$fp-ink : var(--fp-ink, $_fp-ink-hex); +$fp-ink-soft : var(--fp-ink-soft, $_fp-ink-soft-hex); +$fp-ink-mute : var(--fp-ink-mute, $_fp-ink-mute-hex); +$fp-ink-faint : var(--fp-ink-faint, $_fp-ink-faint-hex); + +// Action colour — Odoo's primary. Same in both bundles (brand purple). +$fp-accent : var(--o-action, #714B67); // ---------- Elevation — explicit rgba shadows -------------------------------- // Explicit rgba values (not color-mix) so they render identically across