Files
mainty/docs/superpowers/plans/2026-05-01-dark-mode-support.md

20 KiB

Dark Mode Support Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add cookie-persisted dark/light theming with a floating top-right toggle on all regular views, while keeping export always light.

Architecture: Theme behavior is centralized in one reusable partial with two include modes (head and body) so every themed page gets identical bootstrap, cookie logic, and toggle UI. Existing views keep structure/layout classes and only adjust color-related classes needed for dark-mode readability. Export is explicitly excluded and remains light-only.

Tech Stack: PHP 8, server-rendered PHP views, Tailwind via CDN, Bootstrap Icons, vanilla JavaScript, browser cookies.


Chunk 1: Shared Theme Unit and Core Integration

Task 1: Create shared theme partial with explicit section contract

Files:

  • Create: views/partials/theme.php

  • Test: browser manual validation on /login after integration

  • Step 1: Add section-gated partial skeleton

<?php
$themeSection = $themeSection ?? null;

if ($themeSection === 'head') {
    // output head-only theme bootstrap and shared CSS
} elseif ($themeSection === 'body') {
    // output body-only floating toggle and behavior script
}
  • Step 2: Implement head output with early theme bootstrap script
<script>
(function() {
    function readThemeCookie() {
        var match = document.cookie.match(/(?:^|; )theme=([^;]+)/);
        if (!match) return null;
        var value = decodeURIComponent(match[1]);
        return (value === 'dark' || value === 'light') ? value : null;
    }

    var cookieTheme = readThemeCookie();
    var systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    var resolved = cookieTheme || (systemDark ? 'dark' : 'light');

    document.documentElement.classList.toggle('dark', resolved === 'dark');
    document.documentElement.setAttribute('data-theme', resolved);
})();
</script>

Expected output:

  • <html> has dark class before body render when resolved theme is dark.

  • data-theme is set to dark or light.

  • Step 3: Implement head shared CSS tokens and semantic utility classes

<style>
:root {
    --theme-bg: #f3f4f6;
    --theme-surface: #ffffff;
    --theme-surface-alt: #f9fafb;
    --theme-text: #1f2937;
    --theme-text-muted: #4b5563;
    --theme-border: #d1d5db;
}

html.dark {
    --theme-bg: #0f172a;
    --theme-surface: #111827;
    --theme-surface-alt: #1f2937;
    --theme-text: #e5e7eb;
    --theme-text-muted: #9ca3af;
    --theme-border: #374151;
}

.theme-page { background-color: var(--theme-bg); color: var(--theme-text); }
.theme-surface { background-color: var(--theme-surface); color: var(--theme-text); }
.theme-surface-alt { background-color: var(--theme-surface-alt); }
.theme-text { color: var(--theme-text); }
.theme-text-muted { color: var(--theme-text-muted); }
.theme-border { border-color: var(--theme-border); }
.theme-input { background-color: var(--theme-surface); color: var(--theme-text); border-color: var(--theme-border); }

#themeToggle {
    position: fixed;
    top: 1rem;
    right: 1rem;
    z-index: 50;
}

#themeToggle:focus-visible {
    outline: 2px solid #2563eb;
    outline-offset: 2px;
}
</style>

Expected output:

  • Shared classes exist for page/surface/text/border/input mappings.

  • Toggle has fixed top-right placement and z-index 50.

  • Step 4: Implement body output with toggle markup and no-JS fallback

<button id="themeToggle" type="button" aria-label="Switch to dark mode" title="Switch to dark mode">
    <i id="themeToggleIcon" class="bi bi-moon-stars-fill"></i>
</button>
<noscript><style>#themeToggle{display:none;}</style></noscript>
  • Step 5: Implement body behavior script with cookie helpers and state sync
<script>
(function() {
    var toggle = document.getElementById('themeToggle');
    var icon = document.getElementById('themeToggleIcon');

    function readThemeCookie() {
        var match = document.cookie.match(/(?:^|; )theme=([^;]+)/);
        if (!match) return null;
        var value = decodeURIComponent(match[1]);
        return (value === 'dark' || value === 'light') ? value : null;
    }

    function writeThemeCookie(theme) {
        document.cookie = 'theme=' + encodeURIComponent(theme) + '; max-age=31536000; path=/; SameSite=Lax';
    }

    function applyTheme(theme) {
        var isDark = theme === 'dark';
        document.documentElement.classList.toggle('dark', isDark);
        document.documentElement.setAttribute('data-theme', theme);
    }

    function syncToggle(theme) {
        var isDark = theme === 'dark';
        icon.className = isDark ? 'bi bi-sun-fill' : 'bi bi-moon-stars-fill';
        var nextAction = isDark ? 'Switch to light mode' : 'Switch to dark mode';
        toggle.setAttribute('aria-label', nextAction);
        toggle.setAttribute('title', nextAction);
    }

    var currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : (readThemeCookie() || 'light');
    syncToggle(currentTheme);

    toggle.addEventListener('click', function() {
        currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
        applyTheme(currentTheme);
        writeThemeCookie(currentTheme);
        syncToggle(currentTheme);
    });
})();
</script>

