InfraGuard

Light / Dark Theme

Switch the console between light and dark — a single data-theme attribute on the root, persisted in localStorage (infr-theme), a header toggle, and all colors as --infr-* CSS variable tokens.

Overview — what it does

The InfraGuard console ships with two themes: dark (default) and light. The theme is switched by a single attribute on the document root (<html>), data-theme, whose value swaps the entire set of --infr-* CSS variable tokens defined in SCSS. Every component uses var(--infr-*) tokens instead of hardcoded hex, so flipping one word (dark ↔ light) instantly re-paints the whole UI.

The default is dark — as a NOC (network operations center) console it uses a deep blue-black background with teal signal accents so severity colors (crit / warn / good) read crisply against a dark field.

<html data-theme="dark">   ← default, NOC dark
<html data-theme="light">  ← light (bright office environments)

The theme is a pure front-end preference. It is never stored on the server and has nothing to do with your account or the database — it lives only in the browser's localStorage. It survives logout, and if someone else signs in with the same account on a different device, that device follows its own setting.


When to use

Once chosen it persists in that browser, so you normally set it only once.


Layout

Header toggle button

Both the console shell (layouts/app.blade.php) and the login screen (layouts/simple.blade.php) place a theme toggle button at the right of the top bar.

Item Value
Markup hook <button data-theme-toggle>
Icon mdi-theme-light-dark (half-moon)
aria-label msg.action.theme (localized)
Action Click cycles dark ↔ light
Position Top-bar actions area (next to the language switcher, left of the user menu)

The pre-login (guest) screen carries the same toggle, so you can change theme without authenticating.

Color token groups

resources/scss/_variables.scss defines the same token names with different values in two blocks: :root[data-theme="dark"] and :root[data-theme="light"]. Representative tokens:

Token Purpose dark light
--infr-bg Page background #0b1220 #f6f8fb
--infr-surface Panel / card surface #111b2c #ffffff
--infr-fg Body text #e9eff8 #111a28
--infr-fg-muted Secondary text #8493ac #63728b
--infr-border Borders #1e2c43 #dbe2ec
--infr-accent Accent (teal) #2dd4bf #0d9488
--infr-crit Critical severity #fb7185 #e11d48
--infr-warn Warning severity #fbbf24 #b45309
--infr-good Healthy severity #34d399 #0f9d6a
--infr-info Info #38bdf8 #0369a1

Beyond these, each theme also defines --infr-surface-2/hi, --infr-border-2, --infr-accent-soft/ink, --infr-on-accent (button text on accent), TCP state colors (--infr-state-*), chart colors (--infr-chart-2), shadow/glow (--infr-shadow / --infr-glow), and the top-bar glass (--infr-topbar-*). Each block also declares color-scheme: dark|light so native form controls and scrollbars match the theme.

The SSH terminal palette is an exception. The --infr-term-* tokens stay a deep, immersive dark regardless of data-theme. Even in light theme the SSH console keeps a dark terminal.


How to use

Switching

  1. Click the half-moon icon (theme toggle) at the right of the top bar.
  2. It flips dark ↔ light immediately, and the choice auto-saves.
  3. On the next visit or a page reload, your last choice is restored.

Under the hood (developer reference)

The front end is wired in resources/js/app.js. Core logic:

const THEME_KEY = 'infr-theme';                 // localStorage key
const get_theme = () => localStorage.getItem(THEME_KEY) || 'dark';   // default dark
const set_theme = (t) => {
    document.documentElement.setAttribute('data-theme', t);   // root attribute
    localStorage.setItem(THEME_KEY, t);                        // persist
    window.dispatchEvent(new CustomEvent('infr:theme', { detail: t })); // re-color charts
};
// wire the toggle button
document.querySelectorAll('[data-theme-toggle]')
    .forEach(btn => btn.addEventListener('click',
        () => set_theme(get_theme() === 'dark' ? 'light' : 'dark')));

Behavior summary:

Step What
Storage key localStorage['infr-theme'] = "dark" | "light"
Applied to data-theme attribute on document.documentElement (the <html>)
Default dark (when localStorage is empty or unreadable)
Event Each switch fires an infr:theme CustomEvent on window (detail = current theme)
Cross-tab sync Listens for the storage event → other tabs in the same browser sync automatically

Flash-of-unstyled-content (FOUC) prevention — pre-paint script

A tiny inline script at the very top of <head> (shared by app.blade.php and simple.blade.php, before the CSS @vite load) sets data-theme before any stylesheet applies. This removes the "dark→light flash" on reload.

<script>
  (function () {
    try { document.documentElement.setAttribute('data-theme',
            localStorage.getItem('infr-theme') || 'dark'); }
    catch (e) { document.documentElement.setAttribute('data-theme', 'dark'); }
  })();
</script>

Even where localStorage is blocked (incognito, third-party storage blocked, etc.), the catch safely falls back to dark.

Charts and editors follow the theme

<cm-editor v-model="script.body" lang="bash" :dark="is_dark_mode"></cm-editor>

Data sources

The theme has no DB or server storage at all. It uses only:

Location Content
localStorage['infr-theme'] Per-browser user choice (dark/light)
<html data-theme> Runtime applied state
resources/scss/_variables.scss Actual per-theme values of the --infr-* tokens (built into CSS)
resources/js/app.js Toggle / persist / event wiring

CLAUDE.md Appendix C (Light/Dark theme tokens) is the source of truth for the palette design.


Access

The theme is a UI preference unrelated to authentication or authorization.


Troubleshooting & FAQ

Q. The theme resets on reload. If the browser blocks localStorage (incognito, strict privacy settings, third-party storage blocking), the choice can't be saved and it reverts to the default dark every time. Grant storage access for the site.

Q. I changed it in one tab but another tab didn't update. The switch propagates to other tabs in the same browser via the storage event. If it doesn't reflect immediately, that tab may not have loaded the listener wiring (app.js) yet — reload it. Different devices/browsers have separate storage and do not sync (this is by design).

Q. I switched to light but the SSH terminal stays dark. That's expected. The terminal uses a dedicated --infr-term-* palette that stays deep-dark regardless of the app theme (readability / immersion).

Q. One component looks wrong (the theme doesn't apply to it). That component hardcoded hex instead of tokens. By convention components must use only var(--infr-*) tokens. Check for direct hex values in resources/scss/pages with the regression grep:

rg -n "#[0-9a-fA-F]{3,6}" resources/scss/pages

Q. I want it to follow my OS dark mode. The current implementation does not follow the OS preference (prefers-color-scheme). The initial value is always dark, and it only remembers what the user explicitly picked with the toggle. OS integration is a roadmap item.


Related

The theme and language switches, top-right
The theme and language switches, top-right