docs(m10): T34 theme dark-mode feasibility spike findings
This commit is contained in:
@@ -0,0 +1,239 @@
|
|||||||
|
# T34 Spike: Theme Dark-Mode Feasibility Findings
|
||||||
|
|
||||||
|
**Date:** 2026-06-18
|
||||||
|
**Task:** Verify whether the external `ZB.MOM.WW.Theme` shell can go dark via `data-bs-theme` CSS override.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. CSS Load Order
|
||||||
|
|
||||||
|
Confirmed from `src/ZB.MOM.WW.ScadaBridge.Host/Components/App.razor`:
|
||||||
|
|
||||||
|
```
|
||||||
|
bootstrap.min.css (line 8)
|
||||||
|
bootstrap-icons.css (line 9)
|
||||||
|
<ThemeHead /> (line 10) → injects theme.css then layout.css
|
||||||
|
Host.styles.css (line 11)
|
||||||
|
site.css (line 12) ← our override point
|
||||||
|
```
|
||||||
|
|
||||||
|
`site.css` loads **after** theme.css and layout.css. A `[data-bs-theme="dark"]` block in site.css has sufficient specificity to shadow every `:root` token defined in theme.css, because `[attr]` selector specificity (0,1,0) beats `:root` (0,0,1) at equal cascade layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Token Inventory (theme.css `:root`)
|
||||||
|
|
||||||
|
All tokens defined in the `:root` block of `~/.nuget/packages/zb.mom.ww.theme/0.3.1/staticwebassets/css/theme.css`:
|
||||||
|
|
||||||
|
### Surfaces & ink
|
||||||
|
| Token | Light value | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `--paper` | `#f4f4f1` | page background |
|
||||||
|
| `--card` | `#ffffff` | raised surfaces, bars, table heads |
|
||||||
|
| `--ink` | `#1b1d21` | primary text |
|
||||||
|
| `--ink-soft` | `#5a6066` | secondary text, labels |
|
||||||
|
| `--ink-faint` | `#8b9097` | tertiary text, captions |
|
||||||
|
| `--rule` | `#e4e4df` | hairline borders, row dividers |
|
||||||
|
| `--rule-strong` | `#d2d2cb` | emphasised hairlines, bar underline, pills |
|
||||||
|
|
||||||
|
### Accent
|
||||||
|
| Token | Light value | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `--accent` | `#2f5fd0` | links, sort arrows, primary actions |
|
||||||
|
| `--accent-deep` | `#1e3f99` | hover/pressed accent |
|
||||||
|
|
||||||
|
### Status — foreground
|
||||||
|
| Token | Light value |
|
||||||
|
|---|---|
|
||||||
|
| `--ok` | `#2f9e44` |
|
||||||
|
| `--warn` | `#e8920c` |
|
||||||
|
| `--bad` | `#e03131` |
|
||||||
|
| `--idle` | `#868e96` |
|
||||||
|
|
||||||
|
### Status — tinted backgrounds
|
||||||
|
| Token | Light value |
|
||||||
|
|---|---|
|
||||||
|
| `--ok-bg` | `#e9f6ec` |
|
||||||
|
| `--warn-bg` | `#fdf1dd` |
|
||||||
|
| `--bad-bg` | `#fceaea` |
|
||||||
|
| `--idle-bg` | `#eef0f2` |
|
||||||
|
|
||||||
|
### Status — borders
|
||||||
|
| Token | Light value |
|
||||||
|
|---|---|
|
||||||
|
| `--ok-border` | `#c6e6cd` |
|
||||||
|
| `--warn-border` | `#efd6a6` |
|
||||||
|
| `--bad-border` | `#eec3c3` |
|
||||||
|
| `--ok-border-soft` | `#bfe3c6` |
|
||||||
|
| `--warn-border-soft` | `#f0d9ab` |
|
||||||
|
| `--bad-border-soft` | `#f0c0c0` |
|
||||||
|
|
||||||
|
### Warning ink
|
||||||
|
| Token | Light value |
|
||||||
|
|---|---|
|
||||||
|
| `--warn-ink` | `#b56a00` |
|
||||||
|
| `--warn-ink-deep` | `#8a5a00` |
|
||||||
|
|
||||||
|
### Info tint
|
||||||
|
| Token | Light value |
|
||||||
|
|---|---|
|
||||||
|
| `--info-bg` | `#e7ecfb` |
|
||||||
|
| `--info-border` | `#cdd9f7` |
|
||||||
|
|
||||||
|
### Neutral surface washes
|
||||||
|
| Token | Light value | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `--zebra-bg` | `#fbfbf9` | even-row / sticky-head fill |
|
||||||
|
| `--hover-bg` | `#f3f6fd` | row / rail-link hover |
|
||||||
|
| `--active-bg` | `#eef2fc` | active rail-link fill |
|
||||||
|
|
||||||
|
### Bootstrap overrides (derive from above)
|
||||||
|
| Token | Value |
|
||||||
|
|---|---|
|
||||||
|
| `--bs-body-bg` | `var(--paper)` |
|
||||||
|
| `--bs-body-color` | `var(--ink)` |
|
||||||
|
| `--bs-body-font-family` | `var(--sans)` |
|
||||||
|
| `--bs-body-font-size` | `0.9rem` |
|
||||||
|
| `--bs-primary` | `var(--accent)` |
|
||||||
|
| `--bs-border-color` | `var(--rule)` |
|
||||||
|
| `--bs-emphasis-color` | `var(--ink)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Rail Tokenization Audit (layout.css)
|
||||||
|
|
||||||
|
Every colour-bearing rule in the side-rail uses CSS custom properties. No hard-coded hex or rgb values appear anywhere in layout.css. Key rules:
|
||||||
|
|
||||||
|
**Rail background and border:**
|
||||||
|
```css
|
||||||
|
.side-rail {
|
||||||
|
background: var(--card);
|
||||||
|
border-right: 1px solid var(--rule-strong);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Brand block:**
|
||||||
|
```css
|
||||||
|
.side-rail .brand {
|
||||||
|
color: var(--ink);
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
.side-rail .brand .mark { color: var(--accent); }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section eyebrow / toggle:**
|
||||||
|
```css
|
||||||
|
.rail-eyebrow-toggle { color: var(--ink-faint); }
|
||||||
|
.rail-eyebrow-toggle:hover { color: var(--ink); }
|
||||||
|
.rail-section > summary::before { color: var(--ink-faint); }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nav links (default / hover / active):**
|
||||||
|
```css
|
||||||
|
.rail-link { color: var(--ink-soft); }
|
||||||
|
.rail-link:hover { background: var(--hover-bg); color: var(--ink); }
|
||||||
|
.rail-link.active { background: var(--active-bg); border-left-color: var(--accent); color: var(--accent-deep); }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rail foot / session block:**
|
||||||
|
```css
|
||||||
|
.rail-foot { border-top: 1px solid var(--rule); }
|
||||||
|
.rail-btn { color: var(--ink-soft); background: var(--card); border: 1px solid var(--rule-strong); }
|
||||||
|
.rail-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.rail-roles { color: var(--ink-faint); }
|
||||||
|
.rail-ico { color: var(--ink-faint); }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Login title:**
|
||||||
|
```css
|
||||||
|
.login-title { color: var(--ink); }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. VERDICT
|
||||||
|
|
||||||
|
**RAIL TOKENIZES → dark feasible by token override.**
|
||||||
|
|
||||||
|
Every rail background, text, and border colour in layout.css resolves through a CSS custom property from the token set. Overriding the token values under `[data-bs-theme="dark"]` in site.css (which loads after theme.css/layout.css) will fully recolour the side-rail, app chrome, panels, data tables, chips, and status elements — no Theme-package coordination required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Concrete Dark Token Override List
|
||||||
|
|
||||||
|
Paste this entire block under `[data-bs-theme="dark"]` in site.css. Accent colour (`--accent`) is kept at `#4d7fe8` — a lighter shade of the brand blue that reads well on dark backgrounds while preserving brand identity.
|
||||||
|
|
||||||
|
```css
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
/* Surfaces & ink */
|
||||||
|
--paper: #1b1d21;
|
||||||
|
--card: #25282d;
|
||||||
|
--ink: #e6e8ec;
|
||||||
|
--ink-soft: #9aa0aa;
|
||||||
|
--ink-faint: #6b727c;
|
||||||
|
--rule: #33373e;
|
||||||
|
--rule-strong: #3a3f46;
|
||||||
|
|
||||||
|
/* Accent — lightened for contrast on dark */
|
||||||
|
--accent: #4d7fe8;
|
||||||
|
--accent-deep: #7aa0ef;
|
||||||
|
|
||||||
|
/* Status — foregrounds unchanged (inherently visible on dark) */
|
||||||
|
--ok: #40b856;
|
||||||
|
--warn: #f0a030;
|
||||||
|
--bad: #f05050;
|
||||||
|
--idle: #7a8290;
|
||||||
|
|
||||||
|
/* Status — tinted backgrounds (dark surface tints) */
|
||||||
|
--ok-bg: #1a2e1e;
|
||||||
|
--warn-bg: #2e2210;
|
||||||
|
--bad-bg: #2e1616;
|
||||||
|
--idle-bg: #25282d;
|
||||||
|
|
||||||
|
/* Status — borders on dark tints */
|
||||||
|
--ok-border: #2a5030;
|
||||||
|
--warn-border: #4a3510;
|
||||||
|
--bad-border: #4a2020;
|
||||||
|
--ok-border-soft: #264a2c;
|
||||||
|
--warn-border-soft: #44300e;
|
||||||
|
--bad-border-soft: #44201e;
|
||||||
|
|
||||||
|
/* Warning ink — brightened amber for dark contrast */
|
||||||
|
--warn-ink: #d4891a;
|
||||||
|
--warn-ink-deep: #b8741a;
|
||||||
|
|
||||||
|
/* Info tint */
|
||||||
|
--info-bg: #1a2240;
|
||||||
|
--info-border: #2a3660;
|
||||||
|
|
||||||
|
/* Neutral surface washes */
|
||||||
|
--zebra-bg: #202328;
|
||||||
|
--hover-bg: #2a2e38;
|
||||||
|
--active-bg: #1e2640;
|
||||||
|
|
||||||
|
/* Bootstrap bridge tokens */
|
||||||
|
--bs-body-bg: var(--paper);
|
||||||
|
--bs-body-color: var(--ink);
|
||||||
|
--bs-border-color: var(--rule);
|
||||||
|
--bs-emphasis-color: var(--ink);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Caveats
|
||||||
|
|
||||||
|
1. **Bootstrap intrinsic dark utilities**: Bootstrap 5.3 has its own `[data-bs-theme="dark"]` rules for components (alerts, badges, modals, etc.). Our token block sits in site.css at cascade level 0; Bootstrap's own dark-mode block in bootstrap.min.css (loaded earlier) will also fire. The Bootstrap rules and our token rules are additive and complementary — they should not conflict, but any Bootstrap component that ignores our tokens and uses Bootstrap's built-in dark-mode palette may have a slightly different feel. Inspect on first run.
|
||||||
|
|
||||||
|
2. **`body` gradient in theme.css**: The `body` rule in theme.css is:
|
||||||
|
```css
|
||||||
|
body {
|
||||||
|
background: radial-gradient(1200px 480px at 88% -8%, var(--card) 0%, rgba(255,255,255,0) 70%),
|
||||||
|
var(--paper);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
On dark mode, `rgba(255,255,255,0)` in the gradient is transparent white and will remain unchanged. The gradient will still render; `var(--card)` will resolve to the dark card value and the effect will be a subtle lighter sheen in the top-right corner — acceptable, but the gradient can be suppressed in site.css for a flatter dark background if preferred.
|
||||||
|
|
||||||
|
3. **Status tints on dark**: The status background tokens (`--ok-bg`, `--warn-bg`, `--bad-bg`) are deliberately dark-surface equivalents (very low-saturation tints). Verify contrast ratios of status chip text against these backgrounds before shipping.
|
||||||
|
|
||||||
|
4. **`data-bs-theme` placement**: The attribute must be set on `<html>` (or `<body>`) so the CSS selector matches all descendants. The toggle implementation (next task) should write `document.documentElement.setAttribute('data-bs-theme', 'dark')`.
|
||||||
Reference in New Issue
Block a user