Skip to content

feat(ui): gate dark mode at build time - #110

Merged
Luhaozhu merged 1 commit into
fix/desktop-dark-mode-shellfrom
feat/dark-mode-build-gate
Aug 17, 2026
Merged

feat(ui): gate dark mode at build time#110
Luhaozhu merged 1 commit into
fix/desktop-dark-mode-shellfrom
feat/dark-mode-build-gate

Conversation

@Luhaozhu

Copy link
Copy Markdown
Contributor

Stacked on #109 — base is fix/desktop-dark-mode-shell, so the diff here stays clean and the baseline reflects an already-cleaned desktop shell. Retarget to main once #109 merges.

Dark mode is applied by toggling <html data-theme="dark"> and letting variables.css override the token block. Any style that bypasses a token and hardcodes a colour looks fine in light and becomes a bright patch in dark — and nobody switches themes twice before pushing, so every new feature adds another one. Fixing them one at a time does not converge; there is no feedback loop.

The gate

scripts/check-dark-mode.mjs runs before the build (next to check-i18n) and scans five blind spots:

rule what it catches
css-literal-color a colour literal in a CSS declaration
css-var-literal-color a literal hidden inside a custom property's value — bulk rename scripts never catch these
js-inline-color literals in TSX inline styles / SVG attributes
undefined-var-fallback var(--undefined, #fff) — the fallback always wins, so it is a hardcode
prefers-color-scheme the media query fights the manual light/dark/system toggle

It is a ratchet, not a wall: dark-mode-baseline.json freezes current debt per file per rule, exceeding it fails the build, dropping below it prints a hint to tighten via --update. Existing debt can only shrink.

Deliberate hardcodes are exempted with dark-ok: reason on the line or the comment above it, and long templates with dark-ok-begin / dark-ok-end. A reason is mandatory.

Three precision details keep it from crying wolf: white inside var(--color-text-white) is a name, not a colour; a fallback on an already defined token is dead code rather than an accident; and multi-line declarations report their starting line so the dark-ok comment has somewhere natural to sit.

It also covers the desktop shell

desktop/src-tauri/src/**.rs embeds whole HTML pages as Rust string literals. To a user that is the same UI, but it is neither .css nor .tsx, so it had no feedback at all — which is exactly how #109's bugs survived. CSS is lifted out of <style> blocks by blanking everything else rather than slicing, so reported line numbers are the real ones in the .rs file.

Paired category tokens

Adds --color-tint-{purple,cyan,teal,amber,green,red,blue}-bg plus matching foregrounds. Only semantic colours (success/warning/error/primary) had pairs before, so badges that exist purely to tell categories apart had nothing to reach for and each feature picked its own light hex — the slash panel, skill/plugin chips, loop status badges and channel connection status all fell into it.

State

  • Baseline starts at 630 findings across 58 files — this repo's debt frozen as-is.
  • npm run build passes today (check-i18n → check-dark-mode → tsc → vite).
  • Verified the gate fails on a planted violation and reports the correct file and line.
  • Three files (ProjectCard.tsx, ProjectDetailPanel.tsx, chat.css) keep their existing literals — their upstream cleanup did not transfer to this tree — so they stay in the baseline for a later pass.

…t by component

Dark mode is applied by toggling `<html data-theme="dark">` and letting
`variables.css` override the token block. Any style that bypasses a token and
hardcodes a colour looks fine in light and becomes a bright patch in dark —
and nobody switches themes twice before pushing, so every new feature adds
another one. Fixing them one at a time does not converge; there is no feedback
loop.

`scripts/check-dark-mode.mjs` runs before the build (next to check-i18n) and
scans five blind spots:

    css-literal-color       a colour literal in a CSS declaration
    css-var-literal-color   a literal hidden inside a custom property's value
                            (bulk rename scripts have never caught these)
    js-inline-color         literals in TSX inline styles / SVG attributes
    undefined-var-fallback  var(--undefined, #fff) — the fallback always wins,
                            so it is a hardcode
    prefers-color-scheme    the media query fights the manual light/dark/system
                            toggle

It is a **ratchet**, not a wall: `dark-mode-baseline.json` freezes the current
debt per file per rule, exceeding it fails the build, and dropping below it
prints a hint to tighten with `--update`. Existing debt can only shrink.
Deliberate hardcodes are exempted with `dark-ok: reason` on the line or the
comment above it, and long templates with `dark-ok-begin` / `dark-ok-end` — a
reason is mandatory.

Three precision details keep it from crying wolf: `white` inside
`var(--color-text-white)` is a name and not a colour; a fallback on an
*already defined* token is dead code rather than an accident; and multi-line
declarations report their starting line so the `dark-ok` comment has somewhere
natural to sit.

The gate also scans the **desktop shell** (`desktop/src-tauri/src/**.rs`),
which embeds whole HTML pages as Rust string literals. It is the same UI to a
user but is neither `.css` nor `.tsx`, so it had no feedback at all. CSS is
lifted out of `<style>` blocks by blanking everything else rather than slicing,
so reported line numbers are the real ones in the `.rs` file.

Also adds the paired `--color-tint-{purple,cyan,teal,amber,green,red,blue}-bg`
tokens plus matching foregrounds. Only semantic colours (success/warning/error/
primary) had pairs before, so badges that exist purely to tell categories apart
had nothing to reach for and each feature picked its own light hex — the slash
panel, skill/plugin chips, loop status badges and channel connection status all fell into it.

Baseline starts at 630 findings across 58 files, i.e. this repo's current debt
frozen as-is; the build passes today and can only improve from here.

Note: three files (ProjectCard.tsx, ProjectDetailPanel.tsx, chat.css) keep
their existing literals — their upstream cleanup did not transfer to this tree
— so those stay in the baseline for a later pass.
@Luhaozhu
Luhaozhu merged commit 155835b into fix/desktop-dark-mode-shell Aug 17, 2026
5 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.

1 participant