Expected output:

  • Click toggles theme immediately, updates icon, aria-label, and title.

  • Cookie write attempt does not block visual switch if persistence is restricted.

  • Step 6: Run syntax check for new partial

Run: php -l views/partials/theme.php

Expected: No syntax errors detected in views/partials/theme.php.

  • Step 7: Commit Task 1
git add views/partials/theme.php
git commit -m "feat: add shared theme partial with cookie-based dark mode"

Task 2: Integrate theme partial into login and setup views

Files:

  • Modify: views/login.php

  • Modify: views/setup.php

  • Test: /login, /setup

  • Step 1: Insert head include in views/login.php

<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>

Placement: in <head>, after Tailwind/bootstrap includes, before </head>.

  • Step 2: Insert body include in views/login.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>

Placement: before </body>.

  • Step 3: Apply color-only updates in views/login.php

Apply concrete replacements:

  • <body class="bg-gray-100 ..."> -> <body class="theme-page ...">
  • login card bg-white -> theme-surface
  • heading text-gray-800 -> theme-text
  • label text text-gray-700 -> theme-text
  • input border-gray-300 -> add theme-input theme-border

Expected outcome: login card, labels, and input fields are readable in both themes without layout shift.

  • Step 4: Insert head include in views/setup.php
<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>

Placement: in <head>, after Tailwind/bootstrap includes, before </head>.

  • Step 5: Insert body include in views/setup.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>

Placement: before </body>.

  • Step 6: Apply color-only updates in views/setup.php

Apply concrete replacements:

  • <body class="bg-gray-100 ..."> -> <body class="theme-page ...">
  • setup card bg-white -> theme-surface
  • title and labels text-gray-* -> theme-text / theme-text-muted
  • password inputs border-gray-300 -> add theme-input theme-border

Expected outcome: setup page remains visually consistent, with readable text/inputs in both themes.

  • Step 7: Run syntax checks for auth/setup views

Run: php -l views/login.php && php -l views/setup.php

Expected: both report No syntax errors detected.

  • Step 8: Manual verification on /login and /setup

Expected:

  • toggle appears fixed top-right

  • keyboard Tab focuses toggle with visible focus style

  • Enter/Space toggles theme

  • icon and aria-label/title change each toggle

  • reload preserves preference

  • Step 9: Commit Task 2

git add views/login.php views/setup.php
git commit -m "feat: integrate dark mode toggle on auth and setup views"

Chunk 2: Main Views, Export Exception, and Final Verification

Task 3: Integrate theme partial into home view

Files:

  • Modify: views/index.php

  • Test: /home

  • Step 1: Add head include in views/index.php

<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>

Placement requirement: after Tailwind/bootstrap includes and before </head>.

Expected outcome: exactly one head include in required position.

  • Step 2: Add body include in views/index.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>

Placement requirement: before local page scripts and before </body>.

Expected outcome: exactly one body include in required position.

  • Step 3: Update page and header surfaces in views/index.php

Concrete updates:

  • body wrapper bg-gray-100 -> theme-page
  • header/card wrappers bg-white -> theme-surface
  • heading/body text text-gray-* -> theme-text / theme-text-muted

Expected outcome: page shell and header are readable in both themes.

  • Step 4: Update table surfaces in views/index.php

Concrete updates:

  • table header <thead class="bg-gray-50 border-b"> -> use theme-surface-alt theme-border
  • table body divider divide-gray-200 -> theme-border
  • table cell text classes text-gray-500, text-gray-600, text-gray-900 -> theme-text/theme-text-muted

Expected outcome: table head/body text and borders remain readable in both themes.

  • Step 5: Update vehicle cards/list rows in views/index.php

Concrete updates:

  • grid cards bg-white -> theme-surface
  • list container bg-white -> theme-surface
  • row hover hover:bg-gray-50 -> hover:opacity-95 and apply theme-surface-alt on row container where needed
  • small metadata text text-gray-* -> theme-text-muted

Expected outcome: vehicle cards/list rows remain readable in both themes.

  • Step 6: Update modal container and input surfaces in views/index.php

Concrete updates:

  • modal panel bg-white -> theme-surface
  • modal close/cancel gray text -> theme-text-muted
  • inputs with border-gray-300 -> add theme-input theme-border
  • optional helper text gray shades -> theme-text-muted

Expected outcome: cards, table, and modal inputs remain readable/usable in both themes.

  • Step 7: Verify and adjust flash message contrast in views/index.php

Concrete updates:

  • keep existing light-mode classes for base state (bg-red-100 border-red-400 text-red-700, bg-green-100 border-green-400 text-green-700)
  • add dark-only variants (using dark: classes or equivalent conditional class strategy) for error/success contrast in dark mode
  • keep existing rounded/padding/layout classes unchanged

Expected outcome: error and success flash messages are legible and visually distinct in both light and dark modes.

  • Step 8: Run syntax check for views/index.php

Run: php -l views/index.php

Expected: No syntax errors detected in views/index.php.

  • Step 9: Manual verification for /home

Expected:

  • theme toggles correctly

  • theme persists after reload

  • cards/tables/modals remain readable in both themes

  • Step 10: Commit Task 3

git add views/index.php
git commit -m "feat: integrate dark mode support in home view"

Task 4: Integrate theme partial into vehicle detail view

Files:

  • Modify: views/vehicle.php

  • Test: /vehicles/{id}

  • Step 1: Add head include in views/vehicle.php

<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>

Placement requirement: after Tailwind/bootstrap includes and before </head>.

Expected outcome: exactly one head include in required position.

  • Step 2: Add body include in views/vehicle.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>

Placement requirement: before page-local scripts and before </body>.

Expected outcome: exactly one body include in required position.

  • Step 3: Update page shell, vehicle card, and maintenance form in views/vehicle.php

Concrete updates:

  • body/header/card backgrounds bg-white/bg-gray-* -> theme classes
  • text grays on headings/body -> theme-text / theme-text-muted
  • form inputs/borders -> theme-input theme-border

Expected outcome: top section and add-maintenance form are readable in both themes.

  • Step 4: Update dropdown menu surfaces in views/vehicle.php

Concrete updates:

  • suggestions box bg-white border-gray-300 -> theme-surface theme-border
  • export menu panel bg-white -> theme-surface
  • export menu links hover:bg-gray-100 -> hover:opacity-95
  • export menu link text grays -> theme-text

Expected outcome: dropdown menus remain readable and interactive in both themes.

  • Step 5: Update maintenance history surfaces in views/vehicle.php

Concrete updates:

  • history container bg-white -> theme-surface
  • item cards border-gray-200 -> theme-border
  • item card hover hover:border-blue-300 -> hover:border-blue-500
  • item metadata text gray shades -> theme-text-muted

Expected outcome: maintenance history cards remain readable in both themes.

  • Step 6: Update modal panel and input surfaces in views/vehicle.php

Concrete updates:

  • edit modal panels bg-white -> theme-surface
  • modal input border-gray-300 -> theme-input theme-border
  • modal close/cancel text grays -> theme-text-muted

Expected outcome: dropdowns, history cards, and modals remain readable and interactive in both themes.

  • Step 7: Run syntax check for views/vehicle.php

Run: php -l views/vehicle.php

Expected: No syntax errors detected in views/vehicle.php.

  • Step 8: Manual verification for /vehicles/{id}

Expected:

  • theme toggles and persists

  • forms/dropdowns/modals remain readable in both themes

  • navigation to/from /home keeps cookie-based preference

  • Step 9: Commit Task 4

git add views/vehicle.php
git commit -m "feat: integrate dark mode support in vehicle detail view"

Task 5: Integrate theme partial into settings view

Files:

  • Modify: views/settings.php

  • Test: /settings

  • Step 1: Add head include in views/settings.php

<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>

Placement requirement: after Tailwind/bootstrap includes and before </head>.

Expected outcome: exactly one head include in required position.

  • Step 2: Add body include in views/settings.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>

Placement requirement: before local scripts (if any) and before </body>.

Expected outcome: exactly one body include in required position.

  • Step 3: Update page/header/card surfaces in views/settings.php

Concrete updates:

  • body/header/cards bg-white/bg-gray-* -> theme-page/theme-surface/theme-surface-alt
  • gray text classes -> theme-text / theme-text-muted

Expected outcome: page shell and cards remain readable in both themes.

  • Step 4: Update list rows and password form surfaces in views/settings.php

Concrete updates:

  • quick-task row hover/background and borders -> theme classes
  • password input borders/backgrounds -> theme-input theme-border

Expected outcome: list rows and form fields are readable and distinguishable in both themes.

  • Step 5: Verify action/footer contrast in views/settings.php

Concrete updates:

  • logout action danger colors may be adjusted if needed for dark-mode contrast while preserving danger semantics
  • powered footer card bg-white -> theme-surface
  • powered footer title/body grays -> theme-text / theme-text-muted
  • GitHub icon/link muted grays -> theme-text-muted while preserving blue link emphasis

Expected outcome: call-to-action and footer text remain legible in both themes.

  • Step 6: Run syntax check for views/settings.php

Run: php -l views/settings.php

Expected: No syntax errors detected in views/settings.php.

  • Step 7: Manual verification for /settings

Expected:

  • theme toggles and persists

  • cards/list rows/inputs remain readable in both themes

  • navigation to/from /home keeps cookie-based preference

  • Step 8: Commit Task 5

git add views/settings.php
git commit -m "feat: integrate dark mode support in settings view"

Task 6: Validate export exception and complete end-to-end checks

Files:

  • Verify unchanged: views/export.php

  • Verify behavior: views/partials/theme.php

  • Verify integrated views: views/login.php, views/setup.php, views/index.php, views/vehicle.php, views/settings.php

  • Step 1: Verify views/export.php has no theme include/toggle

Expected:

  • no $themeSection include

  • no #themeToggle markup

  • no dark-mode bootstrap script and no html.dark application path in export template

  • Step 2: Verify cookie contract in devtools

Flow:

  • toggle theme on any themed page
  • inspect cookie attributes

Expected:

  • cookie name theme

  • value dark or light

  • Max-Age=31536000

  • Path=/

  • SameSite=Lax

  • Step 3: Verify accessibility state updates on toggle

Flow:

  • focus toggle via keyboard
  • activate twice

Expected per click:

  • icon swaps moon/sun

  • aria-label and title update to next action

  • Step 4: Verify export is always light

Flow:

  • set theme=dark
  • open /vehicles/{id}/export/html

Expected:

  • export remains light styled

  • toggle not present

  • <html> does not receive dark class on export, even when theme=dark

  • Step 5: Verify toggle stays visible above modals/dropdowns

Flow:

  • open each page with modal/dropdown support (/home, /vehicles/{id})
  • open add/edit modals and export/suggestions dropdowns

Expected:

  • floating toggle remains visible and clickable above overlays

  • Step 5a: Apply layering remediation if Step 5 fails

Flow:

  • if toggle is hidden or unclickable over overlays, adjust stacking order in views/partials/theme.php

Concrete remediation:

  • increase #themeToggle z-index above conflicting overlay layer
  • if conflict persists, reduce non-critical overlay z-index where safe without breaking modal usability

Expected:

  • toggle remains visible/clickable while modals/dropdowns still function correctly

  • Step 6: Verify no-cookie fallback uses system preference

Flow:

  • delete theme cookie
  • load /home

Expected:

  • resolved theme follows current prefers-color-scheme

  • Step 7: Verify invalid-cookie fallback

Flow:

  • set theme=invalid
  • reload /home

Expected:

  • invalid cookie ignored

  • resolved theme follows current system preference

  • Step 8: Verify no-JS fallback

Flow:

  • disable JS in browser
  • open /login

Expected:

  • light mode only

  • no visible floating toggle

  • login form remains usable

  • Step 9: Verify cookie-write-failure behavior

Flow:

  • use a browser mode/policy that blocks cookie writes (or simulate via devtools/privacy settings)
  • click toggle on /home

Expected:

  • theme still switches immediately on the current page

  • no error message is shown to the user

  • persistence across reload is not required in this scenario

  • Step 10: Run final PHP syntax sweep

Run:

php -l views/partials/theme.php && \
php -l views/login.php && \
php -l views/setup.php && \
php -l views/index.php && \
php -l views/vehicle.php && \
php -l views/settings.php

Expected: all report No syntax errors detected.

  • Step 11: No additional commit for verification-only task

Expected:

  • Task 6 records verification results only.
  • No new commit is created unless this task introduces actual file changes.